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.
Files changed (3) hide show
  1. package/README.md +16 -9
  2. package/dist/clixad.mjs +455 -125
  3. 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
- **This is an early beta and is published under the `beta` dist-tag only.** There is deliberately no
6
- `latest` tag yet, so `npx clixad` and `npm install clixad` do not resolve. To try the beta
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
- npm install -g clixad@beta
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. Interfaces, commands and credit mechanics may change without notice between beta
18
- versions.
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. Published to reserve the package name; no rights to use, copy,
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) throw new Error(`signup failed: ${res.status} ${await res.text()}`);
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
- async models() {
165
- const res = await fetch(`${this.config.gatewayUrl}/v1/models`);
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
- const body = await res.json();
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
- const body = await res.json();
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 }).unref();
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
- { name: "earn", desc: "earn credits (watch an ad)" },
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 (+${err.grantPerAd} credits on completion) \u2014 I'll continue automatically.
2120
- Offers you are screened out of pay nothing; that is normal, just start another.`
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
- [client, contextWindow, handleEvent, model, permit, push, root, session?.title]
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
- while (Date.now() < deadline && !credited) {
2152
- await new Promise((r) => setTimeout(r, 3e3));
2153
- const w = await client.wallet().catch(() => void 0);
2154
- if (w && w.balance > before) {
2155
- setBalance(w.balance);
2156
- credited = true;
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
- setBusy(false);
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
- case "compact": {
2202
- setBusy(true);
2203
- try {
2204
- const res = await compact(client, model, [
2205
- { role: "system", content: contextRef.current.systemPrompt },
2206
- ...messagesRef.current
2207
- ]);
2208
- if (!res) push({ kind: "notice", text: " nothing to compact yet" });
2209
- else {
2210
- setMessages(res.messages.slice(1));
2211
- setBalance(res.balance);
2212
- setSpent((s) => s + res.creditsCharged);
2213
- push({
2214
- kind: "notice",
2215
- text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
2216
- });
2217
- }
2218
- } catch (err) {
2219
- push({ kind: "notice", tone: "error", text: ` ${err.message}` });
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
- setBusy(true);
2231
- const models = catalogRef.current.length ? catalogRef.current : await client.models().catch(() => []);
2232
- catalogRef.current = models;
2233
- setBusy(false);
2234
- if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
2235
- if (arg && !models.some((m) => m.id === arg)) {
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
- case "models": {
2260
- setBusy(true);
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
- } catch (err) {
2269
- push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2270
- }
2271
- setBusy(false);
2498
+ });
2272
2499
  return;
2273
- }
2274
- case "wallet": {
2275
- setBusy(true);
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
- } catch (err) {
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 abortRef.current?.abort();
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) abortRef.current?.abort();
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
- " working\u2026 ",
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[0]);
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
- async function login(client, config, email) {
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 sleep(interval * 1e3);
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
- const res = await client.signupDev(email);
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 sleep = (ms) => new Promise((r) => setTimeout(r, ms));
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(String(w.balance))} credits`);
2774
- console.log(`${earnedToday(w)} \xB7 typical grant: ${w.grant_per_ad} on completion`);
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 sign = e.delta >= 0 ? c.green(`+${e.delta}`) : c.red(String(e.delta));
2779
- console.log(c.dim(` ${sign.padEnd(18)} ${e.reason.padEnd(14)} -> ${e.balanceAfter}`));
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 sleep(3e3);
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 sleep(3e3);
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 (+${err.grantPerAd} credits on completion) or buy credits.`
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 (+${err.grantPerAd} credits on completion) or buy credits.`)
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.0",
3
+ "version": "0.0.1-beta.2",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",