clixad 0.0.1-beta.22 → 0.0.1-beta.24

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 +417 -97
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -327,6 +327,13 @@ function extraFormatIds(raw) {
327
327
  function imagePart(image) {
328
328
  return { type: "image_url", image_url: { url: `data:${image.mimeType};base64,${image.data}` } };
329
329
  }
330
+ function withoutReasoning(messages) {
331
+ return messages.map((message) => {
332
+ if (!message.reasoning_details) return message;
333
+ const { reasoning_details: _dropped, ...rest } = message;
334
+ return rest;
335
+ });
336
+ }
330
337
  function messageText(message) {
331
338
  const content = message.content;
332
339
  if (typeof content === "string") return content;
@@ -350,12 +357,48 @@ function asSignupClosed(status, data) {
350
357
  const d = data;
351
358
  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
359
  }
360
+ function shortfallHeadline(err, model) {
361
+ const n = (v) => Math.max(0, Math.round(v)).toLocaleString("en-US");
362
+ if (err.balance <= 0) return `Out of credits (this needs about ${n(err.estimatedCost)}).`;
363
+ return `Not enough credits for this turn${model ? ` on ${model}` : ""}: it needs about ${n(err.estimatedCost)}, and the balance is ${n(err.balance)}.`;
364
+ }
353
365
  function paywallError(body) {
354
366
  const free = Array.isArray(body.free_models) ? body.free_models.filter(
355
367
  (m) => !!m && typeof m.id === "string" && m.id.length > 0 && Number.isFinite(m.turns_left)
356
368
  ).map((m) => ({ id: m.id, turnsLeft: Math.max(0, Math.floor(m.turns_left)) })).filter((m) => m.turnsLeft > 0) : [];
357
369
  return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, free);
358
370
  }
371
+ function neverConnected(code) {
372
+ return code !== void 0 && NEVER_CONNECTED.has(code);
373
+ }
374
+ function networkCode(err) {
375
+ let cause = err?.cause;
376
+ for (let depth = 0; depth < 3 && cause; depth++) {
377
+ const code = cause.code;
378
+ if (typeof code === "string" && code) return code;
379
+ const inner = cause.errors;
380
+ if (Array.isArray(inner)) {
381
+ const first = inner.find((e) => typeof e?.code === "string");
382
+ if (first) return first.code;
383
+ }
384
+ cause = cause.cause;
385
+ }
386
+ return void 0;
387
+ }
388
+ async function gatewayFetch(url, init) {
389
+ try {
390
+ return await fetch(url, init);
391
+ } catch (err) {
392
+ const aborted = init?.signal?.aborted || err?.name === "AbortError";
393
+ if (aborted || !(err instanceof TypeError)) throw err;
394
+ let host = String(url);
395
+ try {
396
+ host = new URL(url).host;
397
+ } catch {
398
+ }
399
+ throw new GatewayUnreachableError(host, networkCode(err));
400
+ }
401
+ }
359
402
  async function authFailure(res, what) {
360
403
  const body = await res.text().catch(() => "");
361
404
  const isJson = (res.headers.get("content-type") ?? "").includes("json");
@@ -411,6 +454,26 @@ function accumulateToolCall(calls, part, fallbackIndex) {
411
454
  if (part.function?.arguments) current.function.arguments += part.function.arguments;
412
455
  calls.set(index, current);
413
456
  }
457
+ function mergeReasoningDetails(acc, delta) {
458
+ if (!Array.isArray(delta)) return;
459
+ for (const piece of delta) {
460
+ if (!piece || typeof piece !== "object") continue;
461
+ const part = piece;
462
+ const existing = typeof part.index === "number" ? acc.find((block) => block.index === part.index) : void 0;
463
+ if (!existing) {
464
+ acc.push({ ...part });
465
+ continue;
466
+ }
467
+ for (const [key, value] of Object.entries(part)) {
468
+ if (value === null || value === void 0) continue;
469
+ if ((key === "text" || key === "summary" || key === "data") && typeof value === "string") {
470
+ existing[key] = `${typeof existing[key] === "string" ? existing[key] : ""}${value}`;
471
+ } else {
472
+ existing[key] = value;
473
+ }
474
+ }
475
+ }
476
+ }
414
477
  async function* parseSSE(body) {
415
478
  const reader = body.getReader();
416
479
  const decoder = new TextDecoder();
@@ -429,7 +492,7 @@ async function* parseSSE(body) {
429
492
  }
430
493
  }
431
494
  }
432
- var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, AuthError, GatewayClient;
495
+ var CHAT_ENCODED_CONTENT_TYPE, LEDGER_REASONS, SignupClosedError, PaywallError, GatewayUnreachableError, NEVER_CONNECTED, AuthError, GatewayClient;
433
496
  var init_client = __esm({
434
497
  "src/client.ts"() {
435
498
  "use strict";
@@ -478,6 +541,33 @@ var init_client = __esm({
478
541
  grantPerAd;
479
542
  freeModels;
480
543
  };
544
+ GatewayUnreachableError = class extends Error {
545
+ constructor(host, code) {
546
+ const where = `${host}${code ? ` (${code})` : ""}`;
547
+ super(
548
+ 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.`
549
+ );
550
+ this.host = host;
551
+ this.code = code;
552
+ this.name = "GatewayUnreachableError";
553
+ }
554
+ host;
555
+ code;
556
+ };
557
+ NEVER_CONNECTED = /* @__PURE__ */ new Set([
558
+ "ENOTFOUND",
559
+ "EAI_AGAIN",
560
+ "ECONNREFUSED",
561
+ "ENETUNREACH",
562
+ "EHOSTUNREACH",
563
+ "EADDRNOTAVAIL",
564
+ "UND_ERR_CONNECT_TIMEOUT",
565
+ "CERT_HAS_EXPIRED",
566
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
567
+ "SELF_SIGNED_CERT_IN_CHAIN",
568
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
569
+ "ERR_TLS_CERT_ALTNAME_INVALID"
570
+ ]);
481
571
  AuthError = class extends Error {
482
572
  constructor() {
483
573
  super("Not logged in. Run `clixad login` first.");
@@ -518,7 +608,7 @@ var init_client = __esm({
518
608
  }
519
609
  /** Dev-only signup shortcut (production replaces this with GitHub OAuth). */
520
610
  async signupDev(email, invite) {
521
- const res = await fetch(`${this.config.gatewayUrl}/dev/users`, {
611
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/dev/users`, {
522
612
  method: "POST",
523
613
  headers: this.headers(false),
524
614
  body: JSON.stringify({ ...email ? { email } : {}, ...invite ? { invite } : {} })
@@ -534,7 +624,7 @@ var init_client = __esm({
534
624
  /** Begin GitHub device-flow login. Returns null if the gateway has no GitHub
535
625
  * configured (501), so the caller can fall back to the dev shortcut. */
536
626
  async deviceStart() {
537
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
627
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
538
628
  method: "POST",
539
629
  headers: this.headers(false)
540
630
  });
@@ -544,7 +634,7 @@ var init_client = __esm({
544
634
  }
545
635
  /** Poll once for device-flow completion. `invite` unlocks a gated signup. */
546
636
  async devicePoll(session, invite) {
547
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
637
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
548
638
  method: "POST",
549
639
  headers: this.headers(false),
550
640
  body: JSON.stringify(invite ? { session, invite } : { session })
@@ -566,7 +656,7 @@ var init_client = __esm({
566
656
  */
567
657
  async handoffCode() {
568
658
  try {
569
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
659
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
570
660
  method: "POST",
571
661
  headers: this.headers()
572
662
  });
@@ -593,7 +683,7 @@ var init_client = __esm({
593
683
  */
594
684
  async claimStreak(signal) {
595
685
  try {
596
- const res = await fetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
686
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
597
687
  method: "POST",
598
688
  headers: this.headers(),
599
689
  ...signal ? { signal } : {}
@@ -628,7 +718,7 @@ var init_client = __esm({
628
718
  */
629
719
  async sendFeedback(report, signal) {
630
720
  try {
631
- const res = await fetch(`${this.config.gatewayUrl}/v1/feedback`, {
721
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/feedback`, {
632
722
  method: "POST",
633
723
  headers: this.headers(),
634
724
  ...signal ? { signal } : {},
@@ -663,7 +753,7 @@ var init_client = __esm({
663
753
  * that did not run, and saying so beats printing an empty summary.
664
754
  */
665
755
  async referral(signal) {
666
- const res = await fetch(`${this.config.gatewayUrl}/v1/referrals`, {
756
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/referrals`, {
667
757
  headers: this.headers(),
668
758
  ...signal ? { signal } : {}
669
759
  });
@@ -694,7 +784,7 @@ var init_client = __esm({
694
784
  * 200 with a status, because that is an answer rather than a failure.
695
785
  */
696
786
  async redeem(code, signal) {
697
- const res = await fetch(`${this.config.gatewayUrl}/v1/redeem`, {
787
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/redeem`, {
698
788
  method: "POST",
699
789
  headers: this.headers(),
700
790
  ...signal ? { signal } : {},
@@ -715,7 +805,7 @@ var init_client = __esm({
715
805
  * them with the input box locked and "esc to stop" on screen.
716
806
  */
717
807
  async creditPacks(signal) {
718
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
808
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
719
809
  if (!res.ok) throw new Error(`packs failed: ${res.status}`);
720
810
  return res.json();
721
811
  }
@@ -726,7 +816,7 @@ var init_client = __esm({
726
816
  * could not deliver instantly is worse than not starting one.
727
817
  */
728
818
  async checkout(pack, consent, signal) {
729
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
819
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
730
820
  method: "POST",
731
821
  headers: this.headers(),
732
822
  ...signal ? { signal } : {},
@@ -742,13 +832,37 @@ var init_client = __esm({
742
832
  // the better part of a minute) would otherwise be indistinguishable from a
743
833
  // hung terminal. Nothing else passes one — a one-shot CLI command has Ctrl+C.
744
834
  async models(signal) {
745
- const res = await fetch(`${this.config.gatewayUrl}/v1/models`, { signal });
835
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/models`, { signal });
746
836
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
747
837
  const data = await res.json();
748
838
  this.seen = { web_search: data.capabilities?.web_search === true };
749
839
  this.shape = parseTaskShape(data.task_shape);
840
+ this.latest = typeof data.cli_latest === "string" ? data.cli_latest : void 0;
841
+ const live = new Set(data.data.map((m) => m.id));
842
+ this.retired = {};
843
+ if (data.retired_models && typeof data.retired_models === "object") {
844
+ for (const [from, to] of Object.entries(data.retired_models)) {
845
+ if (typeof to === "string" && live.has(to) && !live.has(from)) this.retired[from] = to;
846
+ }
847
+ }
750
848
  return data.data;
751
849
  }
850
+ /**
851
+ * Model ids that were retired, and the live model that serves each one, as of
852
+ * the last catalog fetch. Empty from a gateway that does not send the map.
853
+ */
854
+ get retiredModels() {
855
+ return this.retired;
856
+ }
857
+ retired = {};
858
+ /**
859
+ * The newest `clixad` published to npm, as the gateway last read it, or
860
+ * undefined when it did not say. Undefined means unknown, never up to date.
861
+ */
862
+ get latestCli() {
863
+ return this.latest;
864
+ }
865
+ latest;
752
866
  /**
753
867
  * The task shape the cost preview assumes, as of the last catalog fetch, or
754
868
  * undefined when the gateway sent none (or one that did not parse). Undefined
@@ -773,7 +887,7 @@ var init_client = __esm({
773
887
  }
774
888
  seen = { web_search: false };
775
889
  async wallet(signal) {
776
- const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
890
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
777
891
  if (res.status === 401) throw await authFailure(res, "wallet");
778
892
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
779
893
  return res.json();
@@ -809,7 +923,7 @@ var init_client = __esm({
809
923
  */
810
924
  async sponsorConfig(signal) {
811
925
  try {
812
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
926
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
813
927
  headers: this.headers(),
814
928
  ...signal ? { signal } : {}
815
929
  });
@@ -847,7 +961,7 @@ var init_client = __esm({
847
961
  */
848
962
  async reportImpression(report, signal) {
849
963
  try {
850
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
964
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
851
965
  method: "POST",
852
966
  headers: this.headers(),
853
967
  ...signal ? { signal } : {},
@@ -880,7 +994,7 @@ var init_client = __esm({
880
994
  const url = new URL(`${this.config.gatewayUrl}/v1/web/search`);
881
995
  url.searchParams.set("q", query);
882
996
  if (opts.count) url.searchParams.set("count", String(opts.count));
883
- const res = await fetch(url, {
997
+ const res = await gatewayFetch(url, {
884
998
  headers: this.headers(),
885
999
  ...opts.signal ? { signal: opts.signal } : {}
886
1000
  });
@@ -920,7 +1034,7 @@ var init_client = __esm({
920
1034
  /** One POST to the chat route, in whichever transport is currently in use. */
921
1035
  sendChat(payload, encoded, signal) {
922
1036
  const json = JSON.stringify(payload);
923
- return fetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
1037
+ return gatewayFetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
924
1038
  method: "POST",
925
1039
  headers: encoded ? { ...this.headers(), "Content-Type": CHAT_ENCODED_CONTENT_TYPE } : this.headers(),
926
1040
  body: encoded ? encodeChatBody(json) : json,
@@ -1063,6 +1177,7 @@ var init_client = __esm({
1063
1177
  let balance = 0;
1064
1178
  let streamError;
1065
1179
  const calls = /* @__PURE__ */ new Map();
1180
+ const reasoning = [];
1066
1181
  for await (const evt of parseSSE(res.body)) {
1067
1182
  if (evt === "[DONE]") break;
1068
1183
  let json;
@@ -1080,6 +1195,7 @@ var init_client = __esm({
1080
1195
  for (const [index, part] of (choice?.delta?.tool_calls ?? []).entries()) {
1081
1196
  accumulateToolCall(calls, part, index);
1082
1197
  }
1198
+ mergeReasoningDetails(reasoning, choice?.delta?.reasoning_details);
1083
1199
  if (choice?.finish_reason) finishReason = choice.finish_reason;
1084
1200
  if (json.error) streamError = json.error.message || "the model stream failed";
1085
1201
  if (json.clixad) {
@@ -1092,6 +1208,7 @@ var init_client = __esm({
1092
1208
  const message = { role: "assistant", content: content || null };
1093
1209
  if (calls.size > 0) {
1094
1210
  message.tool_calls = [...calls.entries()].sort(([a], [b]) => a - b).map(([, c2]) => c2);
1211
+ if (reasoning.length) message.reasoning_details = reasoning;
1095
1212
  }
1096
1213
  return { message, finishReason, creditsCharged, balance, ...free };
1097
1214
  }
@@ -3973,6 +4090,19 @@ ${NO_SEARCH_INSTRUCTION}`,
3973
4090
 
3974
4091
  (the sub-agent hit its step limit \u2014 this answer may be incomplete)` : answer;
3975
4092
  }
4093
+ function emitUsage(turn) {
4094
+ emit({
4095
+ type: "usage",
4096
+ creditsCharged: turn.creditsCharged,
4097
+ balance: turn.balance,
4098
+ ...turn.free ? {
4099
+ free: true,
4100
+ creditsWaived: turn.creditsWaived ?? 0,
4101
+ freeCreditsLeft: turn.freeCreditsLeft ?? 0
4102
+ } : {}
4103
+ });
4104
+ }
4105
+ let retriedEmpty = false;
3976
4106
  for (let step = 0; step < maxSteps; step++) {
3977
4107
  if (opts.signal?.aborted) return stop("aborted");
3978
4108
  let turn;
@@ -3990,23 +4120,27 @@ ${NO_SEARCH_INSTRUCTION}`,
3990
4120
  creditsWaived += turn.creditsWaived ?? 0;
3991
4121
  if (turn.freeCreditsLeft !== void 0) freeCreditsLeft = turn.freeCreditsLeft;
3992
4122
  balance = turn.balance;
3993
- emit({
3994
- type: "usage",
3995
- creditsCharged: turn.creditsCharged,
3996
- balance: turn.balance,
3997
- ...turn.free ? {
3998
- free: true,
3999
- creditsWaived: turn.creditsWaived ?? 0,
4000
- freeCreditsLeft: turn.freeCreditsLeft ?? 0
4001
- } : {}
4002
- });
4003
- messages.push(turn.message);
4123
+ emitUsage(turn);
4004
4124
  const content = messageText(turn.message);
4005
- if (content) lastText = content;
4006
4125
  const calls = turn.message.tool_calls ?? [];
4126
+ if (calls.length === 0 && !content.trim() && !retriedEmpty) {
4127
+ retriedEmpty = true;
4128
+ continue;
4129
+ }
4130
+ retriedEmpty = false;
4131
+ messages.push(turn.message);
4132
+ if (content) lastText = content;
4007
4133
  if (calls.length === 0) {
4008
4134
  emit({ type: "final", content, creditsCharged, balance });
4009
- return { content, messages, creditsCharged, balance, creditsWaived, freeCreditsLeft, steps: step + 1 };
4135
+ return {
4136
+ content,
4137
+ messages: withoutReasoning(messages),
4138
+ creditsCharged,
4139
+ balance,
4140
+ creditsWaived,
4141
+ freeCreditsLeft,
4142
+ steps: step + 1
4143
+ };
4010
4144
  }
4011
4145
  if (content) emit({ type: "message", content });
4012
4146
  pendingImages = [];
@@ -4030,7 +4164,7 @@ ${NO_SEARCH_INSTRUCTION}`,
4030
4164
  emit({ type: "max_steps" });
4031
4165
  return {
4032
4166
  content: lastText || "(stopped: reached max steps)",
4033
- messages,
4167
+ messages: withoutReasoning(messages),
4034
4168
  creditsCharged,
4035
4169
  balance,
4036
4170
  creditsWaived,
@@ -4040,7 +4174,16 @@ ${NO_SEARCH_INSTRUCTION}`,
4040
4174
  };
4041
4175
  function stop(reason) {
4042
4176
  emit({ type: reason });
4043
- return { content: lastText, messages, creditsCharged, balance, creditsWaived, freeCreditsLeft, steps: 0, stopped: reason };
4177
+ return {
4178
+ content: lastText,
4179
+ messages: withoutReasoning(messages),
4180
+ creditsCharged,
4181
+ balance,
4182
+ creditsWaived,
4183
+ freeCreditsLeft,
4184
+ steps: 0,
4185
+ stopped: reason
4186
+ };
4044
4187
  }
4045
4188
  }
4046
4189
  function toolMessage(call, content) {
@@ -4143,7 +4286,7 @@ var init_agent = __esm({
4143
4286
  init_mcp();
4144
4287
  init_client();
4145
4288
  SUBAGENT_MAX_STEPS = 12;
4146
- SYSTEM_PROMPT = "You are Clixad, a terminal coding agent. You can read, search, write and edit files and run shell commands, all confined to the user's workspace. Find your way around with glob and grep before reading whole files. Prefer edit_file over write_file for existing files. Make minimal correct edits, verify them when a test or build command is available, and stop when the task is complete. Keep your final message short: say what you did and why.\n\nNot every message is a task. If the user asks a question, or asks you to say something, answer in plain text and call no tools at all. Reach for a file or a command only when the request is actually about one, and when a message is ambiguous, prefer the reading of it that changes nothing.\n\nA tool call that was refused did not happen. Never describe it as done, and do not repeat it on a later message unless the user asks for it again.\n\nReach outside the workspace when the answer is outside it: web_search for current documentation, releases and error messages, then web_fetch to read the page. Do not guess at an API you could look up.\n\nStart anything that does not exit on its own \u2014 dev servers, watchers, tailed logs \u2014 with run_command background: true, and read it with read_output. A foreground command is killed after its timeout.\n\nWhen finding something out means opening many files, give it to task instead: the sub-agent reads them in its own context and reports back, which keeps this conversation short. Use todo_write for work with several distinct steps, and keep it current as you go.";
4289
+ SYSTEM_PROMPT = "You are Clixad, a terminal coding agent. You can read, search, write and edit files and run shell commands, all confined to the user's workspace. When someone asks who or what you are, answer that you are Clixad, in their language; do not introduce yourself as the underlying language model or as a product of the company that trained it. If asked which model you run on, say it is the one selected with /model, shown under the input box. Find your way around with glob and grep before reading whole files. Prefer edit_file over write_file for existing files. Make minimal correct edits, verify them when a test or build command is available, and stop when the task is complete. Keep your final message short: say what you did and why.\n\nNot every message is a task. If the user asks a question, or asks you to say something, answer in plain text and call no tools at all. Reach for a file or a command only when the request is actually about one, and when a message is ambiguous, prefer the reading of it that changes nothing.\n\nA tool call that was refused did not happen. Never describe it as done, and do not repeat it on a later message unless the user asks for it again.\n\nReach outside the workspace when the answer is outside it: web_search for current documentation, releases and error messages, then web_fetch to read the page. Do not guess at an API you could look up.\n\nStart anything that does not exit on its own \u2014 dev servers, watchers, tailed logs \u2014 with run_command background: true, and read it with read_output. A foreground command is killed after its timeout.\n\nWhen finding something out means opening many files, give it to task instead: the sub-agent reads them in its own context and reports back, which keeps this conversation short. Use todo_write for work with several distinct steps, and keep it current as you go.";
4147
4290
  NO_SEARCH_INSTRUCTION = [
4148
4291
  "# Web search is not available on this gateway",
4149
4292
  "There is no web_search tool. web_fetch works normally and needs no key, so fetch a URL directly",
@@ -4528,17 +4671,28 @@ var init_kimi = __esm({
4528
4671
  "gpt-5-nano": 4e5,
4529
4672
  "gemini-2.5-flash-lite": 1048576,
4530
4673
  "deepseek-v4-flash": 1048576,
4531
- "gemini-3.1-flash-lite": 1048576,
4532
- "kimi-k2": 131072,
4674
+ "gemini-3.5-flash-lite": 1048576,
4675
+ "kimi-k2.5": 262144,
4676
+ "gemini-3.8-flash": 1048576,
4533
4677
  "kimi-k2.7-code": 262144,
4534
4678
  "claude-haiku-4.5": 2e5,
4535
- "gemini-3.6-flash": 1048576,
4536
- "claude-sonnet-5": 1e6,
4537
- "gpt-5.2": 4e5,
4679
+ "claude-sonnet-5.5": 1e6,
4680
+ "gpt-6-sol": 105e4,
4538
4681
  "kimi-k3": 1048576,
4682
+ "claude-opus-5.5": 1e6,
4683
+ "claude-fable-5.1": 1e6,
4684
+ // Retired on 2026-09-27 and served by their successor (`RETIRED_MODELS` in
4685
+ // packages/pricing), so an install still configured with one compacts
4686
+ // against the window of the model that actually answers.
4687
+ "gemini-3.1-flash-lite": 1048576,
4688
+ "kimi-k2": 262144,
4689
+ "gemini-3.6-flash": 1048576,
4690
+ "gpt-5.2": 105e4,
4539
4691
  "claude-opus-5": 1e6,
4540
4692
  "gpt-5.5": 105e4,
4541
- "gpt-5.2-pro": 4e5
4693
+ "gpt-5.2-pro": 105e4,
4694
+ // Retired on 2026-09-29 for Claude Sonnet 5.5, same window.
4695
+ "claude-sonnet-5": 1e6
4542
4696
  };
4543
4697
  DEFAULT_CONTEXT_TOKENS = 256e3;
4544
4698
  INSTALL_HELP = `Kimi CLI is not installed (or not on PATH).
@@ -4588,6 +4742,14 @@ function bannerTip(index) {
4588
4742
  const i = index ?? Math.floor(Math.random() * BANNER_TIPS.length);
4589
4743
  return BANNER_TIPS[(i % BANNER_TIPS.length + BANNER_TIPS.length) % BANNER_TIPS.length];
4590
4744
  }
4745
+ function greeting(opts, nameWidth) {
4746
+ if (!opts.name) return "Welcome to Clixad!";
4747
+ const name = nameWidth === void 0 ? opts.name : truncate(opts.name, nameWidth);
4748
+ return opts.newAccount ? `Welcome to Clixad, ${name}!` : `Welcome back, ${name}!`;
4749
+ }
4750
+ function balanceLabel(opts) {
4751
+ return opts.balance === void 0 ? "balance unavailable" : `${opts.balance.toLocaleString("en-US")} credits`;
4752
+ }
4591
4753
  function earnedLabel(opts) {
4592
4754
  return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
4593
4755
  }
@@ -4597,7 +4759,7 @@ function welcomeBanner(opts) {
4597
4759
  const rightW = Math.min(RIGHT_MAX, Math.max(26, inner - 30));
4598
4760
  const leftW = inner - rightW;
4599
4761
  const cwd = opts.cwd.replace(homedir6(), "~").replace(/\\/g, "/");
4600
- const welcome = opts.name ? `Welcome back, ${truncate(opts.name, 18)}!` : "Welcome to Clixad!";
4762
+ const welcome = greeting(opts, Math.max(1, Math.min(18, leftW - (opts.newAccount ? 20 : 15))));
4601
4763
  const duck = duckLines();
4602
4764
  const home = truncate(cwd, leftW);
4603
4765
  const hint = bannerTip(opts.tipIndex);
@@ -4623,8 +4785,10 @@ function welcomeBanner(opts) {
4623
4785
  tip("/wallet", "balance & ads"),
4624
4786
  cell("", 0),
4625
4787
  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)
4788
+ cell(TEXT + balanceLabel(opts) + R2, balanceLabel(opts).length),
4789
+ // Without a wallet there is no earned figure either, and "$0.00/$0.00" is
4790
+ // two invented numbers rather than a statement of what is unknown.
4791
+ opts.balance === void 0 ? cell("", 0) : cell(MUTED + earnedLabel(opts) + R2, earnedLabel(opts).length)
4628
4792
  ];
4629
4793
  const rows = Math.max(left.length, right.length);
4630
4794
  const out = [];
@@ -4642,9 +4806,9 @@ function welcomeBanner(opts) {
4642
4806
  }
4643
4807
  function compactBanner(opts) {
4644
4808
  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));
4809
+ const welcome = truncate(greeting(opts), Math.max(1, width - 2));
4646
4810
  const hint = bannerTip(opts.tipIndex);
4647
- const balance = `${opts.balance.toLocaleString("en-US")} credits`;
4811
+ const balance = balanceLabel(opts);
4648
4812
  const room = width - (4 + stringWidth(hint.key) + 1 + 4 + stringWidth(balance));
4649
4813
  const desc = room >= 3 ? truncate(hint.desc, room) : "";
4650
4814
  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 +4942,34 @@ function readVersion() {
4778
4942
  return "unknown";
4779
4943
  }
4780
4944
  }
4945
+ function isNewerVersion(candidate, current) {
4946
+ const parse2 = (v) => {
4947
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v.trim());
4948
+ if (!m) return void 0;
4949
+ return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split(".") : [] };
4950
+ };
4951
+ const a = parse2(candidate);
4952
+ const b = parse2(current);
4953
+ if (!a || !b) return false;
4954
+ for (let i = 0; i < 3; i++) {
4955
+ if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i];
4956
+ }
4957
+ if (!a.pre.length || !b.pre.length) return !a.pre.length && b.pre.length > 0;
4958
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
4959
+ const x = a.pre[i];
4960
+ const y = b.pre[i];
4961
+ if (x === void 0) return false;
4962
+ if (y === void 0) return true;
4963
+ if (x === y) continue;
4964
+ const nx = /^\d+$/.test(x) ? Number(x) : void 0;
4965
+ const ny = /^\d+$/.test(y) ? Number(y) : void 0;
4966
+ if (nx !== void 0 && ny !== void 0) return nx > ny;
4967
+ if (nx !== void 0) return false;
4968
+ if (ny !== void 0) return true;
4969
+ return x > y;
4970
+ }
4971
+ return false;
4972
+ }
4781
4973
  var VERSION, VERSION_LABEL;
4782
4974
  var init_version = __esm({
4783
4975
  "src/version.ts"() {
@@ -5617,7 +5809,7 @@ var init_views = __esm({
5617
5809
  });
5618
5810
 
5619
5811
  // src/tui/app.tsx
5620
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5812
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5621
5813
  import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
5622
5814
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5623
5815
  function buyDeclarationLines(pack) {
@@ -5668,6 +5860,7 @@ function App({
5668
5860
  wallet,
5669
5861
  streak,
5670
5862
  session,
5863
+ newAccount,
5671
5864
  initialTask,
5672
5865
  notices: notices2,
5673
5866
  sponsorServe,
@@ -5690,8 +5883,11 @@ function App({
5690
5883
  text: (cols >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
5691
5884
  width: cols,
5692
5885
  name: config.login ?? config.email?.split("@")[0],
5886
+ ...newAccount ? { newAccount: true } : {},
5693
5887
  model: config.model,
5694
- balance: wallet?.balance ?? 0,
5888
+ // Undefined when the wallet could not be read: the banner says so
5889
+ // rather than drawing "0 credits", which reads as an empty account.
5890
+ balance: wallet?.balance,
5695
5891
  adsToday: wallet?.ads_today ?? 0,
5696
5892
  earnedUsdToday: wallet?.earned_usd_today ?? 0,
5697
5893
  maxRewardUsd: wallet?.max_reward_usd_per_day ?? 0,
@@ -5705,7 +5901,7 @@ function App({
5705
5901
  const [editor, setEditor] = useState(EMPTY);
5706
5902
  const [busy, setBusy] = useState(false);
5707
5903
  const [live, setLive] = useState(null);
5708
- const [balance, setBalance] = useState(wallet?.balance ?? 0);
5904
+ const [balance, setBalance] = useState(wallet?.balance);
5709
5905
  const [spent, setSpent] = useState(0);
5710
5906
  const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5711
5907
  const [model, setModel2] = useState(config.model);
@@ -5761,6 +5957,7 @@ function App({
5761
5957
  const pendingTaskRef = useRef(null);
5762
5958
  const pendingFreeRef = useRef([]);
5763
5959
  const pendingGrantRef = useRef(0);
5960
+ const pendingBalanceRef = useRef(void 0);
5764
5961
  const mcpRef = useRef(McpHub.empty());
5765
5962
  const todosRef = useRef(new TodoList());
5766
5963
  const lastTurnRef = useRef(null);
@@ -5826,6 +6023,9 @@ function App({
5826
6023
  );
5827
6024
  const tallyRef = useRef(createTally());
5828
6025
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
6026
+ const screenRef = useRef(
6027
+ null
6028
+ );
5829
6029
  const push = useCallback((e) => {
5830
6030
  const id = idRef.current++;
5831
6031
  setEntries((prev) => [...prev, { ...e, id }]);
@@ -5862,11 +6062,32 @@ function App({
5862
6062
  deltaBufRef.current = "";
5863
6063
  }, []);
5864
6064
  useEffect(() => dropDelta, [dropDelta]);
6065
+ useEffect(() => {
6066
+ if (wallet) return;
6067
+ const ctl = new AbortController();
6068
+ void client.wallet(ctl.signal).then((w) => {
6069
+ setBalance((current) => current ?? w.balance);
6070
+ setFreeTier((current) => current ?? w.free_tier);
6071
+ }).catch(() => void 0);
6072
+ return () => ctl.abort();
6073
+ }, []);
5865
6074
  useEffect(() => {
5866
6075
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5867
6076
  for (const text2 of notices2 ?? []) push({ kind: "notice", tone: "warn", text: ` ${text2}` });
5868
6077
  const files = contextRef.current.files;
5869
6078
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
6079
+ if (!wallet) {
6080
+ let host = config.gatewayUrl;
6081
+ try {
6082
+ host = new URL(config.gatewayUrl).host;
6083
+ } catch {
6084
+ }
6085
+ push({
6086
+ kind: "notice",
6087
+ tone: "warn",
6088
+ text: ` ${host} did not answer, so the balance is unknown for now. It fills in once a request gets through.`
6089
+ });
6090
+ }
5870
6091
  const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
5871
6092
  if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
5872
6093
  const freeLine = freeTierNotice({ model: config.model, balance: wallet?.balance ?? 0, free: wallet?.free_tier });
@@ -5883,6 +6104,24 @@ function App({
5883
6104
  client.models().then((m) => {
5884
6105
  catalogRef.current = m;
5885
6106
  searchRef.current = client.capabilities?.web_search === true;
6107
+ const successor = client.retiredModels[modelRef.current];
6108
+ if (successor) {
6109
+ const from = modelRef.current;
6110
+ config.model = successor;
6111
+ saveConfig(config);
6112
+ switchModel(successor);
6113
+ push({
6114
+ kind: "notice",
6115
+ text: ` ${from} was retired; now using its successor, ${successor}. /model picks another.`
6116
+ });
6117
+ }
6118
+ const latest = client.latestCli;
6119
+ if (latest && isNewerVersion(latest, VERSION)) {
6120
+ push({
6121
+ kind: "notice",
6122
+ text: ` clixad ${latest} is out (you have ${VERSION}). Update with: npm install -g clixad@latest`
6123
+ });
6124
+ }
5886
6125
  }).catch(() => void 0);
5887
6126
  }, []);
5888
6127
  useEffect(() => {
@@ -6122,7 +6361,7 @@ function App({
6122
6361
  setSpent((s) => s + res.creditsCharged);
6123
6362
  push({
6124
6363
  kind: "notice",
6125
- text: ` compacted context: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
6364
+ text: ` compacted context: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged.toLocaleString("en-US")} credits)`
6126
6365
  });
6127
6366
  }
6128
6367
  }
@@ -6231,19 +6470,29 @@ ${NO_SEARCH_INSTRUCTION}`),
6231
6470
  pendingTaskRef.current = { task, images };
6232
6471
  pendingFreeRef.current = err.freeModels;
6233
6472
  pendingGrantRef.current = err.grantPerAd;
6473
+ pendingBalanceRef.current = err.balance;
6234
6474
  const free = err.freeModels[0];
6235
6475
  push({
6236
6476
  kind: "notice",
6237
6477
  tone: "warn",
6238
- text: free ? ` Out of credits on ${model2} (balance ${err.balance}, need ~${err.estimatedCost}).
6478
+ text: free ? ` ${shortfallHeadline(err, model2)}
6239
6479
  Press enter \u2014 ${free.id} is still free today (~${free.turnsLeft} turn${free.turnsLeft === 1 ? "" : "s"} left), or take an offer instead.
6240
6480
  Your request is kept either way, so you will not have to type it again.
6241
6481
  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}).
6482
+ /buy is there too, if you would rather pay than do an offer.` : ` ${shortfallHeadline(err, model2)}
6243
6483
  Press enter to open the ad wall \u2014 offers pay ${grantLabel(err.grantPerAd)}.
6244
6484
  I'll continue automatically. Offers you are screened out of pay nothing;
6245
6485
  that is normal, just start another.
6246
- /buy is there too, if you would rather pay than do an offer.`
6486
+ ` + // A wallet that is short rather than empty has a third way on,
6487
+ // and it is the cheapest: the same credits on a smaller model.
6488
+ // It names the command and no figure, since what a model costs
6489
+ // is the gateway's to say and `/model` says it.
6490
+ (err.balance > 0 ? ` Or pick a cheaper model with /model, which shows what each one costs.
6491
+ ` : "") + // One clause, last, and it stays last. The wall is how this product
6492
+ // is meant to be paid for and the offer is free; naming /buy here
6493
+ // is discoverability rather than a pitch, and it is the moment
6494
+ // somebody actually wants to know the command exists.
6495
+ ` /buy is there too, if you would rather pay than do an offer.`
6247
6496
  });
6248
6497
  } else if (err instanceof AuthError) {
6249
6498
  push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
@@ -6370,7 +6619,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6370
6619
  const runAdWall = useCallback(async () => {
6371
6620
  const pending = pendingTaskRef.current;
6372
6621
  pendingTaskRef.current = null;
6373
- const before = balance;
6622
+ let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
6374
6623
  const code = await client.handoffCode();
6375
6624
  const base = config.dashboardUrl.replace(/\/+$/, "");
6376
6625
  openBrowser(code ? `${base}/?c=${encodeURIComponent(code)}` : base);
@@ -6388,7 +6637,10 @@ ${NO_SEARCH_INSTRUCTION}`),
6388
6637
  await sleep4(3e3, signal);
6389
6638
  if (signal.aborted) break;
6390
6639
  const w = await client.wallet(signal).catch(() => void 0);
6391
- if (w && w.balance > before) {
6640
+ if (w && before === void 0) {
6641
+ before = w.balance;
6642
+ setBalance(w.balance);
6643
+ } else if (w && before !== void 0 && w.balance > before) {
6392
6644
  setBalance(w.balance);
6393
6645
  credited = true;
6394
6646
  }
@@ -6431,7 +6683,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6431
6683
  ];
6432
6684
  setPickerSel(0);
6433
6685
  setPicker({
6434
- title: "Out of credits",
6686
+ title: (pendingBalanceRef.current ?? 0) > 0 ? "Not enough credits" : "Out of credits",
6435
6687
  subtitle: "Pick a free model to carry on with, or earn credits at the wall.",
6436
6688
  items,
6437
6689
  onPick: (choice) => {
@@ -6482,7 +6734,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6482
6734
  }, [client, push, runBusy]);
6483
6735
  const runCheckout = useCallback(
6484
6736
  async (pack) => {
6485
- const before = balance;
6737
+ let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
6486
6738
  let credited = false;
6487
6739
  let stopped = false;
6488
6740
  await runBusy(WAITING_FOR_PAYMENT, async (signal) => {
@@ -6506,7 +6758,10 @@ ${NO_SEARCH_INSTRUCTION}`),
6506
6758
  await sleep4(3e3, signal);
6507
6759
  if (signal.aborted) break;
6508
6760
  const w = await client.wallet(signal).catch(() => void 0);
6509
- if (w && w.balance > before) {
6761
+ if (w && before === void 0) {
6762
+ before = w.balance;
6763
+ setBalance(w.balance);
6764
+ } else if (w && before !== void 0 && w.balance > before) {
6510
6765
  setBalance(w.balance);
6511
6766
  credited = true;
6512
6767
  }
@@ -6589,7 +6844,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6589
6844
  setSpent((s) => s + res.creditsCharged);
6590
6845
  push({
6591
6846
  kind: "notice",
6592
- text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
6847
+ text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged.toLocaleString("en-US")} credits)`
6593
6848
  });
6594
6849
  });
6595
6850
  return;
@@ -7275,7 +7530,16 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7275
7530
  }, 0);
7276
7531
  }, [entries, cols]);
7277
7532
  const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + buyHeight + busyHeight + liveHeight;
7278
- const spacer = Math.max(0, viewport - printed - used);
7533
+ const lastScreen = screenRef.current;
7534
+ const freshScreen = !lastScreen || lastScreen.epoch !== staticEpoch || lastScreen.rows !== rows || lastScreen.cols !== cols || printed < lastScreen.printed;
7535
+ const onScreen = Math.max(
7536
+ 0,
7537
+ Math.min(freshScreen ? printed : lastScreen.onScreen + (printed - lastScreen.printed), viewport - used)
7538
+ );
7539
+ const spacer = Math.max(0, viewport - onScreen - used);
7540
+ useLayoutEffect(() => {
7541
+ screenRef.current = { epoch: staticEpoch, rows, cols, printed, onScreen };
7542
+ });
7279
7543
  const labelW = Math.min(
7280
7544
  Math.max(1, Math.floor(cols * 0.5)),
7281
7545
  menu.reduce((w, c2) => Math.max(w, visibleLength(c2.label)), 0)
@@ -7299,7 +7563,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
7299
7563
  let estimate;
7300
7564
  const info = catalogRef.current.find((m) => m.id === model);
7301
7565
  const shape = client.taskShape;
7302
- if (!busy && info && shape && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7566
+ if (!busy && info && shape && balance !== void 0 && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7303
7567
  const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7304
7568
  const onFree = Boolean(info.free && freeTier?.models.includes(model));
7305
7569
  const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
@@ -7489,7 +7753,7 @@ function modelHint(m, free) {
7489
7753
  function turnMeta(result) {
7490
7754
  const n = (v) => v.toLocaleString("en-US");
7491
7755
  const balance = `balance ${n(result.balance)}`;
7492
- if (result.creditsWaived <= 0) return `[${result.creditsCharged} credits \xB7 ${balance}]`;
7756
+ if (result.creditsWaived <= 0) return `[${n(result.creditsCharged)} credits \xB7 ${balance}]`;
7493
7757
  const left = result.freeCreditsLeft === void 0 ? "" : ` \xB7 ${n(result.freeCreditsLeft)} free credits left`;
7494
7758
  if (result.creditsCharged > 0) {
7495
7759
  return `[free: ${n(result.creditsWaived)} credits \xB7 then ${n(result.creditsCharged)} charged \xB7 ${balance}]`;
@@ -7508,7 +7772,12 @@ function statusRow(o, estimate, cols) {
7508
7772
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
7509
7773
  const clauses = [
7510
7774
  { text: o.model, rank: KEEP_ALWAYS },
7511
- { text: `${o.balance.toLocaleString("en-US")} cr`, rank: KEEP_ALWAYS },
7775
+ {
7776
+ // "? cr" rather than "0 cr": unknown is not empty, and an empty-looking
7777
+ // wallet is the one misreading on this row that frightens somebody.
7778
+ text: o.balance === void 0 ? "? cr" : `${o.balance.toLocaleString("en-US")} cr`,
7779
+ rank: KEEP_ALWAYS
7780
+ },
7512
7781
  // Above the context warning, below the balance. It is the second number
7513
7782
  // deciding whether the next turn costs anything, and unlike the spend it is
7514
7783
  // *news* — it only exists while a free model is selected and it counts down.
@@ -7746,7 +8015,7 @@ async function main() {
7746
8015
  for (const text of configNotices) console.error(c.yellow(text));
7747
8016
  switch (cmd) {
7748
8017
  case "login":
7749
- return login(client, config, rest);
8018
+ return void await login(client, config, rest, { nextStep: true });
7750
8019
  case "logout":
7751
8020
  return logout(config);
7752
8021
  case "whoami":
@@ -7810,7 +8079,7 @@ async function main() {
7810
8079
  return repl(client, config, { task: [cmd, ...rest].join(" ").trim(), notices: configNotices });
7811
8080
  }
7812
8081
  }
7813
- async function login(client, config, args) {
8082
+ async function login(client, config, args, opts = {}) {
7814
8083
  const parsed = parseLoginArgs(args);
7815
8084
  for (const line2 of TERMS_NOTICE.split("\n")) console.log(c.dim(` ${line2}`));
7816
8085
  const outcome = await performLogin(client, config, parsed, {
@@ -7836,20 +8105,25 @@ async function login(client, config, args) {
7836
8105
  if (account.model) console.log(` model: ${c.bold(account.model)} ${c.dim("(the default for new accounts)")}`);
7837
8106
  await showStreak(client);
7838
8107
  console.log(c.dim(` token stored in ${configPath()}`));
7839
- return;
8108
+ if (opts.nextStep) {
8109
+ console.log(`
8110
+ Next: open a project folder and run ${c.cyan("clixad")} to start coding.`);
8111
+ }
8112
+ return { created: account.created };
7840
8113
  }
7841
8114
  case "closed":
7842
8115
  reportSignupClosed(outcome.message, outcome.hadInvite);
7843
8116
  process.exitCode = 1;
7844
- return;
8117
+ return void 0;
7845
8118
  case "timeout":
7846
8119
  console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
7847
- return;
8120
+ return void 0;
7848
8121
  case "aborted":
7849
- return;
8122
+ return void 0;
7850
8123
  default:
7851
8124
  console.log(c.red(`
7852
8125
  login failed: ${outcome.error}`));
8126
+ return void 0;
7853
8127
  }
7854
8128
  }
7855
8129
  function reportSignupClosed(message, hadInvite) {
@@ -8051,7 +8325,7 @@ ${NO_SEARCH_INSTRUCTION}`),
8051
8325
  } catch (err) {
8052
8326
  if (err instanceof PaywallError) {
8053
8327
  console.log(c.yellow(`
8054
- Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8328
+ ${shortfallHeadline(err, config.model)}`));
8055
8329
  const freeLine = freeAlternativeLine(err.freeModels);
8056
8330
  if (freeLine) console.log(c.green(freeLine));
8057
8331
  console.log(
@@ -8102,7 +8376,7 @@ async function codeCmd(client, config, rest) {
8102
8376
  const w = await safeWallet(client);
8103
8377
  if (w) {
8104
8378
  console.log(
8105
- c.dim(` wallet: ${w.balance} credits \xB7 model ${config.model} \xB7 gateway ${config.gatewayUrl}`)
8379
+ c.dim(` wallet: ${credits(w.balance)} credits \xB7 model ${config.model} \xB7 gateway ${config.gatewayUrl}`)
8106
8380
  );
8107
8381
  if (w.balance <= 0) {
8108
8382
  console.log(c.yellow(" You're out of credits \u2014 run `clixad earn` first, or the agent will hit a paywall."));
@@ -8137,8 +8411,9 @@ async function repl(client, config, opts = {}) {
8137
8411
  console.log(c.yellow('Interactive mode needs a terminal. Use `clixad -p "..."` for scripted runs.'));
8138
8412
  return;
8139
8413
  }
8414
+ let newAccount = false;
8140
8415
  if (!config.token) {
8141
- await login(client, config, []);
8416
+ newAccount = Boolean((await login(client, config, []))?.created);
8142
8417
  if (!config.token) return;
8143
8418
  }
8144
8419
  let session;
@@ -8149,8 +8424,7 @@ async function repl(client, config, opts = {}) {
8149
8424
  session = loadSession(opts.resume);
8150
8425
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
8151
8426
  }
8152
- const streak = await client.claimStreak();
8153
- const wallet = await safeWallet(client);
8427
+ const { streak, wallet } = await startupReads(client, config.gatewayUrl);
8154
8428
  const cwd = process.cwd();
8155
8429
  const initHint = shouldSuggestInit(cwd) ? { returning: hasSessionIn(cwd) } : void 0;
8156
8430
  clearScreen();
@@ -8161,24 +8435,30 @@ async function repl(client, config, opts = {}) {
8161
8435
  wallet,
8162
8436
  streak,
8163
8437
  session,
8438
+ newAccount,
8164
8439
  initialTask: opts.task,
8165
8440
  ...opts.notices?.length ? { notices: opts.notices } : {},
8166
8441
  ...initHint ? { initHint } : {}
8167
8442
  });
8168
8443
  }
8169
8444
  async function runTurn(client, config, messages) {
8445
+ let started = false;
8170
8446
  try {
8171
- process.stdout.write(c.green("assistant ") + "");
8172
8447
  const result = await client.chatStream(messages, config.model, {
8173
- onDelta: (chunk) => process.stdout.write(chunk)
8448
+ onDelta: (chunk) => {
8449
+ if (!started) process.stdout.write(c.green("assistant "));
8450
+ started = true;
8451
+ process.stdout.write(chunk);
8452
+ }
8174
8453
  });
8454
+ if (!started) process.stdout.write(c.green("assistant "));
8175
8455
  process.stdout.write("\n");
8176
- console.log(c.dim(` [${result.creditsCharged} credits \xB7 balance ${result.balance}]`));
8456
+ console.log(c.dim(` [${credits(result.creditsCharged)} credits \xB7 balance ${credits(result.balance)}]`));
8177
8457
  return result;
8178
8458
  } catch (err) {
8179
- process.stdout.write("\n");
8459
+ if (started) process.stdout.write("\n");
8180
8460
  if (err instanceof PaywallError) {
8181
- console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8461
+ console.log(c.yellow(shortfallHeadline(err, config.model)));
8182
8462
  const freeLine = freeAlternativeLine(err.freeModels);
8183
8463
  if (freeLine) console.log(c.green(freeLine));
8184
8464
  console.log(
@@ -8208,29 +8488,69 @@ async function safeWallet(client) {
8208
8488
  return void 0;
8209
8489
  }
8210
8490
  }
8491
+ var CONNECTING_AFTER_MS = 700;
8492
+ var WAKING_AFTER_MS = 6e3;
8493
+ async function startupReads(client, gatewayUrl) {
8494
+ const host = (() => {
8495
+ try {
8496
+ return new URL(gatewayUrl).host;
8497
+ } catch {
8498
+ return gatewayUrl;
8499
+ }
8500
+ })();
8501
+ const say = (text) => {
8502
+ if (process.stdout.isTTY) console.log(c.dim(text));
8503
+ };
8504
+ const timers = [
8505
+ setTimeout(() => say(` connecting to ${host}\u2026`), CONNECTING_AFTER_MS),
8506
+ setTimeout(
8507
+ () => say(" still waiting: the server sleeps after a quiet spell and takes up to a minute to wake. ctrl+c cancels."),
8508
+ WAKING_AFTER_MS
8509
+ )
8510
+ ];
8511
+ try {
8512
+ const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client)]);
8513
+ const balance = streak?.claimed ? streak.balance : wallet?.balance;
8514
+ return {
8515
+ streak,
8516
+ wallet: wallet && balance !== void 0 ? { ...wallet, balance } : wallet
8517
+ };
8518
+ } finally {
8519
+ for (const t of timers) clearTimeout(t);
8520
+ }
8521
+ }
8211
8522
  function printHelp() {
8212
- console.log(`${c.bold("clixad")} \u2014 free AI coding in your terminal, funded by ads
8523
+ const rows = [
8524
+ ["", "", "start the interactive coding agent here (signs you in if needed)"],
8525
+ ['"<task>"', "", "the same, with a task already running"],
8526
+ ["--continue", "", "resume the last session in this directory"],
8527
+ ["--resume", "[id]", "list saved sessions, or resume one"],
8528
+ ["-p", '"<task>"', "headless agent run (read-only tools)"],
8529
+ ["ask", '"<prompt>"', "one-shot answer, no tools"],
8530
+ ["models", "", "models with credit prices, and which are free today"],
8531
+ ["model", "<id>", "set the default model"],
8532
+ ["wallet", "", "balance, offers today, recent activity"],
8533
+ ["earn", "", "open the offer wall to earn credits"],
8534
+ ["refer", "", "your invite code, and what it has earned"],
8535
+ ["redeem", "<code>", "spend a code you were given"],
8536
+ ["login", "[email]", "sign in or create an account (--invite <code> if you have one)"],
8537
+ ["logout", "", "forget the stored token"],
8538
+ ["whoami", "", "account and balance"],
8539
+ ["code", '["<task>"]', "launch the Kimi CLI coding agent on this directory"],
8540
+ ["--version", "", "print the version and exit"]
8541
+ ];
8542
+ const plain = (name, arg) => `clixad${name ? ` ${name}` : ""}${arg ? ` ${arg}` : ""}`;
8543
+ const width = Math.max(...rows.map(([name, arg]) => plain(name, arg).length)) + 2;
8544
+ const lines = rows.map(([name, arg, desc]) => {
8545
+ const shown = `${c.cyan("clixad")}${name ? ` ${c.cyan(name)}` : ""}${arg ? ` ${arg}` : ""}`;
8546
+ return ` ${shown}${" ".repeat(width - plain(name, arg).length)}${desc}`;
8547
+ });
8548
+ console.log(`${c.bold("clixad")}: free AI coding in your terminal, funded by ads
8213
8549
 
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
8550
+ ${lines.join("\n")}
8232
8551
 
8233
- ${c.dim("Discord: https://discord.gg/pwxPM3QyF \u2014 bugs and feature requests are read there")}`);
8552
+ Inside the agent, ${c.cyan("/help")} lists everything else, including ${c.cyan("/buy")} for credit packs.
8553
+ ${c.dim("Discord: https://discord.gg/pwxPM3QyF (bugs and feature requests are read there)")}`);
8234
8554
  }
8235
8555
  main().catch((err) => {
8236
8556
  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.24",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",