clixad 0.0.1-beta.1 → 0.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/clixad.mjs +166 -92
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -210,14 +210,18 @@ var init_client = __esm({
210
210
  if (!res.ok) throw new Error(`checkout failed: ${res.status} ${await res.text()}`);
211
211
  return res.json();
212
212
  }
213
- async models() {
214
- const res = await fetch(`${this.config.gatewayUrl}/v1/models`);
213
+ // Both take an optional signal for the REPL's sake: they are awaited while the
214
+ // input box is locked, and a gateway that is cold-starting (Render's free tier,
215
+ // the better part of a minute) would otherwise be indistinguishable from a
216
+ // hung terminal. Nothing else passes one — a one-shot CLI command has Ctrl+C.
217
+ async models(signal) {
218
+ const res = await fetch(`${this.config.gatewayUrl}/v1/models`, { signal });
215
219
  if (!res.ok) throw new Error(`models failed: ${res.status}`);
216
220
  const data = await res.json();
217
221
  return data.data;
218
222
  }
219
- async wallet() {
220
- const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers() });
223
+ async wallet(signal) {
224
+ const res = await fetch(`${this.config.gatewayUrl}/v1/wallet`, { headers: this.headers(), signal });
221
225
  if (res.status === 401) throw await authFailure(res, "wallet");
222
226
  if (!res.ok) throw new Error(`wallet failed: ${res.status}`);
223
227
  return res.json();
@@ -1483,7 +1487,9 @@ import { spawn as spawn3 } from "node:child_process";
1483
1487
  function openBrowser(url) {
1484
1488
  const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
1485
1489
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1486
- spawn3(cmd, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
1490
+ const child = spawn3(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
1491
+ child.on("error", () => void 0);
1492
+ child.unref();
1487
1493
  }
1488
1494
  var init_browser = __esm({
1489
1495
  "src/browser.ts"() {
@@ -2118,11 +2124,13 @@ function App({ client, config, wallet, session, initialTask }) {
2118
2124
  const [tick, setTick] = useState(0);
2119
2125
  const [startedAt, setStartedAt] = useState(0);
2120
2126
  const [quitHint, setQuitHint] = useState(false);
2127
+ const [busyLabel, setBusyLabel] = useState(WORKING);
2121
2128
  const [sponsor, setSponsor] = useState(null);
2122
2129
  const messagesRef = useRef(messages);
2123
2130
  messagesRef.current = messages;
2124
2131
  const permRef = useRef(createState("normal"));
2125
2132
  const abortRef = useRef(null);
2133
+ const busyAbortRef = useRef(null);
2126
2134
  const filesRef = useRef(null);
2127
2135
  const ctrlCRef = useRef(0);
2128
2136
  const sessionRef = useRef(session ?? { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() });
@@ -2324,6 +2332,30 @@ function App({ client, config, wallet, session, initialTask }) {
2324
2332
  },
2325
2333
  [client, contextWindow, handleEvent, model, nextSponsor, permit, push, root, session?.title]
2326
2334
  );
2335
+ const stopCurrent = useCallback(() => {
2336
+ abortRef.current?.abort();
2337
+ busyAbortRef.current?.abort();
2338
+ }, []);
2339
+ const runBusy = useCallback(
2340
+ async (label, fn) => {
2341
+ const ac = new AbortController();
2342
+ busyAbortRef.current = ac;
2343
+ setBusy(true);
2344
+ setBusyLabel(label);
2345
+ setStartedAt(Date.now());
2346
+ try {
2347
+ await fn(ac.signal);
2348
+ } catch (err) {
2349
+ if (ac.signal.aborted) push({ kind: "notice", tone: "warn", text: " (stopped)" });
2350
+ else push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2351
+ } finally {
2352
+ busyAbortRef.current = null;
2353
+ setBusyLabel(WORKING);
2354
+ setBusy(false);
2355
+ }
2356
+ },
2357
+ [push]
2358
+ );
2327
2359
  const runAdWall = useCallback(async () => {
2328
2360
  const task = pendingTaskRef.current;
2329
2361
  pendingTaskRef.current = null;
@@ -2337,18 +2369,28 @@ function App({ client, config, wallet, session, initialTask }) {
2337
2369
  ` : ` opened ${base} \u2014 paste your token there; waiting for the reward\u2026
2338
2370
  `) + ` Credits land on completion; being screened out of an offer pays nothing and is normal.`
2339
2371
  });
2340
- setBusy(true);
2341
- const deadline = Date.now() + 5 * 6e4;
2342
2372
  let credited = false;
2343
- while (Date.now() < deadline && !credited) {
2344
- await new Promise((r) => setTimeout(r, 3e3));
2345
- const w = await client.wallet().catch(() => void 0);
2346
- if (w && w.balance > before) {
2347
- setBalance(w.balance);
2348
- credited = true;
2373
+ let stopped = false;
2374
+ await runBusy(WAITING_FOR_REWARD, async (signal) => {
2375
+ const deadline = Date.now() + 5 * 6e4;
2376
+ while (Date.now() < deadline && !credited && !signal.aborted) {
2377
+ await sleep(3e3, signal);
2378
+ if (signal.aborted) break;
2379
+ const w = await client.wallet(signal).catch(() => void 0);
2380
+ if (w && w.balance > before) {
2381
+ setBalance(w.balance);
2382
+ credited = true;
2383
+ }
2349
2384
  }
2350
- }
2351
- setBusy(false);
2385
+ stopped = signal.aborted;
2386
+ });
2387
+ if (!credited) pendingTaskRef.current = task;
2388
+ if (stopped)
2389
+ return push({
2390
+ kind: "notice",
2391
+ tone: "warn",
2392
+ text: " stopped waiting. A reward still lands whenever the offer clears \u2014\n /wallet checks the balance, /earn opens the wall again." + (task ? "\n Your last request is still queued: press enter to retry it." : "")
2393
+ });
2352
2394
  if (!credited)
2353
2395
  return push({
2354
2396
  kind: "notice",
@@ -2357,7 +2399,7 @@ function App({ client, config, wallet, session, initialTask }) {
2357
2399
  });
2358
2400
  push({ kind: "notice", tone: "good", text: " credits added \u2014 continuing" });
2359
2401
  if (task) await runTurn2(task);
2360
- }, [balance, client, config.dashboardUrl, push, runTurn2]);
2402
+ }, [balance, client, config.dashboardUrl, push, runBusy, runTurn2]);
2361
2403
  const runCommand = useCallback(
2362
2404
  async (line2) => {
2363
2405
  const [cmd, ...rest] = line2.slice(1).split(" ");
@@ -2390,83 +2432,74 @@ function App({ client, config, wallet, session, initialTask }) {
2390
2432
  push({ kind: "notice", text: ` mode \u2192 ${MODE_LABEL[permRef.current.mode]}` });
2391
2433
  return;
2392
2434
  }
2393
- case "compact": {
2394
- setBusy(true);
2395
- try {
2396
- const res = await compact(client, model, [
2397
- { role: "system", content: contextRef.current.systemPrompt },
2398
- ...messagesRef.current
2399
- ]);
2400
- if (!res) push({ kind: "notice", text: " nothing to compact yet" });
2401
- else {
2402
- setMessages(res.messages.slice(1));
2403
- setBalance(res.balance);
2404
- setSpent((s) => s + res.creditsCharged);
2405
- push({
2406
- kind: "notice",
2407
- text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
2408
- });
2409
- }
2410
- } catch (err) {
2411
- push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2412
- }
2413
- setBusy(false);
2435
+ // A model call like any other, so esc has to stop it like any other —
2436
+ // and stopping it costs nothing, since the summary only replaces the
2437
+ // conversation once the call has come back whole.
2438
+ case "compact":
2439
+ await runBusy(COMPACTING, async (signal) => {
2440
+ const res = await compact(
2441
+ client,
2442
+ model,
2443
+ [{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
2444
+ { signal }
2445
+ );
2446
+ if (!res) return push({ kind: "notice", text: " nothing to compact yet" });
2447
+ setMessages(res.messages.slice(1));
2448
+ setBalance(res.balance);
2449
+ setSpent((s) => s + res.creditsCharged);
2450
+ push({
2451
+ kind: "notice",
2452
+ text: ` compacted: ~${Math.round(res.before / 1e3)}k \u2192 ~${Math.round(res.after / 1e3)}k tokens (${res.creditsCharged} credits)`
2453
+ });
2454
+ });
2414
2455
  return;
2415
- }
2416
2456
  case "init":
2417
2457
  await runTurn2(INIT_PROMPT);
2418
2458
  return;
2419
2459
  // Always the picker — an id you have to remember and type is exactly
2420
2460
  // what a picker is for. A typed id only preselects a row.
2421
- case "model": {
2422
- setBusy(true);
2423
- const models = catalogRef.current.length ? catalogRef.current : await client.models().catch(() => []);
2424
- catalogRef.current = models;
2425
- setBusy(false);
2426
- if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
2427
- if (arg && !models.some((m) => m.id === arg)) {
2428
- push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
2429
- }
2430
- const items = models.map((m) => ({
2431
- value: m.id,
2432
- label: m.id,
2433
- hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
2434
- current: m.id === model
2435
- }));
2436
- const preselect = items.findIndex((i) => i.value === arg);
2437
- setPickerSel(preselect >= 0 ? preselect : Math.max(0, items.findIndex((i) => i.current)));
2438
- setPicker({
2439
- title: "Select model",
2440
- subtitle: "Applies to this session and is saved as your default.",
2441
- items,
2442
- onPick: (choice) => {
2443
- config.model = choice.value;
2444
- saveConfig(config);
2445
- setModel2(choice.value);
2446
- push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
2461
+ case "model":
2462
+ await runBusy(LOADING, async (signal) => {
2463
+ const models = catalogRef.current.length ? catalogRef.current : await client.models(signal);
2464
+ catalogRef.current = models;
2465
+ if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
2466
+ if (arg && !models.some((m) => m.id === arg)) {
2467
+ push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
2447
2468
  }
2469
+ const items = models.map((m) => ({
2470
+ value: m.id,
2471
+ label: m.id,
2472
+ hint: `${m.tier.padEnd(8)}${adsPerTaskLabel(m.est_ads_per_task)}`,
2473
+ current: m.id === model
2474
+ }));
2475
+ const preselect = items.findIndex((i) => i.value === arg);
2476
+ setPickerSel(preselect >= 0 ? preselect : Math.max(0, items.findIndex((i) => i.current)));
2477
+ setPicker({
2478
+ title: "Select model",
2479
+ subtitle: "Applies to this session and is saved as your default.",
2480
+ items,
2481
+ onPick: (choice) => {
2482
+ config.model = choice.value;
2483
+ saveConfig(config);
2484
+ setModel2(choice.value);
2485
+ push({ kind: "notice", text: ` model \u2192 ${choice.value}` });
2486
+ }
2487
+ });
2448
2488
  });
2449
2489
  return;
2450
- }
2451
- case "models": {
2452
- setBusy(true);
2453
- try {
2454
- const ms = await client.models();
2490
+ case "models":
2491
+ await runBusy(LOADING, async (signal) => {
2492
+ const ms = await client.models(signal);
2455
2493
  catalogRef.current = ms;
2456
2494
  push({
2457
2495
  kind: "notice",
2458
2496
  text: ms.map((m) => ` ${m.id.padEnd(24)} ${adsPerTaskLabel(m.est_ads_per_task)}`).join("\n")
2459
2497
  });
2460
- } catch (err) {
2461
- push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2462
- }
2463
- setBusy(false);
2498
+ });
2464
2499
  return;
2465
- }
2466
- case "wallet": {
2467
- setBusy(true);
2468
- try {
2469
- const w = await client.wallet();
2500
+ case "wallet":
2501
+ await runBusy(LOADING, async (signal) => {
2502
+ const w = await client.wallet(signal);
2470
2503
  setBalance(w.balance);
2471
2504
  const reversals = w.ledger.filter((e) => e.reason === "ad_reversal");
2472
2505
  const reversed = reversals.reduce((sum, e) => sum + Math.abs(e.delta), 0);
@@ -2475,12 +2508,8 @@ function App({ client, config, wallet, session, initialTask }) {
2475
2508
  text: ` balance ${w.balance.toLocaleString("en-US")} credits \xB7 ${w.ads_today} offers today \xB7 $${w.earned_usd_today.toFixed(2)}/$${w.max_reward_usd_per_day.toFixed(2)} earned` + (reversals.length ? `
2476
2509
  recently: ${reversed.toLocaleString("en-US")} credits from ${reversals.length} offer${reversals.length === 1 ? "" : "s"} were reversed by the provider. Run \`clixad wallet\` for the full ledger.` : "")
2477
2510
  });
2478
- } catch (err) {
2479
- push({ kind: "notice", tone: "error", text: ` ${err.message}` });
2480
- }
2481
- setBusy(false);
2511
+ });
2482
2512
  return;
2483
- }
2484
2513
  // The same browser handoff the paywall takes, rather than a second way of
2485
2514
  // doing it: /earn used to call the dev-only /v1/ads/reward simulator, so
2486
2515
  // in production the slash command the paywall itself recommends was a 404.
@@ -2491,7 +2520,7 @@ function App({ client, config, wallet, session, initialTask }) {
2491
2520
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
2492
2521
  }
2493
2522
  },
2494
- [client, config, cycleMode, exit, model, push, runTurn2]
2523
+ [client, config, cycleMode, exit, model, push, runAdWall, runBusy, runTurn2]
2495
2524
  );
2496
2525
  const submit = useCallback(
2497
2526
  async (raw) => {
@@ -2540,7 +2569,7 @@ function App({ client, config, wallet, session, initialTask }) {
2540
2569
  );
2541
2570
  useInput((ch, key) => {
2542
2571
  if (key.ctrl && ch === "c") {
2543
- if (busy) return abortRef.current?.abort();
2572
+ if (busy) return stopCurrent();
2544
2573
  if (!isEmpty(editor)) return setEditor(EMPTY);
2545
2574
  if (Date.now() - ctrlCRef.current < 2e3) return exit();
2546
2575
  ctrlCRef.current = Date.now();
@@ -2579,7 +2608,7 @@ function App({ client, config, wallet, session, initialTask }) {
2579
2608
  return;
2580
2609
  }
2581
2610
  if (busy) {
2582
- if (key.escape) abortRef.current?.abort();
2611
+ if (key.escape) stopCurrent();
2583
2612
  return;
2584
2613
  }
2585
2614
  if (key.tab && key.shift) return cycleMode();
@@ -2675,7 +2704,9 @@ function App({ client, config, wallet, session, initialTask }) {
2675
2704
  /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
2676
2705
  busy ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2677
2706
  spinner,
2678
- " working\u2026 ",
2707
+ " ",
2708
+ busyLabel,
2709
+ " ",
2679
2710
  elapsed,
2680
2711
  "s \xB7 esc to stop"
2681
2712
  ] }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor) })
@@ -2702,6 +2733,18 @@ function App({ client, config, wallet, session, initialTask }) {
2702
2733
  ] })
