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