clixad 0.0.1-beta.15 → 0.0.1-beta.16

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 CHANGED
@@ -19,6 +19,10 @@ npm install -g clixad
19
19
 
20
20
  Requires Node.js >= 20.
21
21
 
22
+ ## Community
23
+
24
+ Discord: <https://discord.gg/pwxPM3QyF> — bug reports and feature requests are read there.
25
+
22
26
  ## What it does
23
27
 
24
28
  It reads, searches and edits files and runs shell commands in the directory you start it in, asking
package/dist/clixad.mjs CHANGED
@@ -101,6 +101,52 @@ function grantRangeLabel(range) {
101
101
  const n = (v) => v.toLocaleString("en-US");
102
102
  return low === high ? `${n(low)} credits on completion` : `${n(low)}\u2013${n(high)} credits on completion`;
103
103
  }
104
+ function streakLabel(streak) {
105
+ if (streak.length <= 0 || streak.day <= 0) return null;
106
+ const n = (x) => x.toLocaleString("en-US");
107
+ const where = `day ${streak.day} of ${streak.length}`;
108
+ if (!streak.claimed) return `daily streak: ${where} \xB7 today's already counted`;
109
+ const tomorrow = streak.schedule[Math.min(streak.day, streak.length - 1)];
110
+ const next = tomorrow === void 0 ? "" : ` \xB7 tomorrow ${n(tomorrow)}`;
111
+ return `daily streak: ${where} \xB7 +${n(streak.credits)} credits${next}`;
112
+ }
113
+ function referLines(info) {
114
+ const n = (x) => x.toLocaleString("en-US");
115
+ const lines = [
116
+ `your code: ${info.code}`,
117
+ `share it: clixad login --invite ${info.code}`,
118
+ `you both get ${n(info.inviterCredits)} credits once they complete their first offer`
119
+ ];
120
+ if (info.invited === 0) {
121
+ lines.push("nobody has signed up with it yet");
122
+ return lines;
123
+ }
124
+ const waiting = info.invited - info.credited;
125
+ lines.push(
126
+ `${n(info.invited)} signed up \xB7 ${n(info.credited)} completed an offer \xB7 ${n(info.creditsEarned)} credits earned`
127
+ );
128
+ if (waiting > 0) {
129
+ lines.push(`${n(waiting)} haven't completed an offer yet \u2014 that pays nothing until they do`);
130
+ }
131
+ if (info.remaining === 0 && info.maxCredited > 0) {
132
+ lines.push(`you've been paid for the maximum of ${n(info.maxCredited)} referrals`);
133
+ }
134
+ return lines;
135
+ }
136
+ function redeemMessage(result) {
137
+ switch (result.status) {
138
+ case "redeemed":
139
+ return { ok: true, text: `+${result.credited.toLocaleString("en-US")} credits` };
140
+ case "used":
141
+ return { ok: false, text: "that code has already been used" };
142
+ case "expired":
143
+ return { ok: false, text: "that code has expired" };
144
+ case "wrong_account":
145
+ return { ok: false, text: "that code belongs to a different account \u2014 check who you're signed in as" };
146
+ default:
147
+ return { ok: false, text: "that isn't a code we recognise \u2014 check it for typos" };
148
+ }
149
+ }
104
150
  function ledgerReasonLabel(reason) {
105
151
  return LEDGER_REASONS[reason] ?? reason.replace(/_/g, " ");
106
152
  }