2703
2734
  ] });
2704
2735
  }
2736
+ function sleep(ms, signal) {
2737
+ return new Promise((resolve2) => {
2738
+ if (signal.aborted) return resolve2();
2739
+ const done = () => {
2740
+ clearTimeout(timer);
2741
+ signal.removeEventListener("abort", done);
2742
+ resolve2();
2743
+ };
2744
+ const timer = setTimeout(done, ms);
2745
+ signal.addEventListener("abort", done, { once: true });
2746
+ });
2747
+ }
2705
2748
  function renderInput(state) {
2706
2749
  return state.lines.map((line2, row) => {
2707
2750
  const prefix = row === 0 ? "" : "\n";
@@ -2762,7 +2805,7 @@ function tailLines(text, max) {
2762
2805
  const lines = text.split("\n");
2763
2806
  return lines.length <= max ? text : lines.slice(-max).join("\n");
2764
2807
  }
2765
- var SPINNER, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
2808
+ var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
2766
2809
  var init_app = __esm({
2767
2810
  "src/tui/app.tsx"() {
2768
2811
  "use strict";
@@ -2785,6 +2828,10 @@ var init_app = __esm({
2785
2828
  init_suggest();
2786
2829
  init_views();
2787
2830
  SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2831
+ WORKING = "working\u2026";
2832
+ WAITING_FOR_REWARD = "waiting for the offer\u2026";
2833
+ LOADING = "loading\u2026";
2834
+ COMPACTING = "compacting\u2026";
2788
2835
  LIVE_OUTPUT_LINES = 5;
2789
2836
  COMMITTED_OUTPUT_LINES = 4;
2790
2837
  }
@@ -2819,6 +2866,31 @@ init_session();
2819
2866
  init_kimi();
2820
2867
  init_banner();
2821
2868
  init_browser();
2869
+
2870
+ // src/title.ts
2871
+ var APP_TITLE = "Clixad";
2872
+ function titleEnabled({ isTTY, env }) {
2873
+ const flag = env.CLIXAD_TITLE?.trim().toLowerCase();
2874
+ if (flag === "0" || flag === "false" || flag === "off" || flag === "no") return false;
2875
+ if (!isTTY) return false;
2876
+ if (env.TERM === "dumb") return false;
2877
+ if (env.CI !== void 0 && env.CI !== "") return false;
2878
+ return true;
2879
+ }
2880
+ function titleSequence(title) {
2881
+ return `\x1B]0;${title.replace(/[\x00-\x1f\x7f]/g, " ").trim()}\x07`;
2882
+ }
2883
+ function setTerminalTitle(title = APP_TITLE, out = process.stdout) {
2884
+ process.title = title.toLowerCase();
2885
+ if (!titleEnabled({ isTTY: out.isTTY, env: process.env })) return;
2886
+ out.write(titleSequence(title));
2887
+ }
2888
+ function clearTerminalTitle(out = process.stdout) {
2889
+ if (!titleEnabled({ isTTY: out.isTTY, env: process.env })) return;
2890
+ out.write(titleSequence(""));
2891
+ }
2892
+
2893
+ // src/main.ts
2822
2894
  var c = {
2823
2895
  cyan: (s) => `\x1B[36m${s}\x1B[0m`,
2824
2896
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -2828,6 +2900,8 @@ var c = {
2828
2900
  bold: (s) => `\x1B[1m${s}\x1B[0m`
2829
2901
  };
2830
2902
  async function main() {
2903
+ setTerminalTitle();
2904
+ process.on("exit", () => clearTerminalTitle());
2831
2905
  const [cmd, ...rest] = process.argv.slice(2);
2832
2906
  const config = loadConfig();
2833
2907
  const client = new GatewayClient(config);
@@ -2925,7 +2999,7 @@ async function githubLogin(client, config, start, invite) {
2925
2999
  const deadline = Date.now() + start.expires_in * 1e3;
2926
3000
  let interval = Math.max(start.interval, 1);
2927
3001
  while (Date.now() < deadline) {
2928
- await sleep(interval * 1e3);
3002
+ await sleep2(interval * 1e3);
2929
3003
  process.stdout.write(c.dim("."));
2930
3004
  const poll = await client.devicePoll(start.session, invite);
2931
3005
  if (poll.status === "pending") {
@@ -2976,7 +3050,7 @@ async function devLogin(client, config, email, invite) {
2976
3050
  console.log(` balance: ${c.bold(String(res.balance))} credits (signup bonus)`);
2977
3051
  console.log(c.dim(` token stored in ${configPath()}`));
2978
3052
  }
2979
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3053
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
2980
3054
  function logout(config) {
2981
3055
  delete config.token;
2982
3056
  delete config.userId;
@@ -3052,7 +3126,7 @@ async function earn(client, config) {
3052
3126
  process.stdout.write(c.dim(" waiting for an offer to clear"));
3053
3127
  const deadline = Date.now() + 5 * 60 * 1e3;
3054
3128
  while (Date.now() < deadline) {
3055
- await sleep(3e3);
3129
+ await sleep2(3e3);
3056
3130
  process.stdout.write(c.dim("."));
3057
3131
  const balance = (await safeWallet(client))?.balance ?? before;
3058
3132
  if (balance > before) {
@@ -3097,7 +3171,7 @@ async function buyCmd(client, config, pack) {
3097
3171
  process.stdout.write(c.dim(" waiting for payment to clear"));
3098
3172
  const deadline = Date.now() + 5 * 60 * 1e3;
3099
3173
  while (Date.now() < deadline) {
3100
- await sleep(3e3);
3174
+ await sleep2(3e3);
3101
3175
  process.stdout.write(c.dim("."));
3102
3176
  const balance = (await safeWallet(client))?.balance ?? before;
3103
3177
  if (balance > before) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.1",
3
+ "version": "0.0.1-beta.2",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",