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.
Files changed (3) hide show
  1. package/README.md +16 -9
  2. package/dist/clixad.mjs +289 -33
  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
  }
@@ -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
- const body = await res.json();
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
- const body = await res.json();
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
- { name: "earn", desc: "earn credits (watch an ad)" },
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 (+${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.`
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[0]);
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
- async function login(client, config, email) {
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
- async function githubLogin(client, config, start) {
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
- const res = await client.signupDev(email);
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(String(w.balance))} credits`);
2774
- console.log(`${earnedToday(w)} \xB7 typical grant: ${w.grant_per_ad} on completion`);
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 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}`));
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 (+${err.grantPerAd} credits on completion) or buy credits.`
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 (+${err.grantPerAd} credits on completion) or buy credits.`)
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
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.1",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",