clixad 0.0.1-beta.0 → 0.0.1-beta.1
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 +16 -9
- package/dist/clixad.mjs +289 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
# clixad
|
|
2
2
|
|
|
3
|
-
A terminal coding agent that routes model calls through the Clixad metering gateway.
|
|
3
|
+
A terminal coding agent that routes model calls through the Clixad metering gateway. No
|
|
4
|
+
subscription: you earn credits by completing rewarded offers in your browser and spend them on real
|
|
5
|
+
model API calls.
|
|
4
6
|
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
explicitly:
|
|
7
|
+
**Early beta.** Commands, interfaces and credit mechanics may change between beta versions. Run the
|
|
8
|
+
most recent published build with:
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
|
|
11
|
+
npx clixad
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
or install it globally:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g clixad
|
|
11
18
|
```
|
|
12
19
|
|
|
13
20
|
Requires Node.js >= 20.
|
|
14
21
|
|
|
15
22
|
## Status
|
|
16
23
|
|
|
17
|
-
Pre-release
|
|
18
|
-
|
|
24
|
+
Pre-release, published under the `beta` dist-tag. `npx clixad` resolves to the latest published
|
|
25
|
+
version. Account creation is decided by the gateway, so a fresh install may land on a waitlist until
|
|
26
|
+
signups are open.
|
|
19
27
|
|
|
20
28
|
## License
|
|
21
29
|
|
|
22
|
-
UNLICENSED — all rights reserved.
|
|
23
|
-
modify or distribute are granted.
|
|
30
|
+
UNLICENSED — all rights reserved. No rights to use, copy, modify or distribute are granted.
|
package/dist/clixad.mjs
CHANGED
|
@@ -19,6 +19,28 @@ function adsPerTaskLabel(estAdsPerTask) {
|
|
|
19
19
|
const effort = estAdsPerTask >= 1 ? `~${estAdsPerTask} ads/task` : "<1 ad/task";
|
|
20
20
|
return `${effort} on completion`;
|
|
21
21
|
}
|
|
22
|
+
function grantRangeLabel(range) {
|
|
23
|
+
if (!range) return "credits on completion";
|
|
24
|
+
const [low, high] = range;
|
|
25
|
+
const n = (v) => v.toLocaleString("en-US");
|
|
26
|
+
return low === high ? `${n(low)} credits on completion` : `${n(low)}\u2013${n(high)} credits on completion`;
|
|
27
|
+
}
|
|
28
|
+
function ledgerReasonLabel(reason) {
|
|
29
|
+
return LEDGER_REASONS[reason] ?? reason.replace(/_/g, " ");
|
|
30
|
+
}
|
|
31
|
+
function asSignupClosed(status, data) {
|
|
32
|
+
if (status !== 403) return void 0;
|
|
33
|
+
const d = data;
|
|
34
|
+
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;
|
|
35
|
+
}
|
|
36
|
+
function paywallError(body) {
|
|
37
|
+
return new PaywallError(
|
|
38
|
+
body.balance,
|
|
39
|
+
body.estimated_cost,
|
|
40
|
+
body.grant_per_ad,
|
|
41
|
+
body.grant_per_ad_range
|
|
42
|
+
);
|
|
43
|
+
}
|
|
22
44
|
async function authFailure(res, what) {
|
|
23
45
|
const body = await res.text().catch(() => "");
|
|
24
46
|
const isJson = (res.headers.get("content-type") ?? "").includes("json");
|
|
@@ -59,21 +81,39 @@ async function* parseSSE(body) {
|
|
|
59
81
|
}
|
|
60
82
|
}
|
|
61
83
|
}
|
|
62
|
-
var PaywallError, AuthError, GatewayClient;
|
|
84
|
+
var LEDGER_REASONS, SignupClosedError, PaywallError, AuthError, GatewayClient;
|
|
63
85
|
var init_client = __esm({
|
|
64
86
|
"src/client.ts"() {
|
|
65
87
|
"use strict";
|
|
88
|
+
LEDGER_REASONS = {
|
|
89
|
+
signup_bonus: "signup bonus",
|
|
90
|
+
ad_reward: "offer reward",
|
|
91
|
+
ad_reversal: "offer reward reversed by the provider",
|
|
92
|
+
purchase: "credits purchased",
|
|
93
|
+
usage: "model usage",
|
|
94
|
+
refund: "refund"
|
|
95
|
+
};
|
|
96
|
+
SignupClosedError = class extends Error {
|
|
97
|
+
constructor(message, waitlistUrl) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.waitlistUrl = waitlistUrl;
|
|
100
|
+
this.name = "SignupClosedError";
|
|
101
|
+
}
|
|
102
|
+
waitlistUrl;
|
|
103
|
+
};
|
|
66
104
|
PaywallError = class extends Error {
|
|
67
|
-
constructor(balance, estimatedCost, grantPerAd) {
|
|
105
|
+
constructor(balance, estimatedCost, grantPerAd, grantRange) {
|
|
68
106
|
super("Out of credits");
|
|
69
107
|
this.balance = balance;
|
|
70
108
|
this.estimatedCost = estimatedCost;
|
|
71
109
|
this.grantPerAd = grantPerAd;
|
|
110
|
+
this.grantRange = grantRange;
|
|
72
111
|
this.name = "PaywallError";
|
|
73
112
|
}
|
|
74
113
|
balance;
|
|
75
114
|
estimatedCost;
|
|
76
115
|
grantPerAd;
|
|
116
|
+
grantRange;
|
|
77
117
|
};
|
|
78
118
|
AuthError = class extends Error {
|
|
79
119
|
constructor() {
|
|
@@ -95,13 +135,18 @@ var init_client = __esm({
|
|
|
95
135
|
return h;
|
|
96
136
|
}
|
|
97
137
|
/** Dev-only signup shortcut (production replaces this with GitHub OAuth). */
|
|
98
|
-
async signupDev(email) {
|
|
138
|
+
async signupDev(email, invite) {
|
|
99
139
|
const res = await fetch(`${this.config.gatewayUrl}/dev/users`, {
|
|
100
140
|
method: "POST",
|
|
101
141
|
headers: this.headers(false),
|
|
102
|
-
body: JSON.stringify(email ? { email } : {})
|
|
142
|
+
body: JSON.stringify({ ...email ? { email } : {}, ...invite ? { invite } : {} })
|
|
103
143
|
});
|
|
104
|
-
if (!res.ok)
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const data = await res.json().catch(() => null);
|
|
146
|
+
const closed = asSignupClosed(res.status, data);
|
|
147
|
+
if (closed) throw new SignupClosedError(closed.message, closed.waitlist_url);
|
|
148
|
+
throw new Error(`signup failed: ${res.status} ${JSON.stringify(data ?? "")}`);
|
|
149
|
+
}
|
|
105
150
|
return res.json();
|
|
106
151
|
}
|
|
107
152
|
/** Begin GitHub device-flow login. Returns null if the gateway has no GitHub
|
|
@@ -115,14 +160,18 @@ var init_client = __esm({
|
|
|
115
160
|
if (!res.ok) throw new Error(`device start failed: ${res.status} ${await res.text()}`);
|
|
116
161
|
return res.json();
|
|
117
162
|
}
|
|
118
|
-
/** Poll once for device-flow completion. */
|
|
119
|
-
async devicePoll(session) {
|
|
163
|
+
/** Poll once for device-flow completion. `invite` unlocks a gated signup. */
|
|
164
|
+
async devicePoll(session, invite) {
|
|
120
165
|
const res = await fetch(`${this.config.gatewayUrl}/v1/auth/device/poll`, {
|
|
121
166
|
method: "POST",
|
|
122
167
|
headers: this.headers(false),
|
|
123
|
-
body: JSON.stringify({ session })
|
|
168
|
+
body: JSON.stringify(invite ? { session, invite } : { session })
|
|
124
169
|
});
|
|
125
170
|
const data = await res.json().catch(() => ({}));
|
|
171
|
+
const closed = asSignupClosed(res.status, data);
|
|
172
|
+
if (closed) {
|
|
173
|
+
return { status: "closed", message: closed.message, waitlistUrl: closed.waitlist_url };
|
|
174
|
+
}
|
|
126
175
|
if (!res.ok && !("status" in data && data.status)) {
|
|
127
176
|
return { status: "error", error: `http ${res.status}` };
|
|
128
177
|
}
|
|
@@ -191,8 +240,7 @@ var init_client = __esm({
|
|
|
191
240
|
});
|
|
192
241
|
if (res.status === 401) throw await authFailure(res, "chat");
|
|
193
242
|
if (res.status === 402) {
|
|
194
|
-
|
|
195
|
-
throw new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
|
|
243
|
+
throw paywallError(await res.json());
|
|
196
244
|
}
|
|
197
245
|
if (!res.ok) throw new Error(`chat failed: ${res.status} ${await res.text()}`);
|
|
198
246
|
const data = await res.json();
|
|
@@ -220,8 +268,7 @@ var init_client = __esm({
|
|
|
220
268
|
});
|
|
221
269
|
if (res.status === 401) throw await authFailure(res, "chat");
|
|
222
270
|
if (res.status === 402) {
|
|
223
|
-
|
|
224
|
-
throw new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
|
|
271
|
+
throw paywallError(await res.json());
|
|
225
272
|
}
|
|
226
273
|
if (!res.ok || !res.body) throw new Error(`chat failed: ${res.status} ${await res.text()}`);
|
|
227
274
|
let content = "";
|
|
@@ -1505,6 +1552,114 @@ var init_compact = __esm({
|
|
|
1505
1552
|
}
|
|
1506
1553
|
});
|
|
1507
1554
|
|
|
1555
|
+
// src/sponsor/counter.ts
|
|
1556
|
+
function createTally() {
|
|
1557
|
+
return { shown: 0, byLine: {} };
|
|
1558
|
+
}
|
|
1559
|
+
function recordImpression(tally, lineId) {
|
|
1560
|
+
tally.shown += 1;
|
|
1561
|
+
tally.byLine[lineId] = (tally.byLine[lineId] ?? 0) + 1;
|
|
1562
|
+
}
|
|
1563
|
+
var init_counter = __esm({
|
|
1564
|
+
"src/sponsor/counter.ts"() {
|
|
1565
|
+
"use strict";
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
// src/sponsor/lines.ts
|
|
1570
|
+
var HOUSE_LINES;
|
|
1571
|
+
var init_lines = __esm({
|
|
1572
|
+
"src/sponsor/lines.ts"() {
|
|
1573
|
+
"use strict";
|
|
1574
|
+
HOUSE_LINES = [
|
|
1575
|
+
{ id: "modes", text: "shift+tab cycles permission modes \u2014 plan mode is read-only" },
|
|
1576
|
+
{ id: "interrupt", text: "esc stops a run mid-turn; tokens already generated are billed" },
|
|
1577
|
+
{ id: "continue", text: "clixad --continue picks up the last session in this directory" },
|
|
1578
|
+
{ id: "sessions", text: "clixad --resume lists your saved sessions" },
|
|
1579
|
+
{ id: "context-files", text: "a CLIXAD.md or AGENTS.md in the repo root joins every prompt" },
|
|
1580
|
+
{ id: "compaction", text: "long sessions compact themselves before the context fills up" },
|
|
1581
|
+
{ id: "headless", text: 'clixad -p "\u2026" runs headless, with read-only tools' },
|
|
1582
|
+
{ id: "model-switch", text: "/model switches model mid-session \u2014 the conversation carries over" },
|
|
1583
|
+
{ id: "help", text: "/help lists every slash command" },
|
|
1584
|
+
// Placed here rather than at the end of the list on purpose: the rotation is
|
|
1585
|
+
// the list, and appending it would put three business-y lines (earn,
|
|
1586
|
+
// screenout, this) back to back at the close of every cycle — the one stretch
|
|
1587
|
+
// that would read as an ad break.
|
|
1588
|
+
{ id: "referral", text: "pass it on: clixad.io" },
|
|
1589
|
+
{ id: "wallet", text: "/wallet shows your balance and what you've earned today" },
|
|
1590
|
+
{ id: "cheapest-first", text: "/model lists cheapest-turn-first" },
|
|
1591
|
+
{ id: "model-cost", text: "current model: {ads_per_task}", needs: "ads_per_task" },
|
|
1592
|
+
{ id: "earn", text: "/earn: offers pay {grant_range}", needs: "grant_range" },
|
|
1593
|
+
{ id: "screenout", text: "a screenout pays nothing and is normal \u2014 just start another" }
|
|
1594
|
+
];
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// src/sponsor/provider.ts
|
|
1599
|
+
function fill(line2, facts) {
|
|
1600
|
+
if (!line2.needs) return line2.text;
|
|
1601
|
+
const value = line2.needs === "grant_range" ? facts.grantRange : facts.adsPerTask;
|
|
1602
|
+
if (!value) return null;
|
|
1603
|
+
return line2.text.replace(`{${line2.needs}}`, value);
|
|
1604
|
+
}
|
|
1605
|
+
function houseSource(lines = HOUSE_LINES) {
|
|
1606
|
+
return {
|
|
1607
|
+
provider: "house",
|
|
1608
|
+
pick(index, previousId, facts) {
|
|
1609
|
+
if (lines.length === 0) return null;
|
|
1610
|
+
const start = (index % lines.length + lines.length) % lines.length;
|
|
1611
|
+
for (let step = 0; step < lines.length; step++) {
|
|
1612
|
+
const line2 = lines[(start + step) % lines.length];
|
|
1613
|
+
if (line2.id === previousId) continue;
|
|
1614
|
+
const text = fill(line2, facts);
|
|
1615
|
+
if (text) return { id: line2.id, text };
|
|
1616
|
+
}
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function sponsorSource(provider = "house") {
|
|
1622
|
+
switch (provider) {
|
|
1623
|
+
case "house":
|
|
1624
|
+
return houseSource();
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
var init_provider = __esm({
|
|
1628
|
+
"src/sponsor/provider.ts"() {
|
|
1629
|
+
"use strict";
|
|
1630
|
+
init_lines();
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// src/sponsor/render.ts
|
|
1635
|
+
function sponsorEnabled({ isTTY, env, configEnabled }) {
|
|
1636
|
+
const flag = env.CLIXAD_SPONSOR?.trim().toLowerCase();
|
|
1637
|
+
if (flag === "0" || flag === "false" || flag === "off" || flag === "no") return false;
|
|
1638
|
+
const forced = flag === "1" || flag === "true" || flag === "on" || flag === "yes";
|
|
1639
|
+
if (!isTTY) return false;
|
|
1640
|
+
if (env.TERM === "dumb") return false;
|
|
1641
|
+
if (forced) return true;
|
|
1642
|
+
if (configEnabled === false) return false;
|
|
1643
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
1644
|
+
if (env.CI !== void 0 && env.CI !== "") return false;
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
function sponsorText(line2, cols) {
|
|
1648
|
+
const indent = " ";
|
|
1649
|
+
const prefix = `${indent}${SPONSOR_MARKER} `;
|
|
1650
|
+
const room = cols - prefix.length;
|
|
1651
|
+
if (room < 12) return null;
|
|
1652
|
+
const body = line2.length <= room ? line2 : `${line2.slice(0, Math.max(0, room - 1)).trimEnd()}\u2026`;
|
|
1653
|
+
return prefix + body;
|
|
1654
|
+
}
|
|
1655
|
+
var SPONSOR_MARKER;
|
|
1656
|
+
var init_render = __esm({
|
|
1657
|
+
"src/sponsor/render.ts"() {
|
|
1658
|
+
"use strict";
|
|
1659
|
+
SPONSOR_MARKER = "\u2726 clixad \xB7";
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1508
1663
|
// src/tui/commands.ts
|
|
1509
1664
|
function commandLabel(c2) {
|
|
1510
1665
|
return `/${c2.name}${c2.args ? ` ${c2.args}` : ""}`;
|
|
@@ -1528,7 +1683,10 @@ var init_commands = __esm({
|
|
|
1528
1683
|
{ name: "models", desc: "list models + prices" },
|
|
1529
1684
|
{ name: "mode", args: "[name]", desc: "permission mode (or press shift+tab)" },
|
|
1530
1685
|
{ name: "wallet", desc: "balance & ads today" },
|
|
1531
|
-
|
|
1686
|
+
// Not "watch an ad": nothing on the wall is a video. CPX serves sign-up
|
|
1687
|
+
// forms, surveys and app trials, and describing the top-up as a video costs
|
|
1688
|
+
// the user the one expectation that makes a screenout make sense.
|
|
1689
|
+
{ name: "earn", desc: "open the offer wall to earn credits" },
|
|
1532
1690
|
{ name: "compact", desc: "summarise the conversation to free context" },
|
|
1533
1691
|
{ name: "clear", desc: "clear the conversation context" },
|
|
1534
1692
|
{ name: "init", desc: "write a CLIXAD.md for this project" },
|
|
@@ -1960,6 +2118,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
1960
2118
|
const [tick, setTick] = useState(0);
|
|
1961
2119
|
const [startedAt, setStartedAt] = useState(0);
|
|
1962
2120
|
const [quitHint, setQuitHint] = useState(false);
|
|
2121
|
+
const [sponsor, setSponsor] = useState(null);
|
|
1963
2122
|
const messagesRef = useRef(messages);
|
|
1964
2123
|
messagesRef.current = messages;
|
|
1965
2124
|
const permRef = useRef(createState("normal"));
|
|
@@ -1973,6 +2132,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
1973
2132
|
const lastOutputRef = useRef("");
|
|
1974
2133
|
const runningToolRef = useRef(null);
|
|
1975
2134
|
const pendingTaskRef = useRef(null);
|
|
2135
|
+
const sponsorRef = useRef(sponsorSource());
|
|
2136
|
+
const sponsorIdxRef = useRef(0);
|
|
2137
|
+
const sponsorPrevRef = useRef(void 0);
|
|
2138
|
+
const tallyRef = useRef(createTally());
|
|
1976
2139
|
const push = useCallback((e) => {
|
|
1977
2140
|
setEntries((prev) => [...prev, { ...e, id: idRef.current++ }]);
|
|
1978
2141
|
}, []);
|
|
@@ -1993,6 +2156,32 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
1993
2156
|
(id) => catalogRef.current.find((m) => m.id === id)?.context_window ?? contextTokensFor(id),
|
|
1994
2157
|
[]
|
|
1995
2158
|
);
|
|
2159
|
+
const nextSponsor = useCallback(
|
|
2160
|
+
(modelId) => {
|
|
2161
|
+
if (!sponsorEnabled({
|
|
2162
|
+
isTTY: process.stdout.isTTY,
|
|
2163
|
+
env: process.env,
|
|
2164
|
+
configEnabled: config.sponsor
|
|
2165
|
+
})) {
|
|
2166
|
+
setSponsor(null);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
const est = catalogRef.current.find((m) => m.id === modelId)?.est_ads_per_task;
|
|
2170
|
+
const facts = {
|
|
2171
|
+
grantRange: wallet?.grant_per_ad_range ? grantRangeLabel(wallet.grant_per_ad_range) : void 0,
|
|
2172
|
+
adsPerTask: est === void 0 ? void 0 : adsPerTaskLabel(est)
|
|
2173
|
+
};
|
|
2174
|
+
const picked = sponsorRef.current.pick(sponsorIdxRef.current++, sponsorPrevRef.current, facts);
|
|
2175
|
+
if (!picked) {
|
|
2176
|
+
setSponsor(null);
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
sponsorPrevRef.current = picked.id;
|
|
2180
|
+
recordImpression(tallyRef.current, picked.id);
|
|
2181
|
+
setSponsor(picked.text);
|
|
2182
|
+
},
|
|
2183
|
+
[config.sponsor, wallet]
|
|
2184
|
+
);
|
|
1996
2185
|
const askUser = useCallback(
|
|
1997
2186
|
(req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
|
|
1998
2187
|
[]
|
|
@@ -2057,6 +2246,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2057
2246
|
setBusy(true);
|
|
2058
2247
|
setStartedAt(Date.now());
|
|
2059
2248
|
setLive({ text: "" });
|
|
2249
|
+
nextSponsor(model);
|
|
2060
2250
|
let history = messagesRef.current;
|
|
2061
2251
|
try {
|
|
2062
2252
|
const window = contextWindow(model);
|
|
@@ -2116,8 +2306,9 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2116
2306
|
kind: "notice",
|
|
2117
2307
|
tone: "warn",
|
|
2118
2308
|
text: ` Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).
|
|
2119
|
-
Press enter to open the ad wall
|
|
2120
|
-
Offers you are screened out of pay nothing;
|
|
2309
|
+
Press enter to open the ad wall \u2014 offers pay ${grantRangeLabel(err.grantRange)}.
|
|
2310
|
+
I'll continue automatically. Offers you are screened out of pay nothing;
|
|
2311
|
+
that is normal, just start another.`
|
|
2121
2312
|
});
|
|
2122
2313
|
} else if (err instanceof AuthError) {
|
|
2123
2314
|
push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
@@ -2128,9 +2319,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2128
2319
|
abortRef.current = null;
|
|
2129
2320
|
setLive(null);
|
|
2130
2321
|
setBusy(false);
|
|
2322
|
+
setSponsor(null);
|
|
2131
2323
|
}
|
|
2132
2324
|
},
|
|
2133
|
-
[client, contextWindow, handleEvent, model, permit, push, root, session?.title]
|
|
2325
|
+
[client, contextWindow, handleEvent, model, nextSponsor, permit, push, root, session?.title]
|
|
2134
2326
|
);
|
|
2135
2327
|
const runAdWall = useCallback(async () => {
|
|
2136
2328
|
const task = pendingTaskRef.current;
|
|
@@ -2276,9 +2468,12 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2276
2468
|
try {
|
|
2277
2469
|
const w = await client.wallet();
|
|
2278
2470
|
setBalance(w.balance);
|
|
2471
|
+
const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
|
|
2472
|
+
const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
|
|
2279
2473
|
push({
|
|
2280
2474
|
kind: "notice",
|
|
2281
|
-
text: ` balance ${w.balance.toLocaleString("en-US")} credits \xB7 ${w.ads_today} offers today \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)} earned`
|
|
2475
|
+
text: ` balance ${w.balance.toLocaleString("en-US")} credits \xB7 ${w.ads_today} offers today \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)} earned` + (reversals.length ? `
|
|
2476
|
+
recently: ${reversed.toLocaleString("en-US")} credits from ${reversals.length} offer${reversals.length === 1 ? "" : "s"} were reversed by the provider. Run \`clixad wallet\` for the full ledger.` : "")
|
|
2282
2477
|
});
|
|
2283
2478
|
} catch (err) {
|
|
2284
2479
|
push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
@@ -2429,6 +2624,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2429
2624
|
});
|
|
2430
2625
|
const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
2431
2626
|
const spinner = SPINNER[tick % SPINNER.length];
|
|
2627
|
+
const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
|
|
2432
2628
|
const liveText = live?.text ? tailLines(live.text, Math.max(4, rows - 12)) : "";
|
|
2433
2629
|
const liveBlock = [
|
|
2434
2630
|
liveText,
|
|
@@ -2439,7 +2635,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2439
2635
|
const pickerHeight = picker ? picker.items.length + 8 : 0;
|
|
2440
2636
|
const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
|
|
2441
2637
|
const inputBoxHeight = 2 + editor.lines.length;
|
|
2442
|
-
const chromeHeight = inputBoxHeight + 2;
|
|
2638
|
+
const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
|
|
2443
2639
|
const liveHeight = liveBlock ? lineCount(liveBlock, cols) + 1 : 0;
|
|
2444
2640
|
const askHeight = ask2 ? lineCount(askBlock, cols) + 2 : 0;
|
|
2445
2641
|
const printed = useMemo(() => entries.reduce((n, e) => n + entryHeight(e, cols), 0), [entries, cols]);
|
|
@@ -2474,6 +2670,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2474
2670
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 choose \xB7 1-9 jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel" })
|
|
2475
2671
|
] }) : null,
|
|
2476
2672
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
2673
|
+
sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
|
|
2477
2674
|
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO_BRIGHT : MANGO3, paddingX: 1, children: [
|
|
2478
2675
|
/* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
|
|
2479
2676
|
busy ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
@@ -2580,6 +2777,9 @@ var init_app = __esm({
|
|
|
2580
2777
|
init_tools();
|
|
2581
2778
|
init_permissions();
|
|
2582
2779
|
init_session();
|
|
2780
|
+
init_counter();
|
|
2781
|
+
init_provider();
|
|
2782
|
+
init_render();
|
|
2583
2783
|
init_commands();
|
|
2584
2784
|
init_editor();
|
|
2585
2785
|
init_suggest();
|
|
@@ -2633,7 +2833,7 @@ async function main() {
|
|
|
2633
2833
|
const client = new GatewayClient(config);
|
|
2634
2834
|
switch (cmd) {
|
|
2635
2835
|
case "login":
|
|
2636
|
-
return login(client, config, rest
|
|
2836
|
+
return login(client, config, rest);
|
|
2637
2837
|
case "logout":
|
|
2638
2838
|
return logout(config);
|
|
2639
2839
|
case "whoami":
|
|
@@ -2674,14 +2874,49 @@ async function main() {
|
|
|
2674
2874
|
process.exitCode = 1;
|
|
2675
2875
|
}
|
|
2676
2876
|
}
|
|
2677
|
-
|
|
2877
|
+
function takeInvite(args) {
|
|
2878
|
+
const rest = [];
|
|
2879
|
+
let invite;
|
|
2880
|
+
for (let i = 0; i < args.length; i++) {
|
|
2881
|
+
const a = args[i];
|
|
2882
|
+
if (a === "--invite") invite = args[++i];
|
|
2883
|
+
else if (a.startsWith("--invite=")) invite = a.slice("--invite=".length);
|
|
2884
|
+
else rest.push(a);
|
|
2885
|
+
}
|
|
2886
|
+
return { invite: invite?.trim() || void 0, rest };
|
|
2887
|
+
}
|
|
2888
|
+
async function login(client, config, args) {
|
|
2889
|
+
const { invite, rest } = takeInvite(args);
|
|
2890
|
+
const email = rest[0];
|
|
2678
2891
|
if (!email) {
|
|
2679
2892
|
const start = await client.deviceStart().catch(() => null);
|
|
2680
|
-
if (start) return githubLogin(client, config, start);
|
|
2893
|
+
if (start) return githubLogin(client, config, start, invite);
|
|
2681
2894
|
}
|
|
2682
|
-
return devLogin(client, config, email);
|
|
2895
|
+
return devLogin(client, config, email, invite);
|
|
2683
2896
|
}
|
|
2684
|
-
|
|
2897
|
+
function reportSignupClosed(message, hadInvite) {
|
|
2898
|
+
console.log(c.yellow("\n Clixad isn't open yet.\n"));
|
|
2899
|
+
for (const line2 of wrapPlain(message, 68)) console.log(` ${line2}`);
|
|
2900
|
+
if (hadInvite) {
|
|
2901
|
+
console.log(
|
|
2902
|
+
c.dim("\n The invite code you passed wasn't accepted \u2014 check it for typos, or ask for a new one.")
|
|
2903
|
+
);
|
|
2904
|
+
}
|
|
2905
|
+
console.log("");
|
|
2906
|
+
}
|
|
2907
|
+
function wrapPlain(text, width) {
|
|
2908
|
+
const lines = [];
|
|
2909
|
+
let line2 = "";
|
|
2910
|
+
for (const word of text.split(/\s+/)) {
|
|
2911
|
+
if (line2 && line2.length + 1 + word.length > width) {
|
|
2912
|
+
lines.push(line2);
|
|
2913
|
+
line2 = word;
|
|
2914
|
+
} else line2 = line2 ? `${line2} ${word}` : word;
|
|
2915
|
+
}
|
|
2916
|
+
if (line2) lines.push(line2);
|
|
2917
|
+
return lines;
|
|
2918
|
+
}
|
|
2919
|
+
async function githubLogin(client, config, start, invite) {
|
|
2685
2920
|
console.log(`
|
|
2686
2921
|
Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
|
|
2687
2922
|
`);
|
|
@@ -2692,11 +2927,16 @@ async function githubLogin(client, config, start) {
|
|
|
2692
2927
|
while (Date.now() < deadline) {
|
|
2693
2928
|
await sleep(interval * 1e3);
|
|
2694
2929
|
process.stdout.write(c.dim("."));
|
|
2695
|
-
const poll = await client.devicePoll(start.session);
|
|
2930
|
+
const poll = await client.devicePoll(start.session, invite);
|
|
2696
2931
|
if (poll.status === "pending") {
|
|
2697
2932
|
if (poll.interval) interval = poll.interval;
|
|
2698
2933
|
continue;
|
|
2699
2934
|
}
|
|
2935
|
+
if (poll.status === "closed") {
|
|
2936
|
+
reportSignupClosed(poll.message, Boolean(invite));
|
|
2937
|
+
process.exitCode = 1;
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2700
2940
|
if (poll.status === "complete") {
|
|
2701
2941
|
config.token = poll.token;
|
|
2702
2942
|
config.userId = poll.userId;
|
|
@@ -2716,8 +2956,18 @@ async function githubLogin(client, config, start) {
|
|
|
2716
2956
|
}
|
|
2717
2957
|
console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
|
|
2718
2958
|
}
|
|
2719
|
-
async function devLogin(client, config, email) {
|
|
2720
|
-
|
|
2959
|
+
async function devLogin(client, config, email, invite) {
|
|
2960
|
+
let res;
|
|
2961
|
+
try {
|
|
2962
|
+
res = await client.signupDev(email, invite);
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
if (err instanceof SignupClosedError) {
|
|
2965
|
+
reportSignupClosed(err.message, Boolean(invite));
|
|
2966
|
+
process.exitCode = 1;
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
throw err;
|
|
2970
|
+
}
|
|
2721
2971
|
config.token = res.token;
|
|
2722
2972
|
config.userId = res.userId;
|
|
2723
2973
|
config.email = res.email;
|
|
@@ -2770,13 +3020,19 @@ async function setModel(client, config, model) {
|
|
|
2770
3020
|
}
|
|
2771
3021
|
async function showWallet(client) {
|
|
2772
3022
|
const w = await client.wallet();
|
|
2773
|
-
console.log(`balance: ${c.bold(
|
|
2774
|
-
console.log(`${earnedToday(w)} \xB7
|
|
3023
|
+
console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
|
|
3024
|
+
console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
|
|
2775
3025
|
if (w.ledger.length) {
|
|
2776
3026
|
console.log(c.dim("recent:"));
|
|
3027
|
+
const label = (e) => ledgerReasonLabel(e.reason);
|
|
3028
|
+
const width = Math.max(...w.ledger.slice(0, 8).map((e) => label(e).length));
|
|
2777
3029
|
for (const e of w.ledger.slice(0, 8)) {
|
|
2778
|
-
const
|
|
2779
|
-
|
|
3030
|
+
const amount = `${e.delta >= 0 ? "+" : "-"}${Math.abs(e.delta).toLocaleString("en-US")}`;
|
|
3031
|
+
const sign = e.delta >= 0 ? c.green(amount) : c.yellow(amount);
|
|
3032
|
+
const pad = " ".repeat(Math.max(0, 10 - amount.length));
|
|
3033
|
+
console.log(
|
|
3034
|
+
c.dim(` ${sign}${pad} ${label(e).padEnd(width)} -> ${e.balanceAfter.toLocaleString("en-US")}`)
|
|
3035
|
+
);
|
|
2780
3036
|
}
|
|
2781
3037
|
}
|
|
2782
3038
|
}
|
|
@@ -2878,7 +3134,7 @@ async function printCmd(client, config, task) {
|
|
|
2878
3134
|
Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
|
|
2879
3135
|
console.log(
|
|
2880
3136
|
c.yellow(
|
|
2881
|
-
`Run \`clixad earn\` to open the offerwall (
|
|
3137
|
+
`Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantRangeLabel(err.grantRange)} \u2014 or buy credits.`
|
|
2882
3138
|
)
|
|
2883
3139
|
);
|
|
2884
3140
|
console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
|
|
@@ -2975,7 +3231,7 @@ async function runTurn(client, config, messages) {
|
|
|
2975
3231
|
if (err instanceof PaywallError) {
|
|
2976
3232
|
console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
|
|
2977
3233
|
console.log(
|
|
2978
|
-
c.yellow(`Run /earn to open the offerwall (
|
|
3234
|
+
c.yellow(`Run /earn to open the offerwall \u2014 offers pay ${grantRangeLabel(err.grantRange)} \u2014 or buy credits.`)
|
|
2979
3235
|
);
|
|
2980
3236
|
console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
|
|
2981
3237
|
} else if (err instanceof AuthError) {
|
|
@@ -2996,7 +3252,7 @@ async function safeWallet(client) {
|
|
|
2996
3252
|
function printHelp() {
|
|
2997
3253
|
console.log(`${c.bold("clixad")} \u2014 free AI coding in your terminal, funded by ads
|
|
2998
3254
|
|
|
2999
|
-
${c.cyan("login")} [email] create/attach an account
|
|
3255
|
+
${c.cyan("login")} [email] create/attach an account (${c.dim("--invite <code>")} if you have one)
|
|
3000
3256
|
${c.cyan("logout")} forget the stored token
|
|
3001
3257
|
${c.cyan("whoami")} show account + balance
|
|
3002
3258
|
${c.cyan("models")} list models with credit prices
|