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