claude-limitline 1.5.1 → 1.5.3

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/index.js +232 -23
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -35,7 +35,11 @@ var DEFAULT_CONFIG = {
35
35
  enabled: true,
36
36
  displayStyle: "text",
37
37
  barWidth: 10,
38
- showTimeRemaining: true
38
+ showTimeRemaining: true,
39
+ timeDisplay: "remaining",
40
+ timeFormat: "12h",
41
+ showSparkline: false,
42
+ sparklineWidth: 8
39
43
  },
40
44
  weekly: {
41
45
  enabled: true,
@@ -161,32 +165,86 @@ async function getOAuthTokenWindows() {
161
165
  }
162
166
  return null;
163
167
  }
164
- async function getOAuthTokenMacOS() {
168
+ async function findKeychainServiceName() {
165
169
  try {
166
170
  const { stdout } = await execAsync(
167
- `security find-generic-password -s "Claude Code-credentials" -w`,
171
+ `security dump-keychain 2>/dev/null | grep -o '"Claude Code-credentials[^"]*"'`,
168
172
  { timeout: 5e3 }
169
173
  );
170
- const content = stdout.trim();
171
- if (content.startsWith("{")) {
172
- try {
173
- const parsed = JSON.parse(content);
174
- if (parsed.claudeAiOauth && typeof parsed.claudeAiOauth === "object") {
175
- const token = parsed.claudeAiOauth.accessToken;
174
+ const matches = stdout.trim().split("\n").map((s) => s.replace(/^"|"$/g, "")).filter(Boolean).sort((a, b) => b.length - a.length);
175
+ if (matches.length > 0) {
176
+ debug(`Found keychain service: ${matches[0]}`);
177
+ return matches[0];
178
+ }
179
+ } catch (error) {
180
+ debug("Keychain service name lookup failed:", error);
181
+ }
182
+ return "Claude Code-credentials";
183
+ }
184
+ function extractTokenFromKeychainContent(content) {
185
+ if (content.startsWith("{")) {
186
+ try {
187
+ const parsed = JSON.parse(content);
188
+ if (parsed.claudeAiOauth && typeof parsed.claudeAiOauth === "object") {
189
+ const token = parsed.claudeAiOauth.accessToken;
190
+ if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
191
+ return token;
192
+ }
193
+ }
194
+ } catch (parseError) {
195
+ debug("Failed to parse keychain JSON:", parseError);
196
+ }
197
+ }
198
+ if (content.startsWith("sk-ant-oat")) {
199
+ return content;
200
+ }
201
+ return null;
202
+ }
203
+ async function getOAuthTokenMacOS() {
204
+ const serviceName = await findKeychainServiceName();
205
+ for (const name of [serviceName, "Claude Code-credentials"]) {
206
+ try {
207
+ const { stdout } = await execAsync(
208
+ `security find-generic-password -s "${name}" -w`,
209
+ { timeout: 5e3 }
210
+ );
211
+ const token = extractTokenFromKeychainContent(stdout.trim());
212
+ if (token) {
213
+ debug(`Found OAuth token in macOS Keychain (${name})`);
214
+ return token;
215
+ }
216
+ } catch (error) {
217
+ debug(`macOS Keychain retrieval failed for "${name}":`, error);
218
+ }
219
+ }
220
+ const configPaths = [
221
+ path2.join(os2.homedir(), ".claude", ".credentials.json"),
222
+ path2.join(os2.homedir(), ".claude", "credentials.json"),
223
+ path2.join(os2.homedir(), ".config", "claude-code", "credentials.json")
224
+ ];
225
+ for (const configPath of configPaths) {
226
+ try {
227
+ if (fs2.existsSync(configPath)) {
228
+ const content = fs2.readFileSync(configPath, "utf-8");
229
+ const config = JSON.parse(content);
230
+ if (config.claudeAiOauth && typeof config.claudeAiOauth === "object") {
231
+ const token = config.claudeAiOauth.accessToken;
176
232
  if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
177
- debug("Found OAuth token in macOS Keychain under claudeAiOauth.accessToken");
233
+ debug(`Found OAuth token in ${configPath} under claudeAiOauth.accessToken`);
234
+ return token;
235
+ }
236
+ }
237
+ for (const key of ["oauth_token", "token", "accessToken"]) {
238
+ const token = config[key];
239
+ if (token && typeof token === "string" && token.startsWith("sk-ant-oat")) {
240
+ debug(`Found OAuth token in ${configPath} under key ${key}`);
178
241
  return token;
179
242
  }
180
243
  }
181
- } catch (parseError) {
182
- debug("Failed to parse keychain JSON:", parseError);
183
244
  }
245
+ } catch (error) {
246
+ debug(`Failed to read config from ${configPath}:`, error);
184
247
  }
185
- if (content.startsWith("sk-ant-oat")) {
186
- return content;
187
- }
188
- } catch (error) {
189
- debug("macOS Keychain retrieval failed:", error);
190
248
  }
191
249
  return null;
192
250
  }
@@ -286,9 +344,57 @@ async function fetchUsageFromAPI(token) {
286
344
  return null;
287
345
  }
288
346
  }
289
- var cachedUsage = null;
290
- var previousUsage = null;
291
- var cacheTimestamp = 0;
347
+ var USAGE_CACHE_FILE = "limitline-usage-cache.json";
348
+ function getCachePath() {
349
+ return path2.join(os2.homedir(), ".claude", USAGE_CACHE_FILE);
350
+ }
351
+ function loadDiskCache() {
352
+ try {
353
+ const cachePath = getCachePath();
354
+ if (fs2.existsSync(cachePath)) {
355
+ const content = fs2.readFileSync(cachePath, "utf-8");
356
+ const data = JSON.parse(content);
357
+ const rehydrate = (usage) => {
358
+ if (!usage) return null;
359
+ const fix = (ud) => {
360
+ if (!ud) return null;
361
+ return { ...ud, resetAt: new Date(ud.resetAt) };
362
+ };
363
+ return {
364
+ ...usage,
365
+ fiveHour: fix(usage.fiveHour),
366
+ sevenDay: fix(usage.sevenDay),
367
+ sevenDayOpus: fix(usage.sevenDayOpus),
368
+ sevenDaySonnet: fix(usage.sevenDaySonnet)
369
+ };
370
+ };
371
+ return {
372
+ timestamp: data.timestamp,
373
+ usage: rehydrate(data.usage),
374
+ previousUsage: rehydrate(data.previousUsage)
375
+ };
376
+ }
377
+ } catch (error) {
378
+ debug("Failed to load usage cache from disk:", error);
379
+ }
380
+ return null;
381
+ }
382
+ function saveDiskCache(cache) {
383
+ try {
384
+ const cachePath = getCachePath();
385
+ const dir = path2.dirname(cachePath);
386
+ if (!fs2.existsSync(dir)) {
387
+ fs2.mkdirSync(dir, { recursive: true });
388
+ }
389
+ fs2.writeFileSync(cachePath, JSON.stringify(cache));
390
+ } catch (error) {
391
+ debug("Failed to save usage cache to disk:", error);
392
+ }
393
+ }
394
+ var diskCache = loadDiskCache();
395
+ var cachedUsage = diskCache?.usage ?? null;
396
+ var previousUsage = diskCache?.previousUsage ?? null;
397
+ var cacheTimestamp = diskCache?.timestamp ?? 0;
292
398
  var cachedToken = null;
293
399
  function getUsageTrend() {
294
400
  const result = {
@@ -333,6 +439,7 @@ async function getRealtimeUsage(pollIntervalMinutes = 15) {
333
439
  previousUsage = cachedUsage;
334
440
  cachedUsage = usage;
335
441
  cacheTimestamp = now;
442
+ saveDiskCache({ timestamp: now, usage: cachedUsage, previousUsage });
336
443
  debug("Refreshed realtime usage cache");
337
444
  } else {
338
445
  cachedToken = null;
@@ -649,6 +756,75 @@ function getTerminalWidth() {
649
756
  return process.stdout.columns || 80;
650
757
  }
651
758
 
759
+ // src/utils/history.ts
760
+ import fs3 from "fs";
761
+ import path3 from "path";
762
+ import os3 from "os";
763
+ var SPARKLINE_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
764
+ var MAX_HISTORY_AGE_MS = 24 * 60 * 60 * 1e3;
765
+ var HISTORY_FILE = "limitline-history.json";
766
+ function getHistoryPath() {
767
+ return path3.join(os3.homedir(), ".claude", HISTORY_FILE);
768
+ }
769
+ function loadHistory() {
770
+ const historyPath = getHistoryPath();
771
+ try {
772
+ if (fs3.existsSync(historyPath)) {
773
+ const content = fs3.readFileSync(historyPath, "utf-8");
774
+ const data = JSON.parse(content);
775
+ return data;
776
+ }
777
+ } catch (error) {
778
+ debug("Failed to load history:", error);
779
+ }
780
+ return { samples: [] };
781
+ }
782
+ function saveHistory(data) {
783
+ const historyPath = getHistoryPath();
784
+ try {
785
+ const dir = path3.dirname(historyPath);
786
+ if (!fs3.existsSync(dir)) {
787
+ fs3.mkdirSync(dir, { recursive: true });
788
+ }
789
+ fs3.writeFileSync(historyPath, JSON.stringify(data, null, 2));
790
+ } catch (error) {
791
+ debug("Failed to save history:", error);
792
+ }
793
+ }
794
+ function pruneOldSamples(data) {
795
+ const cutoff = Date.now() - MAX_HISTORY_AGE_MS;
796
+ return {
797
+ samples: data.samples.filter((s) => s.timestamp > cutoff)
798
+ };
799
+ }
800
+ function addSample(blockPercent, weeklyPercent) {
801
+ const history = loadHistory();
802
+ history.samples.push({
803
+ timestamp: Date.now(),
804
+ blockPercent,
805
+ weeklyPercent
806
+ });
807
+ const pruned = pruneOldSamples(history);
808
+ saveHistory(pruned);
809
+ }
810
+ function getSparkline(samples, width) {
811
+ const validSamples = samples.filter((s) => s !== null);
812
+ if (validSamples.length === 0) {
813
+ return "";
814
+ }
815
+ const recentSamples = validSamples.slice(-width);
816
+ return recentSamples.map((value) => {
817
+ const clamped = Math.max(0, Math.min(100, value));
818
+ const index = Math.floor(clamped / 100 * 7);
819
+ return SPARKLINE_CHARS[index];
820
+ }).join("");
821
+ }
822
+ function getBlockSparkline(width) {
823
+ const history = loadHistory();
824
+ const blockSamples = history.samples.map((s) => s.blockPercent);
825
+ return getSparkline(blockSamples, width);
826
+ }
827
+
652
828
  // src/renderer.ts
653
829
  var Renderer = class {
654
830
  config;
@@ -704,6 +880,18 @@ var Renderer = class {
704
880
  }
705
881
  return `${minutes}m`;
706
882
  }
883
+ formatAbsoluteTime(resetAt, format) {
884
+ const hours = resetAt.getHours();
885
+ const minutes = resetAt.getMinutes();
886
+ const paddedMinutes = minutes.toString().padStart(2, "0");
887
+ if (format === "24h") {
888
+ const paddedHours = hours.toString().padStart(2, "0");
889
+ return `${paddedHours}:${paddedMinutes}`;
890
+ }
891
+ const hour12 = hours % 12 || 12;
892
+ const ampm = hours < 12 ? "am" : "pm";
893
+ return `${hour12}:${paddedMinutes}${ampm}`;
894
+ }
707
895
  getTrendSymbol(trend) {
708
896
  if (!this.config.showTrend) return "";
709
897
  if (trend === "up") return this.symbols.trendUp;
@@ -812,9 +1000,24 @@ var Renderer = class {
812
1000
  } else {
813
1001
  text = `${Math.round(percent)}%${trend}`;
814
1002
  }
815
- if (showTime && ctx.blockInfo.timeRemaining !== null && !ctx.compact) {
816
- const timeStr = this.formatTimeRemaining(ctx.blockInfo.timeRemaining, ctx.compact);
817
- text += ` (${timeStr})`;
1003
+ const showSparkline = this.config.block?.showSparkline ?? false;
1004
+ if (showSparkline && !ctx.compact) {
1005
+ const sparklineWidth = this.config.block?.sparklineWidth ?? 8;
1006
+ const sparkline = getBlockSparkline(sparklineWidth);
1007
+ if (sparkline) {
1008
+ text += ` ${sparkline}`;
1009
+ }
1010
+ }
1011
+ if (showTime && !ctx.compact) {
1012
+ const timeDisplay = this.config.block?.timeDisplay ?? "remaining";
1013
+ const timeFormat = this.config.block?.timeFormat ?? "12h";
1014
+ if (timeDisplay === "absolute" && ctx.blockInfo.resetAt) {
1015
+ const timeStr = this.formatAbsoluteTime(ctx.blockInfo.resetAt, timeFormat);
1016
+ text += ` (${timeStr})`;
1017
+ } else if (ctx.blockInfo.timeRemaining !== null) {
1018
+ const timeStr = this.formatTimeRemaining(ctx.blockInfo.timeRemaining, ctx.compact);
1019
+ text += ` (${timeStr})`;
1020
+ }
818
1021
  }
819
1022
  return {
820
1023
  text: ` ${icon} ${text} `,
@@ -1136,6 +1339,12 @@ async function main() {
1136
1339
  ]);
1137
1340
  debug("Block info:", JSON.stringify(blockInfo));
1138
1341
  debug("Weekly info:", JSON.stringify(weeklyInfo));
1342
+ if (blockInfo?.percentUsed !== null || weeklyInfo?.percentUsed !== null) {
1343
+ addSample(
1344
+ blockInfo?.percentUsed ?? null,
1345
+ weeklyInfo?.percentUsed ?? null
1346
+ );
1347
+ }
1139
1348
  const trendInfo = config.showTrend ? getUsageTrend() : null;
1140
1349
  debug("Trend info:", JSON.stringify(trendInfo));
1141
1350
  const renderer = new Renderer(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-limitline",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "A statusline for Claude Code showing real-time usage limits and weekly tracking",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
11
+ "prepare": "tsup src/index.ts --format esm --dts --clean",
11
12
  "build": "tsup src/index.ts --format esm --dts --clean",
12
13
  "dev": "tsup src/index.ts --format esm --watch",
13
14
  "start": "node dist/index.js",