@@ -195,6 +241,10 @@ var init_client = __esm({
195
241
  CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
196
242
  LEDGER_REASONS = {
197
243
  signup_bonus: "signup bonus",
244
+ login_streak: "daily streak",
245
+ feedback_bonus: "thanks for the feedback",
246
+ referral_bonus: "referral bonus",
247
+ redeem_code: "code redeemed",
198
248
  ad_reward: "offer reward",
199
249
  ad_screenout_bonus: "screenout bonus (didn't qualify)",
200
250
  ad_sponsor_impression: "sponsored line shown",
@@ -323,6 +373,138 @@ var init_client = __esm({
323
373
  return void 0;
324
374
  }
325
375
  }
376
+ /**
377
+ * Claim today's login-streak grant, or find out that today is already claimed.
378
+ *
379
+ * Safe to call from anywhere and as often as you like: the gateway keys it on
380
+ * `(account, UTC day)`, so the second call of the day credits nothing and
381
+ * answers with the same day number. That is what lets every surface which
382
+ * might be somebody's first contact today — a login, the REPL opening,
383
+ * `wallet` — claim without coordinating.
384
+ *
385
+ * **Never throws.** Like `sponsorConfig`, every caller is decorating something
386
+ * that already works: a gateway too old to have the route, a cold start, a
387
+ * dropped connection and an expired token all mean the same thing here, which
388
+ * is "say nothing about a streak". A missed grant is claimed by the next
389
+ * command; a session that died over one would be a real loss.
390
+ */
391
+ async claimStreak(signal) {
392
+ try {
393
+ const res = await fetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
394
+ method: "POST",
395
+ headers: this.headers(),
396
+ ...signal ? { signal } : {}
397
+ });
398
+ if (!res.ok) return null;
399
+ const body = await res.json();
400
+ if (typeof body.day !== "number" || typeof body.credits !== "number") return null;
401
+ return {
402
+ day: body.day,
403
+ credits: body.credits,
404
+ claimed: body.claimed === true,
405
+ balance: typeof body.balance === "number" ? body.balance : 0,
406
+ schedule: Array.isArray(body.schedule) ? body.schedule : [],
407
+ length: typeof body.length === "number" ? body.length : 0
408
+ };
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+ /**
414
+ * Send one rating, and answer what it was paid.
415
+ *
416
+ * **Never throws**, for `claimStreak`'s reason and one of its own: this is
417
+ * called from a prompt that has already interrupted somebody, and the worst
418
+ * possible outcome is that answering it takes the session down. A gateway too
419
+ * old for the route, a cold start and an expired token all mean the same thing
420
+ * — the rating is lost, which costs the user nothing and us one data point.
421
+ *
422
+ * `null` for "nothing to say"; an outcome with `credited: 0` for "recorded,
423
+ * unpaid", which is what the daily ceiling looks like from here and is worth
424
+ * telling the user apart from a grant.
425
+ */
426
+ async sendFeedback(report, signal) {
427
+ try {
428
+ const res = await fetch(`${this.config.gatewayUrl}/v1/feedback`, {
429
+ method: "POST",
430
+ headers: this.headers(),
431
+ ...signal ? { signal } : {},
432
+ body: JSON.stringify({
433
+ session_id: report.session,
434
+ turn: report.turn,
435
+ rating: report.rating,
436
+ model: report.model,
437
+ prompt: report.prompt,
438
+ response: report.response,
439
+ ...report.steps === void 0 ? {} : { steps: report.steps }
440
+ })
441
+ });
442
+ if (!res.ok) return null;
443
+ const body = await res.json();
444
+ return {
445
+ recorded: body.recorded === true,
446
+ credited: typeof body.credited === "number" ? body.credited : 0,
447
+ balance: typeof body.balance === "number" ? body.balance : 0
448
+ };
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+ /**
454
+ * This account's referral code and standing, minting the code if it has none.
455
+ *
456
+ * **This one throws**, unlike `claimStreak` and `sendFeedback`. Those two
457
+ * decorate something that already works and a failure costs a line of chrome;
458
+ * this *is* the command — `clixad refer` with nothing to print is a command
459
+ * that did not run, and saying so beats printing an empty summary.
460
+ */
461
+ async referral(signal) {
462
+ const res = await fetch(`${this.config.gatewayUrl}/v1/referrals`, {
463
+ headers: this.headers(),
464
+ ...signal ? { signal } : {}
465
+ });
466
+ if (res.status === 401) throw await authFailure(res, "referrals");
467
+ if (!res.ok) throw new Error(await httpFailure(res, "referrals"));
468
+ const body = await res.json();
469
+ const num = (value) => typeof value === "number" ? value : 0;
470
+ if (typeof body.code !== "string" || !body.code) {
471
+ throw new Error("the gateway did not return a referral code");
472
+ }
473
+ return {
474
+ code: body.code,
475
+ invited: num(body.invited),
476
+ credited: num(body.credited),
477
+ creditsEarned: num(body.credits_earned),
478
+ remaining: num(body.remaining),
479
+ maxCredited: num(body.max_credited),
480
+ inviterCredits: num(body.inviter_credits),
481
+ inviteeCredits: num(body.invitee_credits)
482
+ };
483
+ }
484
+ /**
485
+ * Spend a redeem code.
486
+ *
487
+ * **Throws**, like `referral` and unlike `claimStreak`: this is the command
488
+ * itself, and somebody who typed a code and got silence has been told nothing.
489
+ * A refusal is not a throw though — an expired or already-spent code answers
490
+ * 200 with a status, because that is an answer rather than a failure.
491
+ */
492
+ async redeem(code, signal) {
493
+ const res = await fetch(`${this.config.gatewayUrl}/v1/redeem`, {
494
+ method: "POST",
495
+ headers: this.headers(),
496
+ ...signal ? { signal } : {},
497
+ body: JSON.stringify({ code })
498
+ });
499
+ if (res.status === 401) throw await authFailure(res, "redeem");
500
+ if (!res.ok) throw new Error(await httpFailure(res, "redeem"));
501
+ const body = await res.json();
502
+ return {
503
+ status: body.status ?? "unknown",
504
+ credited: typeof body.credited === "number" ? body.credited : 0,
505
+ balance: typeof body.balance === "number" ? body.balance : 0
506
+ };
507
+ }
326
508
  async creditPacks() {
327
509
  const res = await fetch(`${this.config.gatewayUrl}/v1/billing/packs`);
328
510
  if (!res.ok) throw new Error(`packs failed: ${res.status}`);
@@ -4452,6 +4634,15 @@ var init_commands = __esm({
4452
4634
  // forms, surveys and app trials, and describing the top-up as a video costs
4453
4635
  // the user the one expectation that makes a screenout make sense.
4454
4636
  { name: "earn", desc: "open the offer wall to earn credits" },
4637
+ // Deliberately *no* `args`, for /login's reason: an arg hint makes enter
4638
+ // complete the command instead of running it, and printing the code is the
4639
+ // whole command.
4640
+ { name: "refer", desc: "your invite code, and what it has earned" },
4641
+ // This one *does* carry an `args` hint, unlike /refer and /login: the code is
4642
+ // the whole command, and `/redeem` on its own has nothing to do — so enter
4643
+ // completing to "/redeem " is the helpful outcome rather than the surprising
4644
+ // one.
4645
+ { name: "redeem", args: "<code>", desc: "spend a code you were given" },
4455
4646
  // Reachable from inside the REPL because that is where the 401 is printed.
4456
4647
  // "Run `clixad login` first" is not a runnable instruction at this prompt —
4457
4648
  // anything without a leading slash is a prompt, so it went to the model and
@@ -4926,7 +5117,7 @@ function useTerminalSize() {
4926
5117
  }, [stdout]);
4927
5118
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
4928
5119
  }
4929
- function App({ client, config, wallet, session, initialTask, sponsorServe }) {
5120
+ function App({ client, config, wallet, streak, session, initialTask, sponsorServe }) {
4930
5121
  const { exit } = useApp();
4931
5122
  const { stdout, write: writeToStdout } = useStdout();
4932
5123
  const idRef = useRef(1);
@@ -5001,6 +5192,8 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
5001
5192
  const pendingTaskRef = useRef(null);
5002
5193
  const mcpRef = useRef(McpHub.empty());
5003
5194
  const todosRef = useRef(new TodoList());
5195
+ const lastTurnRef = useRef(null);
5196
+ const turnCountRef = useRef(config.turns ?? 0);
5004
5197
  const deltaBufRef = useRef("");
5005
5198
  const deltaTimerRef = useRef(null);
5006
5199
  const sponsorRef = useRef(sponsorSource());
@@ -5080,6 +5273,8 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
5080
5273
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5081
5274
  const files = contextRef.current.files;
5082
5275
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
5276
+ const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
5277
+ if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
5083
5278
  if (session?.messages.length) {
5084
5279
  push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
5085
5280
  }
@@ -5364,6 +5559,16 @@ ${NO_SEARCH_INSTRUCTION}`),
5364
5559
  text: result.content,
5365
5560
  meta: `[${result.creditsCharged} credits \xB7 balance ${result.balance.toLocaleString("en-US")}]`
5366
5561
  });
5562
+ lastTurnRef.current = {
5563
+ turn: turnCountRef.current,
5564
+ model,
5565
+ prompt: task,
5566
+ response: result.content,
5567
+ steps: result.steps
5568
+ };
5569
+ turnCountRef.current += 1;
5570
+ config.turns = turnCountRef.current;
5571
+ saveConfig(config);
5367
5572
  }
5368
5573
  } catch (err) {
5369
5574
  if (err instanceof PaywallError) {
@@ -5458,6 +5663,10 @@ ${NO_SEARCH_INSTRUCTION}`),
5458
5663
  tone: "good",
5459
5664
  text: ` signed in as ${account.login ?? account.email ?? "you"} \xB7 balance ${account.balance.toLocaleString("en-US")} credits` + (account.created ? " (signup bonus)" : "")
5460
5665
  });
5666
+ const claimed = await client.claimStreak(signal);
5667
+ const line2 = claimed?.claimed ? streakLabel(claimed) : null;
5668
+ if (claimed?.claimed) setBalance(claimed.balance);
5669
+ if (line2) push({ kind: "notice", tone: "good", text: ` ${line2}` });
5461
5670
  return;
5462
5671
  }
5463
5672
  case "closed":
@@ -5549,6 +5758,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5549
5758
  expandedRef.current = /* @__PURE__ */ new Set();
5550
5759
  heightCacheRef.current = /* @__PURE__ */ new Map();
5551
5760
  todosRef.current = new TodoList();
5761
+ lastTurnRef.current = null;
5552
5762
  sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
5553
5763
  writeToStdout("\x1B[2J\x1B[3J\x1B[H");
5554
5764
  push({ kind: "notice", text: " cleared \u2014 new session, empty context" });
@@ -5635,16 +5845,19 @@ ${NO_SEARCH_INSTRUCTION}`),
5635
5845
  return;
5636
5846
  case "wallet":
5637
5847
  await runBusy(LOADING, async (signal) => {
5848
+ const streakNow = await client.claimStreak(signal);
5638
5849
  const w = await client.wallet(signal);
5639
5850
  setBalance(w.balance);
5640
5851
  const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
5641
5852
  const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
5642
5853
  const screenouts = w.screenouts_today ?? 0;
5643
5854
  const screenoutCredits = w.screenout_credits_today ?? 0;
5855
+ const streakLine = streakNow && streakLabel(streakNow);
5644
5856
  push({
5645
5857
  kind: "notice",
5646
5858
  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` + (screenouts ? `
5647
- plus ${screenoutCredits.toLocaleString("en-US")} credits from ${screenouts} screenout bonus${screenouts === 1 ? "" : "es"} \u2014 attempts that didn't qualify, which CPX pays a little for anyway.` : "") + (reversals.length ? `
5859
+ plus ${screenoutCredits.toLocaleString("en-US")} credits from ${screenouts} screenout bonus${screenouts === 1 ? "" : "es"} \u2014 attempts that didn't qualify, which CPX pays a little for anyway.` : "") + (streakLine ? `
5860
+ ${streakLine}` : "") + (reversals.length ? `
5648
5861
  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.` : "")
5649
5862
  });
5650
5863
  });
