clixad 0.0.1-beta.21 → 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 (3) hide show
  1. package/README.md +8 -1
  2. package/dist/clixad.mjs +932 -203
  3. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -14,6 +14,94 @@ var __export = (target, all) => {
14
14
  __defProp(target, name, { get: all[name], enumerable: true });
15
15
  };
16
16
 
17
+ // src/estimate.ts
18
+ function parseTaskShape(raw) {
19
+ if (!raw || typeof raw !== "object") return void 0;
20
+ const r = raw;
21
+ const count = (v, min) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= 1e6 ? Math.floor(v) : void 0;
22
+ const callsTypical = count(r.calls_typical, 1);
23
+ const callsLong = count(r.calls_long, 1);
24
+ const growthTokens = count(r.growth_tokens, 0);
25
+ const outputTokens = count(r.output_tokens, 0);
26
+ if (callsTypical === void 0 || callsLong === void 0) return void 0;
27
+ if (growthTokens === void 0 || outputTokens === void 0) return void 0;
28
+ if (callsLong < callsTypical) return void 0;
29
+ return { callsTypical, callsLong, growthTokens, outputTokens };
30
+ }
31
+ function callCredits(model, inputTokens, outputTokens) {
32
+ return Math.ceil(
33
+ (inputTokens * model.credits_per_mtoken_input + outputTokens * model.credits_per_mtoken_output) / 1e6
34
+ );
35
+ }
36
+ function taskCredits(model, contextTokens, calls, shape) {
37
+ const start = Math.max(0, Math.round(contextTokens));
38
+ let total = 0;
39
+ for (let i = 0; i < calls; i++) {
40
+ total += callCredits(model, start + i * shape.growthTokens, shape.outputTokens);
41
+ }
42
+ return total;
43
+ }
44
+ function estimateTask(model, contextTokens, shape) {
45
+ return {
46
+ low: taskCredits(model, contextTokens, shape.callsTypical, shape),
47
+ high: taskCredits(model, contextTokens, shape.callsLong, shape)
48
+ };
49
+ }
50
+ function taskVerdict(range, wallet) {
51
+ const free = Math.max(0, wallet.freeLeft ?? 0);
52
+ if (free > 0 && free >= range.high) return "free";
53
+ const available = Math.max(0, wallet.balance) + free;
54
+ if (available >= range.high) return "fits";
55
+ if (available >= range.low) return "tight";
56
+ return "short";
57
+ }
58
+ function shortCredits(n, direction) {
59
+ const round = direction === "down" ? Math.floor : Math.ceil;
60
+ if (n < 1e3) return String(Math.max(0, Math.round(n)));
61
+ if (n < 1e4) return `${trim(round(n / 100) / 10)}k`;
62
+ if (n < 1e6) return `${round(n / 1e3)}k`;
63
+ return `${trim(round(n / 1e5) / 10)}M`;
64
+ }
65
+ function trim(n) {
66
+ return Number.isInteger(n) ? String(n) : n.toFixed(1);
67
+ }
68
+ function estimateClause(range, verdict, freeElsewhere) {
69
+ const span = range.low === range.high ? shortCredits(range.high, "up") : `${shortCredits(range.low, "down")}\u2013${shortCredits(range.high, "up")}`;
70
+ const head = `task \u2248 ${span}`;
71
+ const tight = `\u2248${span}`;
72
+ switch (verdict) {
73
+ case "free":
74
+ return { variants: [`${head} \xB7 free`, `${tight} free`], warn: false };
75
+ case "fits":
76
+ return { variants: [`${head} cr`, `${tight} cr`], warn: false };
77
+ case "tight":
78
+ return { variants: [`${head} \xB7 may run short`, `${tight}, may run short`, `${tight} tight`], warn: true };
79
+ case "short":
80
+ return {
81
+ variants: [
82
+ `${head} \xB7 not enough, ${freeElsewhere ? "/model" : "/earn"}`,
83
+ `${tight}, not enough`,
84
+ `${tight} short`
85
+ ],
86
+ warn: true
87
+ };
88
+ }
89
+ }
90
+ var init_estimate = __esm({
91
+ "src/estimate.ts"() {
92
+ "use strict";
93
+ }
94
+ });
95
+
96
+ // src/sponsor/formats.ts
97
+ var EXTRA_AD_FORMATS;
98
+ var init_formats = __esm({
99
+ "src/sponsor/formats.ts"() {
100
+ "use strict";
101
+ EXTRA_AD_FORMATS = ["sidebar_card", "status_bar"];
102
+ }
103
+ });
104
+
17
105
  // src/edge.ts
18
106
  function detectEdgeBlock(status, headers, body) {
19
107
  if (status !== 403) return void 0;
@@ -225,6 +313,17 @@ function redeemMessage(result) {
225
313
  function ledgerReasonLabel(reason) {
226
314
  return LEDGER_REASONS[reason] ?? reason.replace(/_/g, " ");
227
315
  }
316
+ function extraFormatIds(raw) {
317
+ const out = {};
318
+ if (typeof raw !== "object" || raw === null) return out;
319
+ for (const format of EXTRA_AD_FORMATS) {
320
+ const entry = raw[format];
321
+ if (typeof entry !== "object" || entry === null) continue;
322
+ const id = entry.publisher_id;
323
+ if (typeof id === "string" && id.trim()) out[format] = id.trim();
324
+ }
325
+ return out;
326
+ }
228
327
  function imagePart(image) {
229
328
  return { type: "image_url", image_url: { url: `data:${image.mimeType};base64,${image.data}` } };
230
329
  }
@@ -251,8 +350,47 @@ function asSignupClosed(status, data) {
251
350
  const d = data;
252
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;
253
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
+ }
254
358
  function paywallError(body) {
255
- return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
359
+ const free = Array.isArray(body.free_models) ? body.free_models.filter(
360
+ (m) => !!m && typeof m.id === "string" && m.id.length > 0 && Number.isFinite(m.turns_left)
361
+ ).map((m) => ({ id: m.id, turnsLeft: Math.max(0, Math.floor(m.turns_left)) })).filter((m) => m.turnsLeft > 0) : [];
362
+ return new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad, free);
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
+ }
256
394
  }
