openlimiter 2.0.2 → 2.0.4

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 (58) hide show
  1. package/README.md +3 -1
  2. package/dist/api-spend.d.ts +27 -0
  3. package/dist/api-spend.d.ts.map +1 -0
  4. package/dist/api-spend.js +109 -0
  5. package/dist/api-spend.js.map +1 -0
  6. package/dist/bin.js +1 -0
  7. package/dist/bin.js.map +1 -1
  8. package/dist/cli.d.ts +13 -20
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +153 -56
  11. package/dist/cli.js.map +1 -1
  12. package/dist/codex-device-login.d.ts.map +1 -1
  13. package/dist/codex-device-login.js +1 -0
  14. package/dist/codex-device-login.js.map +1 -1
  15. package/dist/config.d.ts +4 -2
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +23 -5
  18. package/dist/config.js.map +1 -1
  19. package/dist/hub-auth.d.ts +2 -0
  20. package/dist/hub-auth.d.ts.map +1 -1
  21. package/dist/hub-auth.js +18 -1
  22. package/dist/hub-auth.js.map +1 -1
  23. package/dist/hub-sync.d.ts +4 -2
  24. package/dist/hub-sync.d.ts.map +1 -1
  25. package/dist/hub-sync.js +40 -9
  26. package/dist/hub-sync.js.map +1 -1
  27. package/dist/ingest.d.ts +26 -0
  28. package/dist/ingest.d.ts.map +1 -1
  29. package/dist/ingest.js +46 -62
  30. package/dist/ingest.js.map +1 -1
  31. package/dist/render.d.ts.map +1 -1
  32. package/dist/render.js +42 -11
  33. package/dist/render.js.map +1 -1
  34. package/dist/session.d.ts.map +1 -1
  35. package/dist/session.js +1 -4
  36. package/dist/session.js.map +1 -1
  37. package/dist/statusline-wrapper.d.ts.map +1 -1
  38. package/dist/statusline-wrapper.js +2 -1
  39. package/dist/statusline-wrapper.js.map +1 -1
  40. package/dist/statusline.d.ts +5 -1
  41. package/dist/statusline.d.ts.map +1 -1
  42. package/dist/statusline.js +200 -34
  43. package/dist/statusline.js.map +1 -1
  44. package/dist/terminal-backup.d.ts +85 -1
  45. package/dist/terminal-backup.d.ts.map +1 -1
  46. package/dist/terminal-backup.js +148 -22
  47. package/dist/terminal-backup.js.map +1 -1
  48. package/dist/terminal-fallback.d.ts.map +1 -1
  49. package/dist/terminal-fallback.js +13 -3
  50. package/dist/terminal-fallback.js.map +1 -1
  51. package/dist/terminal-toml.d.ts +8 -0
  52. package/dist/terminal-toml.d.ts.map +1 -1
  53. package/dist/terminal-toml.js +59 -2
  54. package/dist/terminal-toml.js.map +1 -1
  55. package/dist/terminal.d.ts.map +1 -1
  56. package/dist/terminal.js +149 -40
  57. package/dist/terminal.js.map +1 -1
  58. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -1,17 +1,18 @@
