tokelytics 0.3.2 → 0.3.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ prompts or responses.
9
9
 
10
10
  ```
11
11
  npx tokelytics@latest login # opens your browser to approve — no setup, no keys
12
- npx tokelytics@latest watch # watches locally; cloud snapshot up to every 30m
12
+ npx tokelytics@latest watch # local live updates; cloud backup every 30m
13
13
  ```
14
14
 
15
15
  Keep `@latest` in install and onboarding commands. Agents older than 0.3.1
@@ -1781,7 +1781,7 @@ var FirestoreSink = class {
1781
1781
  };
1782
1782
 
1783
1783
  // src/version.ts
1784
- var AGENT_VERSION = "0.3.2";
1784
+ var AGENT_VERSION = "0.3.4";
1785
1785
 
1786
1786
  // src/sync.ts
1787
1787
  async function runSync(connectors, sink, state, device, limitCollectors = [], options = {}) {
@@ -1863,7 +1863,7 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
1863
1863
  lastLocalEventAt: publication.lastLocalEventAt,
1864
1864
  lastCloudWriteAt: publication.lastCloudWriteAt
1865
1865
  };
1866
- if (publication.dashboardDirty && cloudWriteDue && hasBudget) {
1866
+ if (publication.dashboardDirty && cloudWriteDue && hasBudget && !options.suppressCloudPublish) {
1867
1867
  try {
1868
1868
  dashboard.sync = {
1869
1869
  ...dashboard.sync,
@@ -1877,7 +1877,12 @@ async function runSync(connectors, sink, state, device, limitCollectors = [], op
1877
1877
  publication.dashboardDirty = false;
1878
1878
  published = true;
1879
1879
  } catch (error) {
1880
- dashboard.sync.localChangesPending = true;
1880
+ dashboard.sync = {
1881
+ ...dashboard.sync,
1882
+ cloudWritesToday: publication.cloudWritesToday ?? 0,
1883
+ localChangesPending: true,
1884
+ lastCloudWriteAt: publication.lastCloudWriteAt
1885
+ };
1881
1886
  publicationError = error;
1882
1887
  }
1883
1888
  }
@@ -1943,10 +1948,15 @@ async function createRunner() {
1943
1948
  });
1944
1949
  const sink = new FirestoreSink(rest, session.uid);
1945
1950
  return {
1951
+ uid: session.uid,
1946
1952
  connectors,
1947
1953
  watchPaths() {
1948
1954
  return connectors.flatMap((c) => c.watchPaths());
1949
1955
  },
1956
+ async localSnapshot() {
1957
+ const state = await loadState();
1958
+ return state.dashboard ? prepareDashboardSnapshotForCloud(state.dashboard) : void 0;
1959
+ },
1950
1960
  async syncOnce(options = {}) {
1951
1961
  const state = await loadState();
1952
1962
  const device = {
@@ -1968,13 +1978,13 @@ async function createRunner() {
1968
1978
  publication: res.publication
1969
1979
  };
1970
1980
  if (!sameState(state, nextState)) await saveState(nextState);
1971
- if (res.publicationError) throw res.publicationError;
1972
1981
  return {
1973
1982
  processedTurns: res.processedTurns,
1974
1983
  processedLimits: res.processedLimits,
1975
1984
  byProvider: res.byProvider,
1976
1985
  published: res.published,
1977
- cloudWritesToday: res.publication.cloudWritesToday ?? 0
1986
+ cloudWritesToday: res.publication.cloudWritesToday ?? 0,
1987
+ publicationError: res.publicationError
1978
1988
  };
1979
1989
  }
1980
1990
  };
@@ -3690,6 +3700,82 @@ function watch(paths, options = {}) {
3690
3700
  }
3691
3701
  var esm_default = { watch, FSWatcher };
3692
3702
 
3703
+ // src/local-server.ts
3704
+ import { createServer as createServer2 } from "node:http";
3705
+ var LOCAL_DASHBOARD_PORT = 47821;
3706
+ var ALLOWED_ORIGINS = /* @__PURE__ */ new Set([
3707
+ "https://tokelytics.web.app",
3708
+ "https://tokelytics.firebaseapp.com",
3709
+ "http://localhost:3000",
3710
+ "http://127.0.0.1:3000"
3711
+ ]);
3712
+ async function startLocalDashboardServer(getSnapshot, ownerUid, port = LOCAL_DASHBOARD_PORT) {
3713
+ const server = createServer2(async (request, response) => {
3714
+ const origin = request.headers.origin;
3715
+ if (!origin || !ALLOWED_ORIGINS.has(origin)) {
3716
+ response.writeHead(403, { "Content-Type": "application/json" });
3717
+ response.end(JSON.stringify({ error: "Origin is not allowed." }));
3718
+ return;
3719
+ }
3720
+ const corsHeaders = {
3721
+ "Access-Control-Allow-Origin": origin,
3722
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
3723
+ "Access-Control-Allow-Headers": "Content-Type",
3724
+ "Access-Control-Allow-Private-Network": "true",
3725
+ "Cache-Control": "no-store",
3726
+ Vary: "Origin, Access-Control-Request-Private-Network"
3727
+ };
3728
+ if (request.method === "OPTIONS") {
3729
+ response.writeHead(204, corsHeaders);
3730
+ response.end();
3731
+ return;
3732
+ }
3733
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
3734
+ if (request.method !== "GET" || url.pathname !== "/v1/snapshot" || url.searchParams.get("uid") !== ownerUid) {
3735
+ response.writeHead(404, { ...corsHeaders, "Content-Type": "application/json" });
3736
+ response.end(JSON.stringify({ error: "Not found." }));
3737
+ return;
3738
+ }
3739
+ try {
3740
+ const snapshot2 = await getSnapshot();
3741
+ response.writeHead(200, { ...corsHeaders, "Content-Type": "application/json" });
3742
+ response.end(
3743
+ JSON.stringify({
3744
+ service: "tokelytics",
3745
+ version: 1,
3746
+ servedAt: (/* @__PURE__ */ new Date()).toISOString(),
3747
+ snapshot: snapshot2 ?? null
3748
+ })
3749
+ );
3750
+ } catch {
3751
+ response.writeHead(503, { ...corsHeaders, "Content-Type": "application/json" });
3752
+ response.end(JSON.stringify({ error: "Local snapshot is temporarily unavailable." }));
3753
+ }
3754
+ });
3755
+ await listen(server, port);
3756
+ const address = server.address();
3757
+ const resolvedPort = typeof address === "object" && address ? address.port : port;
3758
+ return {
3759
+ port: resolvedPort,
3760
+ close: () => close(server)
3761
+ };
3762
+ }
3763
+ function listen(server, port) {
3764
+ return new Promise((resolve4, reject) => {
3765
+ const onError = (error) => reject(error);
3766
+ server.once("error", onError);
3767
+ server.listen(port, "127.0.0.1", () => {
3768
+ server.off("error", onError);
3769
+ resolve4();
3770
+ });
3771
+ });
3772
+ }
3773
+ function close(server) {
3774
+ return new Promise((resolve4, reject) => {
3775
+ server.close((error) => error ? reject(error) : resolve4());
3776
+ });
3777
+ }
3778
+
3693
3779
  // src/watch.ts
3694
3780
  var DEBOUNCE_MS = 1200;
3695
3781
  var LIMIT_REFRESH_MS = 6e4;
@@ -3700,6 +3786,13 @@ var MAX_CLOUD_WRITES_PER_DAY = 16;
3700
3786
  var QUOTA_RETRY_MS = 15 * 6e4;
3701
3787
  var QUOTA_RETRY_JITTER_MS = 5 * 6e4;
3702
3788
  async function watch2(runner) {
3789
+ let localServer;
3790
+ try {
3791
+ localServer = await startLocalDashboardServer(() => runner.localSnapshot(), runner.uid);
3792
+ console.log(`Local live dashboard ready on 127.0.0.1:${localServer.port}.`);
3793
+ } catch (error) {
3794
+ console.warn(`Local live dashboard unavailable: ${error.message}`);
3795
+ }
3703
3796
  const startupRetryAfter = await safeSync(runner, {
3704
3797
  reason: "startup",
3705
3798
  refreshLimits: true,
@@ -3707,7 +3800,7 @@ async function watch2(runner) {
3707
3800
  deviceHeartbeatMs: DEVICE_HEARTBEAT_MS,
3708
3801
  cloudSyncIntervalMs: CLOUD_SYNC_INTERVAL_MS,
3709
3802
  maxCloudWritesPerDay: MAX_CLOUD_WRITES_PER_DAY
3710
- });
3803
+ }, false);
3711
3804
  const paths = runner.watchPaths();
3712
3805
  const watcher = esm_default.watch(paths, {
3713
3806
  ignoreInitial: true,
@@ -3720,11 +3813,11 @@ async function watch2(runner) {
3720
3813
  let pending;
3721
3814
  let running = false;
3722
3815
  let scheduledAt = 0;
3723
- let blockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
3816
+ let cloudBlockedUntil = startupRetryAfter ? Date.now() + startupRetryAfter : 0;
3724
3817
  const schedule = (request, delay = DEBOUNCE_MS) => {
3725
3818
  pending = mergeRequests(pending, request);
3726
3819
  if (running) return;
3727
- const target = Math.max(Date.now() + delay, blockedUntil);
3820
+ const target = Date.now() + delay;
3728
3821
  if (timer && scheduledAt <= target) return;
3729
3822
  if (timer) clearTimeout(timer);
3730
3823
  scheduledAt = target;
@@ -3735,13 +3828,12 @@ async function watch2(runner) {
3735
3828
  pending = void 0;
3736
3829
  if (!next) return;
3737
3830
  running = true;
3738
- const retryAfterMs = await safeSync(runner, next);
3831
+ const retryAfterMs = await safeSync(runner, next, Date.now() < cloudBlockedUntil);
3739
3832
  running = false;
3740
3833
  if (retryAfterMs) {
3741
- blockedUntil = Date.now() + retryAfterMs;
3742
- pending = mergeRequests(pending, next);
3743
- } else {
3744
- blockedUntil = 0;
3834
+ cloudBlockedUntil = Date.now() + retryAfterMs;
3835
+ } else if (Date.now() >= cloudBlockedUntil) {
3836
+ cloudBlockedUntil = 0;
3745
3837
  }
3746
3838
  if (pending) {
3747
3839
  const queued = pending;
@@ -3776,43 +3868,53 @@ async function watch2(runner) {
3776
3868
  ${paths.join("\n ")}
3777
3869
  Press Ctrl+C to stop.`);
3778
3870
  await new Promise((resolve4) => {
3779
- process.on("SIGINT", () => {
3871
+ const stop = () => {
3780
3872
  clearInterval(refreshTimer);
3781
3873
  clearInterval(fallbackTimer);
3782
3874
  if (timer) clearTimeout(timer);
3783
- void watcher.close().then(resolve4);
3784
- });
3875
+ void Promise.all([watcher.close(), localServer?.close()]).then(() => resolve4());
3876
+ };
3877
+ process.once("SIGINT", stop);
3878
+ process.once("SIGTERM", stop);
3785
3879
  });
3786
3880
  }
3787
- async function safeSync(runner, request) {
3881
+ async function safeSync(runner, request, suppressCloudPublish) {
3788
3882
  try {
3789
3883
  const { reason, ...options } = request;
3790
- const { processedTurns, byProvider, published, cloudWritesToday } = await runner.syncOnce(options);
3884
+ const { processedTurns, byProvider, published, cloudWritesToday, publicationError } = await runner.syncOnce({
3885
+ ...options,
3886
+ suppressCloudPublish
3887
+ });
3791
3888
  if (processedTurns > 0) {
3792
3889
  const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ");
3793
3890
  console.log(
3794
- `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail}); ` + (published ? `cloud snapshot published (${cloudWritesToday}/16 today)` : "cloud snapshot queued")
3891
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] processed ${processedTurns} turn(s) locally (${detail}); ` + (published ? `cloud snapshot published (${cloudWritesToday}/${MAX_CLOUD_WRITES_PER_DAY} today)` : "cloud backup queued")
3795
3892
  );
3796
3893
  } else if (reason === "startup") {
3797
3894
  console.log(
3798
- `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready; ` + (published ? `cloud snapshot published (${cloudWritesToday}/16 today)` : "cloud snapshot queued")
3895
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] local snapshot ready; ` + (published ? `cloud snapshot published (${cloudWritesToday}/${MAX_CLOUD_WRITES_PER_DAY} today)` : "cloud backup queued")
3799
3896
  );
3800
3897
  }
3898
+ if (publicationError) return cloudRetryDelay(publicationError);
3801
3899
  } catch (err) {
3802
- if (err instanceof FirestoreRestError && err.status === 429) {
3803
- const retryAfter = Math.max(
3804
- err.retryAfterMs ?? 0,
3805
- QUOTA_RETRY_MS + Math.floor(Math.random() * QUOTA_RETRY_JITTER_MS)
3806
- );
3807
- console.error(
3808
- `Cloud sync paused: Firestore quota is exhausted; the local snapshot is safe. Retrying in ${Math.ceil(retryAfter / 6e4)}m.`
3809
- );
3810
- return retryAfter;
3811
- }
3812
- console.error(`Sync error: ${err.message}`);
3900
+ console.error(`Local sync error: ${err.message}`);
3813
3901
  return 15e3;
3814
3902
  }
3815
3903
  }
3904
+ function cloudRetryDelay(error) {
3905
+ if (error instanceof FirestoreRestError && error.status === 429) {
3906
+ const retryAfter = Math.max(
3907
+ error.retryAfterMs ?? 0,
3908
+ QUOTA_RETRY_MS + Math.floor(Math.random() * QUOTA_RETRY_JITTER_MS)
3909
+ );
3910
+ console.error(
3911
+ `Cloud backup paused: Firestore quota is exhausted; local live updates remain active. Retrying in ${Math.ceil(retryAfter / 6e4)}m.`
3912
+ );
3913
+ return retryAfter;
3914
+ }
3915
+ console.error(`Cloud backup error: ${error.message}. Local live updates remain active; retrying shortly.`);
3916
+ return 6e4;
3917
+ }
3816
3918
  function mergeRequests(current, incoming) {
3817
3919
  if (!current) return incoming;
3818
3920
  return {
@@ -3902,7 +4004,8 @@ async function main(argv) {
3902
4004
  const lock = await acquireWatchLock();
3903
4005
  try {
3904
4006
  const runner = await createRunner();
3905
- const { processedTurns, processedLimits, byProvider } = await runner.syncOnce();
4007
+ const { processedTurns, processedLimits, byProvider, publicationError } = await runner.syncOnce();
4008
+ if (publicationError) throw publicationError;
3906
4009
  const detail = Object.entries(byProvider).map(([p, n]) => `${p}:${n}`).join(" ") || "none";
3907
4010
  console.log(`Processed ${processedTurns} turn(s) (${detail}); refreshed ${processedLimits} usage provider(s).`);
3908
4011
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokelytics",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Tokelytics sync agent — streams local AI CLI usage logs (Claude Code, Codex) to your Tokelytics dashboard.",
5
5
  "license": "MIT",
6
6
  "type": "module",