257
395
  async function authFailure(res, what) {
258
396
  const body = await res.text().catch(() => "");
@@ -278,16 +416,16 @@ async function httpFailure(res, what) {
278
416
  return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
279
417
  }
280
418
  function sleep(ms, signal) {
281
- return new Promise((resolve2) => {
419
+ return new Promise((resolve3) => {
282
420
  if (!signal) {
283
- setTimeout(resolve2, ms);
421
+ setTimeout(resolve3, ms);
284
422
  return;
285
423
  }
286
- if (signal.aborted) return resolve2();
424
+ if (signal.aborted) return resolve3();
287
425
  const done = () => {
288
426
  clearTimeout(timer);
289
427
  signal.removeEventListener("abort", done);
290
- resolve2();
428
+ resolve3();
291
429
  };
292
430
  const timer = setTimeout(done, ms);
293
431
  signal.addEventListener("abort", done, { once: true });
@@ -327,10 +465,12 @@ async function* parseSSE(body) {
327
465
  }
328
466
  }
329
467
  }
330
- 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;
331
469
  var init_client = __esm({
332
470
  "src/client.ts"() {
333
471
  "use strict";
472
+ init_estimate();
473
+ init_formats();
334
474
  init_edge();
335
475
  CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
336
476
  LEDGER_REASONS = {
@@ -361,17 +501,46 @@ var init_client = __esm({
361
501
  waitlistUrl;
362
502
  };
363
503
  PaywallError = class extends Error {
364
- constructor(balance, estimatedCost, grantPerAd) {
504
+ constructor(balance, estimatedCost, grantPerAd, freeModels = []) {
365
505
  super("Out of credits");
366
506
  this.balance = balance;
367
507
  this.estimatedCost = estimatedCost;
368
508
  this.grantPerAd = grantPerAd;
509
+ this.freeModels = freeModels;
369
510
  this.name = "PaywallError";
370
511
  }
371
512
  balance;
372
513
  estimatedCost;
373
514
  grantPerAd;
515
+ freeModels;
374
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
+ ]);
375
544
  AuthError = class extends Error {
376
545
  constructor() {
377
546
  super("Not logged in. Run `clixad login` first.");
@@ -412,7 +581,7 @@ var init_client = __esm({
412
581
  }
413
582
  /** Dev-only signup shortcut (production replaces this with GitHub OAuth). */
414
583
  async signupDev(email, invite) {
415
- const res = await fetch(`${this.config.gatewayUrl}/dev/users`, {
584
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/dev/users`, {
416
585
  method: "POST",
417
586
  headers: this.headers(false),
418
587
  body: JSON.stringify({ ...email ? { email } : {}, ...invite ? { invite } : {} })
@@ -428,7 +597,7 @@ var init_client = __esm({
428
597
  /** Begin GitHub device-flow login. Returns null if the gateway has no GitHub
429
598
  * configured (501), so the caller can fall back to the dev shortcut. */
430
599
  async deviceStart() {
431
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
600
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/device/start`, {
432
601
  method: "POST",
433
602
  headers: this.headers(false)
434
603
  });
@@ -438,7 +607,7 @@ var init_client = __esm({
438
607
  }
439
608
  /** Poll once for device-flow completion. `invite` unlocks a gated signup. */
440
609
  async devicePoll(session, invite) {
441
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
610
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
442
611
  method: "POST",
443
612
  headers: this.headers(false),
444
613
  body: JSON.stringify(invite ? { session, invite } : { session })
@@ -460,7 +629,7 @@ var init_client = __esm({
460
629
  */
461
630
  async handoffCode() {
462
631
  try {
463
- const res = await fetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
632
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/auth/handoff`, {
464
633
  method: "POST",
465
634
  headers: this.headers()
466
635
  });
@@ -487,7 +656,7 @@ var init_client = __esm({
487
656
  */
488
657
  async claimStreak(signal) {
489
658
  try {
490
- const res = await fetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
659
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
491
660
  method: "POST",
492
661
  headers: this.headers(),
493
662
  ...signal ? { signal } : {}
@@ -522,7 +691,7 @@ var init_client = __esm({
522
691
  */
523
692
  async sendFeedback(report, signal) {
524
693
  try {
525
- const res = await fetch(`${this.config.gatewayUrl}/v1/feedback`, {
694
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/feedback`, {
526
695
  method: "POST",
527
696
  headers: this.headers(),
528
697
  ...signal ? { signal } : {},
@@ -557,7 +726,7 @@ var init_client = __esm({
557
726
  * that did not run, and saying so beats printing an empty summary.
558
727
  */
559
728
  async referral(signal) {
560
- const res = await fetch(`${this.config.gatewayUrl}/v1/referrals`, {
729
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/referrals`, {
561
730
  headers: this.headers(),
562
731
  ...signal ? { signal } : {}
563
732
  });
@@ -588,7 +757,7 @@ var init_client = __esm({
588
757
  * 200 with a status, because that is an answer rather than a failure.
589
758
  */
590
759
  async redeem(code, signal) {
591
- const res = await fetch(`${this.config.gatewayUrl}/v1/redeem`, {
760
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/redeem`, {
592
761
  method: "POST",
593
762
  headers: this.headers(),
594
763
  ...signal ? { signal } : {},
@@ -609,7 +778,7 @@ var init_client = __esm({
609
778
  * them with the input box locked and "esc to stop" on screen.
610
779
  */
611
780
  async creditPacks(signal) {
612
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
781
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/billing/packs`, { signal });
613
782
  if (!res.ok) throw new Error(`packs failed: ${res.status}`);
614
783
  return res.json();
615
784
  }
@@ -620,7 +789,7 @@ var init_client = __esm({
620
789
  * could not deliver instantly is worse than not starting one.
621
790
  */
622
791
  async checkout(pack, consent, signal) {
623
- const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
792
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
624
793
  method: "POST",
625
794
  headers: this.headers(),
626
795
  ...signal ? { signal } : {},
@@ -636,12 +805,46 @@ var init_client = __esm({
636
805
  // the better part of a minute) would otherwise be indistinguishable from a
637
806
  // hung terminal. Nothing else passes one — a one-shot CLI command has Ctrl+C.
638
807
  async models(signal) {
639
- const res = await fetch(`${this.config.gatewayUrl}/v1/models`, { signal });
808
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/models`, { signal });
640
809
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
641
810
  const data = await res.json();
642
811
  this.seen = { web_search: data.capabilities?.web_search === true };
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
+ }
643
821
  return data.data;
644
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;
839
+ /**
840
+ * The task shape the cost preview assumes, as of the last catalog fetch, or
841
+ * undefined when the gateway sent none (or one that did not parse). Undefined
842
+ * means no estimate is shown at all, the silence an older gateway gets.
843
+ */
844
+ get taskShape() {
845
+ return this.shape;
846
+ }
847
+ shape;
645
848
  /**
646
849
  * What the gateway said it can do, as of the last catalog fetch.
647
850
  *
@@ -657,7 +860,7 @@ var init_client = __esm({
657
860
  }
658
861
  seen = { web_search: false };
659
862
  async wallet(signal) {
660
- 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 });
661
864
  if (res.status === 401) throw await authFailure(res, "wallet");
662
865
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
663
866
  return res.json();
@@ -693,7 +896,7 @@ var init_client = __esm({
693
896
  */
694
897
  async sponsorConfig(signal) {
695
898
  try {
696
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
899
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/config`, {
697
900
  headers: this.headers(),
698
901
  ...signal ? { signal } : {}
699
902
  });
@@ -701,11 +904,13 @@ var init_client = __esm({
701
904
  const body = await res.json();
702
905
  const publisherId = typeof body.publisher_id === "string" ? body.publisher_id.trim() : "";
703
906
  if (!publisherId) return null;
907
+ const formats = extraFormatIds(body.formats);
704
908
  return {
705
909
  publisherId,
706
910
  ...typeof body.category === "string" ? { category: body.category } : {},
707
911
  ...typeof body.subject === "string" ? { subject: body.subject } : {},
708
- ...typeof body.dwell_ms === "number" ? { dwellMs: body.dwell_ms } : {}
912
+ ...typeof body.dwell_ms === "number" ? { dwellMs: body.dwell_ms } : {},
913
+ ...Object.keys(formats).length > 0 ? { formats } : {}
709
914
  };
710
915
  } catch {
711
916
  return null;
@@ -729,12 +934,13 @@ var init_client = __esm({
729
934
  */
730
935
  async reportImpression(report, signal) {
731
936
  try {
732
- const res = await fetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
937
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/impression`, {
733
938
  method: "POST",
734
939
  headers: this.headers(),
735
940
  ...signal ? { signal } : {},
736
941
  body: JSON.stringify({
737
942
  impression_id: report.impressionId,
943
+ ...report.format && report.format !== "top_bar" ? { format: report.format } : {},
738
944
  ...report.adId ? { ad_id: report.adId } : {},
739
945
  billable: report.billable,
740
946
  from_cache: report.fromCache,
@@ -761,7 +967,7 @@ var init_client = __esm({
761
967
  const url = new URL(`${this.config.gatewayUrl}/v1/web/search`);
762
968
  url.searchParams.set("q", query);
763
969
  if (opts.count) url.searchParams.set("count", String(opts.count));
764
- const res = await fetch(url, {
970
+ const res = await gatewayFetch(url, {
765
971
  headers: this.headers(),
766
972
  ...opts.signal ? { signal: opts.signal } : {}
767
973
  });
@@ -801,7 +1007,7 @@ var init_client = __esm({
801
1007
  /** One POST to the chat route, in whichever transport is currently in use. */
802
1008
  sendChat(payload, encoded, signal) {
803
1009
  const json = JSON.stringify(payload);
804
- return fetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
1010
+ return gatewayFetch(`${this.config.gatewayUrl}/v1/chat/completions`, {
805
1011
  method: "POST",
806
1012
  headers: encoded ? { ...this.headers(), "Content-Type": CHAT_ENCODED_CONTENT_TYPE } : this.headers(),
807
1013
  body: encoded ? encodeChatBody(json) : json,
@@ -1132,12 +1338,13 @@ function saveConfig(config) {
1132
1338
  function configPath() {
1133
1339
  return CONFIG_PATH;
1134
1340
  }
1135
- var CONFIG_PATH, QUARANTINE_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS, baseline, envApplied, notices;
1341
+ var CONFIG_PATH, QUARANTINE_PATH, FALLBACK_MODEL, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS, baseline, envApplied, notices;
1136
1342
  var init_config = __esm({
1137
1343
  "src/config.ts"() {
1138
1344
  "use strict";
1139
1345
  CONFIG_PATH = join(homedir(), ".clixad", "config.json");
1140
1346
  QUARANTINE_PATH = `${CONFIG_PATH}.corrupt`;
1347
+ FALLBACK_MODEL = "gemini-2.5-flash-lite";
1141
1348
  DEFAULTS = {
1142
1349
  gatewayUrl: "https://clixad.onrender.com",
1143
1350
  // Same origin as the gateway: the ad wall is served by the gateway process
@@ -1145,7 +1352,7 @@ var init_config = __esm({
1145
1352
  // fields stay separate so either can be pointed elsewhere, but they no longer
1146
1353
  // diverge by default.
1147
1354
  dashboardUrl: "https://clixad.onrender.com",
1148
- model: "gemini-2.5-flash-lite"
1355
+ model: FALLBACK_MODEL
1149
1356
  };
1150
1357
  ENV_OVERRIDES = [
1151
1358
  ["gatewayUrl", "CLIXAD_GATEWAY_URL"],
@@ -1188,6 +1395,12 @@ function parseLoginArgs(args) {
1188
1395
  }
1189
1396
  return { invite: invite?.trim() || void 0, email: rest[0] };
1190
1397
  }
1398
+ function adoptNewAccountModel(config, created, named) {
1399
+ if (!created || typeof named !== "string" || !/^[A-Za-z0-9._\/-]{1,64}$/.test(named)) return void 0;
1400
+ if (config.model !== FALLBACK_MODEL || named === config.model) return void 0;
1401
+ config.model = named;
1402
+ return named;
1403
+ }
1191
1404
  async function performLogin(client, config, args, report, signal) {
1192
1405
  if (!args.email) {
1193
1406
  const start = await client.deviceStart().catch(() => null);
@@ -1218,6 +1431,7 @@ async function githubLogin(client, config, start, invite, report, signal) {
1218
1431
  config.email = poll.email;
1219
1432
  config.login = poll.login;
1220
1433
  config.tokenGateway = config.gatewayUrl;
1434
+ const model = adoptNewAccountModel(config, poll.created, poll.default_model);
1221
1435
  saveConfig(config);
1222
1436
  return {
1223
1437
  status: "ok",
@@ -1226,7 +1440,8 @@ async function githubLogin(client, config, start, invite, report, signal) {
1226
1440
  login: poll.login,
1227
1441
  balance: poll.balance,
1228
1442
  created: poll.created,
1229
- signupBonus: poll.signupBonus
1443
+ signupBonus: poll.signupBonus,
1444
+ ...model ? { model } : {}
1230
1445
  }
1231
1446
  };
1232
1447
  }
@@ -1249,20 +1464,24 @@ async function devLogin(client, config, email, invite) {
1249
1464
  config.email = res.email;
1250
1465
  config.tokenGateway = config.gatewayUrl;
1251
1466
  delete config.login;
1467
+ const model = adoptNewAccountModel(config, true, res.default_model);
1252
1468
  saveConfig(config);
1253
- return { status: "ok", account: { email: res.email, balance: res.balance, created: true } };
1469
+ return {
1470
+ status: "ok",
1471
+ account: { email: res.email, balance: res.balance, created: true, ...model ? { model } : {} }
1472
+ };
1254
1473
  }
1255
1474
  function sleep2(ms, signal) {
1256
- return new Promise((resolve2) => {
1475
+ return new Promise((resolve3) => {
1257
1476
  if (!signal) {
1258
- setTimeout(resolve2, ms);
1477
+ setTimeout(resolve3, ms);
1259
1478
  return;
1260
1479
  }
1261
- if (signal.aborted) return resolve2();
1480
+ if (signal.aborted) return resolve3();
1262
1481
  const done = () => {
1263
1482
  clearTimeout(timer);
1264
1483
  signal.removeEventListener("abort", done);
1265
- resolve2();
1484
+ resolve3();
1266
1485
  };
1267
1486
  const timer = setTimeout(done, ms);
1268
1487
  signal.addEventListener("abort", done, { once: true });
@@ -1283,6 +1502,49 @@ and the Privacy Policy \u2014 ${PRIVACY_URL}`;
1283
1502
  }
1284
1503
  });
1285
1504
 
1505
+ // src/free-tier.ts
1506
+ function freeModelToName(free, current) {
1507
+ const left = (id2) => free.turns_left[id2] ?? 0;
1508
+ const usable = (id2) => id2 !== void 0 && free.models.includes(id2) && left(id2) > 0;
1509
+ const id = usable(current) ? current : usable(free.recommended) ? free.recommended : free.models.find(usable);
1510
+ return id === void 0 ? void 0 : { id, turns: left(id) };
1511
+ }
1512
+ function freeTierNotice(o) {
1513
+ const free = o.free;
1514
+ if (!free || !(free.daily_credits > 0)) return void 0;
1515
+ const pick = free.credits_left > 0 ? freeModelToName(free, o.model) : void 0;
1516
+ if (!pick) {
1517
+ return o.balance <= 0 ? "Your balance is empty and today's free turns are used up. They come back at midnight UTC, and /earn tops up before then." : void 0;
1518
+ }
1519
+ if (pick.id === o.model) {
1520
+ return `${pick.id} is free today, ${turns(pick.turns)} left. /model shows the other free models.`;
1521
+ }
1522
+ if (o.balance <= 0) {
1523
+ return `Your balance is empty, but ${pick.id} is still free today (${turns(pick.turns)}). /model switches to it, /earn tops up.`;
1524
+ }
1525
+ return `${free.models.length} models are free today. /model switches, and ${pick.id} has ${turns(pick.turns)} left.`;
1526
+ }
1527
+ function freeHelpLine(free) {
1528
+ if (!free || !(free.daily_credits > 0)) return void 0;
1529
+ if (free.credits_left <= 0) return { label: "free models", desc: "used up today, back at midnight UTC" };
1530
+ return {
1531
+ label: "free today",
1532
+ desc: `${free.models.length} models, ${free.credits_left.toLocaleString("en-US")} free credits left \xB7 /model switches`
1533
+ };
1534
+ }
1535
+ function freeAlternativeLine(freeModels, { rerun = true } = {}) {
1536
+ const first = freeModels.find((m) => m.turnsLeft > 0);
1537
+ if (!first) return void 0;
1538
+ return `${first.id} is still free today (${turns(first.turnsLeft)}). Switch with \`clixad model ${first.id}\`${rerun ? " and run it again" : ""}.`;
1539
+ }
1540
+ var turns;
1541
+ var init_free_tier = __esm({
1542
+ "src/free-tier.ts"() {
1543
+ "use strict";
1544
+ turns = (n) => `about ${n.toLocaleString("en-US")} turn${n === 1 ? "" : "s"}`;
1545
+ }
1546
+ });
1547
+
1286
1548
  // src/diff.ts
1287
1549
  function splitLines(text) {
1288
1550
  const lines = text.split("\n");
@@ -2381,7 +2643,7 @@ var init_mcp = __esm({
2381
2643
  if (this.exited) return Promise.reject(new McpError(this.exited));
2382
2644
  const id = ++this.seq;
2383
2645
  const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
2384
- return new Promise((resolve2, reject) => {
2646
+ return new Promise((resolve3, reject) => {
2385
2647
  const settle = (fn) => {
2386
2648
  clearTimeout(timer);
2387
2649
  signal?.removeEventListener("abort", onAbort);
@@ -2395,7 +2657,7 @@ var init_mcp = __esm({
2395
2657
  const onAbort = () => settle(() => reject(new McpError("cancelled")));
2396
2658
  signal?.addEventListener("abort", onAbort, { once: true });
2397
2659
  this.pending.set(id, {
2398
- resolve: (v) => settle(() => resolve2(v)),
2660
+ resolve: (v) => settle(() => resolve3(v)),
2399
2661
  reject: (e) => settle(() => reject(e))
2400
2662
  });
2401
2663
  this.child.stdin?.write(payload, (err) => {
@@ -3989,7 +4251,7 @@ var init_agent = __esm({
3989
4251
  import { spawnSync } from "node:child_process";
3990
4252
  import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
3991
4253
  import { homedir as homedir3, platform } from "node:os";
3992
- import { join as join4 } from "node:path";
4254
+ import { join as join4, parse, resolve as resolve2 } from "node:path";
3993
4255
  function contextFileCandidates(root) {
3994
4256
  return [join4(homedir3(), ".clixad", "CLIXAD.md"), join4(root, "AGENTS.md"), join4(root, "CLIXAD.md")];
3995
4257
  }
@@ -4050,13 +4312,21 @@ function topLevel(root) {
4050
4312
  return void 0;
4051
4313
  }
4052
4314
  }
4053
- var MAX_CONTEXT_FILE_CHARS, INIT_PROMPT;
4315
+ function shouldSuggestInit(root) {
4316
+ const dir = resolve2(root);
4317
+ if (dir === resolve2(homedir3()) || dir === parse(dir).root) return false;
4318
+ if (existsSync3(join4(dir, "CLIXAD.md")) || existsSync3(join4(dir, "AGENTS.md"))) return false;
4319
+ return PROJECT_MARKERS.some((name) => existsSync3(join4(dir, name)));
4320
+ }
4321
+ var MAX_CONTEXT_FILE_CHARS, INIT_PROMPT, INIT_HINT, PROJECT_MARKERS;
4054
4322
  var init_context = __esm({
4055
4323
  "src/context.ts"() {
4056
4324
  "use strict";
4057
4325
  init_agent();
4058
4326
  MAX_CONTEXT_FILE_CHARS = 2e4;
4059
4327
  INIT_PROMPT = "Create a CLIXAD.md file in the workspace root that tells a future coding agent how to work in this project. Explore the repository first (glob, grep, read the README and package manifests). Keep it under 60 lines and cover: what the project is, the layout, how to build/test/run it, and the conventions a newcomer would otherwise get wrong. If CLIXAD.md already exists, improve it instead of replacing it wholesale.";
4328
+ INIT_HINT = "No CLIXAD.md in this project yet. /init writes one, and every later session here starts out knowing the layout.";
4329
+ PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile"];
4060
4330
  }
4061
4331
  });
4062
4332
 
@@ -4150,7 +4420,18 @@ var init_permissions = __esm({
4150
4420
  });
4151
4421
 
4152
4422
  // src/session.ts
4153
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
4423
+ import {
4424
+ closeSync as closeSync2,
4425
+ existsSync as existsSync4,
4426
+ mkdirSync as mkdirSync3,
4427
+ openSync as openSync2,
4428
+ readdirSync as readdirSync3,
4429
+ readFileSync as readFileSync5,
4430
+ readSync,
4431
+ rmSync,
4432
+ statSync as statSync2,
4433
+ writeFileSync as writeFileSync3
4434
+ } from "node:fs";
4154
4435
  import { homedir as homedir4 } from "node:os";
4155
4436
  import { join as join5 } from "node:path";
4156
4437
  function newSessionId() {
@@ -4187,6 +4468,28 @@ function listSessions(cwd, limit = 20) {
4187
4468
  metas.sort((a, b) => b.updated.localeCompare(a.updated));
4188
4469
  return metas.slice(0, limit);
4189
4470
  }
4471
+ function hasSessionIn(cwd) {
4472
+ if (!existsSync4(SESSION_DIR)) return false;
4473
+ const needle = `"cwd":${JSON.stringify(cwd)}`;
4474
+ try {
4475
+ for (const name of readdirSync3(SESSION_DIR)) {
4476
+ if (!name.endsWith(".json")) continue;
4477
+ const path = join5(SESSION_DIR, name);
4478
+ let head = "";
4479
+ const fd = openSync2(path, "r");
4480
+ try {
4481
+ const buf = Buffer.alloc(4096);
4482
+ head = buf.subarray(0, readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
4483
+ } finally {
4484
+ closeSync2(fd);
4485
+ }
4486
+ if (head.includes(needle)) return true;
4487
+ if (!head.includes('"cwd":') && loadSession(name.slice(0, -5))?.cwd === cwd) return true;
4488
+ }
4489
+ } catch {
4490
+ }
4491
+ return false;
4492
+ }
4190
4493
  function latestSession(cwd) {
4191
4494
  const [meta] = listSessions(cwd, 1);
4192
4495
  return meta ? loadSession(meta.id) : void 0;
@@ -4284,14 +4587,14 @@ function runKimiCode(config, task, opts = {}) {
4284
4587
  OPENAI_BASE_URL: `${config.gatewayUrl.replace(/\/+$/, "")}/v1`,
4285
4588
  OPENAI_API_KEY: config.token
4286
4589
  };
4287
- return new Promise((resolve2) => {
4590
+ return new Promise((resolve3) => {
4288
4591
  const child = process.platform === "win32" ? spawn5([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn5(bin, args, { stdio: "inherit", env });
4289
4592
  child.on("error", (err) => {
4290
4593
  if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
4291
4594
  else console.error(`failed to launch Kimi CLI: ${err.message}`);
4292
- resolve2(127);
4595
+ resolve3(127);
4293
4596
  });
4294
- child.on("exit", (code) => resolve2(code ?? 0));
4597
+ child.on("exit", (code) => resolve3(code ?? 0));
4295
4598
  });
4296
4599
  }
4297
4600
  function quoteWin(s) {
@@ -4312,17 +4615,26 @@ var init_kimi = __esm({
4312
4615
  "gpt-5-nano": 4e5,
4313
4616
  "gemini-2.5-flash-lite": 1048576,
4314
4617
  "deepseek-v4-flash": 1048576,
4315
- "gemini-3.1-flash-lite": 1048576,
4316
- "kimi-k2": 131072,
4618
+ "gemini-3.5-flash-lite": 1048576,
4619
+ "kimi-k2.5": 262144,
4620
+ "gemini-3.8-flash": 1048576,
4317
4621
  "kimi-k2.7-code": 262144,
4318
4622
  "claude-haiku-4.5": 2e5,
4319
- "gemini-3.6-flash": 1048576,
4320
4623
  "claude-sonnet-5": 1e6,
4321
- "gpt-5.2": 4e5,
4624
+ "gpt-6-sol": 105e4,
4322
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,
4323
4635
  "claude-opus-5": 1e6,
4324
4636
  "gpt-5.5": 105e4,
4325
- "gpt-5.2-pro": 4e5
4637
+ "gpt-5.2-pro": 105e4
4326
4638
  };
4327
4639
  DEFAULT_CONTEXT_TOKENS = 256e3;
4328
4640
  INSTALL_HELP = `Kimi CLI is not installed (or not on PATH).
@@ -4372,6 +4684,14 @@ function bannerTip(index) {
4372
4684
  const i = index ?? Math.floor(Math.random() * BANNER_TIPS.length);
4373
4685
  return BANNER_TIPS[(i % BANNER_TIPS.length + BANNER_TIPS.length) % BANNER_TIPS.length];
4374
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
+ }
4375
4695
  function earnedLabel(opts) {
4376
4696
  return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
4377
4697
  }
@@ -4381,7 +4701,7 @@ function welcomeBanner(opts) {
4381
4701
  const rightW = Math.min(RIGHT_MAX, Math.max(26, inner - 30));
4382
4702
  const leftW = inner - rightW;
4383
4703
  const cwd = opts.cwd.replace(homedir6(), "~").replace(/\\/g, "/");
4384
- 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))));
4385
4705
  const duck = duckLines();
4386
4706
  const home = truncate(cwd, leftW);
4387
4707
  const hint = bannerTip(opts.tipIndex);
@@ -4407,8 +4727,10 @@ function welcomeBanner(opts) {
4407
4727
  tip("/wallet", "balance & ads"),
4408
4728
  cell("", 0),
4409
4729
  cell(BOLD + MANGO + "Wallet" + R2, 6),
4410
- cell(TEXT + `${opts.balance.toLocaleString("en-US")} credits` + R2, `${opts.balance.toLocaleString("en-US")} credits`.length),
4411
- 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)
4412
4734
  ];
4413
4735
  const rows = Math.max(left.length, right.length);
4414
4736
  const out = [];
@@ -4426,9 +4748,9 @@ function welcomeBanner(opts) {
4426
4748
  }
4427
4749
  function compactBanner(opts) {
4428
4750
  const width = Math.max(1, opts.width ?? MIN_BANNER_WIDTH);
4429
- 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));
4430
4752
  const hint = bannerTip(opts.tipIndex);
4431
- const balance = `${opts.balance.toLocaleString("en-US")} credits`;
4753
+ const balance = balanceLabel(opts);
4432
4754
  const room = width - (4 + stringWidth(hint.key) + 1 + 4 + stringWidth(balance));
4433
4755
  const desc = room >= 3 ? truncate(hint.desc, room) : "";
4434
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);
@@ -4562,6 +4884,34 @@ function readVersion() {
4562
4884
  return "unknown";
4563
4885
  }
4564
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
+ }
4565
4915
  var VERSION, VERSION_LABEL;
4566
4916
  var init_version = __esm({
4567
4917
  "src/version.ts"() {
@@ -4804,6 +5154,16 @@ function sponsorEnabled({ isTTY, env, configEnabled }) {
4804
5154
  function adFitsWidth(cols) {
4805
5155
  return cols >= AD_MIN_COLS;
4806
5156
  }
5157
+ function adFitsFormat(format, cols) {
5158
+ return format === "sidebar_card" ? cols >= SIDEBAR_MIN_COLS : adFitsWidth(cols);
5159
+ }
5160
+ function sidebarCard(line2, cols) {
5161
+ if (!adFitsFormat("sidebar_card", cols)) return null;
5162
+ const text = line2.trim();
5163
+ if (!text) return null;
5164
+ const body = visibleLength(text) <= SIDEBAR_BODY_COLS ? text : `${sliceColumns(text, SIDEBAR_BODY_COLS - 1).trimEnd()}\u2026`;
5165
+ return { rule: SIDEBAR_RULE, label: `${SPONSOR_SIGIL} ${AD_LABEL}`, body };
5166
+ }
4807
5167
  function sponsorRow(line2, cols, origin) {
4808
5168
  const marker = origin === "house" || cols >= AD_FULL_PREFIX_MIN_COLS ? HOUSE_MARKER_ROW : AD_MARKER_ROW;
4809
5169
  const label = origin === "network" ? LABEL_ROW : null;
@@ -4818,7 +5178,7 @@ function adColorEnabled({ isTTY, env }) {
4818
5178
  if (env.CI !== void 0 && env.CI !== "") return false;
4819
5179
  return true;
4820
5180
  }
4821
- var SPONSOR_SIGIL, SPONSOR_MARKER, AD_LABEL, INDENT, HOUSE_MARKER_ROW, AD_MARKER_ROW, LABEL_ROW, MIN_BODY_COLS, SHORT_AD_COLS, AD_MIN_COLS, AD_FULL_PREFIX_MIN_COLS;
5181
+ var SPONSOR_SIGIL, SPONSOR_MARKER, AD_LABEL, INDENT, HOUSE_MARKER_ROW, AD_MARKER_ROW, LABEL_ROW, MIN_BODY_COLS, SHORT_AD_COLS, AD_MIN_COLS, AD_FULL_PREFIX_MIN_COLS, SIDEBAR_BODY_COLS, SIDEBAR_RULE, SIDEBAR_CARD_COLS, SIDEBAR_MIN_COLS;
4822
5182
  var init_render = __esm({
4823
5183
  "src/sponsor/render.ts"() {
4824
5184
  "use strict";
@@ -4835,6 +5195,10 @@ var init_render = __esm({
4835
5195
  SHORT_AD_COLS = 29;
4836
5196
  AD_MIN_COLS = visibleLength(AD_MARKER_ROW) + visibleLength(LABEL_ROW) + SHORT_AD_COLS;
4837
5197
  AD_FULL_PREFIX_MIN_COLS = visibleLength(HOUSE_MARKER_ROW) + visibleLength(LABEL_ROW) + SHORT_AD_COLS;
5198
+ SIDEBAR_BODY_COLS = SHORT_AD_COLS + 9;
5199
+ SIDEBAR_RULE = "\u2502 ";
5200
+ SIDEBAR_CARD_COLS = visibleLength(SIDEBAR_RULE) + SIDEBAR_BODY_COLS;
5201
+ SIDEBAR_MIN_COLS = 50 + SIDEBAR_CARD_COLS;
4838
5202
  }
4839
5203
  });
4840
5204
 
@@ -4845,7 +5209,7 @@ function sponsorLineText(ad) {
4845
5209
  if (!text) return void 0;
4846
5210
  return text.length > MAX_TEXT_CHARS3 ? text.slice(0, MAX_TEXT_CHARS3).trimEnd() : text;
4847
5211
  }
4848
- function createSponsorSlot(config) {
5212
+ function createSponsorSlot(config, format = "top_bar") {
4849
5213
  const slot = new SponsorSlot({
4850
5214
  publisherId: config.publisherId,
4851
5215
  serveOnly: true,
@@ -4868,7 +5232,7 @@ function createSponsorSlot(config) {
4868
5232
  });
4869
5233
  return {
4870
5234
  async serve(cols, signal) {
4871
- if (!adFitsWidth(cols)) return null;
5235
+ if (!adFitsFormat(format, cols)) return null;
4872
5236
  const ad = await slot.next({
4873
5237
  ...config.subject ? { subject: config.subject } : {},
4874
5238
  ...isCategory(config.category) ? { category: config.category } : {}
@@ -4900,9 +5264,11 @@ var init_network = __esm({
4900
5264
  function commandLabel(c2) {
4901
5265
  return `/${c2.name}${c2.args ? ` ${c2.args}` : ""}`;
4902
5266
  }
4903
- function helpText() {
4904
- const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length));
4905
- return COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
5267
+ function helpText(free) {
5268
+ const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length), free ? free.label.length : 0);
5269
+ return (free ? ` ${free.label.padEnd(w + 2)}${free.desc}
5270
+
5271
+ ` : "") + COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
4906
5272
  " @path attach a file to the message (tab completes)",
4907
5273
  " !command run a shell command yourself, no model call",
4908
5274
  " shift+tab cycle permission mode \xB7 esc stop the current turn",
@@ -5387,7 +5753,7 @@ var init_views = __esm({
5387
5753
  // src/tui/app.tsx
5388
5754
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5389
5755
  import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
5390
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5756
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
5391
5757
  function buyDeclarationLines(pack) {
5392
5758
  return [
5393
5759
  `${pack.label} \u2014 ${pack.credits.toLocaleString("en-US")} credits for $${pack.price_usd.toFixed(2)}`,
@@ -5430,7 +5796,19 @@ function useTerminalSize() {
5430
5796
  }, [stdout]);
5431
5797
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5432
5798
  }
5433
- function App({ client, config, wallet, streak, session, initialTask, notices: notices2, sponsorServe }) {
5799
+ function App({
5800
+ client,
5801
+ config,
5802
+ wallet,
5803
+ streak,
5804
+ session,
5805
+ newAccount,
5806
+ initialTask,
5807
+ notices: notices2,
5808
+ sponsorServe,
5809
+ sponsorFormats,
5810
+ initHint
5811
+ }) {
5434
5812
  const { exit } = useApp();
5435
5813
  const { stdout, write: writeToStdout } = useStdout();
5436
5814
  const idRef = useRef(1);
@@ -5447,8 +5825,11 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5447
5825
  text: (cols >= MIN_BANNER_WIDTH ? welcomeBanner : compactBanner)({
5448
5826
  width: cols,
5449
5827
  name: config.login ?? config.email?.split("@")[0],
5828
+ ...newAccount ? { newAccount: true } : {},
5450
5829
  model: config.model,
5451
- 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,
5452
5833
  adsToday: wallet?.ads_today ?? 0,
5453
5834
  earnedUsdToday: wallet?.earned_usd_today ?? 0,
5454
5835
  maxRewardUsd: wallet?.max_reward_usd_per_day ?? 0,
@@ -5462,11 +5843,16 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5462
5843
  const [editor, setEditor] = useState(EMPTY);
5463
5844
  const [busy, setBusy] = useState(false);
5464
5845
  const [live, setLive] = useState(null);
5465
- const [balance, setBalance] = useState(wallet?.balance ?? 0);
5846
+ const [balance, setBalance] = useState(wallet?.balance);
5466
5847
  const [spent, setSpent] = useState(0);
5467
5848
  const [freeTier, setFreeTier] = useState(wallet?.free_tier);
5468
5849
  const [model, setModel2] = useState(config.model);
5850
+ const modelRef = useRef(config.model);
5469
5851
  const [mode, setMode] = useState("normal");
5852
+ const switchModel = useCallback((id) => {
5853
+ modelRef.current = id;
5854
+ setModel2(id);
5855
+ }, []);
5470
5856
  const [hist, setHist] = useState(() => loadHistory());
5471
5857
  const [histIdx, setHistIdx] = useState(-1);
5472
5858
  const [sel, setSel] = useState(0);
@@ -5480,7 +5866,12 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5480
5866
  const [startedAt, setStartedAt] = useState(0);
5481
5867
  const [quitHint, setQuitHint] = useState(false);
5482
5868
  const [busyLabel, setBusyLabel] = useState(WORKING);
5483
- const [sponsor, setSponsor] = useState(null);
5869
+ const [sponsor, setSponsor] = useState(
5870
+ null
5871
+ );
5872
+ const [adSlots, setAdSlots] = useState(
5873
+ {}
5874
+ );
5484
5875
  const messagesRef = useRef(messages);
5485
5876
  messagesRef.current = messages;
5486
5877
  const busyRef = useRef(false);
@@ -5506,10 +5897,16 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5506
5897
  const spinnerRef = useRef(spinnerFrames());
5507
5898
  const runningToolRef = useRef(null);
5508
5899
  const pendingTaskRef = useRef(null);
5900
+ const pendingFreeRef = useRef([]);
5901
+ const pendingGrantRef = useRef(0);
5902
+ const pendingBalanceRef = useRef(void 0);
5509
5903
  const mcpRef = useRef(McpHub.empty());
5510
5904
  const todosRef = useRef(new TodoList());
5511
5905
  const lastTurnRef = useRef(null);
5512
5906
  const freeSpentNoticeRef = useRef(false);
5907
+ const emptyNoticeRef = useRef(false);
5908
+ const initHintedRef = useRef(false);
5909
+ const sessionTurnsRef = useRef(0);
5513
5910
  const turnCountRef = useRef(config.turns ?? 0);
5514
5911
  const deltaBufRef = useRef("");
5515
5912
  const deltaTimerRef = useRef(null);
@@ -5517,36 +5914,54 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5517
5914
  const sponsorIdxRef = useRef(0);
5518
5915
  const sponsorPrevRef = useRef(void 0);
5519
5916
  const sponsorCallRef = useRef(0);
5520
- const pendingImpressionRef = useRef(null);
5521
- const sponsorSlotRef = useRef(null);
5917
+ const pendingImpressionRef = useRef(/* @__PURE__ */ new Map());
5918
+ const drawnRef = useRef(/* @__PURE__ */ new Set());
5919
+ const sponsorSlotRef = useRef(
5920
+ /* @__PURE__ */ new Map()
5921
+ );
5522
5922
  const colsRef = useRef(cols);
5523
5923
  colsRef.current = cols;
5924
+ const modalOpenRef = useRef(false);
5524
5925
  const sponsorConfigRef = useRef(null);
5525
- const remoteSponsorRef = useRef(
5526
- adtentionSource(async (signal) => {
5527
- let served;
5528
- if (sponsorServe) {
5529
- served = await sponsorServe(colsRef.current, signal);
5530
- } else {
5531
- sponsorConfigRef.current ??= client.sponsorConfig(signal);
5532
- const conf = await sponsorConfigRef.current;
5533
- if (!conf) return null;
5534
- sponsorSlotRef.current ??= createSponsorSlot(conf);
5535
- served = await sponsorSlotRef.current.serve(colsRef.current, signal);
5926
+ const serveFormat = async (format, signal) => {
5927
+ let served;
5928
+ if (sponsorServe) {
5929
+ if (format !== "top_bar" && !sponsorFormats?.includes(format)) return null;
5930
+ served = await sponsorServe(colsRef.current, signal, format);
5931
+ } else {
5932
+ sponsorConfigRef.current ??= client.sponsorConfig(signal);
5933
+ const conf = await sponsorConfigRef.current;
5934
+ if (!conf) return null;
5935
+ const publisherId = format === "top_bar" ? conf.publisherId : conf.formats?.[format];
5936
+ if (!publisherId) return null;
5937
+ let slot = sponsorSlotRef.current.get(format);
5938
+ if (!slot) {
5939
+ slot = createSponsorSlot({ ...conf, publisherId }, format);
5940
+ sponsorSlotRef.current.set(format, slot);
5536
5941
  }
5537
- if (!served) return null;
5538
- return {
5539
- id: served.id,
5540
- text: served.text,
5541
- impression: {
5542
- impressionId: served.impressionId,
5543
- adId: served.id,
5544
- billable: served.billable,
5545
- fromCache: served.fromCache,
5546
- credit: served.credit
5547
- }
5548
- };
5549
- })
5942
+ served = await slot.serve(colsRef.current, signal);
5943
+ }
5944
+ if (!served) return null;
5945
+ return {
5946
+ id: served.id,
5947
+ text: served.text,
5948
+ impression: {
5949
+ impressionId: served.impressionId,
5950
+ ...format === "top_bar" ? {} : { format },
5951
+ adId: served.id,
5952
+ billable: served.billable,
5953
+ fromCache: served.fromCache,
5954
+ credit: served.credit
5955
+ }
5956
+ };
5957
+ };
5958
+ const remoteSponsorRef = useRef(
5959
+ Object.fromEntries(
5960
+ ["top_bar", ...EXTRA_AD_FORMATS].map((format) => [
5961
+ format,
5962
+ adtentionSource((signal) => serveFormat(format, signal))
5963
+ ])
5964
+ )
5550
5965
  );
5551
5966
  const tallyRef = useRef(createTally());
5552
5967
  const heightCacheRef = useRef(/* @__PURE__ */ new Map());
@@ -5586,19 +6001,66 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5586
6001
  deltaBufRef.current = "";
5587
6002
  }, []);
5588
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
+ }, []);
5589
6013
  useEffect(() => {
5590
6014
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5591
6015
  for (const text2 of notices2 ?? []) push({ kind: "notice", tone: "warn", text: ` ${text2}` });
5592
6016
  const files = contextRef.current.files;
5593
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
+ }
5594
6030
  const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
5595
6031
  if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
6032
+ const freeLine = freeTierNotice({ model: config.model, balance: wallet?.balance ?? 0, free: wallet?.free_tier });
6033
+ if (freeLine) {
6034
+ push({ kind: "notice", tone: (wallet?.balance ?? 0) <= 0 ? "warn" : "good", text: ` ${freeLine}` });
6035
+ }
5596
6036
  if (session?.messages.length) {
5597
6037
  push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
5598
6038
  }
6039
+ if (initHint?.returning) {
6040
+ initHintedRef.current = true;
6041
+ push({ kind: "notice", text: ` ${INIT_HINT}` });
6042
+ }
5599
6043
  client.models().then((m) => {
5600
6044
  catalogRef.current = m;
5601
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
+ }
5602
6064
  }).catch(() => void 0);
5603
6065
  }, []);
5604
6066
  useEffect(() => {
@@ -5645,6 +6107,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5645
6107
  configEnabled: config.sponsor
5646
6108
  })) {
5647
6109
  setSponsor(null);
6110
+ setAdSlots({});
5648
6111
  return;
5649
6112
  }
5650
6113
  const est = catalogRef.current.find((m) => m.id === modelId)?.est_ads_per_task;
@@ -5660,31 +6123,47 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5660
6123
  } else {
5661
6124
  setSponsor(null);
5662
6125
  }
5663
- if (!adFitsWidth(colsRef.current)) return;
5664
- void remoteSponsorRef.current.fetch().then((line2) => {
5665
- if (!line2 || call !== sponsorCallRef.current) return;
5666
- if (!sponsorRow(line2.text, colsRef.current, "network")) return;
5667
- sponsorPrevRef.current = line2.id;
5668
- recordImpression(tallyRef.current, line2.id);
5669
- setSponsor({ text: line2.text, origin: "network" });
5670
- pendingImpressionRef.current = { call, impression: line2.impression };
5671
- });
6126
+ for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
6127
+ if (!adFitsFormat(format, colsRef.current)) continue;
6128
+ void remoteSponsorRef.current[format].fetch().then((line2) => {
6129
+ if (!line2 || call !== sponsorCallRef.current) return;
6130
+ if (!adFitsFormat(format, colsRef.current)) return;
6131
+ if (format !== "top_bar" && modalOpenRef.current) return;
6132
+ if (format === "top_bar") {
6133
+ if (!sponsorRow(line2.text, colsRef.current, "network")) return;
6134
+ sponsorPrevRef.current = line2.id;
6135
+ setSponsor({ text: line2.text, origin: "network", impressionId: line2.impression.impressionId });
6136
+ } else {
6137
+ setAdSlots((slots) => ({
6138
+ ...slots,
6139
+ [format]: { text: line2.text, impressionId: line2.impression.impressionId }
6140
+ }));
6141
+ }
6142
+ recordImpression(tallyRef.current, line2.id);
6143
+ pendingImpressionRef.current.set(format, { call, impression: line2.impression });
6144
+ });
6145
+ }
5672
6146
  },
5673
6147
  [config.sponsor, stdout, wallet]
5674
6148
  );
5675
6149
  const reportSponsor = useCallback(() => {
5676
- const pending = pendingImpressionRef.current;
5677
- pendingImpressionRef.current = null;
5678
- if (!pending || pending.call !== sponsorCallRef.current) return;
5679
- void client.reportImpression(pending.impression);
6150
+ const pending = [...pendingImpressionRef.current.values()];
6151
+ const drawn = drawnRef.current;
6152
+ pendingImpressionRef.current = /* @__PURE__ */ new Map();
6153
+ drawnRef.current = /* @__PURE__ */ new Set();
6154
+ const reports = pending.filter((p) => p.call === sponsorCallRef.current).filter((p) => drawn.has(p.impression.impressionId)).map((p) => p.impression);
6155
+ if (reports.length === 0) return;
6156
+ void (async () => {
6157
+ for (const report of reports) await client.reportImpression(report);
6158
+ })();
5680
6159
  }, [client]);
5681
6160
  const askUser = useCallback(
5682
- (req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
6161
+ (req) => new Promise((resolve3) => setAsk({ req, resolve: resolve3 })),
5683
6162
  []
5684
6163
  );
5685
6164
  const permit = useMemo(() => createPermit(() => permRef.current, askUser), [askUser]);
5686
6165
  const approvePlan = useCallback(
5687
- (proposed) => new Promise((resolve2) => {
6166
+ (proposed) => new Promise((resolve3) => {
5688
6167
  setPlan({
5689
6168
  plan: proposed,
5690
6169
  resolve: (decision) => {
@@ -5692,7 +6171,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5692
6171
  permRef.current.mode = decision;
5693
6172
  setMode(decision);
5694
6173
  }
5695
- resolve2(decision);
6174
+ resolve3(decision);
5696
6175
  }
5697
6176
  });
5698
6177
  }),
@@ -5704,7 +6183,7 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5704
6183
  setMode(next);
5705
6184
  }, []);
5706
6185
  const askQuestion = useCallback(
5707
- (req) => new Promise((resolve2) => {
6186
+ (req) => new Promise((resolve3) => {
5708
6187
  setPickerSel(0);
5709
6188
  setPicker({
5710
6189
  title: req.question,
@@ -5716,10 +6195,10 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5716
6195
  })),
5717
6196
  onPick: (choice) => {
5718
6197
  push({ kind: "notice", tone: "good", text: ` answered: ${choice.value}` });
5719
- resolve2(choice.value);
6198
+ resolve3(choice.value);
5720
6199
  },
5721
6200
  onCancel: () => {
5722
- resolve2("nothing \u2014 the user dismissed the question. Do not ask it again.");
6201
+ resolve3("nothing \u2014 the user dismissed the question. Do not ask it again.");
5723
6202
  stopCurrent();
5724
6203
  }
5725
6204
  });
@@ -5798,17 +6277,19 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5798
6277
  );
5799
6278
  const runTurn2 = useCallback(
5800
6279
  async (task, images = []) => {
6280
+ pendingFreeRef.current = [];
6281
+ const model2 = modelRef.current;
5801
6282
  const ac = new AbortController();
5802
6283
  abortRef.current = ac;
5803
6284
  setBusy(true);
5804
6285
  setStartedAt(Date.now());
5805
6286
  setLive({ text: "" });
5806
- nextSponsor(model);
6287
+ nextSponsor(model2);
5807
6288
  let history = messagesRef.current;
5808
6289
  try {
5809
- const window = contextWindow(model);
6290
+ const window = contextWindow(model2);
5810
6291
  if (shouldCompact([{ role: "system", content: contextRef.current.systemPrompt }, ...history], window)) {
5811
- const res = await compact(client, model, [
6292
+ const res = await compact(client, model2, [
5812
6293
  { role: "system", content: contextRef.current.systemPrompt },
5813
6294
  ...history
5814
6295
  ], { signal: ac.signal });
@@ -5819,12 +6300,12 @@ function App({ client, config, wallet, streak, session, initialTask, notices: no
5819
6300
  setSpent((s) => s + res.creditsCharged);
5820
6301
  push({
5821
6302
  kind: "notice",
5822
- 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)`
5823
6304
  });
5824
6305
  }
5825
6306
  }
5826
6307
  const result = await runAgent(client, task, {
5827
- model,
6308
+ model: model2,
5828
6309
  root,
5829
6310
  history,
5830
6311
  attachments: images,
@@ -5866,7 +6347,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5866
6347
  started: sessionRef.current.started,
5867
6348
  updated: (/* @__PURE__ */ new Date()).toISOString(),
5868
6349
  cwd: root,
5869
- model,
6350
+ model: model2,
5870
6351
  title: sessionRef.current.title,
5871
6352
  messages: next
5872
6353
  });
@@ -5882,8 +6363,10 @@ ${NO_SEARCH_INSTRUCTION}`),
5882
6363
  text: result.content,
5883
6364
  meta: turnMeta(result)
5884
6365
  });
5885
- if (freeTier?.models.includes(model) && result.creditsWaived === 0 && result.creditsCharged > 0 && (result.freeCreditsLeft ?? freeTier.credits_left) === 0 && !freeSpentNoticeRef.current) {
6366
+ let spentNotice = false;
6367
+ if (freeTier?.models.includes(model2) && result.creditsWaived === 0 && result.creditsCharged > 0 && (result.freeCreditsLeft ?? freeTier.credits_left) === 0 && !freeSpentNoticeRef.current) {
5886
6368
  freeSpentNoticeRef.current = true;
6369
+ spentNotice = true;
5887
6370
  push({
5888
6371
  kind: "notice",
5889
6372
  tone: "warn",
@@ -5891,9 +6374,18 @@ ${NO_SEARCH_INSTRUCTION}`),
5891
6374
  The free allowance resets at midnight UTC. /earn tops up before then.`
5892
6375
  });
5893
6376
  }
6377
+ if (result.creditsCharged > 0 && result.balance <= 0 && !spentNotice && !emptyNoticeRef.current) {
6378
+ emptyNoticeRef.current = true;
6379
+ const on = model2;
6380
+ void client.wallet().then((w) => {
6381
+ setFreeTier(w.free_tier);
6382
+ const line2 = freeTierNotice({ model: on, balance: w.balance, free: w.free_tier });
6383
+ if (line2) push({ kind: "notice", tone: "warn", text: ` ${line2}` });
6384
+ }).catch(() => void 0);
6385
+ }
5894
6386
  lastTurnRef.current = {
5895
6387
  turn: turnCountRef.current,
5896
- model,
6388
+ model: model2,
5897
6389
  prompt: task,
5898
6390
  response: result.content,
5899
6391
  steps: result.steps,
@@ -5906,18 +6398,40 @@ ${NO_SEARCH_INSTRUCTION}`),
5906
6398
  turnCountRef.current += 1;
5907
6399
  config.turns = turnCountRef.current;
5908
6400
  saveConfig(config);
6401
+ sessionTurnsRef.current += 1;
6402
+ if (initHint && !initHintedRef.current && sessionTurnsRef.current >= INIT_HINT_AFTER_TURNS && shouldSuggestInit(root)) {
6403
+ initHintedRef.current = true;
6404
+ push({ kind: "notice", text: ` ${INIT_HINT}` });
6405
+ }
5909
6406
  }
5910
6407
  } catch (err) {
5911
6408
  if (err instanceof PaywallError) {
5912
6409
  pendingTaskRef.current = { task, images };
6410
+ pendingFreeRef.current = err.freeModels;
6411
+ pendingGrantRef.current = err.grantPerAd;
6412
+ pendingBalanceRef.current = err.balance;
6413
+ const free = err.freeModels[0];
5913
6414
  push({
5914
6415
  kind: "notice",
5915
6416
  tone: "warn",
5916
- text: ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
6417
+ text: free ? ` ${shortfallHeadline(err, model2)}
6418
+ Press enter \u2014 ${free.id} is still free today (~${free.turnsLeft} turn${free.turnsLeft === 1 ? "" : "s"} left), or take an offer instead.
6419
+ Your request is kept either way, so you will not have to type it again.
6420
+ Offers you are screened out of pay nothing; that is normal, just start another.
6421
+ /buy is there too, if you would rather pay than do an offer.` : ` ${shortfallHeadline(err, model2)}
5917
6422
  Press enter to open the ad wall \u2014 offers pay ${grantLabel(err.grantPerAd)}.
5918
6423
  I'll continue automatically. Offers you are screened out of pay nothing;
5919
6424
  that is normal, just start another.
5920
- /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.`
5921
6435
  });
5922
6436
  } else if (err instanceof AuthError) {
5923
6437
  push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
@@ -5932,6 +6446,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5932
6446
  reportSponsor();
5933
6447
  sponsorCallRef.current++;
5934
6448
  setSponsor(null);
6449
+ setAdSlots({});
5935
6450
  }
5936
6451
  },
5937
6452
  [
@@ -6002,10 +6517,21 @@ ${NO_SEARCH_INSTRUCTION}`),
6002
6517
  tone: "good",
6003
6518
  text: ` signed in as ${account.login ?? account.email ?? "you"} \xB7 balance ${account.balance.toLocaleString("en-US")} credits` + (account.created ? " (signup bonus)" : "")
6004
6519
  });
6520
+ if (account.model) {
6521
+ switchModel(account.model);
6522
+ push({ kind: "notice", text: ` model \u2192 ${account.model} (the default for new accounts)` });
6523
+ }
6005
6524
  const claimed = await client.claimStreak(signal);
6006
6525
  const line2 = claimed?.claimed ? streakLabel(claimed) : null;
6007
6526
  if (claimed?.claimed) setBalance(claimed.balance);
6008
6527
  if (line2) push({ kind: "notice", tone: "good", text: ` ${line2}` });
6528
+ const w = await client.wallet(signal).catch(() => void 0);
6529
+ if (w) {
6530
+ setBalance(w.balance);
6531
+ setFreeTier(w.free_tier);
6532
+ const freeLine = freeTierNotice({ model: modelRef.current, balance: w.balance, free: w.free_tier });
6533
+ if (freeLine) push({ kind: "notice", tone: w.balance <= 0 ? "warn" : "good", text: ` ${freeLine}` });
6534
+ }
6009
6535
  return;
6010
6536
  }
6011
6537
  case "closed":
@@ -6032,7 +6558,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6032
6558
  const runAdWall = useCallback(async () => {
6033
6559
  const pending = pendingTaskRef.current;
6034
6560
  pendingTaskRef.current = null;
6035
- const before = balance;
6561
+ let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
6036
6562
  const code = await client.handoffCode();
6037
6563
  const base = config.dashboardUrl.replace(/\/+$/, "");
6038
6564
  openBrowser(code ? `${base}/?c=${encodeURIComponent(code)}` : base);
@@ -6050,7 +6576,10 @@ ${NO_SEARCH_INSTRUCTION}`),
6050
6576
  await sleep4(3e3, signal);
6051
6577
  if (signal.aborted) break;
6052
6578
  const w = await client.wallet(signal).catch(() => void 0);
6053
- 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) {
6054
6583
  setBalance(w.balance);
6055
6584
  credited = true;
6056
6585
  }
@@ -6073,6 +6602,46 @@ ${NO_SEARCH_INSTRUCTION}`),
6073
6602
  push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
6074
6603
  if (pending) await runTurn2(pending.task, pending.images);
6075
6604
  }, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
6605
+ const runPaywallChoice = useCallback(() => {
6606
+ const free = pendingFreeRef.current.filter((m) => m.id !== EARN_ROW);
6607
+ if (free.length === 0) return void runAdWall();
6608
+ const items = [
6609
+ ...free.map((m) => ({
6610
+ value: m.id,
6611
+ label: m.id,
6612
+ hint: `free: ~${m.turnsLeft} turn${m.turnsLeft === 1 ? "" : "s"} left today`,
6613
+ current: m.id === model
6614
+ })),
6615
+ {
6616
+ value: EARN_ROW,
6617
+ label: "earn credits instead",
6618
+ // The wall keeps its figure here: this row is the one that costs time,
6619
+ // and what it pays is the only thing that makes the time worth it.
6620
+ hint: `opens the ad wall \u2014 offers pay ${grantLabel(pendingGrantRef.current)}`
6621
+ }
6622
+ ];
6623
+ setPickerSel(0);
6624
+ setPicker({
6625
+ title: (pendingBalanceRef.current ?? 0) > 0 ? "Not enough credits" : "Out of credits",
6626
+ subtitle: "Pick a free model to carry on with, or earn credits at the wall.",
6627
+ items,
6628
+ onPick: (choice) => {
6629
+ if (choice.value === EARN_ROW) return void runAdWall();
6630
+ const pending = pendingTaskRef.current;
6631
+ pendingTaskRef.current = null;
6632
+ pendingFreeRef.current = [];
6633
+ config.model = choice.value;
6634
+ saveConfig(config);
6635
+ switchModel(choice.value);
6636
+ push({
6637
+ kind: "notice",
6638
+ tone: "good",
6639
+ text: ` model \u2192 ${choice.value} (free today) \u2014 continuing where you left off`
6640
+ });
6641
+ if (pending) void runTurn2(pending.task, pending.images);
6642
+ }
6643
+ });
6644
+ }, [config, model, push, runAdWall, runTurn2, switchModel]);
6076
6645
  const runBuy = useCallback(async () => {
6077
6646
  await runBusy(LOADING, async (signal) => {
6078
6647
  const { enabled, packs } = await client.creditPacks(signal);
@@ -6104,7 +6673,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6104
6673
  }, [client, push, runBusy]);
6105
6674
  const runCheckout = useCallback(
6106
6675
  async (pack) => {
6107
- const before = balance;
6676
+ let before = balance ?? (await client.wallet().catch(() => void 0))?.balance;
6108
6677
  let credited = false;
6109
6678
  let stopped = false;
6110
6679
  await runBusy(WAITING_FOR_PAYMENT, async (signal) => {
@@ -6128,7 +6697,10 @@ ${NO_SEARCH_INSTRUCTION}`),
6128
6697
  await sleep4(3e3, signal);
6129
6698
  if (signal.aborted) break;
6130
6699
  const w = await client.wallet(signal).catch(() => void 0);
6131
- 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) {
6132
6704
  setBalance(w.balance);
6133
6705
  credited = true;
6134
6706
  }
@@ -6160,7 +6732,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6160
6732
  exit();
6161
6733
  return;
6162
6734
  case "help":
6163
- push({ kind: "notice", text: helpText() });
6735
+ push({ kind: "notice", text: helpText(freeHelpLine(freeTier)) });
6164
6736
  return;
6165
6737
  // Clears both halves of what "clear" means: the model's context *and*
6166
6738
  // the screen. It used to drop only the context, so nothing visibly
@@ -6211,11 +6783,12 @@ ${NO_SEARCH_INSTRUCTION}`),
6211
6783
  setSpent((s) => s + res.creditsCharged);
6212
6784
  push({
6213
6785
  kind: "notice",
6214
- 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)`
6215
6787
  });
6216
6788
  });
6217
6789
  return;
6218
6790
  case "init":
6791
+ initHintedRef.current = true;
6219
6792
  await runTurn2(INIT_PROMPT);
6220
6793
  return;
6221
6794
  // Always the picker — an id you have to remember and type is exactly
@@ -6248,7 +6821,7 @@ ${NO_SEARCH_INSTRUCTION}`),
6248
6821
  onPick: (choice) => {
6249
6822
  config.model = choice.value;
6250
6823
  saveConfig(config);
6251
- setModel2(choice.value);
6824
+ switchModel(choice.value);
6252
6825
  push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
6253
6826
  }
6254
6827
  });
@@ -6391,8 +6964,8 @@ ${NO_SEARCH_INSTRUCTION}`),
6391
6964
  // stop" whenever it is locked.
6392
6965
  case "refer":
6393
6966
  await runBusy(LOADING, async (signal) => {
6394
- const info = await client.referral(signal);
6395
- push({ kind: "notice", text: referLines(info).map((line3) => ` ${line3}`).join("\n") });
6967
+ const info2 = await client.referral(signal);
6968
+ push({ kind: "notice", text: referLines(info2).map((line3) => ` ${line3}`).join("\n") });
6396
6969
  });
6397
6970
  return;
6398
6971
  case "buy":
@@ -6488,7 +7061,7 @@ ${output}` }
6488
7061
  lastTurnRef.current = null;
6489
7062
  const amount = turn.free ? wallet?.rewards?.feedback_credits_free ?? wallet?.rewards?.feedback_credits : wallet?.rewards?.feedback_credits;
6490
7063
  const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
6491
- const choice = await new Promise((resolve2) => {
7064
+ const choice = await new Promise((resolve3) => {
6492
7065
  setPickerSel(0);
6493
7066
  setPicker({
6494
7067
  title: "Was that last answer any good?",
@@ -6498,10 +7071,10 @@ ${output}` }
6498
7071
  { value: "down", label: "no \u2014 that missed", hint: "" },
6499
7072
  { value: "skip", label: "skip", hint: "nothing is sent, nothing is stored" }
6500
7073
  ],
6501
- onPick: (item) => resolve2(item.value),
7074
+ onPick: (item) => resolve3(item.value),
6502
7075
  // Esc and "skip" are the same outcome and must stay that way: a skip
6503
7076
  // that cost something is a prompt people stop reading.
6504
- onCancel: () => resolve2(null)
7077
+ onCancel: () => resolve3(null)
6505
7078
  });
6506
7079
  });
6507
7080
  if (choice !== "up" && choice !== "down") return;
@@ -6752,7 +7325,7 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6752
7325
  if (key.return) {
6753
7326
  if (key.meta || key.shift) return setEditor(newline(editor));
6754
7327
  if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
6755
- if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
7328
+ if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runPaywallChoice();
6756
7329
  return void submit(toText(editor));
6757
7330
  }
6758
7331
  if (key.ctrl && ch === "j") return setEditor(newline(editor));
@@ -6777,6 +7350,10 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6777
7350
  const spinner = spinnerRef.current[Math.floor(tick / SPINNER_TICKS_PER_FRAME) % spinnerRef.current.length];
6778
7351
  const sponsorLine = busy && sponsor ? sponsorRow(sponsor.text, cols, sponsor.origin) : null;
6779
7352
  const adColor = adColorEnabled({ isTTY: stdout.isTTY, env: process.env });
7353
+ const modalOpen = Boolean(picker || ask2 || plan || buy);
7354
+ modalOpenRef.current = modalOpen;
7355
+ const card = busy && !modalOpen && adSlots.sidebar_card ? sidebarCard(adSlots.sidebar_card.text, cols) : null;
7356
+ const statusBarLine = busy && !modalOpen && adSlots.status_bar ? sponsorRow(adSlots.status_bar.text, cols, "network") : null;
6780
7357
  const viewport = Math.max(1, rows - 1);
6781
7358
  const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
6782
7359
  const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
@@ -6798,8 +7375,18 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6798
7375
  const showStatus = grant(2) > 0;
6799
7376
  const queueRows = grant(queuedRows.length) > 0 ? queuedRows : [];
6800
7377
  const showSponsor = grant(sponsorLine ? 1 : 0) > 0;
7378
+ const showStatusBar = grant(statusBarLine && showStatus && !quitHint && menu.length === 0 ? 1 : 0) > 0;
7379
+ const showCard = busyHeight > 0 && card !== null;
6801
7380
  const inputBoxHeight = inputRows + (inputBorder ? 2 : 0);
6802
- const chromeHeight = inputBoxHeight + (showStatus ? 2 : 0) + (showSponsor ? 1 : 0) + queueRows.length;
7381
+ const chromeHeight = inputBoxHeight + (showStatus ? 2 : 0) + (showSponsor ? 1 : 0) + (showStatusBar ? 1 : 0) + queueRows.length;
7382
+ const drawnKey = [
7383
+ showSponsor && sponsorLine?.origin === "network" ? sponsor?.impressionId : void 0,
7384
+ showCard ? adSlots.sidebar_card?.impressionId : void 0,
7385
+ showStatusBar ? adSlots.status_bar?.impressionId : void 0
7386
+ ].filter(Boolean).join(" ");
7387
+ useEffect(() => {
7388
+ for (const id of drawnKey.split(" ")) if (id) drawnRef.current.add(id);
7389
+ }, [drawnKey]);
6803
7390
  let budget = Math.max(0, viewport - chromeHeight - menu.length - busyHeight);
6804
7391
  const pickerCompact = Boolean(picker) && budget < PICKER_CHROME + 1;
6805
7392
  const pickerChrome = pickerCompact ? PICKER_CHROME_COMPACT : PICKER_CHROME;
@@ -6887,15 +7474,31 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6887
7474
  Math.max(1, Math.floor(cols * 0.5)),
6888
7475
  menu.reduce((w, c2) => Math.max(w, visibleLength(c2.label)), 0)
6889
7476
  );
7477
+ const conversationUsed = useMemo(
7478
+ () => conversationTokens([{ role: "system", content: contextRef.current.systemPrompt }, ...messages]),
7479
+ [messages]
7480
+ );
6890
7481
  const contextPct = useMemo(() => {
6891
7482
  const window = contextWindow(model);
6892
7483
  if (!window) return 0;
6893
- const used2 = conversationTokens([
6894
- { role: "system", content: contextRef.current.systemPrompt },
6895
- ...messages
6896
- ]);
6897
- return used2 / window;
6898
- }, [contextWindow, messages, model]);
7484
+ return conversationUsed / window;
7485
+ }, [contextWindow, conversationUsed, model]);
7486
+ const schemaTokens = useMemo(
7487
+ () => estimateTokens(
7488
+ JSON.stringify(toolSchemaFor(mode, mcpRef.current.toolSchemas(), { webSearch: searchRef.current }))
7489
+ ),
7490
+ [mode]
7491
+ );
7492
+ const typed = toText(editor);
7493
+ let estimate;
7494
+ const info = catalogRef.current.find((m) => m.id === model);
7495
+ const shape = client.taskShape;
7496
+ if (!busy && info && shape && balance !== void 0 && typed.trim() !== "" && !/^[/!]/.test(typed.trimStart())) {
7497
+ const range = estimateTask(info, conversationUsed + schemaTokens + estimateTokens(typed), shape);
7498
+ const onFree = Boolean(info.free && freeTier?.models.includes(model));
7499
+ const verdict = taskVerdict(range, { balance, ...onFree ? { freeLeft: freeTier.credits_left } : {} });
7500
+ estimate = estimateClause(range, verdict, !onFree && (freeTier?.credits_left ?? 0) > 0);
7501
+ }
6899
7502
  const modeText = ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}`;
6900
7503
  const statusText = fitRow(
6901
7504
  ` ${statusLine(
@@ -6908,12 +7511,19 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6908
7511
  // Only while a free model is selected: on a premium one the allowance
6909
7512
  // is real but irrelevant, and a countdown that has nothing to do with
6910
7513
  // the next turn is a number that teaches people to stop reading the row.
6911
- ...freeTier?.models.includes(model) ? { freeLeft: freeTier.credits_left } : {}
7514
+ ...freeTier?.models.includes(model) ? { freeLeft: freeTier.credits_left } : {},
7515
+ ...estimate ? { estimate: estimate.variants } : {}
6912
7516
  },
6913
7517
  Math.max(1, cols - visibleLength(modeText) - 2)
6914
7518
  )}`,
6915
7519
  Math.max(0, cols - visibleLength(modeText))
6916
7520
  );
7521
+ const statusParts = (() => {
7522
+ const shown = estimate?.warn ? estimate.variants.find((v) => statusText.includes(v)) : void 0;
7523
+ if (shown === void 0) return { before: statusText, after: "" };
7524
+ const at = statusText.indexOf(shown);
7525
+ return { before: statusText.slice(0, at), warn: shown, after: statusText.slice(at + shown.length) };
7526
+ })();
6917
7527
  const hintText = quitHint ? fitRow(" press ctrl+c again to quit", cols) : ` ${pickHint(
6918
7528
  ["\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close", "\u2191\u2193 \xB7 \u23CE run \xB7 tab \xB7 esc", "\u2191\u2193 \u23CE tab esc"],
6919
7529
  Math.max(1, cols - 2)
@@ -6923,12 +7533,47 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6923
7533
  const room = cols - visibleLength(head);
6924
7534
  return { key: item.value, selected: i === sel, head, hint: room > 1 ? fitRow(item.hint, room) : "" };
6925
7535
  });
6926
- const busyText = fitRow(`${busyLabel} ${elapsed}s \xB7 esc to stop`, Math.max(1, cols - visibleLength(`${spinner} `)));
7536
+ const busyCols = showCard ? cols - SIDEBAR_CARD_COLS : cols;
7537
+ const busyText = fitRow(
7538
+ `${busyLabel} ${elapsed}s \xB7 esc to stop`,
7539
+ Math.max(1, busyCols - visibleLength(`${spinner} `))
7540
+ );
6927
7541
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
6928
7542
  /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }, staticEpoch),
6929
7543
  spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
6930
7544
  liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
6931
- busyHeight > 0 ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7545
+ busyHeight > 0 && showCard && card ? (
7546
+ /*
7547
+ The working line with the sidebar card beside it. Two columns, two
7548
+ rows: the working line and the blank row under it on the left, the
7549
+ card on the right. The blank row is the one the block below would
7550
+ otherwise have spent on its margin, so the frame is exactly as tall
7551
+ with the card as without it — the input box does not move when a card
7552
+ arrives or goes, and the budget above never had to count it.
7553
+ */
7554
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
7555
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: busyCols, children: [
7556
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7557
+ /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
7558
+ spinner,
7559
+ " "
7560
+ ] }),
7561
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: busyText })
7562
+ ] }),
7563
+ /* @__PURE__ */ jsx2(Text2, { children: " " })
7564
+ ] }),
7565
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: SIDEBAR_CARD_COLS, children: [
7566
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7567
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: card.rule }),
7568
+ /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: card.label })
7569
+ ] }),
7570
+ /* @__PURE__ */ jsxs2(Box2, { children: [
7571
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: card.rule }),
7572
+ /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_TEXT } : { dimColor: true }, wrap: "truncate-end", children: card.body })
7573
+ ] })
7574
+ ] })
7575
+ ] })
7576
+ ) : busyHeight > 0 ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
6932
7577
  /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
6933
7578
  spinner,
6934
7579
  " "
@@ -6968,20 +7613,9 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6968
7613
  pickerCompact ? null : /* @__PURE__ */ jsx2(Box2, { height: 1 }),
6969
7614
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pickerHint })
6970
7615
  ] }) : null,
6971
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
7616
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showCard ? 0 : 1, children: [
6972
7617
  queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: line2 }, i)),
6973
- showSponsor && sponsorLine ? /* @__PURE__ */ jsxs2(Box2, { children: [
6974
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: sponsorLine.marker }),
6975
- sponsorLine.label ? /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: sponsorLine.label }) : null,
6976
- /* @__PURE__ */ jsx2(
6977
- Text2,
6978
- {
6979
- ...adColor && sponsorLine.origin === "network" ? { color: AD_TEXT } : { dimColor: true },
6980
- wrap: "truncate-end",
6981
- children: sponsorLine.body
6982
- }
6983
- )
6984
- ] }) : null,
7618
+ showSponsor && sponsorLine ? /* @__PURE__ */ jsx2(SponsorRowView, { row: sponsorLine, adColor }) : null,
6985
7619
  inputBorder ? /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
6986
7620
  /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
6987
7621
  /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
@@ -6995,18 +7629,30 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
6995
7629
  ] }, row.key)),
6996
7630
  !showStatus ? null : quitHint || menu.length > 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: hintText }) : /* @__PURE__ */ jsxs2(Box2, { children: [
6997
7631
  /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, wrap: "truncate-end", children: modeText }),
6998
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusText })
6999
- ] })
7632
+ statusParts.warn === void 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusText }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
7633
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: statusParts.before }),
7634
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: statusParts.warn }),
7635
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: statusParts.after })
7636
+ ] })
7637
+ ] }),
7638
+ showStatusBar && statusBarLine ? /* @__PURE__ */ jsx2(SponsorRowView, { row: statusBarLine, adColor }) : null
7000
7639
  ] })