1
- import { ACQUISITION_OUTCOME_SENTENCE, captureStatuslineAccount, pruneSnapshotCache, ACQUISITION_PROVIDERS, registerManagedCodexAccount, CREDENTIAL_FAILURE_SENTENCE, PROVIDER_CODES, acquireRefreshLock, clearRefreshSpawnFailure, mergeAcquiredSnapshots, createFetchTransport, readRefreshSpawnFailure, antigravitySpec, buildAdvice, canonicalJson, claudeSpec, codexSpec, cursorSpec, dedupeFailures, desktopHoldsCache, failureFromConnectorReason, freshness, geminiCliSpec, grokSpec, isProviderDue, kimiSpec, mergeSnapshots, normalizeMeters, normalizeMetersReport, openrouterSpec, readAcquisitionCredential, readAcquisitionSchedule, readSnapshotCache, readWindowsCredentialWith, resolveStateDirectory, runAcquisition, spawnDetachedRefresh, writeAcquisitionSchedule, probeAntigravity, resolveAgyExecutablePath } from "@openlimiter/core";
2
- import { antigravityFixture, claudeFixture, codexFixture, grokFixture, kimiFixture, connectors, manualFixture, opencodeFixture, openrouterFixture, parseAntigravityCodeAssistPayload, parseAntigravityPayload, parseClaudePayload, parseCodexPayload, parseCursorPayload, parseGeminiCliPayload, parseGrokPayload, parseKimiPayload, parseManualPayload, parseOpencodePayload, parseOpenrouterPayload } from "@openlimiter/connectors";
3
- import { AGENT_COMPATIBILITY, agentContextFromCache, agentContextSpillFromCache, agentVersionCompatibility, changeAgentHook, detectAgentInstallation, readAgentHookStatus, runAgentHook, validateAgentExecutableStamp, writeAgentContextSnapshot, loadHostedContextTrust, renderClaudeStatusline } from "@openlimiter/adapters";
1
+ import { ACQUISITION_OUTCOME_SENTENCE, captureStatuslineAccount, pruneSnapshotCache, ACQUISITION_PROVIDERS, registerManagedCodexAccount, CREDENTIAL_FAILURE_SENTENCE, PROVIDER_CODES, acquireRefreshLock, clearRefreshSpawnFailure, mergeAcquiredSnapshots, createFetchTransport, readRefreshSpawnFailure, buildAdvice, canonicalJson, claudeSpec, codexSpec, cursorSpec, dedupeFailures, desktopHoldsCache, failureFromConnectorReason, freshness, geminiCliSpec, grokSpec, isProviderDue, kimiSpec, mergeSnapshotCache, mergeSnapshots, normalizeMeters, normalizeMetersReport, openrouterSpec, readAcquisitionCredential, readAcquisitionSchedule, readSnapshotCache, readWindowsCredentialWith, resolveStateDirectory, runAcquisition, spawnDetachedRefresh, trustedHelperWorkingDirectory, writeAcquisitionSchedule, windowsSystemTool } from "@openlimiter/core";
2
+ import { antigravityFixture, claudeFixture, codexFixture, grokFixture, kimiFixture, connectors, manualFixture, opencodeFixture, openrouterFixture, parseAntigravityPayload, parseClaudePayload, parseCodexPayload, parseCursorPayload, parseGeminiCliPayload, parseGrokPayload, parseKimiPayload, parseManualPayload, parseOpencodePayload, parseOpenrouterPayload } from "@openlimiter/connectors";
3
+ import { AGENT_COMPATIBILITY, agentContextFromCache, agentContextSpillFromCache, agentVersionCompatibility, changeAgentHook, detectAgentInstallation, readAgentHookStatus, runAgentHook, validateAgentExecutableStamp, writeAgentContextSnapshot, loadHostedContextTrust, } from "@openlimiter/adapters";
4
4
  import { execFile, spawn } from "node:child_process";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import packageJson from "../package.json" with { type: "json" };
7
7
  import { homedir } from "node:os";
8
8
  import path from "node:path";
9
9
  import { PROVIDER_KEYS, STATUSLINE_KEYS, defaultConfig, initialize, isProviderKey, isStatuslineKey, providerValueText, readConfig, readProvidersConfig, readStatuslineConfig, setProviderValue, setStatuslineValue, statuslineValueText, writeConfig } from "./config.js";
10
- import { ACQUISITION_PROVENANCE, ANTIGRAVITY_STATUSLINE_PROVENANCE, GROK_STATUSLINE_PROVENANCE, INGEST_PROVENANCE, MANUAL_PROVENANCE, STATUSLINE_PROVENANCE, environmentWithLocalMarkers, STDIN_BYTE_LIMIT, parseAntigravityStatuslinePayload, parseGrokStatuslinePayload, parseJsonText, persistSnapshots, readManualDocument, withProvenance } from "./ingest.js";
10
+ import { ACQUISITION_PROVENANCE, ANTIGRAVITY_STATUSLINE_PROVENANCE, GROK_STATUSLINE_PROVENANCE, INGEST_PROVENANCE, MANUAL_PROVENANCE, STATUSLINE_PROVENANCE, environmentWithLocalMarkers, STDIN_BYTE_LIMIT, parseAntigravityStatuslinePayload, antigravityStatuslineAccount, parseGrokStatuslinePayload, parseJsonText, persistSnapshots, clearStatuslineFailure, readStatuslineFailure, recordStatuslineFailure, readManualDocument, withProvenance } from "./ingest.js";
11
11
  import { UnavailableCredentialStore } from "./credentials.js";
