ctxline-claude 1.2.2 → 1.5.0
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 +16 -4
- package/package.json +1 -1
- package/statusline.js +145 -47
package/README.md
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
<a href="https://github.com/MithunWijayasiri/ctxline-claude/stargazers">
|
|
18
18
|
<img src="https://img.shields.io/github/stars/MithunWijayasiri/ctxline-claude" alt="stars">
|
|
19
19
|
</a>
|
|
20
|
+
<a href="https://ko-fi.com/mithunwijayasiri">
|
|
21
|
+
<img src="https://img.shields.io/badge/Ko--fi-support-ff5e5b?logo=ko-fi&logoColor=white" alt="Support on Ko-fi">
|
|
22
|
+
</a>
|
|
20
23
|
</p>
|
|
21
24
|
|
|
22
25
|
<p align="center">
|
|
@@ -121,6 +124,7 @@ Remove-Item "$env:USERPROFILE\.claude\cache\usage-cache.json" -ErrorAction Silen
|
|
|
121
124
|
| **Context** | Visual bar of context-window usage |
|
|
122
125
|
| **Current** | Live 5-hour session limit + reset countdown (subscription users) |
|
|
123
126
|
| **Weekly** | Weekly usage allowance + time until the weekly reset (subscription users) |
|
|
127
|
+
| **Model limit** | Weekly limit scoped to a single model, when your account has one — labelled by the model's initial (`F` = Fable) |
|
|
124
128
|
| **Cost** | Running session cost in USD (e.g. `$0.42`) |
|
|
125
129
|
| **Task** | The in-progress todo, when there is one |
|
|
126
130
|
|
|
@@ -137,7 +141,7 @@ Remove-Item "$env:USERPROFILE\.claude\cache\usage-cache.json" -ErrorAction Silen
|
|
|
137
141
|
|
|
138
142
|
The statusline is zero-config by default. To **hide segments you don't want**, set the `CTXLINE_DISABLE` environment variable to a comma-separated list of any of:
|
|
139
143
|
|
|
140
|
-
`branch` · `effort` · `cost` · `task` · `usage` (5-hour + weekly)
|
|
144
|
+
`branch` · `effort` · `cost` · `task` · `usage` (5-hour + weekly + model-scoped)
|
|
141
145
|
|
|
142
146
|
Directory, model, and context always show; unknown names are ignored. Example below hides cost and the current task.
|
|
143
147
|
|
|
@@ -181,7 +185,8 @@ To re-enable a segment, remove it from the list (or delete the variable) and res
|
|
|
181
185
|
|
|
182
186
|
## How it works
|
|
183
187
|
|
|
184
|
-
- **Source** — context comes from Claude Code's session data.
|
|
188
|
+
- **Source** — context comes from Claude Code's session data. The 5-hour and weekly bars are read straight from the `rate_limits` field Claude Code pipes in (no network), falling back to `https://api.anthropic.com/api/oauth/usage` when that field isn't present yet. API-key users skip usage entirely.
|
|
189
|
+
- **Model-scoped limits** — `rate_limits` carries only `five_hour` and `seven_day`, so a model-scoped weekly limit can only come from `/usage` (its `limits` array). It's served from the same cache as everything else, so this costs at most one call per 30s no matter how often the line renders.
|
|
185
190
|
- **No network on the fast path** — when `rate_limits` is in the session data, there's no API call at all. The fetch below only runs as a fallback (e.g. the first render of a session, before the field appears).
|
|
186
191
|
- **Adaptive timing** — for the fallback fetch: 1.5s timeout on the first prompt (cold start), 1.2s after (connection reused).
|
|
187
192
|
- **Caching** — the fallback fetch is cached at `~/.claude/cache/usage-cache.json`, shared across sessions. Within 30s the cache renders directly (the API call is skipped); if a live call fails, the last value (up to 10 min old) is shown so the bar never vanishes. The reset countdown recomputes every render.
|
|
@@ -238,10 +243,17 @@ No. Your credentials never leave your machine. On the fast path no token is read
|
|
|
238
243
|
|
|
239
244
|
</details>
|
|
240
245
|
|
|
241
|
-
## License
|
|
242
246
|
|
|
243
|
-
|
|
247
|
+
## Support
|
|
248
|
+
|
|
249
|
+
If you find this project useful, consider supporting its development on [Ko-fi](https://ko-fi.com/mithunwijayasiri). Your donations help keep the project maintained, improve existing features, and fund new open-source tools.
|
|
250
|
+
|
|
251
|
+
Thank you for your support! ❤️
|
|
244
252
|
|
|
245
253
|
## Credits
|
|
246
254
|
|
|
247
255
|
Thanks to [@TahaSabir0](https://github.com/TahaSabir0) for the base config.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT
|
package/package.json
CHANGED
package/statusline.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Claude Code Enhanced Statusline
|
|
3
|
-
// Shows: directory | model | context usage |
|
|
3
|
+
// Shows: directory | model | context usage | 5-hour + weekly + model-scoped usage | current task
|
|
4
4
|
// Auto-detects API key vs subscription usage
|
|
5
5
|
// https://github.com/MithunWijayasiri/ctxline-claude
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ const { execSync, execFileSync } = require('child_process');
|
|
|
13
13
|
const IS_API_KEY = !!process.env.ANTHROPIC_API_KEY;
|
|
14
14
|
|
|
15
15
|
// Optional segment opt-out: CTXLINE_DISABLE is a comma list of segments to hide.
|
|
16
|
-
// Recognized: branch, effort, cost, task, usage (H+W). dir/model/context always render.
|
|
16
|
+
// Recognized: branch, effort, cost, task, usage (H+W+model-scoped). dir/model/context always render.
|
|
17
17
|
// Unknown names are ignored. Disabling a segment also skips its work (git, todo read,
|
|
18
18
|
// usage fetch).
|
|
19
19
|
const DISABLED = new Set(
|
|
@@ -84,6 +84,13 @@ function getUsageColor(percentage) {
|
|
|
84
84
|
return colors.red;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Model-scoped bars skip the H/W thresholds: a line can carry several at once, so a flat
|
|
88
|
+
// orange keeps them readable as one group. Red at >=90 is the one distinction kept — that
|
|
89
|
+
// bar is about to block the model it names.
|
|
90
|
+
function getScopedColor(percentage) {
|
|
91
|
+
return percentage >= 90 ? colors.red : colors.orange;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
// Shorten verbose model names for the statusline: "Opus 4.8 (1M context)" -> "Opus 4.8 (1M)".
|
|
88
95
|
function shortenModel(name) {
|
|
89
96
|
return name.replace(/\s+context\)/i, ')');
|
|
@@ -222,7 +229,8 @@ function getContextBar(remaining) {
|
|
|
222
229
|
// Render a compact usage segment from raw data: "<label><pct> ↺ <countdown>"
|
|
223
230
|
// (e.g. "H81 ↺ 2h21m") — no bar. Called on every read (live or cached) so the reset
|
|
224
231
|
// countdown is always recomputed from resetsAt rather than frozen at fetch time.
|
|
225
|
-
|
|
232
|
+
// `color` overrides the threshold color — the model-scoped bars pass getScopedColor.
|
|
233
|
+
function buildUsageBar(label, percentage, resetsAt, color) {
|
|
226
234
|
let timeStr = '';
|
|
227
235
|
if (resetsAt) {
|
|
228
236
|
const diffMins = Math.max(0, Math.floor((new Date(resetsAt) - new Date()) / 60000));
|
|
@@ -234,18 +242,41 @@ function buildUsageBar(label, percentage, resetsAt) {
|
|
|
234
242
|
else timeStr = `${mins}m`;
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
const
|
|
245
|
+
const barColor = color || getUsageColor(percentage);
|
|
238
246
|
const timePart = timeStr ? `${colors.dim} ↺ ${timeStr}${colors.reset}` : '';
|
|
239
247
|
|
|
240
|
-
return `${
|
|
248
|
+
return `${barColor}${label}${percentage}${colors.reset}${timePart}`;
|
|
241
249
|
}
|
|
242
250
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
251
|
+
// Model-scoped weekly limits (e.g. "Fable weekly limit at 86%"), rendered after the
|
|
252
|
+
// account-wide W bar. The /usage payload reports these in a `limits` array, each entry
|
|
253
|
+
// carrying the model in scope.model.display_name:
|
|
254
|
+
//
|
|
255
|
+
// { kind: "weekly_scoped", percent: 86, severity: "warning",
|
|
256
|
+
// resets_at: "...", scope: { model: { display_name: "Fable" } } }
|
|
257
|
+
//
|
|
258
|
+
// The label is the model's first initial (Fable -> F), so a new model family needs no
|
|
259
|
+
// code change. Older payloads instead exposed flat seven_day_<model> keys, kept below as
|
|
260
|
+
// a fallback for accounts still reporting that shape.
|
|
261
|
+
//
|
|
262
|
+
// NOTE: these appear only in the API payload. Claude Code's statusline stdin carries just
|
|
263
|
+
// five_hour and seven_day under rate_limits, so the scoped limits always come from the
|
|
264
|
+
// cache/API path even when stdin supplies the H and W bars.
|
|
265
|
+
const LEGACY_MODEL_WEEKLY_KEYS = [
|
|
266
|
+
{ key: 'seven_day_opus', label: 'O' },
|
|
267
|
+
{ key: 'seven_day_sonnet', label: 'S' }
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
// Build the usage segments from raw entries. fiveHour/weekly are { percentage, resetsAt }
|
|
271
|
+
// or null/absent; models is an array of { label, percentage, resetsAt } (possibly empty).
|
|
272
|
+
// Returns { current, weekly, models } — the first two rendered strings or null, models a
|
|
273
|
+
// (possibly empty) array of rendered strings. Scoped bars use getScopedColor instead of the
|
|
274
|
+
// H/W thresholds, so the full threshold palette stays exclusive to H/W.
|
|
275
|
+
function buildUsageBars(fiveHour, weekly, models) {
|
|
246
276
|
return {
|
|
247
277
|
current: fiveHour ? buildUsageBar('H', fiveHour.percentage, fiveHour.resetsAt) : null,
|
|
248
|
-
weekly: weekly ? buildUsageBar('W', weekly.percentage, weekly.resetsAt) : null
|
|
278
|
+
weekly: weekly ? buildUsageBar('W', weekly.percentage, weekly.resetsAt) : null,
|
|
279
|
+
models: (models || []).map(m => buildUsageBar(m.label, m.percentage, m.resetsAt, getScopedColor(m.percentage)))
|
|
249
280
|
};
|
|
250
281
|
}
|
|
251
282
|
|
|
@@ -257,11 +288,42 @@ function normalizePercentage(value) {
|
|
|
257
288
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
258
289
|
}
|
|
259
290
|
|
|
291
|
+
// Extract the model-scoped weekly limits from a raw /usage payload as
|
|
292
|
+
// [{ label, percentage, resetsAt }], in payload order. Prefers the `limits` array;
|
|
293
|
+
// falls back to the legacy flat keys only when it yields nothing, so an account
|
|
294
|
+
// reporting both shapes doesn't render the same limit twice.
|
|
295
|
+
function parseScopedLimits(usage) {
|
|
296
|
+
const scoped = [];
|
|
297
|
+
|
|
298
|
+
if (Array.isArray(usage?.limits)) {
|
|
299
|
+
for (const entry of usage.limits) {
|
|
300
|
+
if (!entry || entry.kind !== 'weekly_scoped') continue;
|
|
301
|
+
const name = entry.scope?.model?.display_name;
|
|
302
|
+
const pct = normalizePercentage(entry.percent);
|
|
303
|
+
if (typeof name !== 'string' || !name.trim() || pct == null) continue;
|
|
304
|
+
scoped.push({
|
|
305
|
+
label: name.trim().charAt(0).toUpperCase(),
|
|
306
|
+
percentage: pct,
|
|
307
|
+
resetsAt: entry.resets_at || null
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (scoped.length) return scoped;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const { key, label } of LEGACY_MODEL_WEEKLY_KEYS) {
|
|
314
|
+
const seg = usage?.[key];
|
|
315
|
+
const pct = seg ? normalizePercentage(seg.utilization) : null;
|
|
316
|
+
if (pct != null) scoped.push({ label, percentage: pct, resetsAt: seg.resets_at || null });
|
|
317
|
+
}
|
|
318
|
+
return scoped;
|
|
319
|
+
}
|
|
320
|
+
|
|
260
321
|
// Build usage bars from stdin `rate_limits` (Claude.ai Pro/Max, present only after the
|
|
261
322
|
// first API response of a session). Same data as the OAuth usage API, so reading it here
|
|
262
323
|
// skips the network/credentials/cache path entirely. `resets_at` is a Unix epoch in
|
|
263
|
-
// SECONDS (not ISO) — ×1000 before Date. Returns {
|
|
264
|
-
// rate_limits is absent or the required five_hour segment is unusable (caller falls
|
|
324
|
+
// SECONDS (not ISO) — ×1000 before Date. Returns raw { fiveHour, weekly } entries, or null
|
|
325
|
+
// when rate_limits is absent or the required five_hour segment is unusable (caller falls
|
|
326
|
+
// back). Model-scoped weekly limits are never present here — see LEGACY_MODEL_WEEKLY_KEYS.
|
|
265
327
|
function buildUsageFromStdin(data) {
|
|
266
328
|
const rl = data?.rate_limits;
|
|
267
329
|
if (!rl) return null;
|
|
@@ -284,8 +346,7 @@ function buildUsageFromStdin(data) {
|
|
|
284
346
|
|
|
285
347
|
const fiveHour = toEntry(rl.five_hour);
|
|
286
348
|
if (!fiveHour) return null; // five_hour is the required bar
|
|
287
|
-
|
|
288
|
-
return buildUsageBars(fiveHour, weekly);
|
|
349
|
+
return { fiveHour, weekly: toEntry(rl.seven_day) };
|
|
289
350
|
}
|
|
290
351
|
|
|
291
352
|
// Validate a single usage entry ({ percentage, resetsAt }). Returns true only for a
|
|
@@ -307,13 +368,18 @@ function readCachedUsage() {
|
|
|
307
368
|
const cache = JSON.parse(fs.readFileSync(USAGE_CACHE_FILE, 'utf8'));
|
|
308
369
|
if (!cache || !Number.isFinite(cache.timestamp) || cache.timestamp <= 0) return null;
|
|
309
370
|
|
|
310
|
-
// Validate data. fiveHour is required; weekly
|
|
311
|
-
// This also rejects the legacy single-{percentage,resetsAt} format from
|
|
312
|
-
// versions, which had no fiveHour key, so stale caches are ignored on read.
|
|
371
|
+
// Validate data. fiveHour is required; weekly and models are optional (the API may
|
|
372
|
+
// omit either). This also rejects the legacy single-{percentage,resetsAt} format from
|
|
373
|
+
// older versions, which had no fiveHour key, so stale caches are ignored on read.
|
|
374
|
+
// A cache written before model bars existed simply has no models key — still valid.
|
|
313
375
|
const data = cache.data;
|
|
314
376
|
if (!data || typeof data !== 'object') return null;
|
|
315
377
|
if (!isValidUsageEntry(data.fiveHour)) return null;
|
|
316
378
|
if (data.weekly != null && !isValidUsageEntry(data.weekly)) return null;
|
|
379
|
+
if (data.models != null) {
|
|
380
|
+
if (!Array.isArray(data.models)) return null;
|
|
381
|
+
if (!data.models.every(m => typeof m?.label === 'string' && isValidUsageEntry(m))) return null;
|
|
382
|
+
}
|
|
317
383
|
|
|
318
384
|
return { age: Date.now() - cache.timestamp, data };
|
|
319
385
|
} catch (e) {
|
|
@@ -412,9 +478,13 @@ function getApiUsage(callback) {
|
|
|
412
478
|
resetsAt: usage.seven_day.resets_at || null
|
|
413
479
|
} : null;
|
|
414
480
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
481
|
+
// Model-scoped weekly limits, rendered only when the account reports them.
|
|
482
|
+
const models = parseScopedLimits(usage);
|
|
483
|
+
|
|
484
|
+
// Cache the raw data (shared across sessions); callers render from it.
|
|
485
|
+
const resolved = { fiveHour, weekly, models };
|
|
486
|
+
setCachedUsage(resolved);
|
|
487
|
+
callback(resolved);
|
|
418
488
|
} else {
|
|
419
489
|
callback(null);
|
|
420
490
|
}
|
|
@@ -436,28 +506,42 @@ function getApiUsage(callback) {
|
|
|
436
506
|
}
|
|
437
507
|
}
|
|
438
508
|
|
|
439
|
-
//
|
|
440
|
-
function
|
|
509
|
+
// Resolve raw usage data ({ fiveHour, weekly, models }), cache-first. Callers render it.
|
|
510
|
+
function getRawUsage(callback) {
|
|
441
511
|
const cached = readCachedUsage();
|
|
442
512
|
|
|
443
|
-
// Cache is fresh ->
|
|
513
|
+
// Cache is fresh -> use it and skip the API entirely (fewer calls, faster).
|
|
444
514
|
if (cached && cached.age < FRESH_TTL_MS) {
|
|
445
|
-
return callback(
|
|
515
|
+
return callback(cached.data);
|
|
446
516
|
}
|
|
447
517
|
|
|
448
518
|
// Cache is stale or missing -> refresh from the API.
|
|
449
|
-
getApiUsage((
|
|
450
|
-
if (
|
|
451
|
-
callback(
|
|
519
|
+
getApiUsage((fresh) => {
|
|
520
|
+
if (fresh) {
|
|
521
|
+
callback(fresh);
|
|
452
522
|
} else if (cached && cached.age < STALE_TTL_MS) {
|
|
453
523
|
// API failed/timed out, but recent cache exists -> show it instead of nothing.
|
|
454
|
-
callback(
|
|
524
|
+
callback(cached.data);
|
|
455
525
|
} else {
|
|
456
526
|
callback(null);
|
|
457
527
|
}
|
|
458
528
|
});
|
|
459
529
|
}
|
|
460
530
|
|
|
531
|
+
// Get usage, cache-first, rendered.
|
|
532
|
+
function getUsageWithCache(callback) {
|
|
533
|
+
getRawUsage((data) => {
|
|
534
|
+
callback(data ? buildUsageBars(data.fiveHour, data.weekly, data.models) : null);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Model-scoped weekly limits only, cache-first. Used alongside the stdin H/W bars, which
|
|
539
|
+
// can't carry them. Falls back to the stale cache and finally to [] so a failed or slow
|
|
540
|
+
// call costs the scoped bars but never the bars stdin already gave us.
|
|
541
|
+
function getScopedModels(callback) {
|
|
542
|
+
getRawUsage((data) => callback(data?.models || []));
|
|
543
|
+
}
|
|
544
|
+
|
|
461
545
|
// Session cost from stdin `cost.total_cost_usd` (USD float, computed client-side by
|
|
462
546
|
// Claude Code as tokens × per-model API pricing). Pure stdin — no network/cache.
|
|
463
547
|
// Returns "$0.00" rendered dim, or '' when absent/non-finite so the segment is omitted.
|
|
@@ -537,6 +621,7 @@ function outputStatus(data, usage) {
|
|
|
537
621
|
const line2 = [];
|
|
538
622
|
if (usage?.current) line2.push(usage.current);
|
|
539
623
|
if (usage?.weekly) line2.push(usage.weekly);
|
|
624
|
+
if (usage?.models?.length) line2.push(...usage.models);
|
|
540
625
|
if (cost) line2.push(cost);
|
|
541
626
|
if (task) line2.push(`${colors.dim}${task}${colors.reset}`);
|
|
542
627
|
|
|
@@ -551,6 +636,7 @@ function outputFallback(usage) {
|
|
|
551
636
|
const parts = ['~', 'Claude', contextBar];
|
|
552
637
|
if (usage?.current) parts.push(usage.current);
|
|
553
638
|
if (usage?.weekly) parts.push(usage.weekly);
|
|
639
|
+
if (usage?.models?.length) parts.push(...usage.models);
|
|
554
640
|
process.stdout.write(parts.join(' \u2502 '));
|
|
555
641
|
}
|
|
556
642
|
|
|
@@ -563,7 +649,12 @@ function resolveUsage(data, callback) {
|
|
|
563
649
|
}
|
|
564
650
|
const fromStdin = buildUsageFromStdin(data);
|
|
565
651
|
if (fromStdin) {
|
|
566
|
-
|
|
652
|
+
// stdin covers H and W with no network. Model-scoped weekly limits only exist in the
|
|
653
|
+
// API payload, so they come from the cache — refreshed on the same TTL as every other
|
|
654
|
+
// usage read, which keeps at most one call per FRESH_TTL_MS regardless of render rate.
|
|
655
|
+
return getScopedModels((models) => {
|
|
656
|
+
callback(buildUsageBars(fromStdin.fiveHour, fromStdin.weekly, models));
|
|
657
|
+
});
|
|
567
658
|
}
|
|
568
659
|
getUsageWithCache(callback);
|
|
569
660
|
}
|
|
@@ -591,24 +682,31 @@ function emit(data) {
|
|
|
591
682
|
});
|
|
592
683
|
}
|
|
593
684
|
|
|
594
|
-
|
|
595
|
-
|
|
685
|
+
// Entry point, guarded so tests can require this file to exercise payload parsing
|
|
686
|
+
// directly (the /usage response shape is the easiest thing here to get wrong, and it
|
|
687
|
+
// can't be reached through stdin). Running the script normally is unchanged.
|
|
688
|
+
if (require.main === module) {
|
|
689
|
+
if (process.stdin.isTTY) {
|
|
690
|
+
emit(null);
|
|
691
|
+
} else {
|
|
692
|
+
let input = '';
|
|
693
|
+
let timeoutReached = false;
|
|
694
|
+
|
|
695
|
+
const overallTimeout = IS_API_KEY ? 500 : (fs.existsSync(USAGE_CACHE_FILE) ? 1300 : 1600);
|
|
696
|
+
|
|
697
|
+
const timeout = setTimeout(() => {
|
|
698
|
+
timeoutReached = true;
|
|
699
|
+
emit(parseInput(input));
|
|
700
|
+
}, overallTimeout);
|
|
701
|
+
|
|
702
|
+
process.stdin.setEncoding('utf8');
|
|
703
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
704
|
+
process.stdin.on('end', () => {
|
|
705
|
+
if (timeoutReached) return;
|
|
706
|
+
clearTimeout(timeout);
|
|
707
|
+
emit(parseInput(input));
|
|
708
|
+
});
|
|
709
|
+
}
|
|
596
710
|
} else {
|
|
597
|
-
|
|
598
|
-
let timeoutReached = false;
|
|
599
|
-
|
|
600
|
-
const overallTimeout = IS_API_KEY ? 500 : (fs.existsSync(USAGE_CACHE_FILE) ? 1300 : 1600);
|
|
601
|
-
|
|
602
|
-
const timeout = setTimeout(() => {
|
|
603
|
-
timeoutReached = true;
|
|
604
|
-
emit(parseInput(input));
|
|
605
|
-
}, overallTimeout);
|
|
606
|
-
|
|
607
|
-
process.stdin.setEncoding('utf8');
|
|
608
|
-
process.stdin.on('data', chunk => input += chunk);
|
|
609
|
-
process.stdin.on('end', () => {
|
|
610
|
-
if (timeoutReached) return;
|
|
611
|
-
clearTimeout(timeout);
|
|
612
|
-
emit(parseInput(input));
|
|
613
|
-
});
|
|
711
|
+
module.exports = { parseScopedLimits, normalizePercentage };
|
|
614
712
|
}
|