clixad 0.0.1-beta.16 → 0.0.1-beta.18

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 +202 -24
  2. package/package.json +1 -1
package/dist/clixad.mjs CHANGED
@@ -246,6 +246,11 @@ var init_client = __esm({
246
246
  referral_bonus: "referral bonus",
247
247
  redeem_code: "code redeemed",
248
248
  ad_reward: "offer reward",
249
+ // Our own survey, and it is deliberately not called an offer. Every other
250
+ // earn line on this list is a network paying for something; this one is us.
251
+ // The name is the only place a user would ever learn the difference, and
252
+ // "offer reward" would file it next to the wall it did not come from.
253
+ own_survey_reward: "Clixad survey",
249
254
  ad_screenout_bonus: "screenout bonus (didn't qualify)",
250
255
  ad_sponsor_impression: "sponsored line shown",
251
256
  ad_reversal: "offer reward reversed by the provider",
@@ -510,11 +515,17 @@ var init_client = __esm({
510
515
  if (!res.ok) throw new Error(`packs failed: ${res.status}`);
511
516
  return res.json();
512
517
  }
513
- async checkout(pack) {
518
+ /**
519
+ * `consent` carries the two declarations § 356 (5) BGB requires before we may
520
+ * deliver digital content inside the withdrawal period. It is not optional:
521
+ * the gateway refuses a checkout without it, because starting a purchase we
522
+ * could not deliver instantly is worse than not starting one.
523
+ */
524
+ async checkout(pack, consent) {
514
525
  const res = await fetch(`${this.config.gatewayUrl}/v1/billing/checkout`, {
515
526
  method: "POST",
516
527
  headers: this.headers(),
517
- body: JSON.stringify({ pack })
528
+ body: JSON.stringify({ pack, consent })
518
529
  });
519
530
  if (res.status === 401) throw await authFailure(res, "checkout");
520
531
  if (res.status === 501) throw new Error("Buying credits isn't enabled (Stripe not configured on the gateway).");
@@ -820,7 +831,18 @@ var init_client = __esm({
820
831
  });
821
832
 
822
833
  // src/config.ts
823
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
834
+ import {
835
+ chmodSync,
836
+ closeSync,
837
+ fsyncSync,
838
+ mkdirSync,
839
+ openSync,
840
+ readFileSync,
841
+ renameSync,
842
+ unlinkSync,
843
+ writeFileSync,
844
+ writeSync
845
+ } from "node:fs";
824
846
  import { homedir } from "node:os";
825
847
  import { dirname, join } from "node:path";
826
848
  function sameEndpoint(a, b) {
@@ -835,11 +857,42 @@ function sameEndpoint(a, b) {
835
857
  };
836
858
  return norm(a) === norm(b);
837
859
  }
838
- function loadConfig() {
839
- let stored = {};
860
+ function takeConfigNotices() {
861
+ const out = notices;
862
+ notices = [];
863
+ return out;
864
+ }
865
+ function readStored() {
866
+ let raw;
867
+ try {
868
+ raw = readFileSync(CONFIG_PATH, "utf8");
869
+ } catch (err) {
870
+ const code = err.code;
871
+ if (code === "ENOENT") return { stored: {} };
872
+ return { stored: {}, problem: `could not be read (${code ?? err.message})` };
873
+ }
874
+ if (raw.trim() === "") return { stored: {}, problem: "was empty", raw };
875
+ let parsed;
840
876
  try {
841
- stored = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
877
+ parsed = JSON.parse(raw);
842
878
  } catch {
879
+ return { stored: {}, problem: "is not valid JSON", raw };
880
+ }
881
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
882
+ return { stored: {}, problem: "is not a JSON object", raw };
883
+ }
884
+ return { stored: parsed };
885
+ }
886
+ function loadConfig() {
887
+ notices = [];
888
+ envApplied.clear();
889
+ const read = readStored();
890
+ const stored = read.stored;
891
+ if (read.problem) {
892
+ notices.push(
893
+ `Your Clixad config (${CONFIG_PATH}) ${read.problem}, so this session starts signed out.
894
+ It is kept at ${QUARANTINE_PATH} the next time anything is saved \u2014 sign in again with \`/login\`.`
895
+ );
843
896
  }
844
897
  const mintedAgainst = stored.gatewayUrl;
845
898
  for (const [key, stale] of Object.entries(SUPERSEDED_DEFAULTS)) {
@@ -851,17 +904,76 @@ function loadConfig() {
851
904
  const config = { ...DEFAULTS, ...stored };
852
905
  for (const [key, envVar] of ENV_OVERRIDES) {
853
906
  const value = process.env[envVar];
854
- if (value) config[key] = value;
907
+ if (value) {
908
+ config[key] = value;
909
+ envApplied.add(key);
910
+ }
855
911
  }
856
- const migratedGateway = mintedAgainst !== void 0 && stored.gatewayUrl === void 0;
857
- if (migratedGateway && !sameEndpoint(mintedAgainst, config.gatewayUrl)) {
912
+ const boundTo = stored.tokenGateway ?? mintedAgainst;
913
+ const redirected = envApplied.has("gatewayUrl");
914
+ if (!redirected && boundTo !== void 0 && !sameEndpoint(boundTo, config.gatewayUrl)) {
915
+ if (stored.token !== void 0) {
916
+ notices.push(
917
+ `Signed out: the saved account belongs to ${boundTo}, and this run talks to ${config.gatewayUrl}.
918
+ Sign in again with \`/login\` (or \`clixad login\`).`
919
+ );
920
+ }
858
921
  for (const key of IDENTITY_KEYS) delete config[key];
859
922
  }
923
+ baseline = { ...config };
860
924
  return config;
861
925
  }
926
+ function changedKeys(config) {
927
+ const current = config;
928
+ if (!baseline) return Object.keys(current);
929
+ const before = baseline;
930
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(current)]);
931
+ return [...keys].filter((key) => before[key] !== current[key]);
932
+ }
933
+ function writeAtomically(contents) {
934
+ const tmp = `${CONFIG_PATH}.${process.pid}.tmp`;
935
+ try {
936
+ const fd = openSync(tmp, "w", 384);
937
+ try {
938
+ writeSync(fd, contents);
939
+ fsyncSync(fd);
940
+ } finally {
941
+ closeSync(fd);
942
+ }
943
+ renameSync(tmp, CONFIG_PATH);
944
+ } catch (err) {
945
+ try {
946
+ unlinkSync(tmp);
947
+ } catch {
948
+ }
949
+ throw err;
950
+ }
951
+ }
862
952
  function saveConfig(config) {
863
953
  mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
864
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
954
+ const changed = changedKeys(config);
955
+ const current = readStored();
956
+ let base = {};
957
+ if (current.problem) {
958
+ if (current.raw !== void 0) {
959
+ try {
960
+ writeFileSync(QUARANTINE_PATH, current.raw, { encoding: "utf8", mode: 384 });
961
+ } catch {
962
+ }
963
+ }
964
+ } else {
965
+ base = current.stored;
966
+ }
967
+ const next = { ...base };
968
+ const source = config;
969
+ for (const key of changed) {
970
+ const value = source[key];
971
+ if (value === void 0) delete next[key];
972
+ else next[key] = value;
973
+ }
974
+ writeAtomically(`${JSON.stringify(next, null, 2)}
975
+ `);
976
+ baseline = { ...config };
865
977
  try {
866
978
  chmodSync(CONFIG_PATH, 384);
867
979
  } catch {
@@ -870,11 +982,12 @@ function saveConfig(config) {
870
982
  function configPath() {
871
983
  return CONFIG_PATH;
872
984
  }
873
- var CONFIG_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS;
985
+ var CONFIG_PATH, QUARANTINE_PATH, DEFAULTS, ENV_OVERRIDES, SUPERSEDED_DEFAULTS, IDENTITY_KEYS, baseline, envApplied, notices;
874
986
  var init_config = __esm({
875
987
  "src/config.ts"() {
876
988
  "use strict";
877
989
  CONFIG_PATH = join(homedir(), ".clixad", "config.json");
990
+ QUARANTINE_PATH = `${CONFIG_PATH}.corrupt`;
878
991
  DEFAULTS = {
879
992
  gatewayUrl: "https://clixad.onrender.com",
880
993
  // Same origin as the gateway: the ad wall is served by the gateway process
@@ -892,7 +1005,9 @@ var init_config = __esm({
892
1005
  gatewayUrl: ["http://127.0.0.1:8787", "http://localhost:8787"],
893
1006
  dashboardUrl: ["http://127.0.0.1:8788", "http://localhost:8788"]
894
1007
  };
895
- IDENTITY_KEYS = ["token", "userId", "email", "login"];
1008
+ IDENTITY_KEYS = ["token", "userId", "email", "login", "tokenGateway"];
1009
+ envApplied = /* @__PURE__ */ new Set();
1010
+ notices = [];
896
1011
  }
897
1012
  });
898
1013
 
@@ -952,6 +1067,7 @@ async function githubLogin(client, config, start, invite, report, signal) {
952
1067
  config.userId = poll.userId;
953
1068
  config.email = poll.email;
954
1069
  config.login = poll.login;
1070
+ config.tokenGateway = config.gatewayUrl;
955
1071
  saveConfig(config);
956
1072
  return {
957
1073
  status: "ok",
@@ -981,6 +1097,7 @@ async function devLogin(client, config, email, invite) {
981
1097
  config.token = res.token;
982
1098
  config.userId = res.userId;
983
1099
  config.email = res.email;
1100
+ config.tokenGateway = config.gatewayUrl;
984
1101
  delete config.login;
985
1102
  saveConfig(config);
986
1103
  return { status: "ok", account: { email: res.email, balance: res.balance, created: true } };
@@ -1001,12 +1118,18 @@ function sleep(ms, signal) {
1001
1118
  signal.addEventListener("abort", done, { once: true });
1002
1119
  });
1003
1120
  }
1121
+ var TERMS_URL, PRIVACY_URL, TERMS_NOTICE;
1004
1122
  var init_login = __esm({
1005
1123
  "src/login.ts"() {
1006
1124
  "use strict";
1007
1125
  init_client();
1008
1126
  init_browser();
1009
1127
  init_config();
1128
+ TERMS_URL = "https://clixad.io/terms.html";
1129
+ PRIVACY_URL = "https://clixad.io/privacy.html";
1130
+ TERMS_NOTICE = `By creating an account you confirm that you are at least 18 and that you agree to
1131
+ the Terms \u2014 ${TERMS_URL}
1132
+ and the Privacy Policy \u2014 ${PRIVACY_URL}`;
1010
1133
  }
1011
1134
  });
1012
1135
 
@@ -5117,7 +5240,7 @@ function useTerminalSize() {
5117
5240
  }, [stdout]);
5118
5241
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5119
5242
  }
5120
- function App({ client, config, wallet, streak, session, initialTask, sponsorServe }) {
5243
+ function App({ client, config, wallet, streak, session, initialTask, notices: notices2, sponsorServe }) {
5121
5244
  const { exit } = useApp();
5122
5245
  const { stdout, write: writeToStdout } = useStdout();
5123
5246
  const idRef = useRef(1);
@@ -5271,6 +5394,7 @@ function App({ client, config, wallet, streak, session, initialTask, sponsorServ
5271
5394
  useEffect(() => dropDelta, [dropDelta]);
5272
5395
  useEffect(() => {
5273
5396
  client.onNotice = (text2) => push({ kind: "notice", tone: "warn", text: text2 });
5397
+ for (const text2 of notices2 ?? []) push({ kind: "notice", tone: "warn", text: ` ${text2}` });
5274
5398
  const files = contextRef.current.files;
5275
5399
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
5276
5400
  const streakLine = streak && streak.claimed ? streakLabel(streak) : null;
@@ -5639,6 +5763,7 @@ ${NO_SEARCH_INSTRUCTION}`),
5639
5763
  const runLogin = useCallback(
5640
5764
  async (arg) => {
5641
5765
  const args = parseLoginArgs(arg.split(/\s+/).filter(Boolean));
5766
+ push({ kind: "notice", text: TERMS_NOTICE });
5642
5767
  await runBusy(SIGNING_IN, async (signal) => {
5643
5768
  const outcome = await performLogin(
5644
5769
  client,
@@ -6719,6 +6844,7 @@ init_kimi();
6719
6844
  init_banner();
6720
6845
  init_browser();
6721
6846
  init_color();
6847
+ import { createInterface } from "node:readline/promises";
6722
6848
 
6723
6849
  // src/title.ts
6724
6850
  var APP_TITLE = "Clixad";
@@ -6755,6 +6881,8 @@ async function main() {
6755
6881
  const [cmd, ...rest] = process.argv.slice(2);
6756
6882
  const config = loadConfig();
6757
6883
  const client = new GatewayClient(config);
6884
+ const configNotices = takeConfigNotices();
6885
+ for (const text of configNotices) console.error(c.yellow(text));
6758
6886
  switch (cmd) {
6759
6887
  case "login":
6760
6888
  return login(client, config, rest);
@@ -6779,15 +6907,15 @@ async function main() {
6779
6907
  case "ask":
6780
6908
  return ask(client, config, rest.join(" "));
6781
6909
  case "agent":
6782
- return repl(client, config, { task: rest.join(" ").trim() || void 0 });
6910
+ return repl(client, config, { task: rest.join(" ").trim() || void 0, notices: configNotices });
6783
6911
  case "-p":
6784
6912
  case "--print":
6785
6913
  return printCmd(client, config, rest.join(" ").trim());
6786
6914
  case "-c":
6787
6915
  case "--continue":
6788
- return repl(client, config, { resume: "latest" });
6916
+ return repl(client, config, { resume: "latest", notices: configNotices });
6789
6917
  case "--resume":
6790
- return repl(client, config, { resume: rest[0] ?? "pick" });
6918
+ return repl(client, config, { resume: rest[0] ?? "pick", notices: configNotices });
6791
6919
  case "code":
6792
6920
  return codeCmd(client, config, rest);
6793
6921
  case "help":
@@ -6802,7 +6930,7 @@ async function main() {
6802
6930
  case "-v":
6803
6931
  return void console.log(VERSION);
6804
6932
  case void 0:
6805
- return repl(client, config);
6933
+ return repl(client, config, { notices: configNotices });
6806
6934
  default:
6807
6935
  if (cmd.startsWith("-")) {
6808
6936
  console.error(c.red(`unknown option: ${cmd}`));
@@ -6810,11 +6938,12 @@ async function main() {
6810
6938
  process.exitCode = 1;
6811
6939
  return;
6812
6940
  }
6813
- return repl(client, config, { task: [cmd, ...rest].join(" ").trim() });
6941
+ return repl(client, config, { task: [cmd, ...rest].join(" ").trim(), notices: configNotices });
6814
6942
  }
6815
6943
  }
6816
6944
  async function login(client, config, args) {
6817
6945
  const parsed = parseLoginArgs(args);
6946
+ for (const line2 of TERMS_NOTICE.split("\n")) console.log(c.dim(` ${line2}`));
6818
6947
  const outcome = await performLogin(client, config, parsed, {
6819
6948
  verify(start) {
6820
6949
  console.log(`
@@ -6878,10 +7007,7 @@ function wrapPlain(text, width) {
6878
7007
  var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
6879
7008
  var credits = (n) => n.toLocaleString("en-US");
6880
7009
  function logout(config) {
6881
- delete config.token;
6882
- delete config.userId;
6883
- delete config.email;
6884
- delete config.login;
7010
+ for (const key of IDENTITY_KEYS) delete config[key];
6885
7011
  saveConfig(config);
6886
7012
  console.log(c.green("\u2713 logged out"));
6887
7013
  }
@@ -7020,9 +7146,20 @@ async function buyCmd(client, config, pack) {
7020
7146
  console.log(c.dim("\n buy one with: clixad buy <id>"));
7021
7147
  return;
7022
7148
  }
7149
+ const chosen = packs.find((x) => x.id === pack);
7150
+ if (!chosen) {
7151
+ console.log(c.red(` unknown pack: ${pack}`));
7152
+ return console.log(c.dim(` try one of: ${packs.map((x) => x.id).join(", ")}`));
7153
+ }
7154
+ const consented = await confirmWithdrawalWaiver(chosen);
7155
+ if (!consented) return;
7023
7156
  let checkout;
7024
7157
  try {
7025
- checkout = await client.checkout(pack);
7158
+ checkout = await client.checkout(pack, {
7159
+ requestImmediatePerformance: true,
7160
+ acknowledgeLossOfWithdrawal: true,
7161
+ at: (/* @__PURE__ */ new Date()).toISOString()
7162
+ });
7026
7163
  } catch (err) {
7027
7164
  return console.log(c.red(err.message));
7028
7165
  }
@@ -7045,6 +7182,39 @@ async function buyCmd(client, config, pack) {
7045
7182
  }
7046
7183
  console.log(c.yellow("\n No credits yet \u2014 the webhook may be delayed. Check `clixad wallet`."));
7047
7184
  }
7185
+ async function confirmWithdrawalWaiver(pack) {
7186
+ if (!process.stdin.isTTY) {
7187
+ console.log(c.yellow("\n Buying credits needs an interactive terminal."));
7188
+ console.log(
7189
+ c.dim(" We have to record your confirmation before charging you, and a pipe has nobody to ask.")
7190
+ );
7191
+ return false;
7192
+ }
7193
+ const price = "$" + pack.price_usd.toFixed(2);
7194
+ console.log(`
7195
+ ${c.bold(pack.label)} \u2014 ${credits(pack.credits)} credits for ${c.bold(price)}`);
7196
+ console.log(c.dim(` One purchase. No subscription, nothing to cancel. Terms: ${TERMS_URL}`));
7197
+ console.log("\n Before you pay, please confirm:");
7198
+ console.log(" \xB7 You request that we deliver the credits immediately, before the");
7199
+ console.log(" 14-day withdrawal period has expired.");
7200
+ console.log(" \xB7 You acknowledge that your right of withdrawal lapses once they");
7201
+ console.log(" have been delivered.");
7202
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7203
+ let answer = "";
7204
+ try {
7205
+ answer = (await rl.question(`
7206
+ Type ${c.bold("yes")} to continue: `)).trim().toLowerCase();
7207
+ } catch {
7208
+ answer = "";
7209
+ } finally {
7210
+ rl.close();
7211
+ }
7212
+ if (answer !== "yes" && answer !== "y") {
7213
+ console.log(c.dim("\n Cancelled. You have not been charged."));
7214
+ return false;
7215
+ }
7216
+ return true;
7217
+ }
7048
7218
  async function ask(client, config, prompt) {
7049
7219
  if (!prompt) return console.error(c.red('usage: clixad ask "your prompt"'));
7050
7220
  const messages = [{ role: "user", content: prompt }];
@@ -7176,7 +7346,15 @@ async function repl(client, config, opts = {}) {
7176
7346
  const wallet = await safeWallet(client);
7177
7347
  clearScreen();
7178
7348
  const { startTui: startTui2 } = await Promise.resolve().then(() => (init_tui(), tui_exports));
7179
- await startTui2({ client, config, wallet, streak, session, initialTask: opts.task });
7349
+ await startTui2({
7350
+ client,
7351
+ config,
7352
+ wallet,
7353
+ streak,
7354
+ session,
7355
+ initialTask: opts.task,
7356
+ ...opts.notices?.length ? { notices: opts.notices } : {}
7357
+ });
7180
7358
  }
7181
7359
  async function runTurn(client, config, messages) {
7182
7360
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.16",
3
+ "version": "0.0.1-beta.18",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",