7001
7640
  ] });
7002
7641
  }
7642
+ function SponsorRowView({ row, adColor }) {
7643
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
7644
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, wrap: "truncate-end", children: row.marker }),
7645
+ row.label ? /* @__PURE__ */ jsx2(Text2, { ...adColor ? { color: AD_MARK } : { dimColor: true }, wrap: "truncate-end", children: row.label }) : null,
7646
+ /* @__PURE__ */ jsx2(Text2, { ...adColor && row.origin === "network" ? { color: AD_TEXT } : { dimColor: true }, wrap: "truncate-end", children: row.body })
7647
+ ] });
7648
+ }
7003
7649
  function sleep4(ms, signal) {
7004
- return new Promise((resolve2) => {
7005
- if (signal.aborted) return resolve2();
7650
+ return new Promise((resolve3) => {
7651
+ if (signal.aborted) return resolve3();
7006
7652
  const done = () => {
7007
7653
  clearTimeout(timer);
7008
7654
  signal.removeEventListener("abort", done);
7009
- resolve2();
7655
+ resolve3();
7010
7656
  };
7011
7657
  const timer = setTimeout(done, ms);
7012
7658
  signal.addEventListener("abort", done, { once: true });
@@ -7030,14 +7676,14 @@ function renderInput(state, from, rows) {
7030
7676
  function modelHint(m, free) {
7031
7677
  const effort = adsPerTaskLabel(m.est_ads_per_task);
7032
7678
  if (!m.free || !free?.models.includes(m.id)) return effort;
7033
- const turns = free.turns_left[m.id] ?? 0;
7034
- if (turns <= 0) return `free: none left today \xB7 ${effort}`;
7035
- return `free: ~${turns} turn${turns === 1 ? "" : "s"} left \xB7 ${effort}`;
7679
+ const turns2 = free.turns_left[m.id] ?? 0;
7680
+ if (turns2 <= 0) return `free: none left today \xB7 ${effort}`;
7681
+ return `free: ~${turns2} turn${turns2 === 1 ? "" : "s"} left \xB7 ${effort}`;
7036
7682
  }
7037
7683
  function turnMeta(result) {
7038
7684
  const n = (v) => v.toLocaleString("en-US");
7039
7685
  const balance = `balance ${n(result.balance)}`;
7040
- if (result.creditsWaived <= 0) return `[${result.creditsCharged} credits \xB7 ${balance}]`;
7686
+ if (result.creditsWaived <= 0) return `[${n(result.creditsCharged)} credits \xB7 ${balance}]`;
7041
7687
  const left = result.freeCreditsLeft === void 0 ? "" : ` \xB7 ${n(result.freeCreditsLeft)} free credits left`;
7042
7688
  if (result.creditsCharged > 0) {
7043
7689
  return `[free: ${n(result.creditsWaived)} credits \xB7 then ${n(result.creditsCharged)} charged \xB7 ${balance}]`;
@@ -7045,10 +7691,23 @@ function turnMeta(result) {
7045
7691
  return `[free \xB7 ${n(result.creditsWaived)} credits waived${left}]`;
7046
7692
  }
7047
7693
  function statusLine(o, cols) {
7694
+ const phrasings = o.estimate === void 0 ? [] : typeof o.estimate === "string" ? [o.estimate] : [...o.estimate];
7695
+ for (const estimate of [...phrasings, void 0]) {
7696
+ const line2 = statusRow(o, estimate, cols);
7697
+ if (estimate === void 0 || visibleLength(line2) <= cols) return fitRow(line2, cols);
7698
+ }
7699
+ return fitRow(statusRow(o, void 0, cols), cols);
7700
+ }
7701
+ function statusRow(o, estimate, cols) {
7048
7702
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
7049
7703
  const clauses = [
7050
7704
  { text: o.model, rank: KEEP_ALWAYS },
7051
- { 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
+ },
7052
7711
  // Above the context warning, below the balance. It is the second number
7053
7712
  // deciding whether the next turn costs anything, and unlike the spend it is
7054
7713
  // *news* — it only exists while a free model is selected and it counts down.
@@ -7056,6 +7715,7 @@ function statusLine(o, cols) {
7056
7715
  text: o.freeLeft === void 0 ? void 0 : `free ${o.freeLeft.toLocaleString("en-US")}`,
7057
7716
  rank: 4
7058
7717
  },
7718
+ { text: estimate, rank: 5 },
7059
7719
  { text: o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0, rank: 2 },
7060
7720
  { text: burn, rank: 1 },
7061
7721
  {
@@ -7067,7 +7727,7 @@ function statusLine(o, cols) {
7067
7727
  const join7 = (minRank) => clauses.filter((c2) => c2.rank >= minRank).map((c2) => c2.text).join(" \xB7 ");
7068
7728
  let line2 = join7(0);
7069
7729
  for (let minRank = 1; minRank <= 5 && visibleLength(line2) > cols; minRank++) line2 = join7(minRank);
7070
- return fitRow(line2, cols);
7730
+ return line2;
7071
7731
  }
7072
7732
  function windowStart(sel, total, size) {
7073
7733
  if (total <= size) return 0;
@@ -7118,7 +7778,7 @@ function tailLines(text, max) {
7118
7778
  const lines = text.split("\n");
7119
7779
  return lines.length <= max ? text : lines.slice(-max).join("\n");
7120
7780
  }
7121
- var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, WAITING_FOR_PAYMENT, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, BUY_CHROME, BUY_CHROME_COMPACT, ASK_HINT, PLAN_HINT, BUY_HINT, BUY_CONFIRM_WORD, BUY_TYPED_MAX, BUY_TOO_SHORT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
7781
+ var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, WAITING_FOR_PAYMENT, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, BUY_CHROME, BUY_CHROME_COMPACT, ASK_HINT, PLAN_HINT, BUY_HINT, BUY_CONFIRM_WORD, BUY_TYPED_MAX, BUY_TOO_SHORT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, INIT_HINT_AFTER_TURNS, EARN_ROW, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
7122
7782
  var init_app = __esm({
7123
7783
  "src/tui/app.tsx"() {
7124
7784
  "use strict";
@@ -7133,6 +7793,8 @@ var init_app = __esm({
7133
7793
  init_context();
7134
7794
  init_mentions();
7135
7795
  init_compact();
7796
+ init_estimate();
7797
+ init_free_tier();
7136
7798
  init_kimi();
7137
7799
  init_browser();
7138
7800
  init_version();
@@ -7143,6 +7805,7 @@ var init_app = __esm({
7143
7805
  init_counter();
7144
7806
  init_provider();
7145
7807
  init_network();
7808
+ init_formats();
7146
7809
  init_render();
7147
7810
  init_commands();
7148
7811
  init_editor();
@@ -7195,6 +7858,8 @@ var init_app = __esm({
7195
7858
  EXPAND_MAX_LINES = 400;
7196
7859
  FALLBACK_ROWS = 24;
7197
7860
  FALLBACK_COLS = 80;
7861
+ INIT_HINT_AFTER_TURNS = 3;
7862
+ EARN_ROW = "\0earn";
7198
7863
  CONTEXT_NOTICE_AT = 0.6;
7199
7864
  KEEP_ALWAYS = Number.POSITIVE_INFINITY;
7200
7865
  }
@@ -7228,6 +7893,7 @@ var init_tui = __esm({
7228
7893
  init_client();
7229
7894
  init_config();
7230
7895
  init_login();
7896
+ init_free_tier();
7231
7897
  init_agent();
7232
7898
  init_tools();
7233
7899
  init_context();
@@ -7279,7 +7945,7 @@ async function main() {
7279
7945
  for (const text of configNotices) console.error(c.yellow(text));
7280
7946
  switch (cmd) {
7281
7947
  case "login":
7282
- return login(client, config, rest);
7948
+ return void await login(client, config, rest, { nextStep: true });
7283
7949
  case "logout":
7284
7950
  return logout(config);
7285
7951
  case "whoami":
@@ -7343,7 +8009,7 @@ async function main() {
7343
8009
  return repl(client, config, { task: [cmd, ...rest].join(" ").trim(), notices: configNotices });
7344
8010
  }
7345
8011
  }
7346
- async function login(client, config, args) {
8012
+ async function login(client, config, args, opts = {}) {
7347
8013
  const parsed = parseLoginArgs(args);
7348
8014
  for (const line2 of TERMS_NOTICE.split("\n")) console.log(c.dim(` ${line2}`));
7349
8015
  const outcome = await performLogin(client, config, parsed, {
@@ -7366,22 +8032,28 @@ async function login(client, config, args) {
7366
8032
  \u2713 logged in as ${who}`) + suffix);
7367
8033
  const bonus = account.created ? c.dim(account.signupBonus === void 0 ? " (signup bonus)" : ` (signup bonus ${credits(account.signupBonus)})`) : "";
7368
8034
  console.log(` balance: ${c.bold(credits(account.balance))} credits${bonus}`);
8035
+ if (account.model) console.log(` model: ${c.bold(account.model)} ${c.dim("(the default for new accounts)")}`);
7369
8036
  await showStreak(client);
7370
8037
  console.log(c.dim(` token stored in ${configPath()}`));
7371
- 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 };
7372
8043
  }
7373
8044
  case "closed":
7374
8045
  reportSignupClosed(outcome.message, outcome.hadInvite);
7375
8046
  process.exitCode = 1;
7376
- return;
8047
+ return void 0;
7377
8048
  case "timeout":
7378
8049
  console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
7379
- return;
8050
+ return void 0;
7380
8051
  case "aborted":
7381
- return;
8052
+ return void 0;
7382
8053
  default:
7383
8054
  console.log(c.red(`
7384
8055
  login failed: ${outcome.error}`));
8056
+ return void 0;
7385
8057
  }
7386
8058
  }
7387
8059
  function reportSignupClosed(message, hadInvite) {
@@ -7429,8 +8101,8 @@ async function listModels(client) {
7429
8101
  console.log(c.bold("Models (credit prices per 1M tokens)"));
7430
8102
  for (const m of models) {
7431
8103
  const tag = m.ad_fundable ? "" : c.dim(" [paid/BYOK]");
7432
- const turns = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
7433
- const freeTag = !m.free ? "" : turns === void 0 ? c.green(" [free]") : turns > 0 ? c.green(` [free: ~${turns} turn${turns === 1 ? "" : "s"} left today]`) : c.dim(" [free: none left today]");
8104
+ const turns2 = free?.models.includes(m.id) ? free.turns_left[m.id] ?? 0 : void 0;
8105
+ const freeTag = !m.free ? "" : turns2 === void 0 ? c.green(" [free]") : turns2 > 0 ? c.green(` [free: ~${turns2} turn${turns2 === 1 ? "" : "s"} left today]`) : c.dim(" [free: none left today]");
7434
8106
  console.log(
7435
8107
  ` ${c.cyan(m.id.padEnd(22))} ${m.tier.padEnd(8)} in ${String(m.credits_per_mtoken_input).padStart(9)} out ${String(m.credits_per_mtoken_output).padStart(9)} ${adsPerTaskLabel(m.est_ads_per_task)}${tag}${freeTag}`
7436
8108
  );
@@ -7583,7 +8255,9 @@ ${NO_SEARCH_INSTRUCTION}`),
7583
8255
  } catch (err) {
7584
8256
  if (err instanceof PaywallError) {
7585
8257
  console.log(c.yellow(`
7586
- Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8258
+ ${shortfallHeadline(err, config.model)}`));
8259
+ const freeLine = freeAlternativeLine(err.freeModels);
8260
+ if (freeLine) console.log(c.green(freeLine));
7587
8261
  console.log(
7588
8262
  c.yellow(
7589
8263
  `Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantLabel(err.grantPerAd)}.`
@@ -7632,10 +8306,14 @@ async function codeCmd(client, config, rest) {
7632
8306
  const w = await safeWallet(client);
7633
8307
  if (w) {
7634
8308
  console.log(
7635
- 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}`)
7636
8310
  );
7637
8311
  if (w.balance <= 0) {
7638
8312
  console.log(c.yellow(" You're out of credits \u2014 run `clixad earn` first, or the agent will hit a paywall."));
8313
+ const pick = w.free_tier && w.free_tier.credits_left > 0 ? freeModelToName(w.free_tier, config.model) : void 0;
8314
+ if (pick && pick.id !== config.model) {
8315
+ console.log(c.green(` ${freeAlternativeLine([{ id: pick.id, turnsLeft: pick.turns }], { rerun: false })}`));
8316
+ }
7639
8317
  }
7640
8318
  }
7641
8319
  console.log(c.dim(" launching Kimi CLI (Clixad-configured)\u2026\n"));
@@ -7663,8 +8341,9 @@ async function repl(client, config, opts = {}) {
7663
8341
  console.log(c.yellow('Interactive mode needs a terminal. Use `clixad -p "..."` for scripted runs.'));
7664
8342
  return;
7665
8343
  }
8344
+ let newAccount = false;
7666
8345
  if (!config.token) {
7667
- await login(client, config, []);
8346
+ newAccount = Boolean((await login(client, config, []))?.created);
7668
8347
  if (!config.token) return;
7669
8348
  }
7670
8349
  let session;
@@ -7675,8 +8354,9 @@ async function repl(client, config, opts = {}) {
7675
8354
  session = loadSession(opts.resume);
7676
8355
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
7677
8356
  }
7678
- const streak = await client.claimStreak();
7679
- const wallet = await safeWallet(client);
8357
+ const { streak, wallet } = await startupReads(client, config.gatewayUrl);
8358
+ const cwd = process.cwd();
8359
+ const initHint = shouldSuggestInit(cwd) ? { returning: hasSessionIn(cwd) } : void 0;
7680
8360
  clearScreen();
7681
8361
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
7682
8362
  await startTui2({
@@ -7685,23 +8365,32 @@ async function repl(client, config, opts = {}) {
7685
8365
  wallet,
7686
8366
  streak,
7687
8367
  session,
8368
+ newAccount,
7688
8369
  initialTask: opts.task,
7689
- ...opts.notices?.length ? { notices: opts.notices } : {}
8370
+ ...opts.notices?.length ? { notices: opts.notices } : {},
8371
+ ...initHint ? { initHint } : {}
7690
8372
  });
7691
8373
  }
7692
8374
  async function runTurn(client, config, messages) {
8375
+ let started = false;
7693
8376
  try {
7694
- process.stdout.write(c.green("assistant ") + "");
7695
8377
  const result = await client.chatStream(messages, config.model, {
7696
- 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
+ }
7697
8383
  });
8384
+ if (!started) process.stdout.write(c.green("assistant "));
7698
8385
  process.stdout.write("\n");
7699
- 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)}]`));
7700
8387
  return result;
7701
8388
  } catch (err) {
7702
- process.stdout.write("\n");
8389
+ if (started) process.stdout.write("\n");
7703
8390
  if (err instanceof PaywallError) {
7704
- console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
8391
+ console.log(c.yellow(shortfallHeadline(err, config.model)));
8392
+ const freeLine = freeAlternativeLine(err.freeModels);
8393
+ if (freeLine) console.log(c.green(freeLine));
7705
8394
  console.log(
7706
8395
  c.yellow(`Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantLabel(err.grantPerAd)}.`)
7707
8396
  );
@@ -7729,29 +8418,69 @@ async function safeWallet(client) {
7729
8418
  return void 0;
7730
8419
  }
7731
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
+ }
7732
8452
  function printHelp() {
7733
- 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
7734
8479
 
7735
- ${c.cyan("login")} [email] create/attach an account (${c.dim("--invite <code>")} if you have one)
7736
- ${c.cyan("logout")} forget the stored token
7737
- ${c.cyan("whoami")} show account + balance
7738
- ${c.cyan("models")} list models with credit prices
7739
- ${c.cyan("model")} <id> set default model
7740
- ${c.cyan("wallet")} balance, ads today, ledger
7741
- ${c.cyan("earn")} open the ad wall to earn credits
7742
- ${c.cyan("refer")} your invite code, and what it has earned
7743
- ${c.cyan("redeem")} <code> spend a code you were given
7744
- ${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
7745
- ${c.cyan("agent")} "<task>" open the REPL with a task already running
7746
- ${c.cyan("-p")} "<task>" headless agent run (read-only tools)
7747
- ${c.cyan("code")} ["<task>"] launch the full Kimi CLI coding agent on the current dir
7748
- ${c.cyan("--continue")} resume the last session in this directory
7749
- ${c.cyan("--resume")} [id] list saved sessions, or resume one
7750
- ${c.cyan("--version")} print the version and exit
7751
- (no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
7752
- "<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task
8480
+ ${lines.join("\n")}
7753
8481
 
7754
- ${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)")}`);
7755
8484
  }
7756
8485
  main().catch((err) => {
7757
8486
  console.error(c.red(err.message));