clixad 0.0.1-beta.0 → 0.0.1-beta.2
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 +455 -125
- 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
|
}
|
|
@@ -161,14 +210,18 @@ var init_client = __esm({
|
|
|
161
210
|
if (!res.ok) throw new Error(`checkout failed: ${res.status} ${await res.text()}`);
|
|
162
211
|
return res.json();
|
|
163
212
|
}
|
|
164
|
-
|
|
165
|
-
|
|
213
|
+
// Both take an optional signal for the REPL's sake: they are awaited while the
|
|
214
|
+
// input box is locked, and a gateway that is cold-starting (Render's free tier,
|
|
215
|
+
// the better part of a minute) would otherwise be indistinguishable from a
|
|
216
|
+
// hung terminal. Nothing else passes one — a one-shot CLI command has Ctrl+C.
|
|
217
|
+
async models(signal) {
|
|
218
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/models`, { signal });
|
|
166
219
|
if (!res.ok) throw new Error(`models failed: ${res.status}`);
|
|
167
220
|
const data = await res.json();
|
|
168
221
|
return data.data;
|
|
169
222
|
}
|
|
170
|
-
async wallet() {
|
|
171
|
-
const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers() });
|
|
223
|
+
async wallet(signal) {
|
|
224
|
+
const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
|
|
172
225
|
if (res.status === 401) throw await authFailure(res, "wallet");
|
|
173
226
|
if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
|
|
174
227
|
return res.json();
|
|
@@ -191,8 +244,7 @@ var init_client = __esm({
|
|
|
191
244
|
});
|
|
192
245
|
if (res.status === 401) throw await authFailure(res, "chat");
|
|
193
246
|
if (res.status === 402) {
|
|
194
|
-
|
|
195
|
-
throw new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
|
|
247
|
+
throw paywallError(await res.json());
|
|
196
248
|
}
|
|
197
249
|
if (!res.ok) throw new Error(`chat failed: ${res.status} ${await res.text()}`);
|
|
198
250
|
const data = await res.json();
|
|
@@ -220,8 +272,7 @@ var init_client = __esm({
|
|
|
220
272
|
});
|
|
221
273
|
if (res.status === 401) throw await authFailure(res, "chat");
|
|
222
274
|
if (res.status === 402) {
|
|
223
|
-
|
|
224
|
-
throw new PaywallError(body.balance, body.estimated_cost, body.grant_per_ad);
|
|
275
|
+
throw paywallError(await res.json());
|
|
225
276
|
}
|
|
226
277
|
if (!res.ok || !res.body) throw new Error(`chat failed: ${res.status} ${await res.text()}`);
|
|
227
278
|
let content = "";
|
|
@@ -1436,7 +1487,9 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
1436
1487
|
function openBrowser(url) {
|
|
1437
1488
|
const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1438
1489
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1439
|
-
spawn3(cmd, args, { detached: true, stdio: "ignore", windowsHide: true })
|
|
1490
|
+
const child = spawn3(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
1491
|
+
child.on("error", () => void 0);
|
|
1492
|
+
child.unref();
|
|
1440
1493
|
}
|
|
1441
1494
|
var init_browser = __esm({
|
|
1442
1495
|
"src/browser.ts"() {
|
|
@@ -1505,6 +1558,114 @@ var init_compact = __esm({
|
|
|
1505
1558
|
}
|
|
1506
1559
|
});
|
|
1507
1560
|
|
|
1561
|
+
// src/sponsor/counter.ts
|
|
1562
|
+
function createTally() {
|
|
1563
|
+
return { shown: 0, byLine: {} };
|
|
1564
|
+
}
|
|
1565
|
+
function recordImpression(tally, lineId) {
|
|
1566
|
+
tally.shown += 1;
|
|
1567
|
+
tally.byLine[lineId] = (tally.byLine[lineId] ?? 0) + 1;
|
|
1568
|
+
}
|
|
1569
|
+
var init_counter = __esm({
|
|
1570
|
+
"src/sponsor/counter.ts"() {
|
|
1571
|
+
"use strict";
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
// src/sponsor/lines.ts
|
|
1576
|
+
var HOUSE_LINES;
|
|
1577
|
+
var init_lines = __esm({
|
|
1578
|
+
"src/sponsor/lines.ts"() {
|
|
1579
|
+
"use strict";
|
|
1580
|
+
HOUSE_LINES = [
|
|
1581
|
+
{ id: "modes", text: "shift+tab cycles permission modes \u2014 plan mode is read-only" },
|
|
1582
|
+
{ id: "interrupt", text: "esc stops a run mid-turn; tokens already generated are billed" },
|
|
1583
|
+
{ id: "continue", text: "clixad --continue picks up the last session in this directory" },
|
|
1584
|
+
{ id: "sessions", text: "clixad --resume lists your saved sessions" },
|
|
1585
|
+
{ id: "context-files", text: "a CLIXAD.md or AGENTS.md in the repo root joins every prompt" },
|
|
1586
|
+
{ id: "compaction", text: "long sessions compact themselves before the context fills up" },
|
|
1587
|
+
{ id: "headless", text: 'clixad -p "\u2026" runs headless, with read-only tools' },
|
|
1588
|
+
{ id: "model-switch", text: "/model switches model mid-session \u2014 the conversation carries over" },
|
|
1589
|
+
{ id: "help", text: "/help lists every slash command" },
|
|
1590
|
+
// Placed here rather than at the end of the list on purpose: the rotation is
|
|
1591
|
+
// the list, and appending it would put three business-y lines (earn,
|
|
1592
|
+
// screenout, this) back to back at the close of every cycle — the one stretch
|
|
1593
|
+
// that would read as an ad break.
|
|
1594
|
+
{ id: "referral", text: "pass it on: clixad.io" },
|
|
1595
|
+
{ id: "wallet", text: "/wallet shows your balance and what you've earned today" },
|
|
1596
|
+
{ id: "cheapest-first", text: "/model lists cheapest-turn-first" },
|
|
1597
|
+
{ id: "model-cost", text: "current model: {ads_per_task}", needs: "ads_per_task" },
|
|
1598
|
+
{ id: "earn", text: "/earn: offers pay {grant_range}", needs: "grant_range" },
|
|
1599
|
+
{ id: "screenout", text: "a screenout pays nothing and is normal \u2014 just start another" }
|
|
1600
|
+
];
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
// src/sponsor/provider.ts
|
|
1605
|
+
function fill(line2, facts) {
|
|
1606
|
+
if (!line2.needs) return line2.text;
|
|
1607
|
+
const value = line2.needs === "grant_range" ? facts.grantRange : facts.adsPerTask;
|
|
1608
|
+
if (!value) return null;
|
|
1609
|
+
return line2.text.replace(`{${line2.needs}}`, value);
|
|
1610
|
+
}
|
|
1611
|
+
function houseSource(lines = HOUSE_LINES) {
|
|
1612
|
+
return {
|
|
1613
|
+
provider: "house",
|
|
1614
|
+
pick(index, previousId, facts) {
|
|
1615
|
+
if (lines.length === 0) return null;
|
|
1616
|
+
const start = (index % lines.length + lines.length) % lines.length;
|
|
1617
|
+
for (let step = 0; step < lines.length; step++) {
|
|
1618
|
+
const line2 = lines[(start + step) % lines.length];
|
|
1619
|
+
if (line2.id === previousId) continue;
|
|
1620
|
+
const text = fill(line2, facts);
|
|
1621
|
+
if (text) return { id: line2.id, text };
|
|
1622
|
+
}
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
function sponsorSource(provider = "house") {
|
|
1628
|
+
switch (provider) {
|
|
1629
|
+
case "house":
|
|
1630
|
+
return houseSource();
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
var init_provider = __esm({
|
|
1634
|
+
"src/sponsor/provider.ts"() {
|
|
1635
|
+
"use strict";
|
|
1636
|
+
init_lines();
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// src/sponsor/render.ts
|
|
1641
|
+
function sponsorEnabled({ isTTY, env, configEnabled }) {
|
|
1642
|
+
const flag = env.CLIXAD_SPONSOR?.trim().toLowerCase();
|
|
1643
|
+
if (flag === "0" || flag === "false" || flag === "off" || flag === "no") return false;
|
|
1644
|
+
const forced = flag === "1" || flag === "true" || flag === "on" || flag === "yes";
|
|
1645
|
+
if (!isTTY) return false;
|
|
1646
|
+
if (env.TERM === "dumb") return false;
|
|
1647
|
+
if (forced) return true;
|
|
1648
|
+
if (configEnabled === false) return false;
|
|
1649
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
1650
|
+
if (env.CI !== void 0 && env.CI !== "") return false;
|
|
1651
|
+
return true;
|
|
1652
|
+
}
|
|
1653
|
+
function sponsorText(line2, cols) {
|
|
1654
|
+
const indent = " ";
|
|
1655
|
+
const prefix = `${indent}${SPONSOR_MARKER} `;
|
|
1656
|
+
const room = cols - prefix.length;
|
|
1657
|
+
if (room < 12) return null;
|
|
1658
|
+
const body = line2.length <= room ? line2 : `${line2.slice(0, Math.max(0, room - 1)).trimEnd()}\u2026`;
|
|
1659
|
+
return prefix + body;
|
|
1660
|
+
}
|
|
1661
|
+
var SPONSOR_MARKER;
|
|
1662
|
+
var init_render = __esm({
|
|
1663
|
+
"src/sponsor/render.ts"() {
|
|
1664
|
+
"use strict";
|
|
1665
|
+
SPONSOR_MARKER = "\u2726 clixad \xB7";
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1508
1669
|
// src/tui/commands.ts
|
|
1509
1670
|
function commandLabel(c2) {
|
|
1510
1671
|
return `/${c2.name}${c2.args ? ` ${c2.args}` : ""}`;
|
|
@@ -1528,7 +1689,10 @@ var init_commands = __esm({
|
|
|
1528
1689
|
{ name: "models", desc: "list models + prices" },
|
|
1529
1690
|
{ name: "mode", args: "[name]", desc: "permission mode (or press shift+tab)" },
|
|
1530
1691
|
{ name: "wallet", desc: "balance & ads today" },
|
|
1531
|
-
|
|
1692
|
+
// Not "watch an ad": nothing on the wall is a video. CPX serves sign-up
|
|
1693
|
+
// forms, surveys and app trials, and describing the top-up as a video costs
|
|
1694
|
+
// the user the one expectation that makes a screenout make sense.
|
|
1695
|
+
{ name: "earn", desc: "open the offer wall to earn credits" },
|
|
1532
1696
|
{ name: "compact", desc: "summarise the conversation to free context" },
|
|
1533
1697
|
{ name: "clear", desc: "clear the conversation context" },
|
|
1534
1698
|
{ name: "init", desc: "write a CLIXAD.md for this project" },
|
|
@@ -1960,10 +2124,13 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
1960
2124
|
const [tick, setTick] = useState(0);
|
|
1961
2125
|
const [startedAt, setStartedAt] = useState(0);
|
|
1962
2126
|
const [quitHint, setQuitHint] = useState(false);
|
|
2127
|
+
const [busyLabel, setBusyLabel] = useState(WORKING);
|
|
2128
|
+
const [sponsor, setSponsor] = useState(null);
|
|
1963
2129
|
const messagesRef = useRef(messages);
|
|
1964
2130
|
messagesRef.current = messages;
|
|
1965
2131
|
const permRef = useRef(createState("normal"));
|
|
1966
2132
|
const abortRef = useRef(null);
|
|
2133
|
+
const busyAbortRef = useRef(null);
|
|
1967
2134
|
const filesRef = useRef(null);
|
|
1968
2135
|
const ctrlCRef = useRef(0);
|
|
1969
2136
|
const sessionRef = useRef(session ?? { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -1973,6 +2140,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
1973
2140
|
const lastOutputRef = useRef("");
|
|
1974
2141
|
const runningToolRef = useRef(null);
|
|
1975
2142
|
const pendingTaskRef = useRef(null);
|
|
2143
|
+
const sponsorRef = useRef(sponsorSource());
|
|
2144
|
+
const sponsorIdxRef = useRef(0);
|
|
2145
|
+
const sponsorPrevRef = useRef(void 0);
|
|
2146
|
+
const tallyRef = useRef(createTally());
|
|
1976
2147
|
const push = useCallback((e) => {
|
|
1977
2148
|
setEntries((prev) => [...prev, { ...e, id: idRef.current++ }]);
|
|
1978
2149
|
}, []);
|
|
@@ -1993,6 +2164,32 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
1993
2164
|
(id) => catalogRef.current.find((m) => m.id === id)?.context_window ?? contextTokensFor(id),
|
|
1994
2165
|
[]
|
|
1995
2166
|
);
|
|
2167
|
+
const nextSponsor = useCallback(
|
|
2168
|
+
(modelId) => {
|
|
2169
|
+
if (!sponsorEnabled({
|
|
2170
|
+
isTTY: process.stdout.isTTY,
|
|
2171
|
+
env: process.env,
|
|
2172
|
+
configEnabled: config.sponsor
|
|
2173
|
+
})) {
|
|
2174
|
+
setSponsor(null);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const est = catalogRef.current.find((m) => m.id === modelId)?.est_ads_per_task;
|
|
2178
|
+
const facts = {
|
|
2179
|
+
grantRange: wallet?.grant_per_ad_range ? grantRangeLabel(wallet.grant_per_ad_range) : void 0,
|
|
2180
|
+
adsPerTask: est === void 0 ? void 0 : adsPerTaskLabel(est)
|
|
2181
|
+
};
|
|
2182
|
+
const picked = sponsorRef.current.pick(sponsorIdxRef.current++, sponsorPrevRef.current, facts);
|
|
2183
|
+
if (!picked) {
|
|
2184
|
+
setSponsor(null);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
sponsorPrevRef.current = picked.id;
|
|
2188
|
+
recordImpression(tallyRef.current, picked.id);
|
|
2189
|
+
setSponsor(picked.text);
|
|
2190
|
+
},
|
|
2191
|
+
[config.sponsor, wallet]
|
|
2192
|
+
);
|
|
1996
2193
|
const askUser = useCallback(
|
|
1997
2194
|
(req) => new Promise((resolve2) => setAsk({ req, resolve: resolve2 })),
|
|
1998
2195
|
[]
|
|
@@ -2057,6 +2254,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2057
2254
|
setBusy(true);
|
|
2058
2255
|
setStartedAt(Date.now());
|
|
2059
2256
|
setLive({ text: "" });
|
|
2257
|
+
nextSponsor(model);
|
|
2060
2258
|
let history = messagesRef.current;
|
|
2061
2259
|
try {
|
|
2062
2260
|
const window = contextWindow(model);
|
|
@@ -2116,8 +2314,9 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2116
2314
|
kind: "notice",
|
|
2117
2315
|
tone: "warn",
|
|
2118
2316
|
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;
|
|
2317
|
+
Press enter to open the ad wall \u2014 offers pay ${grantRangeLabel(err.grantRange)}.
|
|
2318
|
+
I'll continue automatically. Offers you are screened out of pay nothing;
|
|
2319
|
+
that is normal, just start another.`
|
|
2121
2320
|
});
|
|
2122
2321
|
} else if (err instanceof AuthError) {
|
|
2123
2322
|
push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
@@ -2128,9 +2327,34 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2128
2327
|
abortRef.current = null;
|
|
2129
2328
|
setLive(null);
|
|
2130
2329
|
setBusy(false);
|
|
2330
|
+
setSponsor(null);
|
|
2331
|
+
}
|
|
2332
|
+
},
|
|
2333
|
+
[client, contextWindow, handleEvent, model, nextSponsor, permit, push, root, session?.title]
|
|
2334
|
+
);
|
|
2335
|
+
const stopCurrent = useCallback(() => {
|
|
2336
|
+
abortRef.current?.abort();
|
|
2337
|
+
busyAbortRef.current?.abort();
|
|
2338
|
+
}, []);
|
|
2339
|
+
const runBusy = useCallback(
|
|
2340
|
+
async (label, fn) => {
|
|
2341
|
+
const ac = new AbortController();
|
|
2342
|
+
busyAbortRef.current = ac;
|
|
2343
|
+
setBusy(true);
|
|
2344
|
+
setBusyLabel(label);
|
|
2345
|
+
setStartedAt(Date.now());
|
|
2346
|
+
try {
|
|
2347
|
+
await fn(ac.signal);
|
|
2348
|
+
} catch (err) {
|
|
2349
|
+
if (ac.signal.aborted) push({ kind: "notice", tone: "warn", text: " (stopped)" });
|
|
2350
|
+
else push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
2351
|
+
} finally {
|
|
2352
|
+
busyAbortRef.current = null;
|
|
2353
|
+
setBusyLabel(WORKING);
|
|
2354
|
+
setBusy(false);
|
|
2131
2355
|
}
|
|
2132
2356
|
},
|
|
2133
|
-
[
|
|
2357
|
+
[push]
|
|
2134
2358
|
);
|
|
2135
2359
|
const runAdWall = useCallback(async () => {
|
|
2136
2360
|
const task = pendingTaskRef.current;
|
|
@@ -2145,18 +2369,28 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2145
2369
|
` : ` opened ${base} \u2014 paste your token there; waiting for the reward\u2026
|
|
2146
2370
|
`) + ` Credits land on completion; being screened out of an offer pays nothing and is normal.`
|
|
2147
2371
|
});
|
|
2148
|
-
setBusy(true);
|
|
2149
|
-
const deadline = Date.now() + 5 * 6e4;
|
|
2150
2372
|
let credited = false;
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2373
|
+
let stopped = false;
|
|
2374
|
+
await runBusy(WAITING_FOR_REWARD, async (signal) => {
|
|
2375
|
+
const deadline = Date.now() + 5 * 6e4;
|
|
2376
|
+
while (Date.now() < deadline && !credited && !signal.aborted) {
|
|
2377
|
+
await sleep(3e3, signal);
|
|
2378
|
+
if (signal.aborted) break;
|
|
2379
|
+
const w = await client.wallet(signal).catch(() => void 0);
|
|
2380
|
+
if (w && w.balance > before) {
|
|
2381
|
+
setBalance(w.balance);
|
|
2382
|
+
credited = true;
|
|
2383
|
+
}
|
|
2157
2384
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2385
|
+
stopped = signal.aborted;
|
|
2386
|
+
});
|
|
2387
|
+
if (!credited) pendingTaskRef.current = task;
|
|
2388
|
+
if (stopped)
|
|
2389
|
+
return push({
|
|
2390
|
+
kind: "notice",
|
|
2391
|
+
tone: "warn",
|
|
2392
|
+
text: " stopped waiting. A reward still lands whenever the offer clears \u2014\n /wallet checks the balance, /earn opens the wall again." + (task ? "\n Your last request is still queued: press enter to retry it." : "")
|
|
2393
|
+
});
|
|
2160
2394
|
if (!credited)
|
|
2161
2395
|
return push({
|
|
2162
2396
|
kind: "notice",
|
|
@@ -2165,7 +2399,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2165
2399
|
});
|
|
2166
2400
|
push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
|
|
2167
2401
|
if (task) await runTurn2(task);
|
|
2168
|
-
}, [balance, client, config.dashboardUrl, push, runTurn2]);
|
|
2402
|
+
}, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
|
|
2169
2403
|
const runCommand = useCallback(
|
|
2170
2404
|
async (line2) => {
|
|
2171
2405
|
const [cmd, ...rest] = line2.slice(1).split(" ");
|
|
@@ -2198,94 +2432,84 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2198
2432
|
push({ kind: "notice", text: ` mode \u2192 ${MODE_LABEL[permRef.current.mode]}` });
|
|
2199
2433
|
return;
|
|
2200
2434
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
}
|
|
2221
|
-
setBusy(false);
|
|
2435
|
+
// A model call like any other, so esc has to stop it like any other —
|
|
2436
|
+
// and stopping it costs nothing, since the summary only replaces the
|
|
2437
|
+
// conversation once the call has come back whole.
|
|
2438
|
+
case "compact":
|
|
2439
|
+
await runBusy(COMPACTING, async (signal) => {
|
|
2440
|
+
const res = await compact(
|
|
2441
|
+
client,
|
|
2442
|
+
model,
|
|
2443
|
+
[{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
|
|
2444
|
+
{ signal }
|
|
2445
|
+
);
|
|
2446
|
+
if (!res) return push({ kind: "notice", text: " nothing to compact yet" });
|
|
2447
|
+
setMessages(res.messages.slice(1));
|
|
2448
|
+
setBalance(res.balance);
|
|
2449
|
+
setSpent((s) => s + res.creditsCharged);
|
|
2450
|
+
push({
|
|
2451
|
+
kind: "notice",
|
|
2452
|
+
text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
|
|
2453
|
+
});
|
|
2454
|
+
});
|
|
2222
2455
|
return;
|
|
2223
|
-
}
|
|
2224
2456
|
case "init":
|
|
2225
2457
|
await runTurn2(INIT_PROMPT);
|
|
2226
2458
|
return;
|
|
2227
2459
|
// Always the picker — an id you have to remember and type is exactly
|
|
2228
2460
|
// what a picker is for. A typed id only preselects a row.
|
|
2229
|
-
case "model":
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
|
|
2237
|
-
}
|
|
2238
|
-
const items = models.map((m) => ({
|
|
2239
|
-
value: m.id,
|
|
2240
|
-
label: m.id,
|
|
2241
|
-
hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
|
|
2242
|
-
current: m.id === model
|
|
2243
|
-
}));
|
|
2244
|
-
const preselect = items.findIndex((i) => i.value === arg);
|
|
2245
|
-
setPickerSel(preselect >= 0 ? preselect : Math.max(0, items.findIndex((i) => i.current)));
|
|
2246
|
-
setPicker({
|
|
2247
|
-
title: "Select model",
|
|
2248
|
-
subtitle: "Applies to this session and is saved as your default.",
|
|
2249
|
-
items,
|
|
2250
|
-
onPick: (choice) => {
|
|
2251
|
-
config.model = choice.value;
|
|
2252
|
-
saveConfig(config);
|
|
2253
|
-
setModel2(choice.value);
|
|
2254
|
-
push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
|
|
2461
|
+
case "model":
|
|
2462
|
+
await runBusy(LOADING, async (signal) => {
|
|
2463
|
+
const models = catalogRef.current.length ? catalogRef.current : await client.models(signal);
|
|
2464
|
+
catalogRef.current = models;
|
|
2465
|
+
if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
|
|
2466
|
+
if (arg && !models.some((m) => m.id === arg)) {
|
|
2467
|
+
push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
|
|
2255
2468
|
}
|
|
2469
|
+
const items = models.map((m) => ({
|
|
2470
|
+
value: m.id,
|
|
2471
|
+
label: m.id,
|
|
2472
|
+
hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
|
|
2473
|
+
current: m.id === model
|
|
2474
|
+
}));
|
|
2475
|
+
const preselect = items.findIndex((i) => i.value === arg);
|
|
2476
|
+
setPickerSel(preselect >= 0 ? preselect : Math.max(0, items.findIndex((i) => i.current)));
|
|
2477
|
+
setPicker({
|
|
2478
|
+
title: "Select model",
|
|
2479
|
+
subtitle: "Applies to this session and is saved as your default.",
|
|
2480
|
+
items,
|
|
2481
|
+
onPick: (choice) => {
|
|
2482
|
+
config.model = choice.value;
|
|
2483
|
+
saveConfig(config);
|
|
2484
|
+
setModel2(choice.value);
|
|
2485
|
+
push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2256
2488
|
});
|
|
2257
2489
|
return;
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
try {
|
|
2262
|
-
const ms = await client.models();
|
|
2490
|
+
case "models":
|
|
2491
|
+
await runBusy(LOADING, async (signal) => {
|
|
2492
|
+
const ms = await client.models(signal);
|
|
2263
2493
|
catalogRef.current = ms;
|
|
2264
2494
|
push({
|
|
2265
2495
|
kind: "notice",
|
|
2266
2496
|
text: ms.map((m) => ` ${m.id.padEnd(24)} ${adsPerTaskLabel(m.est_ads_per_task)}`).join("\n")
|
|
2267
2497
|
});
|
|
2268
|
-
}
|
|
2269
|
-
push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
2270
|
-
}
|
|
2271
|
-
setBusy(false);
|
|
2498
|
+
});
|
|
2272
2499
|
return;
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
try {
|
|
2277
|
-
const w = await client.wallet();
|
|
2500
|
+
case "wallet":
|
|
2501
|
+
await runBusy(LOADING, async (signal) => {
|
|
2502
|
+
const w = await client.wallet(signal);
|
|
2278
2503
|
setBalance(w.balance);
|
|
2504
|
+
const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
|
|
2505
|
+
const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
|
|
2279
2506
|
push({
|
|
2280
2507
|
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`
|
|
2508
|
+
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 ? `
|
|
2509
|
+
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
2510
|
});
|
|
2283
|
-
}
|
|
2284
|
-
push({ kind: "notice", tone: "error", text: ` ${err.message}` });
|
|
2285
|
-
}
|
|
2286
|
-
setBusy(false);
|
|
2511
|
+
});
|
|
2287
2512
|
return;
|
|
2288
|
-
}
|
|
2289
2513
|
// The same browser handoff the paywall takes, rather than a second way of
|
|
2290
2514
|
// doing it: /earn used to call the dev-only /v1/ads/reward simulator, so
|
|
2291
2515
|
// in production the slash command the paywall itself recommends was a 404.
|
|
@@ -2296,7 +2520,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2296
2520
|
push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
|
|
2297
2521
|
}
|
|
2298
2522
|
},
|
|
2299
|
-
[client, config, cycleMode, exit, model, push, runTurn2]
|
|
2523
|
+
[client, config, cycleMode, exit, model, push, runAdWall, runBusy, runTurn2]
|
|
2300
2524
|
);
|
|
2301
2525
|
const submit = useCallback(
|
|
2302
2526
|
async (raw) => {
|
|
@@ -2345,7 +2569,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2345
2569
|
);
|
|
2346
2570
|
useInput((ch, key) => {
|
|
2347
2571
|
if (key.ctrl && ch === "c") {
|
|
2348
|
-
if (busy) return
|
|
2572
|
+
if (busy) return stopCurrent();
|
|
2349
2573
|
if (!isEmpty(editor)) return setEditor(EMPTY);
|
|
2350
2574
|
if (Date.now() - ctrlCRef.current < 2e3) return exit();
|
|
2351
2575
|
ctrlCRef.current = Date.now();
|
|
@@ -2384,7 +2608,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2384
2608
|
return;
|
|
2385
2609
|
}
|
|
2386
2610
|
if (busy) {
|
|
2387
|
-
if (key.escape)
|
|
2611
|
+
if (key.escape) stopCurrent();
|
|
2388
2612
|
return;
|
|
2389
2613
|
}
|
|
2390
2614
|
if (key.tab && key.shift) return cycleMode();
|
|
@@ -2429,6 +2653,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2429
2653
|
});
|
|
2430
2654
|
const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
2431
2655
|
const spinner = SPINNER[tick % SPINNER.length];
|
|
2656
|
+
const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
|
|
2432
2657
|
const liveText = live?.text ? tailLines(live.text, Math.max(4, rows - 12)) : "";
|
|
2433
2658
|
const liveBlock = [
|
|
2434
2659
|
liveText,
|
|
@@ -2439,7 +2664,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2439
2664
|
const pickerHeight = picker ? picker.items.length + 8 : 0;
|
|
2440
2665
|
const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
|
|
2441
2666
|
const inputBoxHeight = 2 + editor.lines.length;
|
|
2442
|
-
const chromeHeight = inputBoxHeight + 2;
|
|
2667
|
+
const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
|
|
2443
2668
|
const liveHeight = liveBlock ? lineCount(liveBlock, cols) + 1 : 0;
|
|
2444
2669
|
const askHeight = ask2 ? lineCount(askBlock, cols) + 2 : 0;
|
|
2445
2670
|
const printed = useMemo(() => entries.reduce((n, e) => n + entryHeight(e, cols), 0), [entries, cols]);
|
|
@@ -2474,11 +2699,14 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2474
2699
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 choose \xB7 1-9 jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel" })
|
|
2475
2700
|
] }) : null,
|
|
2476
2701
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
2702
|
+
sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
|
|
2477
2703
|
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO_BRIGHT : MANGO3, paddingX: 1, children: [
|
|
2478
2704
|
/* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
|
|
2479
2705
|
busy ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2480
2706
|
spinner,
|
|
2481
|
-
"
|
|
2707
|
+
" ",
|
|
2708
|
+
busyLabel,
|
|
2709
|
+
" ",
|
|
2482
2710
|
elapsed,
|
|
2483
2711
|
"s \xB7 esc to stop"
|
|
2484
2712
|
] }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor) })
|
|
@@ -2505,6 +2733,18 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2505
2733
|
] })
|
|
2506
2734
|
] });
|
|
2507
2735
|
}
|
|
2736
|
+
function sleep(ms, signal) {
|
|
2737
|
+
return new Promise((resolve2) => {
|
|
2738
|
+
if (signal.aborted) return resolve2();
|
|
2739
|
+
const done = () => {
|
|
2740
|
+
clearTimeout(timer);
|
|
2741
|
+
signal.removeEventListener("abort", done);
|
|
2742
|
+
resolve2();
|
|
2743
|
+
};
|
|
2744
|
+
const timer = setTimeout(done, ms);
|
|
2745
|
+
signal.addEventListener("abort", done, { once: true });
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2508
2748
|
function renderInput(state) {
|
|
2509
2749
|
return state.lines.map((line2, row) => {
|
|
2510
2750
|
const prefix = row === 0 ? "" : "\n";
|
|
@@ -2565,7 +2805,7 @@ function tailLines(text, max) {
|
|
|
2565
2805
|
const lines = text.split("\n");
|
|
2566
2806
|
return lines.length <= max ? text : lines.slice(-max).join("\n");
|
|
2567
2807
|
}
|
|
2568
|
-
var SPINNER, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
|
|
2808
|
+
var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
|
|
2569
2809
|
var init_app = __esm({
|
|
2570
2810
|
"src/tui/app.tsx"() {
|
|
2571
2811
|
"use strict";
|
|
@@ -2580,11 +2820,18 @@ var init_app = __esm({
|
|
|
2580
2820
|
init_tools();
|
|
2581
2821
|
init_permissions();
|
|
2582
2822
|
init_session();
|
|
2823
|
+
init_counter();
|
|
2824
|
+
init_provider();
|
|
2825
|
+
init_render();
|
|
2583
2826
|
init_commands();
|
|
2584
2827
|
init_editor();
|
|
2585
2828
|
init_suggest();
|
|
2586
2829
|
init_views();
|
|
2587
2830
|
SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2831
|
+
WORKING = "working\u2026";
|
|
2832
|
+
WAITING_FOR_REWARD = "waiting for the offer\u2026";
|
|
2833
|
+
LOADING = "loading\u2026";
|
|
2834
|
+
COMPACTING = "compacting\u2026";
|
|
2588
2835
|
LIVE_OUTPUT_LINES = 5;
|
|
2589
2836
|
COMMITTED_OUTPUT_LINES = 4;
|
|
2590
2837
|
}
|
|
@@ -2619,6 +2866,31 @@ init_session();
|
|
|
2619
2866
|
init_kimi();
|
|
2620
2867
|
init_banner();
|
|
2621
2868
|
init_browser();
|
|
2869
|
+
|
|
2870
|
+
// src/title.ts
|
|
2871
|
+
var APP_TITLE = "Clixad";
|
|
2872
|
+
function titleEnabled({ isTTY, env }) {
|
|
2873
|
+
const flag = env.CLIXAD_TITLE?.trim().toLowerCase();
|
|
2874
|
+
if (flag === "0" || flag === "false" || flag === "off" || flag === "no") return false;
|
|
2875
|
+
if (!isTTY) return false;
|
|
2876
|
+
if (env.TERM === "dumb") return false;
|
|
2877
|
+
if (env.CI !== void 0 && env.CI !== "") return false;
|
|
2878
|
+
return true;
|
|
2879
|
+
}
|
|
2880
|
+
function titleSequence(title) {
|
|
2881
|
+
return `\x1B]0;${title.replace(/[\x00-\x1f\x7f]/g, " ").trim()}\x07`;
|
|
2882
|
+
}
|
|
2883
|
+
function setTerminalTitle(title = APP_TITLE, out = process.stdout) {
|
|
2884
|
+
process.title = title.toLowerCase();
|
|
2885
|
+
if (!titleEnabled({ isTTY: out.isTTY, env: process.env })) return;
|
|
2886
|
+
out.write(titleSequence(title));
|
|
2887
|
+
}
|
|
2888
|
+
function clearTerminalTitle(out = process.stdout) {
|
|
2889
|
+
if (!titleEnabled({ isTTY: out.isTTY, env: process.env })) return;
|
|
2890
|
+
out.write(titleSequence(""));
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// src/main.ts
|
|
2622
2894
|
var c = {
|
|
2623
2895
|
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
2624
2896
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -2628,12 +2900,14 @@ var c = {
|
|
|
2628
2900
|
bold: (s) => `\x1B[1m${s}\x1B[0m`
|
|
2629
2901
|
};
|
|
2630
2902
|
async function main() {
|
|
2903
|
+
setTerminalTitle();
|
|
2904
|
+
process.on("exit", () => clearTerminalTitle());
|
|
2631
2905
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
2632
2906
|
const config = loadConfig();
|
|
2633
2907
|
const client = new GatewayClient(config);
|
|
2634
2908
|
switch (cmd) {
|
|
2635
2909
|
case "login":
|
|
2636
|
-
return login(client, config, rest
|
|
2910
|
+
return login(client, config, rest);
|
|
2637
2911
|
case "logout":
|
|
2638
2912
|
return logout(config);
|
|
2639
2913
|
case "whoami":
|
|
@@ -2674,14 +2948,49 @@ async function main() {
|
|
|
2674
2948
|
process.exitCode = 1;
|
|
2675
2949
|
}
|
|
2676
2950
|
}
|
|
2677
|
-
|
|
2951
|
+
function takeInvite(args) {
|
|
2952
|
+
const rest = [];
|
|
2953
|
+
let invite;
|
|
2954
|
+
for (let i = 0; i < args.length; i++) {
|
|
2955
|
+
const a = args[i];
|
|
2956
|
+
if (a === "--invite") invite = args[++i];
|
|
2957
|
+
else if (a.startsWith("--invite=")) invite = a.slice("--invite=".length);
|
|
2958
|
+
else rest.push(a);
|
|
2959
|
+
}
|
|
2960
|
+
return { invite: invite?.trim() || void 0, rest };
|
|
2961
|
+
}
|
|
2962
|
+
async function login(client, config, args) {
|
|
2963
|
+
const { invite, rest } = takeInvite(args);
|
|
2964
|
+
const email = rest[0];
|
|
2678
2965
|
if (!email) {
|
|
2679
2966
|
const start = await client.deviceStart().catch(() => null);
|
|
2680
|
-
if (start) return githubLogin(client, config, start);
|
|
2967
|
+
if (start) return githubLogin(client, config, start, invite);
|
|
2681
2968
|
}
|
|
2682
|
-
return devLogin(client, config, email);
|
|
2969
|
+
return devLogin(client, config, email, invite);
|
|
2970
|
+
}
|
|
2971
|
+
function reportSignupClosed(message, hadInvite) {
|
|
2972
|
+
console.log(c.yellow("\n Clixad isn't open yet.\n"));
|
|
2973
|
+
for (const line2 of wrapPlain(message, 68)) console.log(` ${line2}`);
|
|
2974
|
+
if (hadInvite) {
|
|
2975
|
+
console.log(
|
|
2976
|
+
c.dim("\n The invite code you passed wasn't accepted \u2014 check it for typos, or ask for a new one.")
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
console.log("");
|
|
2980
|
+
}
|
|
2981
|
+
function wrapPlain(text, width) {
|
|
2982
|
+
const lines = [];
|
|
2983
|
+
let line2 = "";
|
|
2984
|
+
for (const word of text.split(/\s+/)) {
|
|
2985
|
+
if (line2 && line2.length + 1 + word.length > width) {
|
|
2986
|
+
lines.push(line2);
|
|
2987
|
+
line2 = word;
|
|
2988
|
+
} else line2 = line2 ? `${line2} ${word}` : word;
|
|
2989
|
+
}
|
|
2990
|
+
if (line2) lines.push(line2);
|
|
2991
|
+
return lines;
|
|
2683
2992
|
}
|
|
2684
|
-
async function githubLogin(client, config, start) {
|
|
2993
|
+
async function githubLogin(client, config, start, invite) {
|
|
2685
2994
|
console.log(`
|
|
2686
2995
|
Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
|
|
2687
2996
|
`);
|
|
@@ -2690,13 +2999,18 @@ async function githubLogin(client, config, start) {
|
|
|
2690
2999
|
const deadline = Date.now() + start.expires_in * 1e3;
|
|
2691
3000
|
let interval = Math.max(start.interval, 1);
|
|
2692
3001
|
while (Date.now() < deadline) {
|
|
2693
|
-
await
|
|
3002
|
+
await sleep2(interval * 1e3);
|
|
2694
3003
|
process.stdout.write(c.dim("."));
|
|
2695
|
-
const poll = await client.devicePoll(start.session);
|
|
3004
|
+
const poll = await client.devicePoll(start.session, invite);
|
|
2696
3005
|
if (poll.status === "pending") {
|
|
2697
3006
|
if (poll.interval) interval = poll.interval;
|
|
2698
3007
|
continue;
|
|
2699
3008
|
}
|
|
3009
|
+
if (poll.status === "closed") {
|
|
3010
|
+
reportSignupClosed(poll.message, Boolean(invite));
|
|
3011
|
+
process.exitCode = 1;
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
2700
3014
|
if (poll.status === "complete") {
|
|
2701
3015
|
config.token = poll.token;
|
|
2702
3016
|
config.userId = poll.userId;
|
|
@@ -2716,8 +3030,18 @@ async function githubLogin(client, config, start) {
|
|
|
2716
3030
|
}
|
|
2717
3031
|
console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
|
|
2718
3032
|
}
|
|
2719
|
-
async function devLogin(client, config, email) {
|
|
2720
|
-
|
|
3033
|
+
async function devLogin(client, config, email, invite) {
|
|
3034
|
+
let res;
|
|
3035
|
+
try {
|
|
3036
|
+
res = await client.signupDev(email, invite);
|
|
3037
|
+
} catch (err) {
|
|
3038
|
+
if (err instanceof SignupClosedError) {
|
|
3039
|
+
reportSignupClosed(err.message, Boolean(invite));
|
|
3040
|
+
process.exitCode = 1;
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
throw err;
|
|
3044
|
+
}
|
|
2721
3045
|
config.token = res.token;
|
|
2722
3046
|
config.userId = res.userId;
|
|
2723
3047
|
config.email = res.email;
|
|
@@ -2726,7 +3050,7 @@ async function devLogin(client, config, email) {
|
|
|
2726
3050
|
console.log(` balance: ${c.bold(String(res.balance))} credits (signup bonus)`);
|
|
2727
3051
|
console.log(c.dim(` token stored in ${configPath()}`));
|
|
2728
3052
|
}
|
|
2729
|
-
var
|
|
3053
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2730
3054
|
function logout(config) {
|
|
2731
3055
|
delete config.token;
|
|
2732
3056
|
delete config.userId;
|
|
@@ -2770,13 +3094,19 @@ async function setModel(client, config, model) {
|
|
|
2770
3094
|
}
|
|
2771
3095
|
async function showWallet(client) {
|
|
2772
3096
|
const w = await client.wallet();
|
|
2773
|
-
console.log(`balance: ${c.bold(
|
|
2774
|
-
console.log(`${earnedToday(w)} \xB7
|
|
3097
|
+
console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
|
|
3098
|
+
console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
|
|
2775
3099
|
if (w.ledger.length) {
|
|
2776
3100
|
console.log(c.dim("recent:"));
|
|
3101
|
+
const label = (e) => ledgerReasonLabel(e.reason);
|
|
3102
|
+
const width = Math.max(...w.ledger.slice(0, 8).map((e) => label(e).length));
|
|
2777
3103
|
for (const e of w.ledger.slice(0, 8)) {
|
|
2778
|
-
const
|
|
2779
|
-
|
|
3104
|
+
const amount = `${e.delta >= 0 ? "+" : "-"}${Math.abs(e.delta).toLocaleString("en-US")}`;
|
|
3105
|
+
const sign = e.delta >= 0 ? c.green(amount) : c.yellow(amount);
|
|
3106
|
+
const pad = " ".repeat(Math.max(0, 10 - amount.length));
|
|
3107
|
+
console.log(
|
|
3108
|
+
c.dim(` ${sign}${pad} ${label(e).padEnd(width)} -> ${e.balanceAfter.toLocaleString("en-US")}`)
|
|
3109
|
+
);
|
|
2780
3110
|
}
|
|
2781
3111
|
}
|
|
2782
3112
|
}
|
|
@@ -2796,7 +3126,7 @@ async function earn(client, config) {
|
|
|
2796
3126
|
process.stdout.write(c.dim(" waiting for an offer to clear"));
|
|
2797
3127
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
2798
3128
|
while (Date.now() < deadline) {
|
|
2799
|
-
await
|
|
3129
|
+
await sleep2(3e3);
|
|
2800
3130
|
process.stdout.write(c.dim("."));
|
|
2801
3131
|
const balance = (await safeWallet(client))?.balance ?? before;
|
|
2802
3132
|
if (balance > before) {
|
|
@@ -2841,7 +3171,7 @@ async function buyCmd(client, config, pack) {
|
|
|
2841
3171
|
process.stdout.write(c.dim(" waiting for payment to clear"));
|
|
2842
3172
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
2843
3173
|
while (Date.now() < deadline) {
|
|
2844
|
-
await
|
|
3174
|
+
await sleep2(3e3);
|
|
2845
3175
|
process.stdout.write(c.dim("."));
|
|
2846
3176
|
const balance = (await safeWallet(client))?.balance ?? before;
|
|
2847
3177
|
if (balance > before) {
|
|
@@ -2878,7 +3208,7 @@ async function printCmd(client, config, task) {
|
|
|
2878
3208
|
Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
|
|
2879
3209
|
console.log(
|
|
2880
3210
|
c.yellow(
|
|
2881
|
-
`Run \`clixad earn\` to open the offerwall (
|
|
3211
|
+
`Run \`clixad earn\` to open the offerwall \u2014 offers pay ${grantRangeLabel(err.grantRange)} \u2014 or buy credits.`
|
|
2882
3212
|
)
|
|
2883
3213
|
);
|
|
2884
3214
|
console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
|
|
@@ -2975,7 +3305,7 @@ async function runTurn(client, config, messages) {
|
|
|
2975
3305
|
if (err instanceof PaywallError) {
|
|
2976
3306
|
console.log(c.yellow(`Out of credits (balance ${err.balance}, need ~${err.estimatedCost}).`));
|
|
2977
3307
|
console.log(
|
|
2978
|
-
c.yellow(`Run /earn to open the offerwall (
|
|
3308
|
+
c.yellow(`Run /earn to open the offerwall \u2014 offers pay ${grantRangeLabel(err.grantRange)} \u2014 or buy credits.`)
|
|
2979
3309
|
);
|
|
2980
3310
|
console.log(c.dim(" Offers you are screened out of pay nothing \u2014 that is normal, just start another."));
|
|
2981
3311
|
} else if (err instanceof AuthError) {
|
|
@@ -2996,7 +3326,7 @@ async function safeWallet(client) {
|
|
|
2996
3326
|
function printHelp() {
|
|
2997
3327
|
console.log(`${c.bold("clixad")} \u2014 free AI coding in your terminal, funded by ads
|
|
2998
3328
|
|
|
2999
|
-
${c.cyan("login")} [email] create/attach an account
|
|
3329
|
+
${c.cyan("login")} [email] create/attach an account (${c.dim("--invite <code>")} if you have one)
|
|
3000
3330
|
${c.cyan("logout")} forget the stored token
|
|
3001
3331
|
${c.cyan("whoami")} show account + balance
|
|
3002
3332
|
${c.cyan("models")} list models with credit prices
|