fleetlens 0.2.7 → 0.2.9

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 (23) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +3 -3
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/chunks/ssr/_0xies90._.js +1 -1
  12. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  13. package/app/apps/web/.next/server/pages/500.html +1 -1
  14. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  15. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  16. package/app/apps/web/package.json +1 -1
  17. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  18. package/dist/daemon-worker.js +74 -21
  19. package/dist/index.js +83 -20
  20. package/package.json +1 -1
  21. /package/app/apps/web/.next/static/{Suh-utGyqGIs9TqCHz3q5 → 2rEsiJssG30QsuNT0Lp9S}/_buildManifest.js +0 -0
  22. /package/app/apps/web/.next/static/{Suh-utGyqGIs9TqCHz3q5 → 2rEsiJssG30QsuNT0Lp9S}/_clientMiddlewareManifest.js +0 -0
  23. /package/app/apps/web/.next/static/{Suh-utGyqGIs9TqCHz3q5 → 2rEsiJssG30QsuNT0Lp9S}/_ssgManifest.js +0 -0
@@ -8,7 +8,7 @@ import { execFileSync } from "node:child_process";
8
8
  import { readFileSync } from "node:fs";
9
9
  import { homedir, platform } from "node:os";
10
10
  import { join } from "node:path";
11
- function readOAuthToken() {
11
+ function readOAuthCredentials() {
12
12
  if (platform() === "darwin") {
13
13
  return readFromMacKeychain() ?? readFromCredentialsFile();
14
14
  }
@@ -21,7 +21,7 @@ function readFromMacKeychain() {
21
21
  ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
22
22
  { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }
23
23
  );
24
- return extractToken(blob);
24
+ return extractCredentials(blob);
25
25
  } catch {
26
26
  return null;
27
27
  }
@@ -34,23 +34,32 @@ function readFromCredentialsFile() {
34
34
  for (const path of candidates) {
35
35
  try {
36
36
  const blob = readFileSync(path, "utf8");
37
- const token = extractToken(blob);
38
- if (token) return token;
37
+ const creds = extractCredentials(blob);
38
+ if (creds) return creds;
39
39
  } catch {
40
40
  }
41
41
  }
42
42
  return null;
43
43
  }
44
- function extractToken(blob) {
44
+ function isUsable(creds, now, skewMs = 6e4) {
45
+ return creds.expiresAt - skewMs > now;
46
+ }
47
+ function extractCredentials(blob) {
45
48
  try {
46
49
  const parsed = JSON.parse(blob);
47
- return parsed.claudeAiOauth?.accessToken ?? null;
50
+ const oauth = parsed.claudeAiOauth;
51
+ if (!oauth) return null;
52
+ const accessToken = typeof oauth.accessToken === "string" ? oauth.accessToken : null;
53
+ const expiresAt = typeof oauth.expiresAt === "number" ? oauth.expiresAt : null;
54
+ if (!accessToken || expiresAt === null) return null;
55
+ return { accessToken, expiresAt };
48
56
  } catch {
49
57
  return null;
50
58
  }
51
59
  }
52
60
 
53
61
  // src/usage/api.ts
62
+ var EXPIRY_SKEW_MS = 60 * 1e3;
54
63
  var USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage";
55
64
  var BETA_HEADER = "oauth-2025-04-20";
56
65
  var UsageApiError = class extends Error {
@@ -60,18 +69,24 @@ var UsageApiError = class extends Error {
60
69
  }
61
70
  };
62
71
  async function fetchUsage() {
63
- const token = readOAuthToken();
64
- if (!token) {
72
+ const creds = readOAuthCredentials();
73
+ if (!creds) {
65
74
  throw new UsageApiError(
66
75
  "No Claude Code OAuth token found. Run `claude` to log in first.",
67
76
  "no_token"
68
77
  );
69
78
  }
79
+ if (creds.expiresAt - EXPIRY_SKEW_MS <= Date.now()) {
80
+ throw new UsageApiError(
81
+ "Claude Code OAuth token expired. Open Claude Code to refresh it.",
82
+ "expired"
83
+ );
84
+ }
70
85
  let res;
71
86
  try {
72
87
  res = await fetch(USAGE_ENDPOINT, {
73
88
  headers: {
74
- Authorization: `Bearer ${token}`,
89
+ Authorization: `Bearer ${creds.accessToken}`,
75
90
  "anthropic-beta": BETA_HEADER
76
91
  }
77
92
  });
@@ -132,12 +147,20 @@ function appendSnapshot(filePath, snapshot) {
132
147
  appendFileSync(filePath, JSON.stringify(snapshot) + "\n", "utf8");
133
148
  }
134
149
 
150
+ // src/usage/backoff.ts
151
+ var BASE_INTERVAL_MS = 5 * 60 * 1e3;
152
+ var MAX_INTERVAL_MS = 60 * 60 * 1e3;
153
+ function nextIntervalMs(current, outcome) {
154
+ if (outcome === "success") return BASE_INTERVAL_MS;
155
+ if (outcome === "network") return current;
156
+ return Math.min(Math.max(current, BASE_INTERVAL_MS) * 2, MAX_INTERVAL_MS);
157
+ }
158
+
135
159
  // src/daemon-worker.ts
136
160
  var STATE_DIR = join2(homedir2(), ".cclens");
137
161
  var USAGE_LOG = join2(STATE_DIR, "usage.jsonl");
138
162
  var DAEMON_LOG = join2(STATE_DIR, "daemon.log");
139
- var POLL_INTERVAL_MS = 5 * 60 * 1e3;
140
- var WATCHDOG_INTERVAL_MS = 30 * 1e3;
163
+ var WATCHDOG_INTERVAL_MS = 5 * 1e3;
141
164
  mkdirSync2(dirname2(USAGE_LOG), { recursive: true });
142
165
  function log(level, message) {
143
166
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level.toUpperCase()} ${message}
@@ -147,9 +170,10 @@ function log(level, message) {
147
170
  } catch {
148
171
  }
149
172
  }
150
- var lastPollAtMs = 0;
173
+ var nextPollAtMs = 0;
174
+ var currentIntervalMs = BASE_INTERVAL_MS;
175
+ var waitingForRefresh = false;
151
176
  async function tick() {
152
- lastPollAtMs = Date.now();
153
177
  try {
154
178
  const snapshot = await fetchUsage();
155
179
  appendSnapshot(USAGE_LOG, snapshot);
@@ -157,36 +181,65 @@ async function tick() {
157
181
  "info",
158
182
  `snapshot 5h=${snapshot.five_hour.utilization}% 7d=${snapshot.seven_day.utilization}%`
159
183
  );
184
+ return "success";
160
185
  } catch (err) {
161
186
  if (err instanceof UsageApiError) {
162
187
  log("warn", `poll failed (${err.code}): ${err.message}`);
163
- } else {
164
- log("error", `unexpected error: ${err.stack ?? err}`);
188
+ return err.code === "network" ? "network" : "auth";
165
189
  }
190
+ log("error", `unexpected error: ${err.stack ?? err}`);
191
+ return "auth";
166
192
  }
167
193
  }
194
+ function scheduleAfter(now, outcome) {
195
+ const prev = currentIntervalMs;
196
+ currentIntervalMs = nextIntervalMs(currentIntervalMs, outcome);
197
+ if (currentIntervalMs !== prev) {
198
+ log(
199
+ "info",
200
+ `poll interval ${outcome === "success" ? "reset" : "backoff"} to ${currentIntervalMs / 1e3}s`
201
+ );
202
+ }
203
+ nextPollAtMs = now + currentIntervalMs;
204
+ }
168
205
  function sleep(ms) {
169
206
  return new Promise((resolve) => setTimeout(resolve, ms));
170
207
  }
171
208
  async function runLoop() {
172
209
  while (true) {
173
210
  const now = Date.now();
174
- const elapsed = now - lastPollAtMs;
175
- if (elapsed >= POLL_INTERVAL_MS) {
176
- if (lastPollAtMs > 0 && elapsed > POLL_INTERVAL_MS * 1.5) {
211
+ if (now >= nextPollAtMs) {
212
+ if (nextPollAtMs > 0 && now - nextPollAtMs > currentIntervalMs) {
177
213
  log(
178
214
  "info",
179
- `wake-from-sleep catch-up: ${Math.round(elapsed / 1e3)}s since last poll (expected ${POLL_INTERVAL_MS / 1e3}s)`
215
+ `wake-from-sleep catch-up: ${Math.round((now - nextPollAtMs + currentIntervalMs) / 1e3)}s since last poll`
180
216
  );
181
217
  }
182
- await tick();
218
+ const creds = readOAuthCredentials();
219
+ if (!creds) {
220
+ log("warn", "no Claude Code OAuth token found; waiting");
221
+ waitingForRefresh = true;
222
+ nextPollAtMs = now + BASE_INTERVAL_MS;
223
+ } else if (!isUsable(creds, now)) {
224
+ if (!waitingForRefresh) {
225
+ log("info", "token expired; waiting for Claude Code to refresh it");
226
+ waitingForRefresh = true;
227
+ }
228
+ } else {
229
+ if (waitingForRefresh) {
230
+ log("info", "token refreshed; resuming polls");
231
+ waitingForRefresh = false;
232
+ }
233
+ const outcome = await tick();
234
+ scheduleAfter(Date.now(), outcome);
235
+ }
183
236
  }
184
237
  await sleep(WATCHDOG_INTERVAL_MS);
185
238
  }
186
239
  }
187
240
  log(
188
241
  "info",
189
- `daemon started (pid=${process.pid}, interval=${POLL_INTERVAL_MS / 1e3}s, watchdog=${WATCHDOG_INTERVAL_MS / 1e3}s)`
242
+ `daemon started (pid=${process.pid}, interval=${BASE_INTERVAL_MS / 1e3}s, watchdog=${WATCHDOG_INTERVAL_MS / 1e3}s)`
190
243
  );
191
244
  void runLoop();
192
245
  process.on("SIGTERM", () => {
package/dist/index.js CHANGED
@@ -454,9 +454,9 @@ Installed: ${PACKAGE_NAME}@${verify.installedVersion} at ${verify.installedPath
454
454
  console.warn(
455
455
  ` \u2022 reinstall in the correct Node env: 'npm install -g ${PACKAGE_NAME}@latest'`
456
456
  );
457
- } else if (verify.installedVersion !== "0.2.7") {
457
+ } else if (verify.installedVersion !== "0.2.9") {
458
458
  console.log(
459
- ` \u2192 This process is still running ${"0.2.7"}. Next invocation will use ${verify.installedVersion}.`
459
+ ` \u2192 This process is still running ${"0.2.9"}. Next invocation will use ${verify.installedVersion}.`
460
460
  );
461
461
  }
462
462
  }
@@ -492,7 +492,7 @@ async function checkForUpdate() {
492
492
  if (process.env.__FLEETLENS_UPDATED === "1" || process.env.__CCLENS_UPDATED === "1") return;
493
493
  const latest = await fetchLatestVersion();
494
494
  if (latest === null) return;
495
- const current = "0.2.7";
495
+ const current = "0.2.9";
496
496
  if (!shouldUpdate(current, latest)) return;
497
497
  console.log(`Updating ${PACKAGE_NAME} ${current} \u2192 ${latest}...`);
498
498
  await stopRunningServices();
@@ -510,10 +510,17 @@ async function checkForUpdate() {
510
510
  }
511
511
  }
512
512
  async function stopRunningServices() {
513
+ const state = {
514
+ serverWasRunning: false,
515
+ serverPort: null,
516
+ daemonWasRunning: false
517
+ };
513
518
  try {
514
519
  const { getServerStatus: getServerStatus2, stopServer: stopServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
515
520
  const status2 = getServerStatus2();
516
521
  if (status2.running) {
522
+ state.serverWasRunning = true;
523
+ state.serverPort = status2.port;
517
524
  stopServer2();
518
525
  console.log(` \u2713 Stopped old server (PID ${status2.pid})`);
519
526
  }
@@ -523,33 +530,77 @@ async function stopRunningServices() {
523
530
  const { stopDaemonSilent: stopDaemonSilent2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
524
531
  const result = stopDaemonSilent2();
525
532
  if (result.stopped) {
533
+ state.daemonWasRunning = true;
526
534
  console.log(` \u2713 Stopped old daemon (PID ${result.pid})`);
527
535
  }
528
536
  } catch {
529
537
  }
538
+ return state;
539
+ }
540
+ async function restartServices(state) {
541
+ if (!state.serverWasRunning && !state.daemonWasRunning) return;
542
+ if (state.serverWasRunning) {
543
+ try {
544
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
545
+ const result = await startServer2({ port: state.serverPort ?? void 0 });
546
+ console.log(
547
+ ` \u2713 Restarted server on http://localhost:${result.port} (PID ${result.pid})`
548
+ );
549
+ } catch (err) {
550
+ console.warn(
551
+ ` ! Could not restart server: ${err.message}
552
+ Run 'fleetlens start' manually.`
553
+ );
554
+ }
555
+ }
556
+ if (state.daemonWasRunning) {
557
+ try {
558
+ const { startDaemonSilent: startDaemonSilent2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
559
+ const result = startDaemonSilent2();
560
+ if (result.started) {
561
+ console.log(` \u2713 Restarted daemon (PID ${result.pid})`);
562
+ } else if (result.alreadyRunning) {
563
+ console.log(` \u2713 Daemon already running (PID ${result.pid})`);
564
+ } else {
565
+ console.warn(` ! Could not restart daemon: ${result.error}`);
566
+ }
567
+ } catch (err) {
568
+ console.warn(
569
+ ` ! Could not restart daemon: ${err.message}
570
+ Run 'fleetlens daemon start' manually.`
571
+ );
572
+ }
573
+ }
530
574
  }
531
575
  async function forceUpdate() {
532
576
  const latest = await fetchLatestVersion();
533
- const current = "0.2.7";
577
+ const current = "0.2.9";
534
578
  if (latest === null) {
535
579
  console.error("Could not reach npm registry. Check your network.");
536
580
  process.exit(1);
537
581
  }
538
- if (shouldUpdate(current, latest)) {
582
+ const isRealUpgrade = shouldUpdate(current, latest);
583
+ let priorState = null;
584
+ if (isRealUpgrade) {
539
585
  console.log(`Updating ${PACKAGE_NAME} ${current} \u2192 ${latest}...`);
540
- await stopRunningServices();
586
+ priorState = await stopRunningServices();
541
587
  } else {
542
588
  console.log(`Already on latest (${current}). Reinstalling...`);
543
589
  }
544
590
  const ok = runNpmInstall();
545
- if (ok) {
546
- reportInstallOutcome(latest);
547
- } else {
591
+ if (!ok) {
548
592
  console.error("Update failed.");
549
593
  console.error(` \u2192 Try manually: npm install -g ${PACKAGE_NAME}@latest`);
550
594
  console.error(` \u2192 Or with sudo if your global npm prefix needs it.`);
551
595
  process.exit(1);
552
596
  }
597
+ reportInstallOutcome(latest);
598
+ if (isRealUpgrade && priorState) {
599
+ if (priorState.serverWasRunning || priorState.daemonWasRunning) {
600
+ console.log("");
601
+ await restartServices(priorState);
602
+ }
603
+ }
553
604
  }
554
605
  var PACKAGE_NAME, CHECK_TIMEOUT_MS;
555
606
  var init_updater = __esm({
@@ -1462,7 +1513,7 @@ import { execFileSync } from "node:child_process";
1462
1513
  import { readFileSync as readFileSync5 } from "node:fs";
1463
1514
  import { homedir as homedir3, platform } from "node:os";
1464
1515
  import { join as join4 } from "node:path";
1465
- function readOAuthToken() {
1516
+ function readOAuthCredentials() {
1466
1517
  if (platform() === "darwin") {
1467
1518
  return readFromMacKeychain() ?? readFromCredentialsFile();
1468
1519
  }
@@ -1475,7 +1526,7 @@ function readFromMacKeychain() {
1475
1526
  ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
1476
1527
  { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }
1477
1528
  );
1478
- return extractToken(blob);
1529
+ return extractCredentials(blob);
1479
1530
  } catch {
1480
1531
  return null;
1481
1532
  }
@@ -1488,17 +1539,22 @@ function readFromCredentialsFile() {
1488
1539
  for (const path2 of candidates) {
1489
1540
  try {
1490
1541
  const blob = readFileSync5(path2, "utf8");
1491
- const token = extractToken(blob);
1492
- if (token) return token;
1542
+ const creds = extractCredentials(blob);
1543
+ if (creds) return creds;
1493
1544
  } catch {
1494
1545
  }
1495
1546
  }
1496
1547
  return null;
1497
1548
  }
1498
- function extractToken(blob) {
1549
+ function extractCredentials(blob) {
1499
1550
  try {
1500
1551
  const parsed = JSON.parse(blob);
1501
- return parsed.claudeAiOauth?.accessToken ?? null;
1552
+ const oauth = parsed.claudeAiOauth;
1553
+ if (!oauth) return null;
1554
+ const accessToken = typeof oauth.accessToken === "string" ? oauth.accessToken : null;
1555
+ const expiresAt = typeof oauth.expiresAt === "number" ? oauth.expiresAt : null;
1556
+ if (!accessToken || expiresAt === null) return null;
1557
+ return { accessToken, expiresAt };
1502
1558
  } catch {
1503
1559
  return null;
1504
1560
  }
@@ -1511,18 +1567,24 @@ var init_token = __esm({
1511
1567
 
1512
1568
  // src/usage/api.ts
1513
1569
  async function fetchUsage() {
1514
- const token = readOAuthToken();
1515
- if (!token) {
1570
+ const creds = readOAuthCredentials();
1571
+ if (!creds) {
1516
1572
  throw new UsageApiError(
1517
1573
  "No Claude Code OAuth token found. Run `claude` to log in first.",
1518
1574
  "no_token"
1519
1575
  );
1520
1576
  }
1577
+ if (creds.expiresAt - EXPIRY_SKEW_MS <= Date.now()) {
1578
+ throw new UsageApiError(
1579
+ "Claude Code OAuth token expired. Open Claude Code to refresh it.",
1580
+ "expired"
1581
+ );
1582
+ }
1521
1583
  let res;
1522
1584
  try {
1523
1585
  res = await fetch(USAGE_ENDPOINT, {
1524
1586
  headers: {
1525
- Authorization: `Bearer ${token}`,
1587
+ Authorization: `Bearer ${creds.accessToken}`,
1526
1588
  "anthropic-beta": BETA_HEADER
1527
1589
  }
1528
1590
  });
@@ -1574,11 +1636,12 @@ function normalizeExtra(raw) {
1574
1636
  utilization: typeof r.utilization === "number" ? r.utilization : null
1575
1637
  };
1576
1638
  }
1577
- var USAGE_ENDPOINT, BETA_HEADER, UsageApiError;
1639
+ var EXPIRY_SKEW_MS, USAGE_ENDPOINT, BETA_HEADER, UsageApiError;
1578
1640
  var init_api = __esm({
1579
1641
  "src/usage/api.ts"() {
1580
1642
  "use strict";
1581
1643
  init_token();
1644
+ EXPIRY_SKEW_MS = 60 * 1e3;
1582
1645
  USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage";
1583
1646
  BETA_HEADER = "oauth-2025-04-20";
1584
1647
  UsageApiError = class extends Error {
@@ -1761,7 +1824,7 @@ async function main() {
1761
1824
  case "version":
1762
1825
  case "--version":
1763
1826
  case "-v":
1764
- console.log(`fleetlens ${"0.2.7"}`);
1827
+ console.log(`fleetlens ${"0.2.9"}`);
1765
1828
  break;
1766
1829
  case "help":
1767
1830
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetlens",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "fleetlens — local-only dashboard and usage tracker for Claude Code sessions and agent fleets",
5
5
  "license": "MIT",
6
6
  "type": "module",