12
12
  import { failureLine, failureLines, renderTable, supportsColor } from "./render.js";
13
- import { isStatuslineHost, renderStatuslineLayout, statuslineColor, statuslineUnicode } from "./statusline.js";
13
+ import { isStatuslineHost, renderPlainStatusline, renderStatuslineLayout, resolveProviderOrder, statuslineColor, statuslineUnicode } from "./statusline.js";
14
14
  import { parseStatuslineSession } from "./statusline-ingest.js";
15
+ import { readApiSpend } from "./api-spend.js";
15
16
  import { TERMINAL_HOST_NAMES, installHost, terminalHide, terminalShow, terminalStatusTable, uninstallHost } from "./terminal.js";
16
17
  import { createFetchHubTransport, hubConfigured } from "./hub.js";
17
18
  import { DELIVERY_UNCONFIRMED_SENTENCE, REVOKED_SENTENCE, ensureFreshSession, isAborted, runDeviceLogin } from "./hub-auth.js";
@@ -48,8 +49,8 @@ function defaults() {
48
49
  throw new Error("No acquisition transport was injected");
49
50
  },
50
51
  spawnDetached: () => undefined,
51
- probeAntigravity: async () => ({ ok: false, reason: "not_running" }),
52
52
  promptChoice: async () => "",
53
+ interactive: false,
53
54
  hubTransport: async () => {
54
55
  throw new Error("No hub transport was injected");
55
56
  },