@@ -5752,6 +5965,32 @@ ${NO_SEARCH_INSTRUCTION}`),
5752
5965
  case "earn":
5753
5966
  await runAdWall();
5754
5967
  return;
5968
+ // Through `runBusy` like every other gateway call: the free instance can
5969
+ // cold-start for the better part of a minute, and the box says "esc to
5970
+ // stop" whenever it is locked.
5971
+ case "refer":
5972
+ await runBusy(LOADING, async (signal) => {
5973
+ const info = await client.referral(signal);
5974
+ push({ kind: "notice", text: referLines(info).map((line3) => ` ${line3}`).join("\n") });
5975
+ });
5976
+ return;
5977
+ case "redeem": {
5978
+ if (!arg) {
5979
+ push({ kind: "notice", tone: "warn", text: " usage: /redeem <code>" });
5980
+ return;
5981
+ }
5982
+ await runBusy(LOADING, async (signal) => {
5983
+ const result = await client.redeem(arg, signal);
5984
+ const { ok, text: text2 } = redeemMessage(result);
5985
+ if (ok) setBalance(result.balance);
5986
+ push({
5987
+ kind: "notice",
5988
+ tone: ok ? "good" : "warn",
5989
+ text: ok ? ` ${text2} \xB7 balance ${result.balance.toLocaleString("en-US")}` : ` ${text2}`
5990
+ });
5991
+ });
5992
+ return;
5993
+ }
5755
5994
  case "login":
5756
5995
  await runLogin(arg);
5757
5996
  return;
@@ -5819,14 +6058,58 @@ ${output}` }
5819
6058
  },
