clixad 0.0.1-beta.30 → 0.0.1-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,10 @@ cover it, for example `task ≈ 560–3.1k · free` or `task ≈ 7.5k–47k · n
15
15
  estimate from the size of the conversation and how tasks usually run; what is charged is what the
16
16
  model actually used.
17
17
 
18
+ The same account also works in the browser, at <https://clixad.onrender.com/chat>: sign in with the
19
+ GitHub account you use here. A model, a reasoning setting, your name and instructions set there or on
20
+ its account page hold here too (from 0.0.1-beta.31), and `/model` and `/effort` here hold there.
21
+
18
22
  `/effort` sets how long the model thinks before it answers, saved separately for each model.
19
23
  Thinking is billed as output tokens like the answer, so a higher setting costs more credits per turn
20
24
  and a lower one costs less. `auto` leaves the model at its own default, and a model with nothing to
package/dist/clixad.mjs CHANGED
@@ -432,6 +432,22 @@ function extraFormatIds(raw) {
432
432
  }
433
433
  return out;
434
434
  }
435
+ function parseAccountSettings(body) {
436
+ if (!body || typeof body !== "object") return null;
437
+ const b = body;
438
+ const efforts = {};
439
+ if (b.efforts && typeof b.efforts === "object" && !Array.isArray(b.efforts)) {
440
+ for (const [k, v] of Object.entries(b.efforts)) if (typeof v === "string") efforts[k] = v;
441
+ }
442
+ const str = (v) => typeof v === "string" && v.trim() ? v : null;
443
+ return {
444
+ model: str(b.model),
445
+ efforts,
446
+ name: str(b.name),
447
+ instructions: str(b.instructions),
448
+ sponsorLine: typeof b.sponsor_line === "boolean" ? b.sponsor_line : null
449
+ };
450
+ }
435
451
  function imagePart(image) {
436
452
  return { type: "image_url", image_url: { url: `data:${image.mimeType};base64,${image.data}` } };
437
453
  }
@@ -840,6 +856,42 @@ var init_client = __esm({
840
856
  * is "say nothing about a streak". A missed grant is claimed by the next
841
857
  * command; a session that died over one would be a real loss.
842
858
  */
859
+ /**
860
+ * The account's preferences (gateway 0039), kept with the account so they
861
+ * hold in the browser's chat and here. Null when they cannot be read, and on
862
+ * a gateway too old to have them (404): a preference is never a reason for
863
+ * the REPL not to open.
864
+ */
865
+ async getSettings(signal) {
866
+ try {
867
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/account/settings`, {
868
+ headers: this.headers(),
869
+ ...signal ? { signal } : {}
870
+ });
871
+ if (!res.ok) return null;
872
+ return parseAccountSettings(await res.json());
873
+ } catch {
874
+ return null;
875
+ }
876
+ }
877
+ /**
878
+ * Change the preferences a request names. Best-effort and never throws: a
879
+ * model picked here is saved locally whatever the gateway says, and a
880
+ * failed sync costs only that the browser's chat does not follow.
881
+ */
882
+ async saveSettings(change) {
883
+ try {
884
+ const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/account/settings`, {
885
+ method: "PUT",
886
+ headers: { ...this.headers(), "Content-Type": "application/json" },
887
+ body: JSON.stringify(change)
888
+ });
889
+ if (!res.ok) return null;
890
+ return parseAccountSettings(await res.json());
891
+ } catch {
892
+ return null;
893
+ }
894
+ }
843
895
  async claimStreak(signal) {
844
896
  try {
845
897
  const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/streak/claim`, {
@@ -1638,6 +1690,46 @@ var init_config = __esm({
1638
1690
  }
1639
1691
  });
1640
1692
 
1693
+ // src/preferences.ts
1694
+ async function syncPreferences(client, config, settings) {
1695
+ if (!settings) return { name: null, instructions: null, sponsorLine: null };
1696
+ const upload = {};
1697
+ if (settings.model && settings.model !== config.model) {
1698
+ config.model = settings.model;
1699
+ saveConfig(config);
1700
+ } else if (!settings.model && config.model && config.model !== FALLBACK_MODEL) {
1701
+ upload.model = config.model;
1702
+ }
1703
+ const local = config.effort ?? {};
1704
+ for (const [model, level] of Object.entries(settings.efforts)) {
1705
+ if (local[model] !== level) saveEffort(config, model, level);
1706
+ }
1707
+ const localOnly = {};
1708
+ for (const [model, level] of Object.entries(local)) {
1709
+ if (!(model in settings.efforts)) localOnly[model] = level;
1710
+ }
1711
+ if (Object.keys(localOnly).length) upload.efforts = localOnly;
1712
+ if (Object.keys(upload).length) await client.saveSettings(upload).catch(() => null);
1713
+ return { name: settings.name, instructions: settings.instructions, sponsorLine: settings.sponsorLine };
1714
+ }
1715
+ function preferencesInstruction(prefs) {
1716
+ if (!prefs) return "";
1717
+ const parts = [];
1718
+ if (prefs.name) parts.push(`The person's name is ${prefs.name}.`);
1719
+ if (prefs.instructions) parts.push(`What the person wants you to keep in mind in every answer:
1720
+ ${prefs.instructions}`);
1721
+ return parts.join("\n\n");
1722
+ }
1723
+ function sponsorPreference(config, prefs) {
1724
+ return prefs?.sponsorLine === false ? false : config.sponsor;
1725
+ }
1726
+ var init_preferences = __esm({
1727
+ "src/preferences.ts"() {
1728
+ "use strict";
1729
+ init_config();
1730
+ }
1731
+ });
1732
+
1641
1733
  // src/browser.ts
1642
1734
  import { spawn } from "node:child_process";
1643
1735
  function openBrowser(url) {
@@ -6224,9 +6316,11 @@ function App({
6224
6316
  sponsorServe,
6225
6317
  sponsorFormats,
6226
6318
  sponsorDirect,
6227
- initHint
6319
+ initHint,
6320
+ preferences
6228
6321
  }) {
6229
6322
  const { exit } = useApp();
6323
+ const preferencesText = preferencesInstruction(preferences);
6230
6324
  const { stdout, write: writeToStdout } = useStdout();
6231
6325
  const idRef = useRef(1);
6232
6326
  const { rows, cols } = useTerminalSize();
@@ -6270,7 +6364,8 @@ function App({
6270
6364
  const switchModel = useCallback((id) => {
6271
6365
  modelRef.current = id;
6272
6366
  setModel2(id);
6273
- }, []);
6367
+ void client.saveSettings?.({ model: id });
6368
+ }, [client]);
6274
6369
  const [hist, setHist] = useState(() => loadHistory());
6275
6370
  const [histIdx, setHistIdx] = useState(-1);
6276
6371
  const [sel, setSel] = useState(0);
@@ -6544,7 +6639,7 @@ function App({
6544
6639
  if (!sponsorEnabled({
6545
6640
  isTTY: stdout.isTTY,
6546
6641
  env: process.env,
6547
- configEnabled: config.sponsor
6642
+ configEnabled: sponsorPreference(config, preferences)
6548
6643
  })) {
6549
6644
  setSponsor(null);
6550
6645
  setAdSlots({});
@@ -6817,7 +6912,11 @@ function App({
6817
6912
 
6818
6913
  ${modeInstruction(permRef.current.mode)}` + (searchRef.current ? "" : `
6819
6914
 
6820
- ${NO_SEARCH_INSTRUCTION}`),
6915
+ ${NO_SEARCH_INSTRUCTION}`) + // The account's name and instructions (gateway 0039), set in the
6916
+ // browser or on the account page, said on every turn.
6917
+ (preferencesText ? `
6918
+
6919
+ ${preferencesText}` : ""),
6821
6920
  permit,
6822
6921
  approvePlan,
6823
6922
  askUser: askQuestion,
@@ -7442,6 +7541,7 @@ ${NO_SEARCH_INSTRUCTION}`),
7442
7541
  const defaultWords = fallback ? effortLabel(fallback) : "its default";
7443
7542
  const apply2 = (choice, note = "") => {
7444
7543
  saveEffort(config, model, choice);
7544
+ void client.saveSettings?.({ efforts: { [model]: choice ?? "auto" } });
7445
7545
  setEffortBy({ ...config.effort ?? {} });
7446
7546
  const now = choice ? effortLabel(choice) : `auto (${defaultWords})`;
7447
7547
  push({ kind: "notice", text: ` ${name}: ${now}${note}` });
@@ -8459,6 +8559,7 @@ var init_app = __esm({
8459
8559
  init_todos();
8460
8560
  init_shells();
8461
8561
  init_config();
8562
+ init_preferences();
8462
8563
  init_effort();
8463
8564
  init_login();
8464
8565
  init_banner();
@@ -8568,6 +8669,7 @@ var init_tui = __esm({
8568
8669
  // src/main.ts
8569
8670
  init_client();
8570
8671
  init_config();
8672
+ init_preferences();
8571
8673
  init_login();
8572
8674
  init_free_tier();
8573
8675
  init_agent();
@@ -9076,7 +9178,8 @@ async function repl(client, config, opts = {}) {
9076
9178
  session = loadSession(opts.resume);
9077
9179
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
9078
9180
  }
9079
- const { streak, wallet } = await startupReads(client, config.gatewayUrl);
9181
+ const { streak, wallet, settings } = await startupReads(client, config.gatewayUrl);
9182
+ const preferences = await syncPreferences(client, config, settings);
9080
9183
  const cwd = process.cwd();
9081
9184
  const initHint = shouldSuggestInit(cwd) ? { returning: hasSessionIn(cwd) } : void 0;
9082
9185
  clearScreen();
@@ -9088,6 +9191,7 @@ async function repl(client, config, opts = {}) {
9088
9191
  streak,
9089
9192
  session,
9090
9193
  newAccount,
9194
+ preferences,
9091
9195
  initialTask: opts.task,
9092
9196
  ...opts.notices?.length ? { notices: opts.notices } : {},
9093
9197
  ...initHint ? { initHint } : {}
@@ -9166,10 +9270,15 @@ async function startupReads(client, gatewayUrl) {
9166
9270
  )
9167
9271
  ];
9168
9272
  try {
9169
- const [streak, wallet] = await Promise.all([client.claimStreak(), safeWallet(client, { premium: true })]);
9273
+ const [streak, wallet, settings] = await Promise.all([
9274
+ client.claimStreak(),
9275
+ safeWallet(client, { premium: true }),
9276
+ client.getSettings()
9277
+ ]);
9170
9278
  const balance = streak?.claimed ? streak.balance : wallet?.balance;
9171
9279
  return {
9172
9280
  streak,
9281
+ settings,
9173
9282
  wallet: wallet && balance !== void 0 ? { ...wallet, balance } : wallet
9174
9283
  };
9175
9284
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.30",
3
+ "version": "0.0.1-beta.31",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",