claude-limitline 1.5.2 → 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.
- package/dist/index.js +205 -25
- 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,57 @@ async function getOAuthTokenWindows() {
|
|
|
161
165
|
}
|
|
162
166
|
return null;
|
|
163
167
|
}
|
|
164
|
-
async function
|
|
168
|
+
async function findKeychainServiceName() {
|
|
165
169
|
try {
|
|
166
170
|
const { stdout } = await execAsync(
|
|
167
|
-
`security
|
|
171
|
+
`security dump-keychain 2>/dev/null | grep -o '"Claude Code-credentials[^"]*"'`,
|
|
168
172
|
{ timeout: 5e3 }
|
|
169
173
|
);
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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;
|
|
180
192
|
}
|
|
181
|
-
} catch (parseError) {
|
|
182
|
-
debug("Failed to parse keychain JSON:", parseError);
|
|
183
193
|
}
|
|
194
|
+
} catch (parseError) {
|
|
195
|
+
debug("Failed to parse keychain JSON:", parseError);
|
|
184
196
|
}
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
187
218
|
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
debug("macOS Keychain retrieval failed:", error);
|
|
190
219
|
}
|
|
191
220
|
const configPaths = [
|
|
192
221
|
path2.join(os2.homedir(), ".claude", ".credentials.json"),
|
|
@@ -315,9 +344,57 @@ async function fetchUsageFromAPI(token) {
|
|
|
315
344
|
return null;
|
|
316
345
|
}
|
|
317
346
|
}
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
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;
|
|
321
398
|
var cachedToken = null;
|
|
322
399
|
function getUsageTrend() {
|
|
323
400
|
const result = {
|
|
@@ -362,6 +439,7 @@ async function getRealtimeUsage(pollIntervalMinutes = 15) {
|
|
|
362
439
|
previousUsage = cachedUsage;
|
|
363
440
|
cachedUsage = usage;
|
|
364
441
|
cacheTimestamp = now;
|
|
442
|
+
saveDiskCache({ timestamp: now, usage: cachedUsage, previousUsage });
|
|
365
443
|
debug("Refreshed realtime usage cache");
|
|
366
444
|
} else {
|
|
367
445
|
cachedToken = null;
|
|
@@ -678,6 +756,75 @@ function getTerminalWidth() {
|
|
|
678
756
|
return process.stdout.columns || 80;
|
|
679
757
|
}
|
|
680
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
|
+
|
|
681
828
|
// src/renderer.ts
|
|
682
829
|
var Renderer = class {
|
|
683
830
|
config;
|
|
@@ -733,6 +880,18 @@ var Renderer = class {
|
|
|
733
880
|
}
|
|
734
881
|
return `${minutes}m`;
|
|
735
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
|
+
}
|
|
736
895
|
getTrendSymbol(trend) {
|
|
737
896
|
if (!this.config.showTrend) return "";
|
|
738
897
|
if (trend === "up") return this.symbols.trendUp;
|
|
@@ -841,9 +1000,24 @@ var Renderer = class {
|
|
|
841
1000
|
} else {
|
|
842
1001
|
text = `${Math.round(percent)}%${trend}`;
|
|
843
1002
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
+
}
|
|
847
1021
|
}
|
|
848
1022
|
return {
|
|
849
1023
|
text: ` ${icon} ${text} `,
|
|
@@ -1165,6 +1339,12 @@ async function main() {
|
|
|
1165
1339
|
]);
|
|
1166
1340
|
debug("Block info:", JSON.stringify(blockInfo));
|
|
1167
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
|
+
}
|
|
1168
1348
|
const trendInfo = config.showTrend ? getUsageTrend() : null;
|
|
1169
1349
|
debug("Trend info:", JSON.stringify(trendInfo));
|
|
1170
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.
|
|
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",
|