clixad 0.0.1-beta.24 → 0.0.1-beta.25

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 +169 -8
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -94,14 +94,62 @@ var init_estimate = __esm({
94
94
  });
95
95
 
96
96
  // src/sponsor/formats.ts
97
- var EXTRA_AD_FORMATS;
97
+ function isAdFormat(value) {
98
+ return typeof value === "string" && AD_FORMATS.includes(value);
99
+ }
100
+ var AD_FORMATS, EXTRA_AD_FORMATS;
98
101
  var init_formats = __esm({
99
102
  "src/sponsor/formats.ts"() {
100
103
  "use strict";
104
+ AD_FORMATS = ["top_bar", "sidebar_card", "status_bar"];
101
105
  EXTRA_AD_FORMATS = ["sidebar_card", "status_bar"];
102
106
  }
103
107
  });
104
108
 
109
+ // src/sponsor/direct.ts
110
+ function sanitizeAdText(raw) {
111
+ return raw.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/[‪-‮⁦-⁩‎‏؜]/g, "").replace(/[​-‍⁠­]/g, "").replace(/\s+/g, " ").trim();
112
+ }
113
+ function parseDirectLines(body) {
114
+ const raw = body?.lines;
115
+ if (!Array.isArray(raw)) return [];
116
+ const out = [];
117
+ const seen = /* @__PURE__ */ new Set();
118
+ for (const item of raw) {
119
+ if (!item || typeof item !== "object") continue;
120
+ const e = item;
121
+ if (!isAdFormat(e.format) || seen.has(e.format)) continue;
122
+ if (e.hold === true) {
123
+ out.push({ format: e.format, hold: true });
124
+ seen.add(e.format);
125
+ continue;
126
+ }
127
+ if (typeof e.text !== "string" || typeof e.serve_token !== "string" || typeof e.click_url !== "string") continue;
128
+ const text = sanitizeAdText(e.text).slice(0, MAX_TEXT);
129
+ if (!text || !e.serve_token || e.serve_token.length > 2e3) continue;
130
+ let clickUrl;
131
+ try {
132
+ const url = new URL(e.click_url);
133
+ if (url.protocol !== "https:" && url.protocol !== "http:") continue;
134
+ clickUrl = url.toString();
135
+ } catch {
136
+ continue;
137
+ }
138
+ out.push({ format: e.format, text, serveToken: e.serve_token, clickUrl });
139
+ seen.add(e.format);
140
+ }
141
+ return out;
142
+ }
143
+ var DIRECT_ASK_TIMEOUT_MS, MAX_TEXT;
144
+ var init_direct = __esm({
145
+ "src/sponsor/direct.ts"() {
146
+ "use strict";
147
+ init_formats();
148
+ DIRECT_ASK_TIMEOUT_MS = 1500;
149
+ MAX_TEXT = 200;
150
+ }
151
+ });
152
+
105
153
  // src/edge.ts
