clixad 0.0.1-beta.15 → 0.0.1-beta.17

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,16 +373,154 @@ 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}`);
329
511
  return res.json();
330
512
  }
331
- async checkout(pack) {
513
+ /**
514
+ * `consent` carries the two declarations § 356 (5) BGB requires before we may
515
+ * deliver digital content inside the withdrawal period. It is not optional:
516
+ * the gateway refuses a checkout without it, because starting a purchase we
517
+ * could not deliver instantly is worse than not starting one.
518
+ */
519
+ async checkout(pack, consent) {
332
520
  const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
333
521
  method: "POST",
334
522
  headers: this.headers(),
335
- body: JSON.stringify({ pack })
523
+ body: JSON.stringify({ pack, consent })
336
524
  });
337
525
  if (res.status === 401) throw await authFailure(res, "checkout");
338
526
  if (res.status === 501) throw new Error("Buying credits isn't enabled (Stripe not configured on the gateway).");
@@ -819,12 +1007,18 @@ function sleep(ms, signal) {
819
1007
  signal.addEventListener("abort", done, { once: true });
820
1008
  });
821
1009
  }
1010
+ var TERMS_URL, PRIVACY_URL, TERMS_NOTICE;
822
1011
  var init_login = __esm({
823
1012
  "src/login.ts"() {
824
1013
  "use strict";
825
1014
  init_client();
826
1015
  init_browser();
827
1016
  init_config();
1017
+ TERMS_URL = "https://clixad.io/terms.html";
1018
+ PRIVACY_URL = "https://clixad.io/privacy.html";
1019
+ TERMS_NOTICE = `By creating an account you confirm that you are at least 18 and that you agree to
1020
+ the Terms \u2014 ${TERMS_URL}
1021
+ and the Privacy Policy \u2014 ${PRIVACY_URL}`;
828
1022
  }
829
1023
  });
830
1024
 
@@ -4452,6 +4646,15 @@ var init_commands = __esm({
4452
4646
  // forms, surveys and app trials, and describing the top-up as a video costs
4453
4647
  // the user the one expectation that makes a screenout make sense.
4454
4648
  { name: "earn", desc: "open the offer wall to earn credits" },
4649
+ // Deliberately *no* `args`, for /login's reason: an arg hint makes enter
4650
+ // complete the command instead of running it, and printing the code is the
4651
+ // whole command.
4652
+ { name: "refer", desc: "your invite code, and what it has earned" },
4653
+ // This one *does* carry an `args` hint, unlike /refer and /login: the code is
4654
+ // the whole command, and `/redeem` on its own has nothing to do — so enter
4655
+ // completing to "/redeem " is the helpful outcome rather than the surprising
4656
+ // one.
4657
+ { name: "redeem", args: "<code>", desc: "spend a code you were given" },
4455
4658
  // Reachable from inside the REPL because that is where the 401 is printed.
4456
4659
  // "Run `clixad login` first" is not a runnable instruction at this prompt —
4457
4660
  // anything without a leading slash is a prompt, so it went to the model and
@@ -4926,7 +5129,7 @@ function useTerminalSize() {
4926
5129
  }, [stdout]);
4927
5130
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
4928
5131
  }
4929
- function App({ client, config, wallet, session, initialTask, sponsorServe }) {
5132
+ function App({ client, config, wallet, streak, session, initialTask, sponsorServe }) {
4930
5133
  const { exit } = useApp();
4931
5134
  const { stdout, write: writeToStdout } = useStdout();
4932
5135
  const idRef = useRef(1);
@@ -5001,6 +5204,8 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
5001
5204
  const pendingTaskRef = useRef(null);
5002
5205
  const mcpRef = useRef(McpHub.empty());
5003
5206
  const todosRef = useRef(new TodoList());
5207
+ const lastTurnRef = useRef(null);
5208
+ const turnCountRef = useRef(config.turns ?? 0);
5004
5209
  const deltaBufRef = useRef("");
5005
5210
  const deltaTimerRef = useRef(null);
5006
5211
  const sponsorRef = useRef(sponsorSource());
@@ -5080,6 +5285,8 @@ function App({ client, config, wallet, session, initialTask, sponsorServe }) {
5080
5285
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5081
5286
  const files = contextRef.current.files;
5082
5287
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
5288
+ const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
5289
+ if (streakLine) push({ kind: "notice", tone: "good", text: ` ${streakLine}` });
5083
5290
  if (session?.messages.length) {
5084
5291
  push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
5085
5292
  }
@@ -5364,6 +5571,16 @@ ${NO_SEARCH_INSTRUCTION}`),
5364
5571
  text: result.content,
