clixad 0.0.1-beta.22 → 0.0.1-beta.23

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 +328 -78
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -350,12 +350,48 @@ function asSignupClosed(status, data) {
350
350
  const d = data;
351
351
  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;
352
352
  }
353
+ function shortfallHeadline(err, model) {
354
+ const n = (v) => Math.max(0, Math.round(v)).toLocaleString("en-US");
355
+ if (err.balance <= 0) return `Out of credits (this needs about ${n(err.estimatedCost)}).`;
356
+ return `Not enough credits for this turn${model ? ` on ${model}` : ""}: it needs about ${n(err.estimatedCost)}, and the balance is ${n(err.balance)}.`;
357
+ }
353
358
  function paywallError(body) {
354
359
  const free = Array.isArray(body.free_models) ? body.free_models.filter(
355
360
  (m) => !!m && typeof m.id === "string" && m.id.length > 0 && Number.isFinite(m.turns_left)
356
361
  ).map((m) => ({ id: m.id, turnsLeft: Math.max(0, Math.floor(m.turns_left)) })).filter((m) => m.turnsLeft > 0) : [];
357
362
  return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, free);
358
363
  }
364
+ function neverConnected(code) {
365
+ return code !== void 0 && NEVER_CONNECTED.has(code);
366
+ }
367
+ function networkCode(err) {
368
+ let cause = err?.cause;
369
+ for (let depth = 0; depth < 3 && cause; depth++) {
370
+ const code = cause.code;
371
+ if (typeof code === "string" && code) return code;
372
+ const inner = cause.errors;
373
+ if (Array.isArray(inner)) {
374
+ const first = inner.find((e) => typeof e?.code === "string");
375
+ if (first) return first.code;
376
+ }
377
+ cause = cause.cause;
378
+ }
379
+ return void 0;
380
+ }
381
+ async function gatewayFetch(url, init) {
382
+ try {
383
+ return await fetch(url, init);
384
+ } catch (err) {
385
+ const aborted = init?.signal?.aborted || err?.name === "AbortError";
386
+ if (aborted || !(err instanceof TypeError)) throw err;
387
+ let host = String(url);
388
+ try {
389
+ host = new URL(url).host;
390
+ } catch {
391
+ }
392
+ throw new GatewayUnreachableError(host, networkCode(err));
393
+ }
394
+ }
359
395
  async function authFailure(res, what) {
360
396
  const body = await res.text().catch(() => "");
361
397
  const isJson = (res.headers.get("content-type") ?? "").includes("json");
@@ -429,7 +465,7 @@ async function* parseSSE(body) {
429
465
  }
430
466
  }
431
467
  }