106
154
  function detectEdgeBlock(status, headers, body) {
107
155
  if (status !== 403) return void 0;
@@ -498,6 +546,7 @@ var init_client = __esm({
498
546
  "use strict";
499
547
  init_estimate();
500
548
  init_formats();
549
+ init_direct();
501
550
  init_edge();
502
551
  CHAT_ENCODED_CONTENT_TYPE = "application/vnd.clixad.chat+gzip";
503
552
  LEDGER_REASONS = {
@@ -645,6 +694,7 @@ var init_client = __esm({
645
694
  return { status: "closed", message: closed.message, waitlistUrl: closed.waitlist_url };
646
695
  }
647
696
  if (!res.ok && !("status" in data && data.status)) {
697
+ if (res.status >= 500 || res.status === 429) return { status: "unavailable", error: `http ${res.status}` };
648
698
  return { status: "error", error: `http ${res.status}` };
649
699
  }
650
700
  return data;
@@ -981,6 +1031,51 @@ var init_client = __esm({
981
1031
  return 0;
982
1032
  }
983
1033
  }
1034
+ /**
1035
+ * Ask for booked lines (step 2): one ask per model call, naming the slots
1036
+ * that fit the terminal now. See `sponsor/direct.ts` for the contract.
1037
+ *
1038
+ * Never throws, for `sponsorConfig`'s reason. A 404 means a gateway that
1039
+ * does not know the route, so `supported: false` tells the caller to stop
1040
+ * asking for the rest of the session; every other failure (a timeout, a 429,
1041
+ * a 5xx, a malformed body) is simply "nothing booked" this time.
1042
+ */
1043
+ async directSponsor(formats, cols, signal) {
1044
+ try {
1045
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/direct`, {
1046
+ method: "POST",
1047
+ headers: this.headers(),
1048
+ ...signal ? { signal } : {},
1049
+ body: JSON.stringify({ formats, cols })
1050
+ });
1051
+ if (res.status === 404) return { supported: false, lines: [] };
1052
+ if (!res.ok) return { supported: true, lines: [] };
1053
+ return { supported: true, lines: parseDirectLines(await res.json()) };
1054
+ } catch {
1055
+ return { supported: true, lines: [] };
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Report the booked lines a turn drew, in one call, after the turn is over.
1060
+ * Never to `/v1/sponsor/impression`, which would credit them as ADtention's.
1061
+ * Never throws; answers what was credited, for the tally and for tests.
1062
+ */
1063
+ async reportDirectImpressions(impressions, signal) {
1064
+ if (impressions.length === 0) return 0;
1065
+ try {
1066
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/direct/impression`, {
1067
+ method: "POST",
1068
+ headers: this.headers(),
1069
+ ...signal ? { signal } : {},
1070
+ body: JSON.stringify({ impressions: impressions.map((i) => ({ format: i.format, serve_token: i.serveToken })) })
1071
+ });
1072
+ if (!res.ok) return 0;
1073
+ const body = await res.json();
1074
+ return typeof body.credited === "number" ? body.credited : 0;
1075
+ } catch {
1076
+ return 0;
1077
+ }
1078
+ }
984
1079
  /**
985
1080
  * Search the web, through the gateway.
986
1081
  *
@@ -1443,11 +1538,24 @@ async function githubLogin(client, config, start, invite, report, signal) {
1443
1538
  openBrowser(start.verification_uri);
1444
1539
  const deadline = Date.now() + start.expires_in * 1e3;
1445
1540
  let interval = Math.max(start.interval, 1);
1541
+ let unavailableSince;
1446
1542
  while (Date.now() < deadline) {
1447
1543
  await sleep2(interval * 1e3, signal);
1448
1544
  if (signal?.aborted) return { status: "aborted" };
1449
1545
  report.tick?.();
1450
- const poll = await client.devicePoll(start.session, invite);
1546
+ let poll;
1547
+ try {
1548
+ poll = await client.devicePoll(start.session, invite);
1549
+ } catch (err) {
1550
+ if (!(err instanceof GatewayUnreachableError)) throw err;
1551
+ poll = { status: "unavailable", error: err.message };
1552
+ }
1553
+ if (poll.status === "unavailable") {
1554
+ unavailableSince ??= Date.now();
1555
+ if (Date.now() - unavailableSince > UNAVAILABLE_GIVE_UP_MS) return { status: "failed", error: poll.error };
1556
+ continue;
1557
+ }
1558
+ unavailableSince = void 0;
1451
1559
  if (poll.status === "pending") {
1452
1560
  if (poll.interval) interval = poll.interval;
1453
1561
  continue;
@@ -1475,10 +1583,19 @@ async function githubLogin(client, config, start, invite, report, signal) {
1475
1583
  }
1476
1584
  };
1477
1585
  }
1478
- return { status: "failed", error: poll.error ?? poll.status };
1586
+ return { status: "failed", error: describePollError(poll.error ?? poll.status) };
1479
1587
  }
1480
1588
  return { status: "timeout" };
1481
1589
  }
1590
+ function describePollError(error) {
1591
+ if (error === "unknown_session") {
1592
+ return "the server restarted while you were signing in, so this code no longer works. Sign in again.";
1593
+ }
1594
+ if (error === "login_unavailable") {
1595
+ return "GitHub approved the sign-in, but the server could not finish it. Try again in a minute.";
1596
+ }
1597
+ return error;
1598
+ }
1482
1599
  async function devLogin(client, config, email, invite) {
1483
1600
  let res;
1484
1601
  try {
@@ -1517,7 +1634,7 @@ function sleep2(ms, signal) {
1517
1634
  signal.addEventListener("abort", done, { once: true });
1518
1635
  });
1519
1636
  }
1520
- var TERMS_URL, PRIVACY_URL, TERMS_NOTICE;
1637
+ var TERMS_URL, PRIVACY_URL, TERMS_NOTICE, UNAVAILABLE_GIVE_UP_MS;
1521
1638
  var init_login = __esm({
1522
1639
  "src/login.ts"() {
1523
1640
  "use strict";
@@ -1529,6 +1646,7 @@ var init_login = __esm({
1529
1646
  TERMS_NOTICE = `By creating an account you confirm that you are at least 18 and that you agree to
1530
1647
  the Terms \u2014 ${TERMS_URL}
1531
1648
  and the Privacy Policy \u2014 ${PRIVACY_URL}`;
1649
+ UNAVAILABLE_GIVE_UP_MS = 18e4;
1532
1650
  }
1533
1651
  });
1534
1652
 
@@ -5865,6 +5983,7 @@ function App({
5865
5983
  notices: notices2,
5866
5984
  sponsorServe,
5867
5985
  sponsorFormats,
5986
+ sponsorDirect,
5868
5987
  initHint
5869
5988
  }) {
5870
5989
  const { exit } = useApp();
@@ -5973,6 +6092,8 @@ function App({
5973
6092
  const sponsorPrevRef = useRef(void 0);
5974
6093
  const sponsorCallRef = useRef(0);
5975
6094
  const pendingImpressionRef = useRef(/* @__PURE__ */ new Map());
6095
+ const pendingDirectRef = useRef(/* @__PURE__ */ new Map());
6096
+ const directSupportedRef = useRef(true);
5976
6097
  const drawnRef = useRef(/* @__PURE__ */ new Set());
5977
6098
  const sponsorSlotRef = useRef(
5978
6099
  /* @__PURE__ */ new Map()
@@ -6184,8 +6305,7 @@ function App({
6184
6305
  } else {
6185
6306
  setSponsor(null);
6186
6307
  }
6187
- for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
6188
- if (!adFitsFormat(format, colsRef.current)) continue;
6308
+ const fetchNetwork = (format) => {
6189
6309
  void remoteSponsorRef.current[format].fetch().then((line2) => {
6190
6310
  if (!line2 || call !== sponsorCallRef.current) return;
6191
6311
  if (!adFitsFormat(format, colsRef.current)) return;
@@ -6203,15 +6323,55 @@ function App({
6203
6323
  recordImpression(tallyRef.current, line2.id);
6204
6324
  pendingImpressionRef.current.set(format, { call, impression: line2.impression });
6205
6325
  });
6206
- }
6326
+ };
6327
+ const paintDirect = (line2) => {
6328
+ const { format } = line2;
6329
+ if (call !== sponsorCallRef.current) return;
6330
+ if (!adFitsFormat(format, colsRef.current)) return;
6331
+ if (format !== "top_bar" && modalOpenRef.current) return;
6332
+ const drawnId = `direct:${call}:${format}`;
6333
+ if (format === "top_bar") {
6334
+ if (!sponsorRow(line2.text, colsRef.current, "network")) return;
6335
+ setSponsor({ text: line2.text, origin: "network", impressionId: drawnId });
6336
+ } else {
6337
+ setAdSlots((slots) => ({ ...slots, [format]: { text: line2.text, impressionId: drawnId } }));
6338
+ }
6339
+ pendingDirectRef.current.set(format, { call, serveToken: line2.serveToken, drawnId });
6340
+ };
6341
+ const fitting = AD_FORMATS.filter((format) => adFitsFormat(format, colsRef.current));
6342
+ void (async () => {
6343
+ let direct = [];
6344
+ if (directSupportedRef.current && fitting.length > 0) {
6345
+ const ask3 = sponsorDirect ?? ((formats, cols2, signal) => client.directSponsor(formats, cols2, signal));
6346
+ let answer = { supported: true, lines: [] };
6347
+ try {
6348
+ answer = await ask3(fitting, colsRef.current, AbortSignal.timeout(DIRECT_ASK_TIMEOUT_MS));
6349
+ } catch {
6350
+ }
6351
+ if (!answer.supported) directSupportedRef.current = false;
6352
+ direct = answer.lines;
6353
+ }
6354
+ if (call !== sponsorCallRef.current) return;
6355
+ for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
6356
+ if (!adFitsFormat(format, colsRef.current)) continue;
6357
+ const booked = direct.find((line2) => line2.format === format);
6358
+ if (booked && "hold" in booked) continue;
6359
+ if (booked) paintDirect(booked);
6360
+ else fetchNetwork(format);
6361
+ }
6362
+ })();
6207
6363
  },
6208
- [config.sponsor, stdout, wallet]
6364
+ [config.sponsor, stdout, wallet, client, sponsorDirect]
6209
6365
  );
6210
6366
  const reportSponsor = useCallback(() => {
6211
6367
  const pending = [...pendingImpressionRef.current.values()];
6368
+ const pendingDirect = [...pendingDirectRef.current.entries()];
6212
6369
  const drawn = drawnRef.current;
6213
6370
  pendingImpressionRef.current = /* @__PURE__ */ new Map();
6371
+ pendingDirectRef.current = /* @__PURE__ */ new Map();
6214
6372
  drawnRef.current = /* @__PURE__ */ new Set();
6373
+ const directReports = pendingDirect.filter(([, p]) => p.call === sponsorCallRef.current && drawn.has(p.drawnId)).map(([format, p]) => ({ format, serveToken: p.serveToken }));
6374
+ if (directReports.length > 0) void client.reportDirectImpressions(directReports);
6215
6375
  const reports = pending.filter((p) => p.call === sponsorCallRef.current).filter((p) => drawn.has(p.impression.impressionId)).map((p) => p.impression);
6216
6376
  if (reports.length === 0) return;
6217
6377
  void (async () => {
@@ -7876,6 +8036,7 @@ var init_app = __esm({
7876
8036
  init_provider();
7877
8037
  init_network();
7878
8038
  init_formats();
8039
+ init_direct();
7879
8040
  init_render();
7880
8041
  init_commands();
7881
8042
  init_editor();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.24",
3
+ "version": "0.0.1-beta.25",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",