5365
5572
  meta: `[${result.creditsCharged} credits \xB7 balance ${result.balance.toLocaleString("en-US")}]`
5366
5573
  });
5574
+ lastTurnRef.current = {
5575
+ turn: turnCountRef.current,
5576
+ model,
5577
+ prompt: task,
5578
+ response: result.content,
5579
+ steps: result.steps
5580
+ };
5581
+ turnCountRef.current += 1;
5582
+ config.turns = turnCountRef.current;
5583
+ saveConfig(config);
5367
5584
  }
5368
5585
  } catch (err) {
5369
5586
  if (err instanceof PaywallError) {
@@ -5434,6 +5651,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5434
5651
  const runLogin = useCallback(
5435
5652
  async (arg) => {
5436
5653
  const args = parseLoginArgs(arg.split(/\s+/).filter(Boolean));
5654
+ push({ kind: "notice", text: TERMS_NOTICE });
5437
5655
  await runBusy(SIGNING_IN, async (signal) => {
5438
5656
  const outcome = await performLogin(
5439
5657
  client,
@@ -5458,6 +5676,10 @@ ${NO_SEARCH_INSTRUCTION}`),
5458
5676
  tone: "good",
5459
5677
  text: ` signed in as ${account.login ?? account.email ?? "you"} \xB7 balance ${account.balance.toLocaleString("en-US")} credits` + (account.created ? " (signup bonus)" : "")
5460
5678
  });
5679
+ const claimed = await client.claimStreak(signal);
5680
+ const line2 = claimed?.claimed ? streakLabel(claimed) : null;
5681
+ if (claimed?.claimed) setBalance(claimed.balance);
5682
+ if (line2) push({ kind: "notice", tone: "good", text: ` ${line2}` });
5461
5683
  return;
5462
5684
  }
5463
5685
  case "closed":
@@ -5549,6 +5771,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5549
5771
  expandedRef.current = /* @__PURE__ */ new Set();
5550
5772
  heightCacheRef.current = /* @__PURE__ */ new Map();
5551
5773
  todosRef.current = new TodoList();
5774
+ lastTurnRef.current = null;
5552
5775
  sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
5553
5776
  writeToStdout("\x1B[2J\x1B[3J\x1B[H");
5554
5777
  push({ kind: "notice", text: " cleared \u2014 new session, empty context" });
@@ -5635,16 +5858,19 @@ ${NO_SEARCH_INSTRUCTION}`),
5635
5858
  return;
5636
5859
  case "wallet":
5637
5860
  await runBusy(LOADING, async (signal) => {
5861
+ const streakNow = await client.claimStreak(signal);
5638
5862
  const w = await client.wallet(signal);
5639
5863
  setBalance(w.balance);
5640
5864
  const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
5641
5865
  const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
5642
5866
  const screenouts = w.screenouts_today ?? 0;
5643
5867
  const screenoutCredits = w.screenout_credits_today ?? 0;
5868
+ const streakLine = streakNow && streakLabel(streakNow);
5644
5869
  push({
5645
5870
  kind: "notice",
5646
5871
  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 ? `
5872
+ 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 ? `
5873
+ ${streakLine}` : "") + (reversals.length ? `
5648
5874
  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
5875
  });
5650
5876
  });
@@ -5752,6 +5978,32 @@ ${NO_SEARCH_INSTRUCTION}`),
5752
5978
  case "earn":
5753
5979
  await runAdWall();
5754
5980
  return;
5981
+ // Through `runBusy` like every other gateway call: the free instance can
5982
+ // cold-start for the better part of a minute, and the box says "esc to
5983
+ // stop" whenever it is locked.
5984
+ case "refer":
5985
+ await runBusy(LOADING, async (signal) => {
5986
+ const info = await client.referral(signal);
5987
+ push({ kind: "notice", text: referLines(info).map((line3) => ` ${line3}`).join("\n") });
5988
+ });
5989
+ return;
5990
+ case "redeem": {
5991
+ if (!arg) {
5992
+ push({ kind: "notice", tone: "warn", text: " usage: /redeem <code>" });
5993
+ return;
5994
+ }
5995
+ await runBusy(LOADING, async (signal) => {
5996
+ const result = await client.redeem(arg, signal);
5997
+ const { ok, text: text2 } = redeemMessage(result);
5998
+ if (ok) setBalance(result.balance);
5999
+ push({
6000
+ kind: "notice",
6001
+ tone: ok ? "good" : "warn",
6002
+ text: ok ? ` ${text2} \xB7 balance ${result.balance.toLocaleString("en-US")}` : ` ${text2}`
6003
+ });
6004
+ });
6005
+ return;
6006
+ }
5755
6007
  case "login":
5756
6008
  await runLogin(arg);
5757
6009
  return;
@@ -5819,14 +6071,58 @@ ${output}` }
5819
6071
  },
5820
6072
  [push, root, runCommand, runShell, runTurn2]
5821
6073
  );
6074
+ const askFeedback = useCallback(async () => {
6075
+ const turn = lastTurnRef.current;
6076
+ if (!turn) return;
6077
+ lastTurnRef.current = null;
6078
+ const amount = wallet?.rewards?.feedback_credits;
6079
+ const pay = amount ? `Both answers pay ${amount.toLocaleString("en-US")} credits` : "Both answers pay the same";
6080
+ const choice = await new Promise((resolve2) => {
6081
+ setPickerSel(0);
6082
+ setPicker({
6083
+ title: "Was that last answer any good?",
6084
+ subtitle: `${pay} \xB7 we keep that exchange to test against \xB7 esc skips`,
6085
+ items: [
6086
+ { value: "up", label: "yes \u2014 that worked", hint: "" },
6087
+ { value: "down", label: "no \u2014 that missed", hint: "" },
6088
+ { value: "skip", label: "skip", hint: "nothing is sent, nothing is stored" }
6089
+ ],
6090
+ onPick: (item) => resolve2(item.value),
6091
+ // Esc and "skip" are the same outcome and must stay that way: a skip
6092
+ // that cost something is a prompt people stop reading.
6093
+ onCancel: () => resolve2(null)
6094
+ });
6095
+ });
6096
+ if (choice !== "up" && choice !== "down") return;
6097
+ await runBusy(SENDING_FEEDBACK, async (signal) => {
6098
+ const outcome = await client.sendFeedback(
6099
+ { ...turn, session: sessionRef.current.id, rating: choice },
6100
+ signal
6101
+ );
6102
+ if (!outcome) return;
6103
+ if (outcome.credited > 0) setBalance(outcome.balance);
6104
+ push({
6105
+ kind: "notice",
6106
+ tone: "good",
6107
+ text: outcome.credited > 0 ? ` thanks \u2014 +${outcome.credited.toLocaleString("en-US")} credits` : (
6108
+ // Recorded and unpaid: the daily ceiling, or a turn already rated.
6109
+ // Worth saying, because the offer of credits was made a moment ago.
6110
+ " thanks \u2014 noted (today's feedback credits are already in)"
6111
+ )
6112
+ });
6113
+ });
6114
+ }, [client, push, runBusy, wallet]);
5822
6115
  const runSerially = useCallback(
5823
6116
  async (line2) => {
5824
6117
  await dispatchLine(line2);
5825
6118
  for (let next = dequeue(); next !== void 0; next = dequeue()) {
5826
6119
  await dispatchLine(next);
5827
6120
  }
6121
+ if (lastTurnRef.current && turnCountRef.current % FEEDBACK_TURN_INTERVAL === 0) {
6122
+ await askFeedback();
6123
+ }
5828
6124
  },
5829
- [dequeue, dispatchLine]
6125
+ [askFeedback, dequeue, dispatchLine]
5830
6126
  );
5831
6127
  const submit = useCallback(
5832
6128
  async (raw) => {
@@ -6326,7 +6622,7 @@ function tailLines(text, max) {
6326
6622
  const lines = text.split("\n");
6327
6623
  return lines.length <= max ? text : lines.slice(-max).join("\n");
6328
6624
  }
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;
6625
+ 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
6626
  var init_app = __esm({
6331
6627
  "src/tui/app.tsx"() {
6332
6628
  "use strict";
@@ -6363,6 +6659,8 @@ var init_app = __esm({
6363
6659
  COMPACTING = "compacting\u2026";
6364
6660
  SIGNING_IN = "signing in\u2026";
6365
6661
  RUNNING = "running\u2026";
6662
+ SENDING_FEEDBACK = "sending\u2026";
6663
+ FEEDBACK_TURN_INTERVAL = 50;
6366
6664
  NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
6367
6665
  LIVE_OUTPUT_LINES = 5;
6368
6666
  COMMITTED_OUTPUT_LINES = 4;
@@ -6434,6 +6732,7 @@ init_kimi();
6434
6732
  init_banner();
6435
6733
  init_browser();
6436
6734
  init_color();
6735
+ import { createInterface } from "node:readline/promises";
6437
6736
 
6438
6737
  // src/title.ts
6439
6738
  var APP_TITLE = "Clixad";
@@ -6485,6 +6784,10 @@ async function main() {
6485
6784
  return showWallet(client);
6486
6785
  case "earn":
6487
6786
  return earn(client, config);
6787
+ case "refer":
6788
+ return refer(client, config);
6789
+ case "redeem":
6790
+ return redeem(client, config, rest[0]);
6488
6791
  case "buy":
6489
6792
  return buyCmd(client, config, rest[0]);
6490
6793
  case "ask":
@@ -6526,6 +6829,7 @@ async function main() {
6526
6829
  }
6527
6830
  async function login(client, config, args) {
6528
6831
  const parsed = parseLoginArgs(args);
6832
+ for (const line2 of TERMS_NOTICE.split("\n")) console.log(c.dim(` ${line2}`));
6529
6833
  const outcome = await performLogin(client, config, parsed, {
6530
6834
  verify(start) {
6531
6835
  console.log(`
@@ -6546,6 +6850,7 @@ async function login(client, config, args) {
6546
6850
  \u2713 logged in as ${who}`) + suffix);
6547
6851
  const bonus = account.created ? c.dim(account.signupBonus === void 0 ? " (signup bonus)" : ` (signup bonus ${credits(account.signupBonus)})`) : "";
6548
6852
  console.log(` balance: ${c.bold(credits(account.balance))} credits${bonus}`);
6853
+ await showStreak(client);
6549
6854
  console.log(c.dim(` token stored in ${configPath()}`));
6550
6855
  return;
6551
6856
  }
@@ -6630,9 +6935,12 @@ async function setModel(client, config, model) {
6630
6935
  console.log(c.green(`\u2713 model set to ${model}`));
6631
6936
  }
6632
6937
  async function showWallet(client) {
6938
+ const streak = await client.claimStreak();
6633
6939
  const w = await client.wallet();
6634
6940
  console.log(`balance: ${c.bold(w.balance.toLocaleString("en-US"))} credits`);
6635
6941
  console.log(`${earnedToday(w)} \xB7 offers pay ${grantRangeLabel(w.grant_per_ad_range)}`);
6942
+ const line2 = streak && streakLabel(streak);
6943
+ if (line2) console.log(streak.claimed ? c.green(line2) : c.dim(line2));
6636
6944
  if (w.ledger.length) {
6637
6945
  console.log(c.dim("recent:"));
6638
6946
  const label = (e) => ledgerReasonLabel(e.reason);
@@ -6679,6 +6987,40 @@ async function earn(client, config) {
6679
6987
  );
6680
6988
  console.log(c.yellow(" pays nothing and is normal. Start another, or check `clixad wallet` later."));
6681
6989
  }
6990
+ async function refer(client, config) {
6991
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
6992
+ let info;
6993
+ try {
6994
+ info = await client.referral();
6995
+ } catch (err) {
6996
+ console.error(c.red(err.message));
6997
+ process.exitCode = 1;
6998
+ return;
6999
+ }
7000
+ const [code, share, deal, ...rest] = referLines(info);
7001
+ console.log(`
7002
+ ${c.bold(code)}`);
7003
+ console.log(` ${c.cyan(share)}`);
7004
+ console.log(c.dim(` ${deal}`));
7005
+ for (const line2 of rest) console.log(c.dim(` ${line2}`));
7006
+ console.log("");
7007
+ }
7008
+ async function redeem(client, config, code) {
7009
+ if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
7010
+ if (!code) return console.error(c.red("usage: clixad redeem <code>"));
7011
+ let result;
7012
+ try {
7013
+ result = await client.redeem(code);
7014
+ } catch (err) {
7015
+ console.error(c.red(err.message));
7016
+ process.exitCode = 1;
7017
+ return;
7018
+ }
7019
+ const { ok, text } = redeemMessage(result);
7020
+ if (!ok) return console.log(c.yellow(` ${text}`));
7021
+ console.log(c.green(`
7022
+ \u2713 ${text}`) + ` \xB7 balance ${c.bold(credits(result.balance))}`);
7023
+ }
6682
7024
  async function buyCmd(client, config, pack) {
6683
7025
  if (!config.token) return console.log(c.yellow("Not logged in. Run `clixad login` first."));
6684
7026
  const { enabled, packs } = await client.creditPacks();
@@ -6693,9 +7035,20 @@ async function buyCmd(client, config, pack) {
6693
7035
  console.log(c.dim("\n buy one with: clixad buy <id>"));
6694
7036
  return;
6695
7037
  }
7038
+ const chosen = packs.find((x) => x.id === pack);
7039
+ if (!chosen) {
7040
+ console.log(c.red(` unknown pack: ${pack}`));
7041
+ return console.log(c.dim(` try one of: ${packs.map((x) => x.id).join(", ")}`));
7042
+ }
7043
+ const consented = await confirmWithdrawalWaiver(chosen);
7044
+ if (!consented) return;
6696
7045
  let checkout;
6697
7046
  try {
6698
- checkout = await client.checkout(pack);
7047
+ checkout = await client.checkout(pack, {
7048
+ requestImmediatePerformance: true,
7049
+ acknowledgeLossOfWithdrawal: true,
7050
+ at: (/* @__PURE__ */ new Date()).toISOString()
7051
+ });
6699
7052
  } catch (err) {
6700
7053
  return console.log(c.red(err.message));
6701
7054
  }
@@ -6718,6 +7071,39 @@ async function buyCmd(client, config, pack) {
6718
7071
  }
6719
7072
  console.log(c.yellow("\n No credits yet \u2014 the webhook may be delayed. Check `clixad wallet`."));
6720
7073
  }
7074
+ async function confirmWithdrawalWaiver(pack) {
7075
+ if (!process.stdin.isTTY) {
7076
+ console.log(c.yellow("\n Buying credits needs an interactive terminal."));
7077
+ console.log(
7078
+ c.dim(" We have to record your confirmation before charging you, and a pipe has nobody to ask.")
7079
+ );
7080
+ return false;
7081
+ }
7082
+ const price = "$" + pack.price_usd.toFixed(2);
7083
+ console.log(`
7084
+ ${c.bold(pack.label)} \u2014 ${credits(pack.credits)} credits for ${c.bold(price)}`);
7085
+ console.log(c.dim(` One purchase. No subscription, nothing to cancel. Terms: ${TERMS_URL}`));
7086
+ console.log("\n Before you pay, please confirm:");
7087
+ console.log(" \xB7 You request that we deliver the credits immediately, before the");
7088
+ console.log(" 14-day withdrawal period has expired.");
7089
+ console.log(" \xB7 You acknowledge that your right of withdrawal lapses once they");
7090
+ console.log(" have been delivered.");
7091
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7092
+ let answer = "";
7093
+ try {
7094
+ answer = (await rl.question(`
7095
+ Type ${c.bold("yes")} to continue: `)).trim().toLowerCase();
7096
+ } catch {
7097
+ answer = "";
7098
+ } finally {
7099
+ rl.close();
7100
+ }
7101
+ if (answer !== "yes" && answer !== "y") {
7102
+ console.log(c.dim("\n Cancelled. You have not been charged."));
7103
+ return false;
7104
+ }
7105
+ return true;
7106
+ }
6721
7107
  async function ask(client, config, prompt) {
6722
7108
  if (!prompt) return console.error(c.red('usage: clixad ask "your prompt"'));
6723
7109
  const messages = [{ role: "user", content: prompt }];
@@ -6845,10 +7231,11 @@ async function repl(client, config, opts = {}) {
6845
7231
  session = loadSession(opts.resume);
6846
7232
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
6847
7233
  }
7234
+ const streak = await client.claimStreak();
6848
7235
  const wallet = await safeWallet(client);
6849
7236
  clearScreen();
6850
7237
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
6851
- await startTui2({ client, config, wallet, session, initialTask: opts.task });
7238
+ await startTui2({ client, config, wallet, streak, session, initialTask: opts.task });
6852
7239
  }
6853
7240
  async function runTurn(client, config, messages) {
6854
7241
  try {
@@ -6875,6 +7262,13 @@ async function runTurn(client, config, messages) {
6875
7262
  return void 0;
6876
7263
  }
6877
7264
  }
7265
+ async function showStreak(client) {
7266
+ const streak = await client.claimStreak();
7267
+ const line2 = streak && streakLabel(streak);
7268
+ if (!line2) return streak;
7269
+ console.log(streak.claimed ? c.green(` ${line2}`) : c.dim(` ${line2}`));
7270
+ return streak;
7271
+ }
6878
7272
  async function safeWallet(client) {
6879
7273
  try {
6880
7274
  return await client.wallet();
@@ -6892,6 +7286,8 @@ function printHelp() {
6892
7286
  ${c.cyan("model")} <id> set default model
6893
7287
  ${c.cyan("wallet")} balance, ads today, ledger
6894
7288
  ${c.cyan("earn")} open the ad wall to earn credits
7289
+ ${c.cyan("refer")} your invite code, and what it has earned
7290
+ ${c.cyan("redeem")} <code> spend a code you were given
6895
7291
  ${c.cyan("buy")} [pack] list credit packs / buy one via Stripe
6896
7292
  ${c.cyan("ask")} "<prompt>" one-shot completion (no tools)
6897
7293
  ${c.cyan("agent")} "<task>" open the REPL with a task already running
@@ -6901,7 +7297,9 @@ function printHelp() {
6901
7297
  ${c.cyan("--resume")} [id] list saved sessions, or resume one
6902
7298
  ${c.cyan("--version")} print the version and exit
6903
7299
  (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`);
7300
+ "<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task
7301
+
7302
+ ${c.dim("Discord: https://discord.gg/pwxPM3QyF \u2014 bugs and feature requests are read there")}`);
6905
7303
  }
6906
7304
  main().catch((err) => {
6907
7305
  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.17",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",