432
- var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, AuthError, GatewayClient;
468
+ var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
433
469
  var init_client = __esm({
434
470
  "src/client.ts"() {
435
471
  "use strict";
@@ -478,6 +514,33 @@ var init_client = __esm({
478
514
  grantPerAd;
479
515
  freeModels;
480
516
  };
517
+ GatewayUnreachableError = class extends Error {
518
+ constructor(host, code) {
519
+ const where = `${host}${code ? ` (${code})` : ""}`;
520
+ super(
521
+ neverConnected(code) ? `Can't reach the Clixad gateway at ${where}. Nothing was sent, so nothing was charged. Check your connection and try again.` : `The connection to the Clixad gateway at ${where} broke before an answer arrived. If that was a model call it may still have been charged, so check your balance before sending it again.`
522
+ );
523
+ this.host = host;
524
+ this.code = code;
525
+ this.name = "GatewayUnreachableError";
526
+ }
527
+ host;
528
+ code;
529
+ };
530
+ NEVER_CONNECTED = /* @__PURE__ */ new Set([
531
+ "ENOTFOUND",
532
+ "EAI_AGAIN",
533
+ "ECONNREFUSED",
534
+ "ENETUNREACH",
535
+ "EHOSTUNREACH",
536
+ "EADDRNOTAVAIL",
537
+ "UND_ERR_CONNECT_TIMEOUT",
538
+ "CERT_HAS_EXPIRED",
539
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
540
+ "SELF_SIGNED_CERT_IN_CHAIN",
541
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
542
+ "ERR_TLS_CERT_ALTNAME_INVALID"
543
+ ]);
481
544
  AuthError = class extends Error {
482
545
  constructor() {
483
546
  super("Not logged in. Run `clixad login` first.");
@@ -518,7 +581,7 @@ var init_client = __esm({
518
581
  }
519
582
  /** Dev-only signup shortcut (production replaces this with GitHub OAuth). */
520
583
  async signupDev(email, invite) {
521
- const res = await fetch(`${this.config.gatewayUrl}/dev/users`, {
584
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/dev/users`, {
522
585
  method: "POST",
523
586
  headers: this.headers(false),
524
587
  body: JSON.stringify({ ...email ? { email } : {}, ...invite ? { invite } : {} })
@@ -534,7 +597,7 @@ var init_client = __esm({
534
597
  /** Begin GitHub device-flow login. Returns null if the gateway has no GitHub
535
598
  * configured (501), so the caller can fall back to the dev shortcut. */
536
599
  async deviceStart() {
537
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
600
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
538
601
  method: "POST",
539
602
  headers: this.headers(false)
540
603
  });
@@ -544,7 +607,7 @@ var init_client = __esm({
544
607
  }
545
608
  /** Poll once for device-flow completion. `invite` unlocks a gated signup. */
546
609
  async devicePoll(session, invite) {
547
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
610
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
548
611
  method: "POST",
549
612
  headers: this.headers(false),
550
613
  body: JSON.stringify(invite ? { session, invite } : { session })
@@ -566,7 +629,7 @@ var init_client = __esm({
566
629
  */
567
630
  async handoffCode() {
568
631
  try {
569
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
632
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
570
633
  method: "POST",
571
634
  headers: this.headers()
572
635
  });
@@ -593,7 +656,7 @@ var init_client = __esm({
593
656
  */
594
657
  async claimStreak(signal) {
595
658
  try {
596
- const res = await fetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
659
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
597
660
  method: "POST",
598
661
  headers: this.headers(),
599
662
  ...signal ? { signal } : {}
@@ -628,7 +691,7 @@ var init_client = __esm({
628
691
  */
629
692
  async sendFeedback(report, signal) {
630
693
  try {
631
- const res = await fetch(`${this.config.gatewayUrl}/v1/feedback`, {
694
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/feedback`, {
632
695
  method: "POST",
633
696
  headers: this.headers(),
634
697
  ...signal ? { signal } : {},
@@ -663,7 +726,7 @@ var init_client = __esm({
663
726
  * that did not run, and saying so beats printing an empty summary.
664
727
  */
665
728
  async referral(signal) {
666
- const res = await fetch(`${this.config.gatewayUrl}/v1/referrals`, {
729
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/referrals`, {
667
730
  headers: this.headers(),
668
731
  ...signal ? { signal } : {}
669
732
  });
@@ -694,7 +757,7 @@ var init_client = __esm({
694
757
  * 200 with a status, because that is an answer rather than a failure.
695
758
  */
696
759
  async redeem(code, signal) {
697
- const res = await fetch(`${this.config.gatewayUrl}/v1/redeem`, {
760
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/redeem`, {
698
761
  method: "POST",
699
762
  headers: this.headers(),
700
763
  ...signal ? { signal } : {},
@@ -715,7 +778,7 @@ var init_client = __esm({
715
778
  * them with the input box locked and "esc to stop" on screen.
716
779
  */
717
780
  async creditPacks(signal) {
718
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
781
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
719
782
  if (!res.ok) throw new Error(`packs failed: ${res.status}`);
720
783
  return res.json();
721
784
  }
@@ -726,7 +789,7 @@ var init_client = __esm({
726
789
  * could not deliver instantly is worse than not starting one.
727
790
  */
728
791
  async checkout(pack, consent, signal) {
729
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
792
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
730
793
  method: "POST",
731
794
  headers: this.headers(),
732
795
  ...signal ? { signal } : {},
@@ -742,13 +805,37 @@ var init_client = __esm({
742
805
  // the better part of a minute) would otherwise be indistinguishable from a
743
806
  // hung terminal. Nothing else passes one — a one-shot CLI command has Ctrl+C.
744
807
  async models(signal) {
745
- const res = await fetch(`${this.config.gatewayUrl}/v1/models`, { signal });
808
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/models`, { signal });
746
809
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
747
810
  const data = await res.json();
748
811
  this.seen = { web_search: data.capabilities?.web_search === true };
749
812
  this.shape = parseTaskShape(data.task_shape);
813
+ this.latest = typeof data.cli_latest === "string" ? data.cli_latest : void 0;
814
+ const live = new Set(data.data.map((m) => m.id));
815
+ this.retired = {};
816
+ if (data.retired_models && typeof data.retired_models === "object") {
817
+ for (const [from, to] of Object.entries(data.retired_models)) {
818
+ if (typeof to === "string" && live.has(to) && !live.has(from)) this.retired[from] = to;
819
+ }
820
+ }
750
821
  return data.data;
751
822
  }
823
+ /**
824
+ * Model ids that were retired, and the live model that serves each one, as of
825
+ * the last catalog fetch. Empty from a gateway that does not send the map.
826
+ */
827
+ get retiredModels() {
828
+ return this.retired;
829
+ }
830
+ retired = {};
831
+ /**
832
+ * The newest `clixad` published to npm, as the gateway last read it, or
833
+ * undefined when it did not say. Undefined means unknown, never up to date.
834
+ */
835
+ get latestCli() {
836
+ return this.latest;
837
+ }
838
+ latest;
752
839
  /**
753
840
  * The task shape the cost preview assumes, as of the last catalog fetch, or
754
841
  * undefined when the gateway sent none (or one that did not parse). Undefined
@@ -773,7 +860,7 @@ var init_client = __esm({
773
860
  }
774
861
  seen = { web_search: false };
775
862
  async wallet(signal) {
776
- const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
863
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
777
864
  if (res.status === 401) throw await authFailure(res, "wallet");
778
865
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
779
866
  return res.json();
@@ -809,7 +896,7 @@ var init_client = __esm({
809
896
  */
810
897
  async sponsorConfig(signal) {
811
898
  try {
812
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
899
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
813
900
  headers: this.headers(),
814
901
  ...signal ? { signal } : {}
815
902
  });
@@ -847,7 +934,7 @@ var init_client = __esm({
847
934
  */
848
935
  async reportImpression(report, signal) {
849
936
  try {
850
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
937
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
851
938
  method: "POST",
852
939
  headers: this.headers(),
853
940
  ...signal ? { signal } : {},
@@ -880,7 +967,7 @@ var init_client = __esm({
880
967
  const url = new URL(`${this.config.gatewayUrl}/v1/web/search`);
881
968
  url.searchParams.set("q", query);
882
969
  if (opts.count) url.searchParams.set("count", String(opts.count));
883
- const res = await fetch(url, {
970
+ const res = await gatewayFetch(url, {
884
971
  headers: this.headers(),
885
972
  ...opts.signal ? { signal: opts.signal } : {}
886
973
  });
@@ -920,7 +1007,7 @@ var init_client = __esm({
920
1007
  /** One POST to the chat route, in whichever transport is currently in use. */
921
1008
  sendChat(payload, encoded, signal) {
922
1009
  const json = JSON.stringify(payload);
923
- return fetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
1010
+ return gatewayFetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
924
1011
  method: "POST",
925
1012
  headers: encoded ? { ...this.headers(), "Content-Type": CHAT_ENCODED_CONTENT_TYPE } : this.headers(),
926
1013
  body: encoded ? encodeChatBody(json) : json,
@@ -4528,17 +4615,26 @@ var init_kimi = __esm({
4528
4615
  "gpt-5-nano": 4e5,
4529
4616
  "gemini-2.5-flash-lite": 1048576,
4530
4617
  "deepseek-v4-flash": 1048576,
4531
- "gemini-3.1-flash-lite": 1048576,
4532
- "kimi-k2": 131072,
4618
+ "gemini-3.5-flash-lite": 1048576,
4619
+ "kimi-k2.5": 262144,
4620
+ "gemini-3.8-flash": 1048576,
4533
4621
  "kimi-k2.7-code": 262144,
4534
4622
  "claude-haiku-4.5": 2e5,
4535
- "gemini-3.6-flash": 1048576,
4536
4623
  "claude-sonnet-5": 1e6,
4537
- "gpt-5.2": 4e5,
4624
+ "gpt-6-sol": 105e4,
4538
4625
  "kimi-k3": 1048576,
4626
+ "claude-opus-5.5": 1e6,
4627
+ "claude-fable-5.1": 1e6,
4628
+ // Retired on 2026-09-27 and served by their successor (`RETIRED_MODELS` in
4629
+ // packages/pricing), so an install still configured with one compacts
4630
+ // against the window of the model that actually answers.
4631
+ "gemini-3.1-flash-lite": 1048576,
4632
+ "kimi-k2": 262144,
4633
+ "gemini-3.6-flash": 1048576,
4634
+ "gpt-5.2": 105e4,
4539
4635
  "claude-opus-5": 1e6,
4540
4636
  "gpt-5.5": 105e4,
4541
- "gpt-5.2-pro": 4e5
4637
+ "gpt-5.2-pro": 105e4
4542
4638
  };
4543
4639
  DEFAULT_CONTEXT_TOKENS = 256e3;
4544
4640
  INSTALL_HELP = `Kimi CLI is not installed (or not on PATH).
@@ -4588,6 +4684,14 @@ function bannerTip(index) {
4588
4684
  const i = index ?? Math.floor(Math.random() * BANNER_TIPS.length);
4589
4685
  return BANNER_TIPS[(i % BANNER_TIPS.length + BANNER_TIPS.length) % BANNER_TIPS.length];
4590
4686
  }
4687
+ function greeting(opts, nameWidth) {
4688
+ if (!opts.name) return "Welcome to Clixad!";
4689
+ const name = nameWidth === void 0 ? opts.name : truncate(opts.name, nameWidth);
4690
+ return opts.newAccount ? `Welcome to Clixad, ${name}!` : `Welcome back, ${name}!`;
4691
+ }
4692
+ function balanceLabel(opts) {
4693
+ return opts.balance === void 0 ? "balance unavailable" : `${opts.balance.toLocaleString("en-US")} credits`;
4694
+ }
4591
4695
  function earnedLabel(opts) {
4592
4696
  return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
4593
4697
  }
@@ -4597,7 +4701,7 @@ function welcomeBanner(opts) {
4597
4701
  const rightW = Math.min(RIGHT_MAX, Math.max(26, inner - 30));
4598
4702
  const leftW = inner - rightW;
4599
4703
  const cwd = opts.cwd.replace(homedir6(), "~").replace(/\\/g, "/");
4600
- const welcome = opts.name ? `Welcome back, ${truncate(opts.name, 18)}!` : "Welcome to Clixad!";
4704
+ const welcome = greeting(opts, Math.max(1, Math.min(18, leftW - (opts.newAccount ? 20 : 15))));
4601
4705
  const duck = duckLines();
4602
4706
  const home = truncate(cwd, leftW);
4603
4707
  const hint = bannerTip(opts.tipIndex);
@@ -4623,8 +4727,10 @@ function welcomeBanner(opts) {
4623
4727
  tip("/wallet", "balance & ads"),
4624
4728
  cell("", 0),
4625
4729
  cell(BOLD + MANGO + "Wallet" + R2, 6),
4626
- cell(TEXT + `${opts.balance.toLocaleString("en-US")} credits` + R2, `${opts.balance.toLocaleString("en-US")} credits`.length),
4627
- cell(MUTED + earnedLabel(opts) + R2, earnedLabel(opts).length)
4730
+ cell(TEXT + balanceLabel(opts) + R2, balanceLabel(opts).length),
4731
+ // Without a wallet there is no earned figure either, and "$0.00/$0.00" is
4732
+ // two invented numbers rather than a statement of what is unknown.
4733
+ opts.balance === void 0 ? cell("", 0) : cell(MUTED + earnedLabel(opts) + R2, earnedLabel(opts).length)
4628
4734
  ];
4629
4735
  const rows = Math.max(left.length, right.length);
4630
4736
  const out = [];
@@ -4642,9 +4748,9 @@ function welcomeBanner(opts) {
4642
4748
  }
4643
4749
  function compactBanner(opts) {
4644
4750
  const width = Math.max(1, opts.width ?? MIN_BANNER_WIDTH);
4645
- const welcome = truncate(opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!", Math.max(1, width - 2));
4751
+ const welcome = truncate(greeting(opts), Math.max(1, width - 2));
4646
4752
  const hint = bannerTip(opts.tipIndex);
4647
- const balance = `${opts.balance.toLocaleString("en-US")} credits`;
4753
+ const balance = balanceLabel(opts);
4648
4754
  const room = width - (4 + stringWidth(hint.key) + 1 + 4 + stringWidth(balance));
4649
4755
  const desc = room >= 3 ? truncate(hint.desc, room) : "";
4650
4756
  const tipRow = desc ? ` ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}${desc}${R2} ${FAINT}\xB7${R2} ${balance}` : truncate(` ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}\xB7${R2} ${balance}`, width);
@@ -4778,6 +4884,34 @@ function readVersion() {
4778
4884
  return "unknown";
4779
4885
  }
4780
4886
  }
4887
+ function isNewerVersion(candidate, current) {
4888
+ const parse2 = (v) => {
4889
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v.trim());
4890
+ if (!m) return void 0;
4891
+ return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split(".") : [] };
4892
+ };
4893
+ const a = parse2(candidate);
4894
+ const b = parse2(current);
4895
+ if (!a || !b) return false;
4896
+ for (let i = 0; i < 3; i++) {
4897
+ if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i];
4898
+ }
4899
+ if (!a.pre.length || !b.pre.length) return !a.pre.length && b.pre.length > 0;
4900
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
4901
+ const x = a.pre[i];
4902
+ const y = b.pre[i];
4903
+ if (x === void 0) return false;
4904
+ if (y === void 0) return true;
4905
+ if (x === y) continue;
4906
+ const nx = /^\d+$/.test(x) ? Number(x) : void 0;
4907
+ const ny = /^\d+$/.test(y) ? Number(y) : void 0;
4908
+ if (nx !== void 0 && ny !== void 0) return nx > ny;
4909
+ if (nx !== void 0) return false;
4910
+ if (ny !== void 0) return true;
4911
+ return x > y;
4912
+ }
4913
+ return false;
4914
+ }
4781
4915
  var VERSION, VERSION_LABEL;
4782
4916
  var init_version = __esm({
4783
4917
  "src/version.ts"() {
@@ -5668,6 +5802,7 @@ function App({
5668
5802
  wallet,
5669
5803
  streak,
5670
5804
  session,
5805
+ newAccount,
5671
5806
  initialTask,
5672
5807
  notices: notices2,
5673
5808
  sponsorServe,
@@ -5690,8 +5825,11 @@ function App({
5690
5825
  text: (cols >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
5691
5826
  width: cols,
5692
5827
  name: config.login ?? config.email?.split("@")[0],
5828
+ ...newAccount ? { newAccount: true } : {},
5693
5829
  model: config.model,
5694
- balance: wallet?.balance ?? 0,
5830
+ // Undefined when the wallet could not be read: the banner says so
5831
+ // rather than drawing "0 credits", which reads as an empty account.
5832
+ balance: wallet?.balance,
5695
5833
  adsToday: wallet?.ads_today ?? 0,
5696
5834
  earnedUsdToday: wallet?.earned_usd_today ?? 0,
5697
5835
  maxRewardUsd: wallet?.max_reward_usd_per_day ?? 0,
@@ -5705,7 +5843,7 @@ function App({
5705
5843
  const [editor, setEditor] = useState(EMPTY);
5706
5844
  const [busy, setBusy] = useState(false);
5707
5845
  const [live, setLive] = useState(null);
5708
- const [balance, setBalance] = useState(wallet?.balance ?? 0);
5846
+ const [balance, setBalance] = useState(wallet?.balance);
5709
5847
  const [spent, setSpent] = useState(0);
5710
5848
  const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5711
5849
  const [model, setModel2] = useState(config.model);
@@ -5761,6 +5899,7 @@ function App({
5761
5899
  const pendingTaskRef = useRef(null);
5762
5900
  const pendingFreeRef = useRef([]);
5763
5901
  const pendingGrantRef = useRef(0);
5902
+ const pendingBalanceRef = useRef(void 0);
5764
5903
  const mcpRef = useRef(McpHub.empty());
5765
5904
  const todosRef = useRef(new TodoList());
5766
5905
  const lastTurnRef = useRef(null);
@@ -5862,11 +6001,32 @@ function App({
5862
6001
  deltaBufRef.current = "";
5863
6002
  }, []);
5864
6003
  useEffect(() => dropDelta, [dropDelta]);
6004
+ useEffect(() => {
6005
+ if (wallet) return;
6006
+ const ctl = new AbortController();
6007
+ void client.wallet(ctl.signal).then((w) => {
6008
+ setBalance((current) => current ?? w.balance);
6009
+ setFreeTier((current) => current ?? w.free_tier);
6010
+ }).catch(() => void 0);
6011
+ return () => ctl.abort();
6012
+ }, []);
5865
6013
  useEffect(() => {
5866
6014
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5867
6015
  for (const text2 of notices2 ?? []) push({ kind: "notice", tone: "warn", text: ` ${text2}` });
5868
6016
  const files = contextRef.current.files;
5869
6017
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
6018
+ if (!wallet) {
6019
+ let host = config.gatewayUrl;
6020
+ try {
6021
+ host = new URL(config.gatewayUrl).host;
6022
+ } catch {
6023
+ }
6024
+ push({
6025
+ kind: "notice",
6026
+ tone: "warn",
6027
+ text: ` ${host} did not answer, so the balance is unknown for now. It fills in once a request gets through.`
6028
+ });
6029
+ }
5870
6030
  const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
5871
6031
  if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
5872
6032
  const freeLine = freeTierNotice({ model: config.model, balance: wallet?.balance ?? 0, free: wallet?.free_tier });
@@ -5883,6 +6043,24 @@ function App({
5883
6043
  client.models().then((m) => {
5884
6044
  catalogRef.current = m;
5885
6045
  searchRef.current = client.capabilities?.web_search === true;
6046
+ const successor = client.retiredModels[modelRef.current];
6047
+ if (successor) {
6048
+ const from = modelRef.current;
6049
+ config.model = successor;
6050
+ saveConfig(config);
6051
+ switchModel(successor);
6052
+ push({
6053
+ kind: "notice",
6054
+ text: ` ${from} was retired; now using its successor, ${successor}. /model picks another.`
6055
+ });
6056
+ }
6057
+ const latest = client.latestCli;
6058
+ if (latest && isNewerVersion(latest, VERSION)) {
6059
+ push({
6060
+ kind: "notice",
6061
+ text: ` clixad ${latest} is out (you have ${VERSION}). Update with: npm install -g clixad@latest`
6062
+ });
6063
+ }
5886
6064
  }).catch(() => void 0);
5887
6065
  }, []);
5888
6066
  useEffect(() => {
@@ -6122,7 +6300,7 @@ function App({
6122
6300
  setSpent((s) => s + res.creditsCharged);
6123
6301
  push({
6124
6302
  kind: "notice",
6125
- text: ` compacted context: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
6303
+ text: ` compacted context: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged.toLocaleString("en-US")} credits)`
6126
6304
  });
6127
6305
  }
6128
6306
  }
@@ -6231,19 +6409,29 @@ ${NO_SEARCH_INSTRUCTION}`),
6231
6409
  pendingTaskRef.current = { task, images };
6232
6410
  pendingFreeRef.current = err.freeModels;
6233
6411
  pendingGrantRef.current = err.grantPerAd;
6412
+ pendingBalanceRef.current = err.balance;
6234
6413
  const free = err.freeModels[0];
6235
6414
  push({
6236
6415
  kind: "notice",
6237
6416
  tone: "warn",
6238
- text: free ? ` Out of credits on ${model2} (balance ${err.balance}, need ~${err.estimatedCost}).
6417
+ text: free ? ` ${shortfallHeadline(err, model2)}
6239
6418
  Press enter \u2014 ${free.id} is still free today (~${free.turnsLeft} turn${free.turnsLeft === 1 ? "" : "s"} left), or take an offer instead.
6240
6419
  Your request is kept either way, so you will not have to type it again.
6241
6420
  Offers you are screened out of pay nothing; that is normal, just start another.
6242
- /buy is there too, if you would rather pay than do an offer.` : ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
6421
+ /buy is there too, if you would rather pay than do an offer.` : ` ${shortfallHeadline(err, model2)}
6243
6422
  Press enter to open the ad wall \u2014 offers pay ${grantLabel(err.grantPerAd)}.
6244
6423
  I'll continue automatically. Offers you are screened out of pay nothing;
6245
6424
  that is normal, just start another.
6246
- /buy is there too, if you would rather pay than do an offer.`
6425
+ ` + // A wallet that is short rather than empty has a third way on,
6426
+ // and it is the cheapest: the same credits on a smaller model.
6427
+ // It names the command and no figure, since what a model costs
6428
+ // is the gateway's to say and `/model` says it.
6429
+ (err.balance > 0 ? ` Or pick a cheaper model with /model, which shows what each one costs.
6430
+ ` : "") + // One clause, last, and it stays last. The wall is how this product
6431
+ // is meant to be paid for and the offer is free; naming /buy here
6432
+ // is discoverability rather than a pitch, and it is the moment
6433
+ // somebody actually wants to know the command exists.
6434
+ ` /buy is there too, if you would rather pay than do an offer.`
6247
6435
  });
6248
6436
  } else if (err instanceof AuthError) {
6249
6437
  push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
@@ -6370,7 +6558,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6370
6558
  const runAdWall = useCallback(async () => {
6371
6559
  const pending = pendingTaskRef.current;
6372
6560
  pendingTaskRef.current = null;
6373
- const before = balance;
6561
+ let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
6374
6562
  const code = await client.handoffCode();
6375
6563
  const base = config.dashboardUrl.replace(/\/+$/, "");
6376
6564
  openBrowser(code ? `${base}/?c=${encodeURIComponent(code)}` : base);
@@ -6388,7 +6576,10 @@ ${NO_SEARCH_INSTRUCTION}`),
6388
6576
  await sleep4(3e3, signal);
6389
6577
  if (signal.aborted) break;
6390
6578
  const w = await client.wallet(signal).catch(() => void 0);
6391
- if (w && w.balance > before) {
6579
+ if (w && before === void 0) {
6580
+ before = w.balance;
6581
+ setBalance(w.balance);
6582
+ } else if (w && before !== void 0 && w.balance > before) {
6392
6583
  setBalance(w.balance);
6393
6584
  credited = true;
6394
6585
  }
@@ -6431,7 +6622,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6431
6622
  ];
6432
6623
  setPickerSel(0);
6433
6624
  setPicker({
6434
- title: "Out of credits",
6625
+ title: (pendingBalanceRef.current ?? 0) > 0 ? "Not enough credits" : "Out of credits",
6435
6626
  subtitle: "Pick a free model to carry on with, or earn credits at the wall.",
6436
6627
  items,
6437
6628
  onPick: (choice) => {
@@ -6482,7 +6673,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6482
6673
  }, [client, push, runBusy]);
6483
6674
  const runCheckout = useCallback(
6484
6675
  async (pack) => {
6485
- const before = balance;
6676
+ let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
6486
6677
  let credited = false;
6487
6678
  let stopped = false;
6488
6679
  await runBusy(WAITING_FOR_PAYMENT, async (signal) => {
@@ -6506,7 +6697,10 @@ ${NO_SEARCH_INSTRUCTION}`),
6506
6697
  await sleep4(3e3, signal);
6507
6698
  if (signal.aborted) break;
6508
6699
  const w = await client.wallet(signal).catch(() => void 0);
6509
- if (w && w.balance > before) {
6700
+ if (w && before === void 0) {
6701
+ before = w.balance;
6702
+ setBalance(w.balance);
6703
+ } else if (w && before !== void 0 && w.balance > before) {
6510
6704
  setBalance(w.balance);
6511
6705
  credited = true;
6512
6706
  }
@@ -6589,7 +6783,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6589
6783
  setSpent((s) => s + res.creditsCharged);
6590
6784
  push({
6591
6785
  kind: "notice",
6592
- text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
6786
+ text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged.toLocaleString("en-US")} credits)`
6593
6787
  });
6594
6788
  });
6595
6789
  return;
@@ -7299,7 +7493,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7299
7493
  let estimate;
7300
7494
  const info = catalogRef.current.find((m) => m.id === model);
7301
7495
  const shape = client.taskShape;
7302
- if (!busy && info && shape && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7496
+ if (!busy && info && shape && balance !== void 0 && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7303
7497
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7304
7498
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
7305
7499
  const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
@@ -7489,7 +7683,7 @@ function modelHint(m, free) {
7489
7683
  function turnMeta(result) {
7490
7684
  const n = (v) => v.toLocaleString("en-US");
7491
7685
  const balance = `balance ${n(result.balance)}`;
7492
- if (result.creditsWaived <= 0) return `[${result.creditsCharged} credits \xB7 ${balance}]`;
7686
+ if (result.creditsWaived <= 0) return `[${n(result.creditsCharged)} credits \xB7 ${balance}]`;
7493
7687
  const left = result.freeCreditsLeft === void 0 ? "" : ` \xB7 ${n(result.freeCreditsLeft)} free credits left`;
7494
7688
  if (result.creditsCharged > 0) {
7495
7689
  return `[free: ${n(result.creditsWaived)} credits \xB7 then ${n(result.creditsCharged)} charged \xB7 ${balance}]`;
@@ -7508,7 +7702,12 @@ function statusRow(o, estimate, cols) {
7508
7702
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
7509
7703
  const clauses = [
7510
7704
  { text: o.model, rank: KEEP_ALWAYS },
7511
- { text: `${o.balance.toLocaleString("en-US")} cr`, rank: KEEP_ALWAYS },
7705
+ {
7706
+ // "? cr" rather than "0 cr": unknown is not empty, and an empty-looking
7707
+ // wallet is the one misreading on this row that frightens somebody.
7708
+ text: o.balance === void 0 ? "? cr" : `${o.balance.toLocaleString("en-US")} cr`,
7709
+ rank: KEEP_ALWAYS
7710
+ },
7512
7711
  // Above the context warning, below the balance. It is the second number
7513
7712
  // deciding whether the next turn costs anything, and unlike the spend it is
7514
7713
  // *news* — it only exists while a free model is selected and it counts down.
@@ -7746,7 +7945,7 @@ async function main() {
7746
7945
  for (const text of configNotices) console.error(c.yellow(text));
7747
7946
  switch (cmd) {
7748
7947
  case "login":
7749
- return login(client, config, rest);
7948
+ return void await login(client, config, rest, { nextStep: true });
7750
7949
  case "logout":
7751
7950
  return logout(config);
7752
7951
  case "whoami":
@@ -7810,7 +8009,7 @@ async function main() {
7810
8009
  return repl(client, config, { task: [cmd, ...rest].join(" ").trim(), notices: configNotices });
7811
8010
  }
7812
8011
  }
7813
- async function login(client, config, args) {
8012
+ async function login(client, config, args, opts = {}) {
7814
8013
  const parsed = parseLoginArgs(args);
7815
8014
  for (const line2 of TERMS_NOTICE.split("\n")) console.log(c.dim(` ${line2}`));
7816
8015
  const outcome = await performLogin(client, config, parsed, {
@@ -7836,20 +8035,25 @@ async function login(client, config, args) {
7836
8035
  if (account.model) console.log(` model: ${c.bold(account.model)} ${c.dim("(the default for new accounts)")}`);
7837
8036
  await showStreak(client);
7838
8037
  console.log(c.dim(` token stored in ${configPath()}`));
7839
- return;
8038
+ if (opts.nextStep) {
8039
+ console.log(`
8040
+ Next: open a project folder and run ${c.cyan("clixad")} to start coding.`);
8041
+ }
8042
+ return { created: account.created };
7840
8043
  }
7841
8044
  case "closed":
7842
8045
  reportSignupClosed(outcome.message, outcome.hadInvite);
7843
8046
  process.exitCode = 1;
7844
- return;
8047
+ return void 0;
7845
8048
  case "timeout":
7846
8049
  console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
7847
- return;
8050
+ return void 0;
7848
8051
  case "aborted":
7849
- return;
8052
+ return void 0;
7850
8053
  default:
7851
8054
  console.log(c.red(`
7852
8055
  login failed: ${outcome.error}`));
8056
+ return void 0;
7853
8057
  }
7854
8058
  }
7855
8059
  function reportSignupClosed(message, hadInvite) {
@@ -8051,7 +8255,7 @@ ${NO_SEARCH_INSTRUCTION}`),
8051
8255
  } catch (err) {
8052
8256
  if (err instanceof PaywallError) {
8053
8257
  console.log(c.yellow(`
8054
- Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8258
+ ${shortfallHeadline(err, config.model)}`));
8055
8259
  const freeLine = freeAlternativeLine(err.freeModels);
8056
8260
  if (freeLine) console.log(c.green(freeLine));
8057
8261
  console.log(
@@ -8102,7 +8306,7 @@ async function codeCmd(client, config, rest) {
8102
8306
  const w = await safeWallet(client);
8103
8307
  if (w) {
8104
8308
  console.log(
8105
- c.dim(` wallet: ${w.balance} credits \xB7 model ${config.model} \xB7 gateway ${config.gatewayUrl}`)
8309
+ c.dim(` wallet: ${credits(w.balance)} credits \xB7 model ${config.model} \xB7 gateway ${config.gatewayUrl}`)
8106
8310
  );
8107
8311
  if (w.balance <= 0) {
8108
8312
  console.log(c.yellow(" You're out of credits \u2014 run `clixad earn` first, or the agent will hit a paywall."));
@@ -8137,8 +8341,9 @@ async function repl(client, config, opts = {}) {
8137
8341
  console.log(c.yellow('Interactive mode needs a terminal. Use `clixad -p "..."` for scripted runs.'));
8138
8342
  return;
8139
8343
  }
8344
+ let newAccount = false;
8140
8345
  if (!config.token) {
8141
- await login(client, config, []);
8346
+ newAccount = Boolean((await login(client, config, []))?.created);
8142
8347
  if (!config.token) return;
8143
8348
  }
8144
8349
  let session;
@@ -8149,8 +8354,7 @@ async function repl(client, config, opts = {}) {
8149
8354
  session = loadSession(opts.resume);
8150
8355
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
8151
8356
  }
8152
- const streak = await client.claimStreak();
8153
- const wallet = await safeWallet(client);
8357
+ const { streak, wallet } = await startupReads(client, config.gatewayUrl);
8154
8358
  const cwd = process.cwd();
8155
8359
  const initHint = shouldSuggestInit(cwd) ? { returning: hasSessionIn(cwd) } : void 0;
8156
8360
  clearScreen();
@@ -8161,24 +8365,30 @@ async function repl(client, config, opts = {}) {
8161
8365
  wallet,
8162
8366
  streak,
8163
8367
  session,
8368
+ newAccount,
8164
8369
  initialTask: opts.task,
8165
8370
  ...opts.notices?.length ? { notices: opts.notices } : {},
8166
8371
  ...initHint ? { initHint } : {}
8167
8372
  });
8168
8373
  }
8169
8374
  async function runTurn(client, config, messages) {
8375
+ let started = false;
8170
8376
  try {
8171
- process.stdout.write(c.green("assistant ") + "");
8172
8377
  const result = await client.chatStream(messages, config.model, {
8173
- onDelta: (chunk) => process.stdout.write(chunk)
8378
+ onDelta: (chunk) => {
8379
+ if (!started) process.stdout.write(c.green("assistant "));
8380
+ started = true;
8381
+ process.stdout.write(chunk);
8382
+ }
8174
8383
  });
8384
+ if (!started) process.stdout.write(c.green("assistant "));
8175
8385
  process.stdout.write("\n");
8176
- console.log(c.dim(` [${result.creditsCharged} credits \xB7 balance ${result.balance}]`));
8386
+ console.log(c.dim(` [${credits(result.creditsCharged)} credits \xB7 balance ${credits(result.balance)}]`));
8177
8387
  return result;
8178
8388
  } catch (err) {
8179
- process.stdout.write("\n");
8389
+ if (started) process.stdout.write("\n");
8180
8390
  if (err instanceof PaywallError) {
8181
- console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8391
+ console.log(c.yellow(shortfallHeadline(err, config.model)));
8182
8392
  const freeLine = freeAlternativeLine(err.freeModels);
8183
8393
  if (freeLine) console.log(c.green(freeLine));
8184
8394
  console.log(
@@ -8208,29 +8418,69 @@ async function safeWallet(client) {
8208
8418
  return void 0;
8209
8419
  }
8210
8420
  }
8421
+ var CONNECTING_AFTER_MS = 700;
8422
+ var WAKING_AFTER_MS = 6e3;
8423
+ async function startupReads(client, gatewayUrl) {
8424
+ const host = (() => {
8425
+ try {
8426
+ return new URL(gatewayUrl).host;
8427
+ } catch {
8428
+ return gatewayUrl;
8429
+ }
8430
+ })();
8431
+ const say = (text) => {
8432
+ if (process.stdout.isTTY) console.log(c.dim(text));
8433
+ };
8434
+ const timers = [
8435
+ setTimeout(() => say(` connecting to ${host}\u2026`), CONNECTING_AFTER_MS),
8436
+ setTimeout(
8437
+ () => say(" still waiting: the server sleeps after a quiet spell and takes up to a minute to wake. ctrl+c cancels."),
8438
+ WAKING_AFTER_MS
8439
+ )
8440
+ ];
8441
+ try {
8442
+ const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client)]);
8443
+ const balance = streak?.claimed ? streak.balance : wallet?.balance;
8444
+ return {
8445
+ streak,
8446
+ wallet: wallet && balance !== void 0 ? { ...wallet, balance } : wallet
8447
+ };
8448
+ } finally {
8449
+ for (const t of timers) clearTimeout(t);
8450
+ }
8451
+ }
8211
8452
  function printHelp() {
8212
- console.log(`${c.bold("clixad")} \u2014 free AI coding in your terminal, funded by ads
8453
+ const rows = [
8454
+ ["", "", "start the interactive coding agent here (signs you in if needed)"],
8455
+ ['"<task>"', "", "the same, with a task already running"],
8456
+ ["--continue", "", "resume the last session in this directory"],
8457
+ ["--resume", "[id]", "list saved sessions, or resume one"],
8458
+ ["-p", '"<task>"', "headless agent run (read-only tools)"],
8459
+ ["ask", '"<prompt>"', "one-shot answer, no tools"],
8460
+ ["models", "", "models with credit prices, and which are free today"],
8461
+ ["model", "<id>", "set the default model"],
8462
+ ["wallet", "", "balance, offers today, recent activity"],
8463
+ ["earn", "", "open the offer wall to earn credits"],
8464
+ ["refer", "", "your invite code, and what it has earned"],
8465
+ ["redeem", "<code>", "spend a code you were given"],
8466
+ ["login", "[email]", "sign in or create an account (--invite <code> if you have one)"],
8467
+ ["logout", "", "forget the stored token"],
8468
+ ["whoami", "", "account and balance"],
8469
+ ["code", '["<task>"]', "launch the Kimi CLI coding agent on this directory"],
8470
+ ["--version", "", "print the version and exit"]
8471
+ ];
8472
+ const plain = (name, arg) => `clixad${name ? ` ${name}` : ""}${arg ? ` ${arg}` : ""}`;
8473
+ const width = Math.max(...rows.map(([name, arg]) => plain(name, arg).length)) + 2;
8474
+ const lines = rows.map(([name, arg, desc]) => {
8475
+ const shown = `${c.cyan("clixad")}${name ? ` ${c.cyan(name)}` : ""}${arg ? ` ${arg}` : ""}`;
8476
+ return ` ${shown}${" ".repeat(width - plain(name, arg).length)}${desc}`;
8477
+ });
8478
+ console.log(`${c.bold("clixad")}: free AI coding in your terminal, funded by ads
8213
8479
 
8214
- ${c.cyan("login")} [email] create/attach an account (${c.dim("--invite <code>")} if you have one)
8215
- ${c.cyan("logout")} forget the stored token
8216
- ${c.cyan("whoami")} show account + balance
8217
- ${c.cyan("models")} list models with credit prices, and which are free today
8218
- ${c.cyan("model")} <id> set default model
8219
- ${c.cyan("wallet")} balance, ads today, ledger
8220
- ${c.cyan("earn")} open the ad wall to earn credits
8221
- ${c.cyan("refer")} your invite code, and what it has earned
8222
- ${c.cyan("redeem")} <code> spend a code you were given
8223
- ${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
8224
- ${c.cyan("agent")} "<task>" open the REPL with a task already running
8225
- ${c.cyan("-p")} "<task>" headless agent run (read-only tools)
8226
- ${c.cyan("code")} ["<task>"] launch the full Kimi CLI coding agent on the current dir
8227
- ${c.cyan("--continue")} resume the last session in this directory
8228
- ${c.cyan("--resume")} [id] list saved sessions, or resume one
8229
- ${c.cyan("--version")} print the version and exit
8230
- (no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
8231
- "<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task
8480
+ ${lines.join("\n")}
8232
8481
 
8233
- ${c.dim("Discord: https://discord.gg/pwxPM3QyF \u2014 bugs and feature requests are read there")}`);
8482
+ Inside the agent, ${c.cyan("/help")} lists everything else, including ${c.cyan("/buy")} for credit packs.
8483
+ ${c.dim("Discord: https://discord.gg/pwxPM3QyF (bugs and feature requests are read there)")}`);
8234
8484
  }
8235
8485
  main().catch((err) => {
8236
8486
  console.error(c.red(err.message));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.22",
3
+ "version": "0.0.1-beta.23",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",