5820
6059
  [push, root, runCommand, runShell, runTurn2]
5821
6060
  );
6061
+ const askFeedback = useCallback(async () => {
6062
+ const turn = lastTurnRef.current;
6063
+ if (!turn) return;
6064
+ lastTurnRef.current = null;
6065
+ const amount = wallet?.rewards?.feedback_credits;
6066
+ const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
6067
+ const choice = await new Promise((resolve2) => {
6068
+ setPickerSel(0);
6069
+ setPicker({
6070
+ title: "Was that last answer any good?",
6071
+ subtitle: `${pay} \xB7 we keep that exchange to test against \xB7 esc skips`,
6072
+ items: [
6073
+ { value: "up", label: "yes \u2014 that worked", hint: "" },
6074
+ { value: "down", label: "no \u2014 that missed", hint: "" },
6075
+ { value: "skip", label: "skip", hint: "nothing is sent, nothing is stored" }
6076
+ ],
6077
+ onPick: (item) => resolve2(item.value),
6078
+ // Esc and "skip" are the same outcome and must stay that way: a skip
6079
+ // that cost something is a prompt people stop reading.
6080
+ onCancel: () => resolve2(null)
6081
+ });
6082
+ });
6083
+ if (choice !== "up" && choice !== "down") return;
6084
+ await runBusy(SENDING_FEEDBACK, async (signal) => {
6085
+ const outcome = await client.sendFeedback(
6086
+ { ...turn, session: sessionRef.current.id, rating: choice },
6087
+ signal
6088
+ );
6089
+ if (!outcome) return;
6090
+ if (outcome.credited > 0) setBalance(outcome.balance);
6091
+ push({
6092
+ kind: "notice",
6093
+ tone: "good",
6094
+ text: outcome.credited > 0 ? ` thanks \u2014 +${outcome.credited.toLocaleString("en-US")} credits` : (
6095
+ // Recorded and unpaid: the daily ceiling, or a turn already rated.
6096
+ // Worth saying, because the offer of credits was made a moment ago.
6097
+ " thanks \u2014 noted (today's feedback credits are already in)"
6098
+ )
6099
+ });
6100
+ });
6101
+ }, [client, push, runBusy, wallet]);
5822
6102
  const runSerially = useCallback(
5823
6103
  async (line2) => {
5824
6104
  await dispatchLine(line2);
5825
6105
  for (let next = dequeue(); next !== void 0; next = dequeue()) {
5826
6106
  await dispatchLine(next);
5827
6107
  }
6108
+ if (lastTurnRef.current && turnCountRef.current % FEEDBACK_TURN_INTERVAL === 0) {
6109
+ await askFeedback();
6110
+ }
5828
6111
  },
5829
- [dequeue, dispatchLine]
6112
+ [askFeedback, dequeue, dispatchLine]
5830
6113
  );
