clixad 0.0.1-beta.5 → 0.0.1-beta.7

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 (2) hide show
  1. package/dist/clixad.mjs +447 -168
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -88,6 +88,7 @@ var init_client = __esm({
88
88
  LEDGER_REASONS = {
89
89
  signup_bonus: "signup bonus",
90
90
  ad_reward: "offer reward",
91
+ ad_screenout_bonus: "screenout bonus (didn't qualify)",
91
92
  ad_reversal: "offer reward reversed by the provider",
92
93
  purchase: "credits purchased",
93
94
  usage: "model usage",
@@ -315,12 +316,25 @@ var init_client = __esm({
315
316
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
316
317
  import { homedir } from "node:os";
317
318
  import { dirname, join } from "node:path";
319
+ function sameEndpoint(a, b) {
320
+ const norm = (raw) => {
321
+ try {
322
+ const url = new URL(raw);
323
+ const host = url.hostname === "localhost" ? "127.0.0.1" : url.hostname;
324
+ return `${url.protocol}//${host}:${url.port}${url.pathname.replace(/\/+$/, "")}`;
325
+ } catch {
326
+ return raw.trim().replace(/\/+$/, "");
327
+ }
328
+ };
329
+ return norm(a) === norm(b);
330
+ }
318
331
  function loadConfig() {
319
332
  let stored = {};
320
333
  try {
321
334
  stored = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
322
335
  } catch {
323
336
  }
337
+ const mintedAgainst = stored.gatewayUrl;
324
338
  for (const [key, stale] of Object.entries(SUPERSEDED_DEFAULTS)) {
325
339
  const value = stored[key];
326
340
  if (typeof value === "string" && stale.includes(value) && value !== DEFAULTS[key]) {
@@ -332,6 +346,10 @@ function loadConfig() {
332
346
  const value = process.env[envVar];
333
347
  if (value) config[key] = value;
334
348
  }
349
+ const migratedGateway = mintedAgainst !== void 0 && stored.gatewayUrl === void 0;
350
+ if (migratedGateway && !sameEndpoint(mintedAgainst, config.gatewayUrl)) {
351
+ for (const key of IDENTITY_KEYS) delete config[key];
352
+ }
335
353
  return config;
336
354
  }
337
355
  function saveConfig(config) {
@@ -345,7 +363,7 @@ function saveConfig(config) {
345
363
  function configPath() {
346
364
  return CONFIG_PATH;
347
365
  }
348
- var CONFIG_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS;
366
+ var CONFIG_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS;
349
367
  var init_config = __esm({
350
368
  "src/config.ts"() {
351
369
  "use strict";
@@ -367,6 +385,121 @@ var init_config = __esm({
367
385
  gatewayUrl: ["http://127.0.0.1:8787", "http://localhost:8787"],
368
386
  dashboardUrl: ["http://127.0.0.1:8788", "http://localhost:8788"]
369
387
  };
388
+ IDENTITY_KEYS = ["token", "userId", "email", "login"];
389
+ }
390
+ });
391
+
392
+ // src/browser.ts
393
+ import { spawn } from "node:child_process";
394
+ function openBrowser(url) {
395
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
396
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
397
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
398
+ child.on("error", () => void 0);
399
+ child.unref();
400
+ }
401
+ var init_browser = __esm({
402
+ "src/browser.ts"() {
403
+ "use strict";
404
+ }
405
+ });
406
+
407
+ // src/login.ts
408
+ function parseLoginArgs(args) {
409
+ let invite;
410
+ const rest = [];
411
+ for (let i = 0; i < args.length; i++) {
412
+ const a = args[i];
413
+ if (a === "--invite") invite = args[++i];
414
+ else if (a.startsWith("--invite=")) invite = a.slice("--invite=".length);
415
+ else rest.push(a);
416
+ }
417
+ return { invite: invite?.trim() || void 0, email: rest[0] };
418
+ }
419
+ async function performLogin(client, config, args, report, signal) {
420
+ if (!args.email) {
421
+ const start = await client.deviceStart().catch(() => null);
422
+ if (start) return githubLogin(client, config, start, args.invite, report, signal);
423
+ }
424
+ return devLogin(client, config, args.email, args.invite);
425
+ }
426
+ async function githubLogin(client, config, start, invite, report, signal) {
427
+ report.verify(start);
428
+ openBrowser(start.verification_uri);
429
+ const deadline = Date.now() + start.expires_in * 1e3;
430
+ let interval = Math.max(start.interval, 1);
431
+ while (Date.now() < deadline) {
432
+ await sleep(interval * 1e3, signal);
433
+ if (signal?.aborted) return { status: "aborted" };
434
+ report.tick?.();
435
+ const poll = await client.devicePoll(start.session, invite);
436
+ if (poll.status === "pending") {
437
+ if (poll.interval) interval = poll.interval;
438
+ continue;
439
+ }
440
+ if (poll.status === "closed") {
441
+ return { status: "closed", message: poll.message, hadInvite: Boolean(invite) };
442
+ }
443
+ if (poll.status === "complete") {
444
+ config.token = poll.token;
445
+ config.userId = poll.userId;
446
+ config.email = poll.email;
447
+ config.login = poll.login;
448
+ saveConfig(config);
449
+ return {
450
+ status: "ok",
451
+ account: {
452
+ email: poll.email,
453
+ login: poll.login,
454
+ balance: poll.balance,
455
+ created: poll.created,
456
+ signupBonus: poll.signupBonus
457
+ }
458
+ };
459
+ }
460
+ return { status: "failed", error: poll.error ?? poll.status };
461
+ }
462
+ return { status: "timeout" };
463
+ }
464
+ async function devLogin(client, config, email, invite) {
465
+ let res;
466
+ try {
467
+ res = await client.signupDev(email, invite);
468
+ } catch (err) {
469
+ if (err instanceof SignupClosedError) {
470
+ return { status: "closed", message: err.message, hadInvite: Boolean(invite) };
471
+ }
472
+ throw err;
473
+ }
474
+ config.token = res.token;
475
+ config.userId = res.userId;
476
+ config.email = res.email;
477
+ delete config.login;
478
+ saveConfig(config);
479
+ return { status: "ok", account: { email: res.email, balance: res.balance, created: true } };
480
+ }
481
+ function sleep(ms, signal) {
482
+ return new Promise((resolve2) => {
483
+ if (!signal) {
484
+ setTimeout(resolve2, ms);
485
+ return;
486
+ }
487
+ if (signal.aborted) return resolve2();
488
+ const done = () => {
489
+ clearTimeout(timer);
490
+ signal.removeEventListener("abort", done);
491
+ resolve2();
492
+ };
493
+ const timer = setTimeout(done, ms);
494
+ signal.addEventListener("abort", done, { once: true });
495
+ });
496
+ }
497
+ var init_login = __esm({
498
+ "src/login.ts"() {
499
+ "use strict";
500
+ init_client();
501
+ init_browser();
502
+ init_config();
370
503
  }
371
504
  });
372
505
 
@@ -493,7 +626,7 @@ var init_diff = __esm({
493
626
  });
494
627
 
495
628
  // src/tools.ts
496
- import { spawn } from "node:child_process";
629
+ import { spawn as spawn2 } from "node:child_process";
497
630
  import { existsSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
498
631
  import { isAbsolute, relative, resolve, dirname as dirname2, join as join2, sep } from "node:path";
499
632
  function safeResolve(root, p) {
@@ -531,7 +664,7 @@ function killTree(child) {
531
664
  return;
532
665
  }
533
666
  if (process.platform === "win32") {
534
- spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
667
+ spawn2("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
535
668
  } else {
536
669
  try {
537
670
  process.kill(-child.pid, "SIGTERM");
@@ -542,7 +675,7 @@ function killTree(child) {
542
675
  }
543
676
  function execute(ctx, command, timeout) {
544
677
  return new Promise((resolvePromise) => {
545
- const child = spawn(command, {
678
+ const child = spawn2(command, {
546
679
  cwd: ctx.root,
547
680
  shell: true,
548
681
  windowsHide: true,
@@ -1237,7 +1370,7 @@ var init_session = __esm({
1237
1370
  });
1238
1371
 
1239
1372
  // src/kimi.ts
1240
- import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
1373
+ import { spawn as spawn3, spawnSync as spawnSync2 } from "node:child_process";
1241
1374
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
1242
1375
  import { homedir as homedir4 } from "node:os";
1243
1376
  import { dirname as dirname3, join as join5 } from "node:path";
@@ -1294,7 +1427,7 @@ function runKimiCode(config, task, opts = {}) {
1294
1427
  OPENAI_API_KEY: config.token
1295
1428
  };
1296
1429
  return new Promise((resolve2) => {
1297
- const child = process.platform === "win32" ? spawn2([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn2(bin, args, { stdio: "inherit", env });
1430
+ const child = process.platform === "win32" ? spawn3([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn3(bin, args, { stdio: "inherit", env });
1298
1431
  child.on("error", (err) => {
1299
1432
  if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
1300
1433
  else console.error(`failed to launch Kimi CLI: ${err.message}`);
@@ -1371,6 +1504,10 @@ function centered(rendered, visibleW, colW) {
1371
1504
  const lead = Math.max(0, Math.floor((colW - visibleW) / 2));
1372
1505
  return cell(" ".repeat(lead) + rendered, lead + visibleW);
1373
1506
  }
1507
+ function bannerTip(index) {
1508
+ const i = index ?? Math.floor(Math.random() * BANNER_TIPS.length);
1509
+ return BANNER_TIPS[(i % BANNER_TIPS.length + BANNER_TIPS.length) % BANNER_TIPS.length];
1510
+ }
1374
1511
  function earnedLabel(opts) {
1375
1512
  return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
1376
1513
  }
@@ -1382,14 +1519,19 @@ function welcomeBanner(opts) {
1382
1519
  const cwd = opts.cwd.replace(homedir5(), "~").replace(/\\/g, "/");
1383
1520
  const welcome = opts.name ? `Welcome back, ${truncate(opts.name, 18)}!` : "Welcome to Clixad!";
1384
1521
  const duck = duckLines();
1385
- const model = truncate(opts.model, leftW - 4);
1386
1522
  const home = truncate(cwd, leftW);
1523
+ const hint = bannerTip(opts.tipIndex);
1524
+ const hintDesc = truncate(hint.desc, Math.max(1, leftW - 3 - hint.key.length));
1387
1525
  const left = [
1388
1526
  centered(BOLD + TEXT + welcome + R2, welcome.length, leftW),
1389
1527
  cell("", 0),
1390
1528
  ...duck.map((d) => centered(d, DUCK_W, leftW)),
1391
1529
  cell("", 0),
1392
- centered(MANGO + "\u25C6 " + R2 + TEXT + model + R2, 2 + model.length, leftW),
1530
+ centered(
1531
+ MANGO + "\u25C6 " + R2 + BOLD + TEXT + hint.key + R2 + FAINT + " " + hintDesc + R2,
1532
+ 2 + hint.key.length + 1 + hintDesc.length,
1533
+ leftW
1534
+ ),
1393
1535
  centered(FAINT + home + R2, home.length, leftW)
1394
1536
  ];
1395
1537
  const tip = (k, d) => cell(MANGO + k + R2 + FAINT + " " + d + R2, k.length + 1 + d.length);
@@ -1420,10 +1562,11 @@ function welcomeBanner(opts) {
1420
1562
  }
1421
1563
  function compactBanner(opts) {
1422
1564
  const welcome = opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!";
1565
+ const hint = bannerTip(opts.tipIndex);
1423
1566
  return duckLines().map((d) => " " + d).join("\n") + `
1424
1567
 
1425
1568
  ${BOLD}${TEXT}${welcome}${R2}
1426
- ${MANGO}\u25C6${R2} ${opts.model} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
1569
+ ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}${hint.desc}${R2} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
1427
1570
  `;
1428
1571
  }
1429
1572
  function clearScreen() {
@@ -1435,7 +1578,7 @@ function clearScreen() {
1435
1578
  function visibleLength(s) {
1436
1579
  return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1437
1580
  }
1438
- var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, MIN_BANNER_WIDTH, RIGHT_MAX;
1581
+ var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, BANNER_TIPS, MIN_BANNER_WIDTH, RIGHT_MAX;
1439
1582
  var init_banner = __esm({
1440
1583
  "src/banner.ts"() {
1441
1584
  "use strict";
@@ -1477,26 +1620,20 @@ var init_banner = __esm({
1477
1620
  cell = (text, w) => ({ text, w });
1478
1621
  padTo = (c2, width) => (c2?.text ?? "") + " ".repeat(Math.max(0, width - (c2?.w ?? 0)));
1479
1622
  truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1623
+ BANNER_TIPS = [
1624
+ { key: "/init", desc: "make a CLIXAD.md" },
1625
+ { key: "shift+tab", desc: "switch mode" },
1626
+ { key: "@file", desc: "add a file to context" },
1627
+ { key: "ctrl+o", desc: "expand tool output" },
1628
+ { key: "esc", desc: "stop the current turn" },
1629
+ { key: "/compact", desc: "free up context" },
1630
+ { key: "/earn", desc: "top up credits" }
1631
+ ];
1480
1632
  MIN_BANNER_WIDTH = 66;
1481
1633
  RIGHT_MAX = 34;
1482
1634
  }
1483
1635
  });
1484
1636
 
1485
- // src/browser.ts
1486
- import { spawn as spawn3 } from "node:child_process";
1487
- function openBrowser(url) {
1488
- const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
1489
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1490
- const child = spawn3(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
1491
- child.on("error", () => void 0);
1492
- child.unref();
1493
- }
1494
- var init_browser = __esm({
1495
- "src/browser.ts"() {
1496
- "use strict";
1497
- }
1498
- });
1499
-
1500
1637
  // src/version.ts
1501
1638
  import { readFileSync as readFileSync5 } from "node:fs";
1502
1639
  function readVersion() {
@@ -1713,6 +1850,17 @@ var init_commands = __esm({
1713
1850
  // forms, surveys and app trials, and describing the top-up as a video costs
1714
1851
  // the user the one expectation that makes a screenout make sense.
1715
1852
  { name: "earn", desc: "open the offer wall to earn credits" },
1853
+ // Reachable from inside the REPL because that is where the 401 is printed.
1854
+ // "Run `clixad login` first" is not a runnable instruction at this prompt —
1855
+ // anything without a leading slash is a prompt, so it went to the model and
1856
+ // came back as the same 401.
1857
+ //
1858
+ // Deliberately *no* `args`, though `/login <email>` works: an arg hint makes
1859
+ // enter complete the command instead of running it, and this is the one
1860
+ // command whose whole job is to be the exit from a stuck REPL. The email form
1861
+ // is the dev shortcut, documented on `clixad login [email]`, not the path
1862
+ // somebody in a 401 loop needs.
1863
+ { name: "login", desc: "sign in, or switch account" },
1716
1864
  { name: "compact", desc: "summarise the conversation to free context" },
1717
1865
  { name: "clear", desc: "clear the conversation context" },
1718
1866
  { name: "init", desc: "write a CLIXAD.md for this project" },
@@ -2029,6 +2177,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
2029
2177
  function lineCount(text, cols) {
2030
2178
  return text.split("\n").reduce((n, line2) => n + Math.max(1, Math.ceil(visibleLength(line2) / Math.max(1, cols))), 0);
2031
2179
  }
2180
+ function tailRows(text, cols, max) {
2181
+ if (max <= 0) return "";
2182
+ const width = Math.max(1, cols);
2183
+ const lines = text.split("\n");
2184
+ const kept = [];
2185
+ let used = 0;
2186
+ for (let i = lines.length - 1; i >= 0; i--) {
2187
+ const line2 = lines[i];
2188
+ const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
2189
+ if (used + h <= max) {
2190
+ kept.unshift(line2);
2191
+ used += h;
2192
+ continue;
2193
+ }
2194
+ const room = max - used;
2195
+ if (room > 0) kept.unshift(line2.slice(-(room * width)));
2196
+ break;
2197
+ }
2198
+ return kept.join("\n");
2199
+ }
2200
+ function headRows(text, cols, max) {
2201
+ if (max <= 0) return "";
2202
+ const width = Math.max(1, cols);
2203
+ const lines = text.split("\n");
2204
+ const kept = [];
2205
+ let used = 0;
2206
+ for (const line2 of lines) {
2207
+ const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
2208
+ if (used + h <= max) {
2209
+ kept.push(line2);
2210
+ used += h;
2211
+ continue;
2212
+ }
2213
+ const room = max - used;
2214
+ if (room > 0) kept.push(line2.slice(0, room * width));
2215
+ break;
2216
+ }
2217
+ return kept.join("\n");
2218
+ }
2032
2219
  function entryHeight(entry, cols) {
2033
2220
  const margin = entry.kind === "banner" ? 0 : 1;
2034
2221
  switch (entry.kind) {
@@ -2075,7 +2262,7 @@ function EntryView({ entry }) {
2075
2262
  ) });
2076
2263
  }
2077
2264
  }
2078
- var MANGO3, MANGO_BRIGHT;
2265
+ var MANGO3, MANGO_BRIGHT, SLATE, MODE_STYLE;
2079
2266
  var init_views = __esm({
2080
2267
  "src/tui/views.tsx"() {
2081
2268
  "use strict";
@@ -2083,6 +2270,12 @@ var init_views = __esm({
2083
2270
  init_markdown();
2084
2271
  MANGO3 = "#f5b841";
2085
2272
  MANGO_BRIGHT = "#ffcf6b";
2273
+ SLATE = "#8b98ae";
2274
+ MODE_STYLE = {
2275
+ normal: { color: SLATE, glyph: "\u23F5" },
2276
+ acceptEdits: { color: "#7ee787", glyph: "\u23F5\u23F5" },
2277
+ plan: { color: "#79c0ff", glyph: "\u23F8" }
2278
+ };
2086
2279
  }
2087
2280
  });
2088
2281
 
@@ -2160,13 +2353,34 @@ function App({ client, config, wallet, session, initialTask }) {
2160
2353
  const lastOutputRef = useRef("");
2161
2354
  const runningToolRef = useRef(null);
2162
2355
  const pendingTaskRef = useRef(null);
2356
+ const deltaBufRef = useRef("");
2357
+ const deltaTimerRef = useRef(null);
2163
2358
  const sponsorRef = useRef(sponsorSource());
2164
2359
  const sponsorIdxRef = useRef(0);
2165
2360
  const sponsorPrevRef = useRef(void 0);
2166
2361
  const tallyRef = useRef(createTally());
2362
+ const heightCacheRef = useRef(/* @__PURE__ */ new Map());
2167
2363
  const push = useCallback((e) => {
2168
2364
  setEntries((prev) => [...prev, { ...e, id: idRef.current++ }]);
2169
2365
  }, []);
2366
+ const flushDelta = useCallback(() => {
2367
+ if (deltaTimerRef.current) {
2368
+ clearTimeout(deltaTimerRef.current);
2369
+ deltaTimerRef.current = null;
2370
+ }
2371
+ const buffered = deltaBufRef.current;
2372
+ if (!buffered) return;
2373
+ deltaBufRef.current = "";
2374
+ setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + buffered }));
2375
+ }, []);
2376
+ const dropDelta = useCallback(() => {
2377
+ if (deltaTimerRef.current) {
2378
+ clearTimeout(deltaTimerRef.current);
2379
+ deltaTimerRef.current = null;
2380
+ }
2381
+ deltaBufRef.current = "";
2382
+ }, []);
2383
+ useEffect(() => dropDelta, [dropDelta]);
2170
2384
  useEffect(() => {
2171
2385
  const files = contextRef.current.files;
2172
2386
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
@@ -2222,10 +2436,13 @@ function App({ client, config, wallet, session, initialTask }) {
2222
2436
  }, []);
2223
2437
  const handleEvent = useCallback(
2224
2438
  (event) => {
2439
+ if (event.type === "delta") {
2440
+ deltaBufRef.current += event.text;
2441
+ if (!deltaTimerRef.current) deltaTimerRef.current = setTimeout(flushDelta, DELTA_FLUSH_MS);
2442
+ return;
2443
+ }
2444
+ flushDelta();
2225
2445
  switch (event.type) {
2226
- case "delta":
2227
- setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + event.text }));
2228
- return;
2229
2446
  case "message":
2230
2447
  push({ kind: "assistant", text: event.content });
2231
2448
  setLive((l) => ({ ...l ?? { text: "" }, text: "" }));
@@ -2265,7 +2482,7 @@ function App({ client, config, wallet, session, initialTask }) {
2265
2482
  return;
2266
2483
  }
2267
2484
  },
2268
- [cols, push]
2485
+ [cols, flushDelta, push]
2269
2486
  );
2270
2487
  const runTurn2 = useCallback(
2271
2488
  async (task) => {
@@ -2339,18 +2556,19 @@ function App({ client, config, wallet, session, initialTask }) {
2339
2556
  that is normal, just start another.`
2340
2557
  });
2341
2558
  } else if (err instanceof AuthError) {
2342
- push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2559
+ push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
2343
2560
  } else {
2344
2561
  push({ kind: "notice", tone: "error", text: ` error: ${err.message}` });
2345
2562
  }
2346
2563
  } finally {
2347
2564
  abortRef.current = null;
2565
+ dropDelta();
2348
2566
  setLive(null);
2349
2567
  setBusy(false);
2350
2568
  setSponsor(null);
2351
2569
  }
2352
2570
  },
2353
- [client, contextWindow, handleEvent, model, nextSponsor, permit, push, root, session?.title]
2571
+ [client, contextWindow, dropDelta, handleEvent, model, nextSponsor, permit, push, root, session?.title]
2354
2572
  );
2355
2573
  const stopCurrent = useCallback(() => {
2356
2574
  abortRef.current?.abort();
@@ -2367,6 +2585,7 @@ function App({ client, config, wallet, session, initialTask }) {
2367
2585
  await fn(ac.signal);
2368
2586
  } catch (err) {
2369
2587
  if (ac.signal.aborted) push({ kind: "notice", tone: "warn", text: " (stopped)" });
2588
+ else if (err instanceof AuthError) push({ kind: "notice", tone: "error", text: NOT_SIGNED_IN });
2370
2589
  else push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2371
2590
  } finally {
2372
2591
  busyAbortRef.current = null;
@@ -2376,6 +2595,56 @@ function App({ client, config, wallet, session, initialTask }) {
2376
2595
  },
2377
2596
  [push]
2378
2597
  );
2598
+ const runLogin = useCallback(
2599
+ async (arg) => {
2600
+ const args = parseLoginArgs(arg.split(/\s+/).filter(Boolean));
2601
+ await runBusy(SIGNING_IN, async (signal) => {
2602
+ const outcome = await performLogin(
2603
+ client,
2604
+ config,
2605
+ args,
2606
+ {
2607
+ verify: (start) => push({
2608
+ kind: "notice",
2609
+ text: ` Open ${start.verification_uri}
2610
+ and enter the code: ${start.user_code}
2611
+ Waiting for GitHub \u2014 esc to cancel.`
2612
+ })
2613
+ },
2614
+ signal
2615
+ );
2616
+ switch (outcome.status) {
2617
+ case "ok": {
2618
+ const { account } = outcome;
2619
+ setBalance(account.balance);
2620
+ push({
2621
+ kind: "notice",
2622
+ tone: "good",
2623
+ text: ` signed in as ${account.login ?? account.email ?? "you"} \xB7 balance ${account.balance.toLocaleString("en-US")} credits` + (account.created ? " (signup bonus)" : "")
2624
+ });
2625
+ return;
2626
+ }
2627
+ case "closed":
2628
+ push({
2629
+ kind: "notice",
2630
+ tone: "warn",
2631
+ text: ` Clixad isn't open yet.
2632
+ ${outcome.message}` + (outcome.hadInvite ? "\n That invite code wasn't accepted \u2014 check it for typos." : "")
2633
+ });
2634
+ return;
2635
+ case "timeout":
2636
+ push({ kind: "notice", tone: "warn", text: " login timed out \u2014 /login to try again." });
2637
+ return;
2638
+ case "aborted":
2639
+ push({ kind: "notice", tone: "warn", text: " (stopped) \u2014 /login to try again." });
2640
+ return;
2641
+ default:
2642
+ push({ kind: "notice", tone: "error", text: ` login failed: ${outcome.error}` });
2643
+ }
2644
+ });
2645
+ },
2646
+ [client, config, push, runBusy]
2647
+ );
2379
2648
  const runAdWall = useCallback(async () => {
2380
2649
  const task = pendingTaskRef.current;
2381
2650
  pendingTaskRef.current = null;
@@ -2394,7 +2663,7 @@ function App({ client, config, wallet, session, initialTask }) {
2394
2663
  await runBusy(WAITING_FOR_REWARD, async (signal) => {
2395
2664
  const deadline = Date.now() + 5 * 6e4;
2396
2665
  while (Date.now() < deadline && !credited && !signal.aborted) {
2397
- await sleep(3e3, signal);
2666
+ await sleep2(3e3, signal);
2398
2667
  if (signal.aborted) break;
2399
2668
  const w = await client.wallet(signal).catch(() => void 0);
2400
2669
  if (w && w.balance > before) {
@@ -2523,9 +2792,12 @@ function App({ client, config, wallet, session, initialTask }) {
2523
2792
  setBalance(w.balance);
2524
2793
  const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
2525
2794
  const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
2795
+ const screenouts = w.screenouts_today ?? 0;
2796
+ const screenoutCredits = w.screenout_credits_today ?? 0;
2526
2797
  push({
2527
2798
  kind: "notice",
2528
- 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 ? `
2799
+ 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 ? `
2800
+ 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 ? `
2529
2801
  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.` : "")
2530
2802
  });
2531
2803
  });
@@ -2536,11 +2808,14 @@ function App({ client, config, wallet, session, initialTask }) {
2536
2808
  case "earn":
2537
2809
  await runAdWall();
2538
2810
  return;
2811
+ case "login":
2812
+ await runLogin(arg);
2813
+ return;
2539
2814
  default:
2540
2815
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
2541
2816
  }
2542
2817
  },
2543
- [client, config, cycleMode, exit, model, push, runAdWall, runBusy, runTurn2]
2818
+ [client, config, cycleMode, exit, model, push, runAdWall, runBusy, runLogin, runTurn2]
2544
2819
  );
2545
2820
  const submit = useCallback(
2546
2821
  async (raw) => {
@@ -2674,62 +2949,95 @@ function App({ client, config, wallet, session, initialTask }) {
2674
2949
  const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
2675
2950
  const spinner = SPINNER[tick % SPINNER.length];
2676
2951
  const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
2677
- const liveText = live?.text ? tailLines(live.text, Math.max(4, rows - 12)) : "";
2678
- const liveBlock = [
2679
- liveText,
2952
+ const viewport = Math.max(6, rows - 1);
2953
+ const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
2954
+ const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
2955
+ const inputBoxHeight = 2 + inputRows;
2956
+ const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
2957
+ let budget = Math.max(0, viewport - chromeHeight - menu.length);
2958
+ const PICKER_CHROME = 8;
2959
+ const pickerRows = picker ? Math.max(1, Math.min(MAX_PICKER_ROWS, picker.items.length, budget - PICKER_CHROME)) : 0;
2960
+ const pickerFrom = picker ? windowStart(pickerSel, picker.items.length, pickerRows) : 0;
2961
+ const pickerItems = picker ? picker.items.slice(pickerFrom, pickerFrom + pickerRows) : [];
2962
+ const pickerHeight = picker ? pickerItems.length + PICKER_CHROME : 0;
2963
+ const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
2964
+ budget -= pickerHeight;
2965
+ const ASK_CHROME = 4;
2966
+ const askSummaryRows = ask2 ? lineCount(ask2.req.summary, cols) : 0;
2967
+ const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, cols, Math.max(1, budget - ASK_CHROME - askSummaryRows)) : "";
2968
+ const askHeight = ask2 ? askSummaryRows + lineCount(askPreview, cols) * (askPreview ? 1 : 0) + ASK_CHROME : 0;
2969
+ budget -= askHeight;
2970
+ const busyHeight = busy ? 2 : 0;
2971
+ budget -= busyHeight;
2972
+ const liveRaw = [
2973
+ // Trailing blank lines are rows the clamp would spend on nothing, and a
2974
+ // streamed answer ends on one more often than not.
2975
+ (live?.text ?? "").replace(/\n+$/, ""),
2680
2976
  live?.tool ? `\u23FA ${live.tool.name} ${live.tool.summary}` : "",
2681
2977
  live?.tool?.output ?? ""
2682
2978
  ].filter(Boolean).join("\n");
2683
- const askBlock = ask2 ? [ask2.req.summary, ask2.req.preview ?? ""].filter(Boolean).join("\n") : "";
2684
- const pickerHeight = picker ? picker.items.length + 8 : 0;
2685
- const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
2686
- const inputBoxHeight = 2 + editor.lines.length;
2687
- const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
2979
+ const liveBlock = tailRows(liveRaw, cols, Math.max(0, budget - 1));
2688
2980
  const liveHeight = liveBlock ? lineCount(liveBlock, cols) + 1 : 0;
2689
- const askHeight = ask2 ? lineCount(askBlock, cols) + 2 : 0;
2690
- const printed = useMemo(() => entries.reduce((n, e) => n + entryHeight(e, cols), 0), [entries, cols]);
2691
- const spacer = Math.max(
2692
- 0,
2693
- rows - 1 - printed - chromeHeight - menu.length - pickerHeight - liveHeight - askHeight
2694
- );
2981
+ const printed = useMemo(() => {
2982
+ const cache = heightCacheRef.current;
2983
+ return entries.reduce((n, e) => {
2984
+ const key = `${e.id}:${cols}`;
2985
+ let h = cache.get(key);
2986
+ if (h === void 0) {
2987
+ h = entryHeight(e, cols);
2988
+ cache.set(key, h);
2989
+ }
2990
+ return n + h;
2991
+ }, 0);
2992
+ }, [entries, cols]);
2993
+ const used = chromeHeight + menu.length + pickerHeight + askHeight + busyHeight + liveHeight;
2994
+ const spacer = Math.max(0, viewport - printed - used);
2695
2995
  const labelW = menu.reduce((w, c2) => Math.max(w, c2.label.length), 0);
2696
2996
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2697
2997
  /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }),
2698
2998
  spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
2699
2999
  liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
3000
+ busy ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
3001
+ /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
3002
+ spinner,
3003
+ " "
3004
+ ] }),
3005
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3006
+ busyLabel,
3007
+ " ",
3008
+ elapsed,
3009
+ "s \xB7 esc to stop"
3010
+ ] })
3011
+ ] }) : null,
2700
3012
  ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2701
3013
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
2702
- ask2.req.preview ? /* @__PURE__ */ jsx2(Text2, { children: ask2.req.preview }) : null,
3014
+ askPreview ? /* @__PURE__ */ jsx2(Text2, { children: askPreview }) : null,
2703
3015
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc cancels" })
2704
3016
  ] }) : null,
2705
3017
  picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
2706
3018
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
2707
3019
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.subtitle }),
2708
3020
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
2709
- picker.items.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
2710
- /* @__PURE__ */ jsxs2(Text2, { color: i === pickerSel ? MANGO_BRIGHT : void 0, bold: i === pickerSel, children: [
2711
- i === pickerSel ? "\u276F " : " ",
2712
- `${i + 1}. `,
2713
- item.label.padEnd(pickerLabelW + 2)
2714
- ] }),
2715
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
2716
- item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
2717
- ] }, item.value)),
3021
+ pickerItems.map((item, i) => {
3022
+ const index = pickerFrom + i;
3023
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
3024
+ /* @__PURE__ */ jsxs2(Text2, { color: index === pickerSel ? MANGO_BRIGHT : void 0, bold: index === pickerSel, children: [
3025
+ index === pickerSel ? "\u276F " : " ",
3026
+ `${index + 1}. `,
3027
+ item.label.padEnd(pickerLabelW + 2)
3028
+ ] }),
3029
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
3030
+ item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
3031
+ ] }, item.value);
3032
+ }),
2718
3033
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
2719
3034
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2191\u2193 choose \xB7 1-9 jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel" })
2720
3035
  ] }) : null,
2721
3036
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
2722
3037
  sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
2723
- /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO_BRIGHT : MANGO3, paddingX: 1, children: [
2724
- /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
2725
- busy ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2726
- spinner,
2727
- " ",
2728
- busyLabel,
2729
- " ",
2730
- elapsed,
2731
- "s \xB7 esc to stop"
2732
- ] }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor) })
3038
+ /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? SLATE : MANGO3, paddingX: 1, children: [
3039
+ /* @__PURE__ */ jsx2(Text2, { color: busy ? SLATE : MANGO_BRIGHT, children: "\u276F " }),
3040
+ busy ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: toText(editor) }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
2733
3041
  ] }),
2734
3042
  menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
2735
3043
  /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
@@ -2738,22 +3046,25 @@ function App({ client, config, wallet, session, initialTask }) {
2738
3046
  ] }),
2739
3047
  /* @__PURE__ */ jsx2(Text2, { dimColor: i !== sel, children: item.hint })
2740
3048
  ] }, item.value)),
2741
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3049
+ quitHint || menu.length > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2742
3050
  " ",
2743
- statusLine({
2744
- menu: menu.length > 0,
2745
- quitHint,
2746
- model,
2747
- balance,
2748
- spent,
2749
- mode,
2750
- minutes: (Date.now() - runStartedAtRef.current) / 6e4
2751
- })
3051
+ quitHint ? "press ctrl+c again to quit" : "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close"
3052
+ ] }) : /* @__PURE__ */ jsxs2(Box2, { children: [
3053
+ /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, children: ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}` }),
3054
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3055
+ " ",
3056
+ statusLine({
3057
+ model,
3058
+ balance,
3059
+ spent,
3060
+ minutes: (Date.now() - runStartedAtRef.current) / 6e4
3061
+ })
3062
+ ] })
2752
3063
  ] })
2753
3064
  ] })
2754
3065
  ] });
2755
3066
  }
2756
- function sleep(ms, signal) {
3067
+ function sleep2(ms, signal) {
2757
3068
  return new Promise((resolve2) => {
2758
3069
  if (signal.aborted) return resolve2();
2759
3070
  const done = () => {
@@ -2765,9 +3076,10 @@ function sleep(ms, signal) {
2765
3076
  signal.addEventListener("abort", done, { once: true });
2766
3077
  });
2767
3078
  }
2768
- function renderInput(state) {
2769
- return state.lines.map((line2, row) => {
2770
- const prefix = row === 0 ? "" : "\n";
3079
+ function renderInput(state, from, rows) {
3080
+ return state.lines.slice(from, from + rows).map((line2, i) => {
3081
+ const row = from + i;
3082
+ const prefix = i === 0 ? "" : "\n";
2771
3083
  if (row !== state.row) return /* @__PURE__ */ jsx2(Text2, { children: prefix + line2 }, row);
2772
3084
  const before = line2.slice(0, state.col);
2773
3085
  const at = line2.slice(state.col, state.col + 1) || " ";
@@ -2780,19 +3092,20 @@ function renderInput(state) {
2780
3092
  });
2781
3093
  }
2782
3094
  function statusLine(o) {
2783
- if (o.quitHint) return "press ctrl+c again to quit";
2784
- if (o.menu) return "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close";
2785
3095
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
2786
3096
  const parts = [
2787
3097
  o.model,
2788
3098
  `${o.balance.toLocaleString("en-US")} cr`,
2789
- o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")} this session` : void 0,
3099
+ o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0,
2790
3100
  burn,
2791
- MODE_LABEL[o.mode],
2792
3101
  "/help"
2793
3102
  ].filter(Boolean);
2794
3103
  return parts.join(" \xB7 ");
2795
3104
  }
3105
+ function windowStart(sel, total, size) {
3106
+ if (total <= size) return 0;
3107
+ return Math.max(0, Math.min(total - size, sel - Math.floor(size / 2)));
3108
+ }
2796
3109
  function toEditorKey(ch, key) {
2797
3110
  const name = key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : key.backspace ? "backspace" : key.delete ? "delete" : void 0;
2798
3111
  return {
@@ -2825,12 +3138,13 @@ function tailLines(text, max) {
2825
3138
  const lines = text.split("\n");
2826
3139
  return lines.length <= max ? text : lines.slice(-max).join("\n");
2827
3140
  }
2828
- var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
3141
+ var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS;
2829
3142
  var init_app = __esm({
2830
3143
  "src/tui/app.tsx"() {
2831
3144
  "use strict";
2832
3145
  init_client();
2833
3146
  init_config();
3147
+ init_login();
2834
3148
  init_banner();
2835
3149
  init_agent();
2836
3150
  init_context();
@@ -2853,8 +3167,13 @@ var init_app = __esm({
2853
3167
  WAITING_FOR_REWARD = "waiting for the offer\u2026";
2854
3168
  LOADING = "loading\u2026";
2855
3169
  COMPACTING = "compacting\u2026";
3170
+ SIGNING_IN = "signing in\u2026";
3171
+ NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
2856
3172
  LIVE_OUTPUT_LINES = 5;
2857
3173
  COMMITTED_OUTPUT_LINES = 4;
3174
+ DELTA_FLUSH_MS = 50;
3175
+ MAX_INPUT_ROWS = 10;
3176
+ MAX_PICKER_ROWS = 12;
2858
3177
  }
2859
3178
  });
2860
3179
 
@@ -2880,6 +3199,7 @@ var init_tui = __esm({
2880
3199
  // src/main.ts
2881
3200
  init_client();
2882
3201
  init_config();
3202
+ init_login();
2883
3203
  init_agent();
2884
3204
  init_context();
2885
3205
  init_permissions();
@@ -2980,25 +3300,44 @@ async function main() {
2980
3300
  process.exitCode = 1;
2981
3301
  }
2982
3302
  }
2983
- function takeInvite(args) {
2984
- const rest = [];
2985
- let invite;
2986
- for (let i = 0; i < args.length; i++) {
2987
- const a = args[i];
2988
- if (a === "--invite") invite = args[++i];
2989
- else if (a.startsWith("--invite=")) invite = a.slice("--invite=".length);
2990
- else rest.push(a);
2991
- }
2992
- return { invite: invite?.trim() || void 0, rest };
2993
- }
2994
3303
  async function login(client, config, args) {
2995
- const { invite, rest } = takeInvite(args);
2996
- const email = rest[0];
2997
- if (!email) {
2998
- const start = await client.deviceStart().catch(() => null);
2999
- if (start) return githubLogin(client, config, start, invite);
3304
+ const parsed = parseLoginArgs(args);
3305
+ const outcome = await performLogin(client, config, parsed, {
3306
+ verify(start) {
3307
+ console.log(`
3308
+ Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
3309
+ `);
3310
+ process.stdout.write(c.dim(" waiting for GitHub authorization\u2026 (Ctrl+C to cancel)"));
3311
+ },
3312
+ tick() {
3313
+ process.stdout.write(c.dim("."));
3314
+ }
3315
+ });
3316
+ switch (outcome.status) {
3317
+ case "ok": {
3318
+ const { account } = outcome;
3319
+ const who = account.login ?? account.email ?? "you";
3320
+ const suffix = account.login && account.email ? c.dim(` (${account.email})`) : "";
3321
+ console.log(c.green(`
3322
+ \u2713 logged in as ${who}`) + suffix);
3323
+ const bonus = account.created ? c.dim(account.signupBonus === void 0 ? " (signup bonus)" : ` (signup bonus ${credits(account.signupBonus)})`) : "";
3324
+ console.log(` balance: ${c.bold(credits(account.balance))} credits${bonus}`);
3325
+ console.log(c.dim(` token stored in ${configPath()}`));
3326
+ return;
3327
+ }
3328
+ case "closed":
3329
+ reportSignupClosed(outcome.message, outcome.hadInvite);
3330
+ process.exitCode = 1;
3331
+ return;
3332
+ case "timeout":
3333
+ console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
3334
+ return;
3335
+ case "aborted":
3336
+ return;
3337
+ default:
3338
+ console.log(c.red(`
3339
+ login failed: ${outcome.error}`));
3000
3340
  }
3001
- return devLogin(client, config, email, invite);
3002
3341
  }
3003
3342
  function reportSignupClosed(message, hadInvite) {
3004
3343
  console.log(c.yellow("\n Clixad isn't open yet.\n"));
@@ -3022,67 +3361,7 @@ function wrapPlain(text, width) {
3022
3361
  if (line2) lines.push(line2);
3023
3362
  return lines;
3024
3363
  }
3025
- async function githubLogin(client, config, start, invite) {
3026
- console.log(`
3027
- Open ${c.cyan(start.verification_uri)} and enter code: ${c.bold(start.user_code)}
3028
- `);
3029
- openBrowser(start.verification_uri);
3030
- process.stdout.write(c.dim(" waiting for GitHub authorization\u2026 (Ctrl+C to cancel)"));
3031
- const deadline = Date.now() + start.expires_in * 1e3;
3032
- let interval = Math.max(start.interval, 1);
3033
- while (Date.now() < deadline) {
3034
- await sleep2(interval * 1e3);
3035
- process.stdout.write(c.dim("."));
3036
- const poll = await client.devicePoll(start.session, invite);
3037
- if (poll.status === "pending") {
3038
- if (poll.interval) interval = poll.interval;
3039
- continue;
3040
- }
3041
- if (poll.status === "closed") {
3042
- reportSignupClosed(poll.message, Boolean(invite));
3043
- process.exitCode = 1;
3044
- return;
3045
- }
3046
- if (poll.status === "complete") {
3047
- config.token = poll.token;
3048
- config.userId = poll.userId;
3049
- config.email = poll.email;
3050
- config.login = poll.login;
3051
- saveConfig(config);
3052
- console.log(c.green(`
3053
- \u2713 logged in as ${poll.login}`) + c.dim(poll.email ? ` (${poll.email})` : ""));
3054
- const bonus = poll.created ? c.dim(` (signup bonus ${credits(poll.signupBonus)})`) : "";
3055
- console.log(` balance: ${c.bold(credits(poll.balance))} credits${bonus}`);
3056
- console.log(c.dim(` token stored in ${configPath()}`));
3057
- return;
3058
- }
3059
- console.log(c.red(`
3060
- login failed: ${poll.error ?? poll.status}`));
3061
- return;
3062
- }
3063
- console.log(c.red("\n login timed out \u2014 run `clixad login` again."));
3064
- }
3065
- async function devLogin(client, config, email, invite) {
3066
- let res;
3067
- try {
3068
- res = await client.signupDev(email, invite);
3069
- } catch (err) {
3070
- if (err instanceof SignupClosedError) {
3071
- reportSignupClosed(err.message, Boolean(invite));
3072
- process.exitCode = 1;
3073
- return;
3074
- }
3075
- throw err;
3076
- }
3077
- config.token = res.token;
3078
- config.userId = res.userId;
3079
- config.email = res.email;
3080
- saveConfig(config);
3081
- console.log(c.green(`\u2713 logged in as ${res.email}`));
3082
- console.log(` balance: ${c.bold(credits(res.balance))} credits (signup bonus)`);
3083
- console.log(c.dim(` token stored in ${configPath()}`));
3084
- }
3085
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
3364
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
3086
3365
  var credits = (n) => n.toLocaleString("en-US");
3087
3366
  function logout(config) {
3088
3367
  delete config.token;
@@ -3159,7 +3438,7 @@ async function earn(client, config) {
3159
3438
  process.stdout.write(c.dim(" waiting for an offer to clear"));
3160
3439
  const deadline = Date.now() + 5 * 60 * 1e3;
3161
3440
  while (Date.now() < deadline) {
3162
- await sleep2(3e3);
3441
+ await sleep3(3e3);
3163
3442
  process.stdout.write(c.dim("."));
3164
3443
  const balance = (await safeWallet(client))?.balance ?? before;
3165
3444
  if (balance > before) {
@@ -3203,7 +3482,7 @@ async function buyCmd(client, config, pack) {
3203
3482
  process.stdout.write(c.dim(" waiting for payment to clear"));
3204
3483
  const deadline = Date.now() + 5 * 60 * 1e3;
3205
3484
  while (Date.now() < deadline) {
3206
- await sleep2(3e3);
3485
+ await sleep3(3e3);
3207
3486
  process.stdout.write(c.dim("."));
3208
3487
  const balance = (await safeWallet(client))?.balance ?? before;
3209
3488
  if (balance > before) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.5",
3
+ "version": "0.0.1-beta.7",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",