@@ -88,7 +89,12 @@ const NEWLINE = "\n";
88
89
  const execFileRunner = async (executable, helperArguments, timeoutMilliseconds) => await new Promise((resolve) => {
89
90
  execFile(executable, [...helperArguments], (() => {
90
91
  if (!executable.toLowerCase().endsWith("powershell.exe")) {
91
- return { timeout: timeoutMilliseconds, maxBuffer: 262_144, windowsHide: true };
92
+ return {
93
+ cwd: trustedHelperWorkingDirectory(process.platform, process.env),
94
+ timeout: timeoutMilliseconds,
95
+ maxBuffer: 262_144,
96
+ windowsHide: true
97
+ };
92
98
  }
93
99
  const environment = { ...process.env };
94
100
  /* Windows environment names ignore case, but a copied process.env keeps
@@ -100,6 +106,7 @@ const execFileRunner = async (executable, helperArguments, timeoutMilliseconds)
100
106
  }
101
107
  environment["PSModulePath"] = path.win32.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "Modules");
102
108
  return {
109
+ cwd: trustedHelperWorkingDirectory(process.platform, environment),
103
110
  timeout: timeoutMilliseconds,
104
111
  maxBuffer: 262_144,
105
112
  windowsHide: true,
@@ -109,17 +116,48 @@ const execFileRunner = async (executable, helperArguments, timeoutMilliseconds)
109
116
  resolve(error === null ? { ok: true, stdout } : { ok: false });
110
117
  });
111
118
  });
119
+ /** Build a direct browser opener invocation without a command shell. */
120
+ export function browserOpenInvocation(url, platform, environment = process.env) {
121
+ if (url.length > 2_048)
122
+ return null;
123
+ try {
124
+ if (new URL(url).protocol !== "https:")
125
+ return null;
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ if (platform === "win32") {
131
+ return {
132
+ executable: windowsSystemTool("rundll32.exe", environment),
133
+ arguments: ["url.dll,FileProtocolHandler", url],
134
+ cwd: trustedHelperWorkingDirectory(platform, environment)
135
+ };
136
+ }
137
+ return platform === "darwin"
138
+ ? {
139
+ executable: "open",
140
+ arguments: [url],
141
+ cwd: trustedHelperWorkingDirectory(platform, environment)
142
+ }
143
+ : {
144
+ executable: "xdg-open",
145
+ arguments: [url],
146
+ cwd: trustedHelperWorkingDirectory(platform, environment)
147
+ };
148
+ }
112
149
  /** Open a URL in the person's browser, best effort and never awaited. */
113
150
  function openBrowserPlatform(url, platform) {
114
- if (!/^https:\/\//u.test(url) || url.length > 2_048)
151
+ const invocation = browserOpenInvocation(url, platform);
152
+ if (invocation === null)
115
153
  return;
116
- const [command, commandArguments] = platform === "win32"
117
- ? ["cmd", ["/c", "start", "", url]]
118
- : platform === "darwin"
119
- ? ["open", [url]]
120
- : ["xdg-open", [url]];
121
154
  try {
122
- const child = spawn(command, commandArguments, { stdio: "ignore", detached: true });
155
+ const child = spawn(invocation.executable, [...invocation.arguments], {
156
+ cwd: invocation.cwd,
157
+ stdio: "ignore",
158
+ detached: true,
159
+ shell: false
160
+ });
123
161
  child.once("error", () => undefined);
124
162
  child.unref();
125
163
  }
@@ -131,13 +169,9 @@ function openBrowserPlatform(url, platform) {
131
169
  export function runtimeDependencies() {
132
170
  return {
133
171
  acquisitionTransport: createFetchTransport(),
134
- probeAntigravity: (options) => probeAntigravity({
135
- ...options,
136
- runCommand: options?.runCommand ?? execFileRunner
137
- }),
138
- resolveExecutablePath: resolveAgyExecutablePath,
139
172
  spawnDetached: (executable, argumentsList, options) => {
140
173
  const child = spawn(executable, [...argumentsList], {
174
+ cwd: options.cwd,
141
175
  detached: true,
142
176
  stdio: "ignore",
143
177
  windowsHide: true
@@ -281,11 +315,13 @@ async function refresh(dependencies, now) {
281
315
  */
282
316
  export function acquisitionSpecs(providers) {
283
317
  return [
284
- claudeSpec({ parse: parseClaudePayload, enabled: providers.claude.poll }),
318
+ claudeSpec({
319
+ parse: parseClaudePayload,
320
+ enabled: providers.claude.recorded && providers.claude.poll
321
+ }),
285
322
  codexSpec(parseCodexPayload),
286
323
  cursorSpec(parseCursorPayload),
287
324
  geminiCliSpec(parseGeminiCliPayload),
288
- antigravitySpec(parseAntigravityCodeAssistPayload),
289
325
  grokSpec(parseGrokPayload),
290
326
  kimiSpec(parseKimiPayload),
291
327
  openrouterSpec(parseOpenrouterPayload)
@@ -330,7 +366,7 @@ function credentialReader(dependencies) {
330
366
  }
331
367
  };
332
368
  }
333
- return await readAcquisitionCredential(provider, {
369
+ const result = await readAcquisitionCredential(provider, {
334
370
  ...(dependencies.stateDirectory === undefined ? {} : { stateDirectory: dependencies.stateDirectory }),
335
371
  platform: dependencies.platform,
336
372
  environment: dependencies.environment,
@@ -342,6 +378,26 @@ function credentialReader(dependencies) {
342
378
  readWindowsCredential: async (target) => await readWindowsCredentialWith(target, { runCommand: runner })
343
379
  })
344
380
  });
381
+ if (provider !== "CODEX" || !result.ok)
382
+ return result;
383
+ const installation = Object.prototype.hasOwnProperty.call(dependencies.detectedAgentInstallations, "codex")
384
+ ? dependencies.detectedAgentInstallations.codex ?? null
385
+ : await detectAgentInstallation("codex", {
386
+ environment: dependencies.environment,
387
+ platform: dependencies.platform,
388
+ runCommand: async (executable, argumentsList, timeoutMilliseconds) => {
389
+ const command = await execFileRunner(executable, argumentsList, timeoutMilliseconds);
390
+ return command.ok
391
+ ? { ok: true, stdout: command.stdout, stderr: "" }
392
+ : command;
393
+ }
394
+ });
395
+ if (installation === null)
396
+ return { ok: false, reason: "absent" };
397
+ return {
398
+ ok: true,
399
+ credential: { ...result.credential, executable: installation.executable }
400
+ };
345
401
  };
346
402
  }
347
403
  /**
@@ -423,6 +479,12 @@ async function acquisitionStatusRow(spec, schedule, readCredential, snapshots, n
423
479
  nextAttemptAt
424
480
  };
425
481
  }
482
+ /** What each status line write failure means, in words with no dashes. */
483
+ const STATUSLINE_FAILURE_SENTENCE = {
484
+ ingest: "the last status line payload could not be read",
485
+ cache_write: "the last status line reading could not be saved to the cache",
486
+ agent_context: "the last status line reading was saved, the agent context file could not be written"
487
+ };
426
488
  /** The acquisition block doctor prints under the connector block. */
427
489
  async function acquisitionDoctorRows(dependencies, snapshots, now) {
428
490
  const providers = await readProvidersConfig(dependencies.stateDirectory);
@@ -437,29 +499,16 @@ async function acquisitionDoctorRows(dependencies, snapshots, now) {
437
499
  rows.push("REFRESH SPAWN FAILED " + failedAt +
438
500
  " the background refresh could not be started on this machine");
439
501
  }
502
+ const statusline = await readStatuslineFailure(dependencies.stateDirectory);
503
+ if (statusline !== null) {
504
+ rows.push(["STATUSLINE WRITE FAILED", statusline.at, statusline.stage, statusline.errorClass,
505
+ STATUSLINE_FAILURE_SENTENCE[statusline.stage]].join(" "));
506
+ }
440
507
  return [ACQUISITION_HEADER, ...rows].join(NEWLINE);
441
508
  }
442
- /**
443
- * Where a person can still see a reading this row could not take.
444
- *
445
- * Antigravity and Gemini CLI meter the same Google Code Assist pool. When
446
- * Google withholds the reading from this client but another tool on the machine
447
- * has already written one, the honest thing is to point at it rather than leave
448
- * a bare refusal, so the person knows the number exists and where.
449
- */
450
- function sharedQuotaNote(row, snapshots) {
451
- if (row.provider !== "ANTIGRAVITY")
452
- return null;
453
- return snapshots.some((snapshot) => snapshot.provider === "GEMINI_CLI")
454
- ? "the shared Code Assist quota is shown under gemini_cli"
455
- : null;
456
- }
457
509
  /** One row of the refresh report, in the space separated grammar doctor uses. */
458
- function acquisitionLine(row, snapshots = []) {
459
- const shared = row.status === "read" ? null : sharedQuotaNote(row, snapshots);
460
- const note = [row.reason ?? row.disclosure ?? "", shared ?? ""]
461
- .filter((part) => part !== "")
462
- .join(", ");
510
+ function acquisitionLine(row, _snapshots = []) {
511
+ const note = row.reason ?? row.disclosure ?? "";
463
512
  return [
464
513
  row.provider.toLowerCase(),
465
514
  row.detected ? "yes" : "no",
@@ -510,13 +559,7 @@ async function refreshCommand(dependencies, now) {
510
559
  now,
511
560
  schedule,
512
561
  readCredential: credentialReader(dependencies),
513
- stamp: acquisitionStamp,
514
- ...(dependencies.probeAntigravity === undefined
515
- ? {}
516
- : { probeAntigravity: dependencies.probeAntigravity }),
517
- ...(dependencies.resolveExecutablePath === undefined
518
- ? {}
519
- : { resolveExecutablePath: dependencies.resolveExecutablePath })
562
+ stamp: acquisitionStamp
520
563
  });
521
564
  /*
522
565
  * Ownership is checked before every write, not once at the start. A round
@@ -702,12 +745,15 @@ async function ingestStandardInput(dependencies, now, host = "claude") {
702
745
  if (host === "codex" || host === "shell")
703
746
  return null;
704
747
  try {
705
- const accountId = await captureStatuslineAccount(host === "antigravity" ? "ANTIGRAVITY" : host === "grok" ? "GROK" : "CLAUDE", {
748
+ const accountIdBeforePayload = host === "antigravity" ? null : await captureStatuslineAccount(host === "grok" ? "GROK" : "CLAUDE", {
706
749
  environment: dependencies.environment, homeDirectory: dependencies.homeDirectory, platform: dependencies.platform, ...(dependencies.stateDirectory ? { stateDirectory: dependencies.stateDirectory } : {}), now
707
750
  });
708
751
  const document = parseJsonText(await dependencies.readStandardInput());
709
752
  if (!document.ok)
710
753
  return null;
754
+ const accountId = host === "antigravity"
755
+ ? antigravityStatuslineAccount(document.value)
756
+ : accountIdBeforePayload;
711
757
  const meters = host === "antigravity"
712
758
  ? parseAntigravityStatuslinePayload(document.value, now)
713
759
  : host === "grok"
@@ -725,22 +771,37 @@ async function ingestStandardInput(dependencies, now, host = "claude") {
725
771
  const incoming = normalizeMeters(withProvenance(meters, provenance).map(meter => ({ ...meter, ...(accountId ? { accountId } : {}) })));
726
772
  if (incoming.length === 0)
727
773
  return { snapshots: null, payload: document.value };
774
+ /* The render still draws when a write fails, so each failure is kept for
775
+ doctor by its stage, and the next full success clears it. */
776
+ let merged;
728
777
  try {
729
- const { merged } = await persistSnapshots(incoming, dependencies.stateDirectory, now);
730
- return { snapshots: merged, payload: document.value };
778
+ merged = (await mergeSnapshotCache(incoming, dependencies.stateDirectory ?? resolveStateDirectory(), Date.parse(now))).merged;
731
779
  }
732
- catch {
780
+ catch (error) {
781
+ await recordStatuslineFailure(dependencies.stateDirectory, now, "cache_write", error);
733
782
  const existing = await cachedSnapshots(dependencies.stateDirectory);
734
783
  return { snapshots: mergeSnapshots(existing, incoming), payload: document.value };
735
784
  }
785
+ try {
786
+ await writeAgentContextSnapshot(merged, dependencies.stateDirectory, now);
787
+ await clearStatuslineFailure(dependencies.stateDirectory);
788
+ }
789
+ catch (error) {
790
+ await recordStatuslineFailure(dependencies.stateDirectory, now, "agent_context", error);
791
+ }
792
+ return { snapshots: merged, payload: document.value };
736
793
  }
737
- catch {
794
+ catch (error) {
795
+ await recordStatuslineFailure(dependencies.stateDirectory, now, "ingest", error);
738
796
  return null;
739
797
  }
740
798
  }
741
799
  async function initCommand(dependencies) {
742
800
  const environment = await environmentWithLocalMarkers(dependencies.environment, dependencies.stateDirectory);
743
801
  try {
802
+ if (!(await ensureClaudePollChoice(dependencies))) {
803
+ return fail(EXIT_FAILURE, "openlimiter init: configuration could not be written, so Claude usage stays off.");
804
+ }
744
805
  const result = await initialize(environment, dependencies.credentialStore, dependencies.promptForSecret, dependencies.stateDirectory);
745
806
  const detected = result.config.connectors
746
807
  .filter((connector) => connector.detected)
@@ -951,14 +1012,16 @@ async function statuslineCommand(dependencies, argumentsList, now) {
951
1012
  }
952
1013
  const advice = buildAdvice(snapshots, now, PROVIDER_CODES);
953
1014
  const config = await readStatuslineConfig(dependencies.stateDirectory);
954
- if (!config.bars)
955
- return succeed(renderClaudeStatusline(advice));
1015
+ if (!config.bars) {
1016
+ return succeed(renderPlainStatusline(advice, snapshots, now, resolveProviderOrder(config.order), config.meters));
1017
+ }
956
1018
  return succeed(renderStatuslineLayout({
957
1019
  advice,
958
1020
  snapshots,
959
1021
  now,
960
1022
  config,
961
1023
  session: parseStatuslineSession(ingested?.payload),
1024
+ apiSpend: await readApiSpend(dependencies.stateDirectory ?? resolveStateDirectory()),
962
1025
  color: statuslineColor(config.color, dependencies.environment, dependencies.colorOutput, host),
963
1026
  unicode: statuslineUnicode(dependencies.environment),
964
1027
  host
@@ -1532,10 +1595,39 @@ async function triggerSyncAfterRefresh(dependencies, now) {
1532
1595
  }
1533
1596
  /* -------------------------------------------------------------- setup */
1534
1597
  const SETUP_SIGN_IN_PROMPT = "Sign in to sync your bars to the hub and your phone (free)";
1598
+ const CLAUDE_POLL_DISCLOSURE = "OpenLimiter can read the same usage Claude shows by sending a direct usage request with your Claude sign in on this computer. Anthropic's policy for tools like this is unresolved.";
1599
+ const CLAUDE_POLL_QUESTION = "Show Fable limit? Enter to turn it on, N to keep it off: ";
1535
1600
  async function promptOrSkip(dependencies, question) {
1601
+ if (!dependencies.interactive)
1602
+ return false;
1536
1603
  const answer = await dependencies.promptChoice?.(question) ?? "";
1537
1604
  return !answer.trim().toLowerCase().startsWith("s");
1538
1605
  }
1606
+ /** Record one real Claude polling choice before any terminal acquisition. */
1607
+ async function ensureClaudePollChoice(dependencies) {
1608
+ const stored = await readConfig(dependencies.stateDirectory);
1609
+ if (!stored.ok && stored.reason !== "missing")
1610
+ return false;
1611
+ const config = stored.ok ? stored.config : defaultConfig(dependencies.environment);
1612
+ if (config.providers.claude.recorded)
1613
+ return true;
1614
+ let enabled = false;
1615
+ if (dependencies.interactive) {
1616
+ dependencies.emit(CLAUDE_POLL_DISCLOSURE);
1617
+ const answer = await dependencies.promptChoice?.(CLAUDE_POLL_QUESTION) ?? "n";
1618
+ enabled = !answer.trim().toLowerCase().startsWith("n");
1619
+ }
1620
+ try {
1621
+ await writeConfig({
1622
+ ...config,
1623
+ providers: { claude: { poll: enabled, recorded: true } }
1624
+ }, dependencies.stateDirectory);
1625
+ return true;
1626
+ }
1627
+ catch {
1628
+ return false;
1629
+ }
1630
+ }
1539
1631
  /** Step one: sign in, or say plainly why this machine did not. */
1540
1632
  async function setupSignInStep(dependencies) {
1541
1633
  const lines = ["1. Sign in", SETUP_SIGN_IN_PROMPT];
@@ -1750,6 +1842,9 @@ async function setupShowBarsStep(dependencies) {
1750
1842
  async function setupCommand(dependencies, now) {
1751
1843
  await setupSignInStep(dependencies);
1752
1844
  await setupConnectStep(dependencies);
1845
+ if (!(await ensureClaudePollChoice(dependencies))) {
1846
+ dependencies.emit("Claude usage stays off because the choice could not be saved.");
1847
+ }
1753
1848
  const collected = await refreshCommand(dependencies, now);
1754
1849
  if (collected.exitCode !== 0 && collected.stderr !== "")
1755
1850
  dependencies.emit(collected.stderr);
@@ -1761,9 +1856,11 @@ async function setupCommand(dependencies, now) {
1761
1856
  export async function runCli(argumentsList, overrides = {}) {
1762
1857
  const setupOutput = [];
1763
1858
  const hasOutputSink = overrides.emit !== undefined;
1859
+ const interactive = overrides.interactive ?? overrides.promptChoice !== undefined;
1764
1860
  const dependencies = {
1765
1861
  ...defaults(),
1766
1862
  ...overrides,
1863
+ interactive,
1767
1864
  ...(hasOutputSink ? {} : { emit: (line) => setupOutput.push(line) })
1768
1865
  };
1769
1866
  const command = argumentsList[0] ?? "setup";