5831
6114
  const submit = useCallback(
5832
6115
  async (raw) => {
@@ -6326,7 +6609,7 @@ function tailLines(text, max) {
6326
6609
  const lines = text.split("\n");
6327
6610
  return lines.length <= max ? text : lines.slice(-max).join("\n");
6328
6611
  }
6329
- var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, ASK_HINT, PLAN_HINT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
6612
+ var WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, SENDING_FEEDBACK, FEEDBACK_TURN_INTERVAL, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, QUEUE_MARKER, PICKER_CURRENT, MIN_PICKER_LABEL_COLS, PICKER_CHROME, PICKER_CHROME_COMPACT, ASK_CHROME, PLAN_CHROME, ASK_HINT, PLAN_HINT, EXPAND_MAX_LINES, FALLBACK_ROWS, FALLBACK_COLS, CONTEXT_NOTICE_AT, KEEP_ALWAYS;
6330
6613
  var init_app = __esm({
6331
6614
  "src/tui/app.tsx"() {
6332
6615
  "use strict";
@@ -6363,6 +6646,8 @@ var init_app = __esm({
6363
6646
  COMPACTING = "compacting\u2026";
6364
6647
  SIGNING_IN = "signing in\u2026";
6365
6648
  RUNNING = "running\u2026";
6649
+ SENDING_FEEDBACK = "sending\u2026";
6650
+ FEEDBACK_TURN_INTERVAL = 50;
6366
6651
  NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
6367
6652
  LIVE_OUTPUT_LINES = 5;
6368
6653
  COMMITTED_OUTPUT_LINES = 4;
@@ -6485,6 +6770,10 @@ async function main() {
6485
6770
  return showWallet(client);
6486
6771
  case "earn":
6487
6772
  return earn(client, config);
6773
+ case "refer":
6774
+ return refer(client, config);
6775
+ case "redeem":
6776
+ return redeem(client, config, rest[0]);
6488
6777
  case "buy":
6489
6778
  return buyCmd(client, config, rest[0]);
6490
6779
  case "ask":
@@ -6546,6 +6835,7 @@ async function login(client, config, args) {
6546
6835
  \u2713 logged in as ${who}`) + suffix);
6547
6836
  const bonus = account.created ? c.dim(account.signupBonus === void 0 ? " (signup bonus)" : ` (signup bonus ${credits(account.signupBonus)})`) : "";
6548
6837
  console.log(` balance: ${c.bold(credits(account.balance))} credits${bonus}`);
6838
+ await showStreak(client);
6549
6839
  console.log(c.dim(` token stored in ${configPath()}`));
6550
6840
  return;
6551
6841
  }
@@ -6630,9 +6920,12 @@ async function setModel(client, config, model) {
6630
6920
  console.log(c.green(`\u2713 model set to ${model}`));
6631
6921
  }
6632
6922
  async function showWallet(client) {
6923
+ const streak = await client.claimStreak();
6633
6924
  const w = await client.wallet();
6634
6925
  console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
6635
6926
  console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
6927
+ const line2 = streak && streakLabel(streak);
6928
+ if (line2) console.log(streak.claimed ? c.green(line2) : c.dim(line2));
6636
6929
  if (w.ledger.length) {
6637
6930
  console.log(c.dim("recent:"));
6638
6931
  const label = (e) => ledgerReasonLabel(e.reason);
@@ -6679,6 +6972,40 @@ async function earn(client, config) {
6679
6972
  );
6680
6973
  console.log(c.yellow(" pays nothing and is normal. Start another, or check `clixad wallet` later."));
6681
6974
  }
6975
+ async function refer(client, config) {
6976
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
6977
+ let info;
6978
+ try {
6979
+ info = await client.referral();
6980
+ } catch (err) {
6981
+ console.error(c.red(err.message));
6982
+ process.exitCode = 1;
6983
+ return;
6984
+ }
6985
+ const [code, share, deal, ...rest] = referLines(info);
6986
+ console.log(`
6987
+ ${c.bold(code)}`);
6988
+ console.log(` ${c.cyan(share)}`);
6989
+ console.log(c.dim(` ${deal}`));
6990
+ for (const line2 of rest) console.log(c.dim(` ${line2}`));
6991
+ console.log("");
6992
+ }
6993
+ async function redeem(client, config, code) {
6994
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
6995
+ if (!code) return console.error(c.red("usage: clixad redeem <code>"));
6996
+ let result;
6997
+ try {
6998
+ result = await client.redeem(code);
6999
+ } catch (err) {
7000
+ console.error(c.red(err.message));
7001
+ process.exitCode = 1;
7002
+ return;
7003
+ }
7004
+ const { ok, text } = redeemMessage(result);
7005
+ if (!ok) return console.log(c.yellow(` ${text}`));
7006
+ console.log(c.green(`
7007
+ \u2713 ${text}`) + ` \xB7 balance ${c.bold(credits(result.balance))}`);
7008
+ }
6682
7009
  async function buyCmd(client, config, pack) {
6683
7010
  if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
6684
7011
  const { enabled, packs } = await client.creditPacks();
@@ -6845,10 +7172,11 @@ async function repl(client, config, opts = {}) {
6845
7172
  session = loadSession(opts.resume);
6846
7173
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
6847
7174
  }
7175
+ const streak = await client.claimStreak();
6848
7176
  const wallet = await safeWallet(client);
6849
7177
  clearScreen();
6850
7178
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
6851
- await startTui2({ client, config, wallet, session, initialTask: opts.task });
7179
+ await startTui2({ client, config, wallet, streak, session, initialTask: opts.task });
6852
7180
  }
6853
7181
  async function runTurn(client, config, messages) {
6854
7182
  try {
@@ -6875,6 +7203,13 @@ async function runTurn(client, config, messages) {
6875
7203
  return void 0;
6876
7204
  }
6877
7205
  }
7206
+ async function showStreak(client) {
7207
+ const streak = await client.claimStreak();
7208
+ const line2 = streak && streakLabel(streak);
7209
+ if (!line2) return streak;
7210
+ console.log(streak.claimed ? c.green(` ${line2}`) : c.dim(` ${line2}`));
7211
+ return streak;
7212
+ }
6878
7213
  async function safeWallet(client) {
6879
7214
  try {
6880
7215
  return await client.wallet();
@@ -6892,6 +7227,8 @@ function printHelp() {
6892
7227
  ${c.cyan("model")} <id> set default model
6893
7228
  ${c.cyan("wallet")} balance, ads today, ledger
6894
7229
  ${c.cyan("earn")} open the ad wall to earn credits
7230
+ ${c.cyan("refer")} your invite code, and what it has earned
7231
+ ${c.cyan("redeem")} <code> spend a code you were given
6895
7232
  ${c.cyan("buy")} [pack] list credit packs / buy one via Stripe
6896
7233
  ${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
6897
7234
  ${c.cyan("agent")} "<task>" open the REPL with a task already running
@@ -6901,7 +7238,9 @@ function printHelp() {
6901
7238
  ${c.cyan("--resume")} [id] list saved sessions, or resume one
6902
7239
  ${c.cyan("--version")} print the version and exit
6903
7240
  (no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
6904
- "<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task`);
7241
+ "<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task
7242
+
7243
+ ${c.dim("Discord: https://discord.gg/pwxPM3QyF \u2014 bugs and feature requests are read there")}`);
6905
7244
  }
6906
7245
  main().catch((err) => {
6907
7246
  console.error(c.red(err.message));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.15",
3
+ "version": "0.0.1-beta.16",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",