cc-context-stats 1.6.2 → 1.8.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/CHANGELOG.md +39 -0
- package/CLAUDE.md +12 -0
- package/README.md +34 -24
- package/docs/ARCHITECTURE.md +52 -25
- package/docs/CSV_FORMAT.md +2 -0
- package/docs/DEPLOYMENT.md +19 -8
- package/docs/DEVELOPMENT.md +48 -12
- package/docs/MODEL_INTELLIGENCE.md +396 -0
- package/docs/configuration.md +35 -0
- package/docs/context-stats.md +12 -1
- package/docs/installation.md +82 -22
- package/docs/scripts.md +47 -23
- package/docs/troubleshooting.md +93 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/statusline-full.sh +171 -37
- package/scripts/statusline.js +214 -32
- package/scripts/statusline.py +195 -47
- package/src/claude_statusline/__init__.py +1 -1
- package/src/claude_statusline/cli/context_stats.py +85 -13
- package/src/claude_statusline/cli/explain.py +228 -0
- package/src/claude_statusline/cli/statusline.py +41 -30
- package/src/claude_statusline/core/colors.py +78 -9
- package/src/claude_statusline/core/config.py +68 -9
- package/src/claude_statusline/core/git.py +16 -5
- package/src/claude_statusline/graphs/intelligence.py +162 -0
- package/src/claude_statusline/graphs/renderer.py +38 -3
- package/tests/bash/test_statusline_full.bats +5 -5
- package/tests/fixtures/mi_test_vectors.json +140 -0
- package/tests/node/intelligence.test.js +98 -0
- package/tests/node/statusline.test.js +4 -4
- package/tests/python/test_colors.py +105 -0
- package/tests/python/test_config_colors.py +78 -0
- package/tests/python/test_explain.py +177 -0
- package/tests/python/test_intelligence.py +314 -0
- package/tests/python/test_layout.py +4 -4
- package/tests/python/test_statusline.py +4 -4
package/scripts/statusline.js
CHANGED
|
@@ -36,6 +36,61 @@ const os = require('os');
|
|
|
36
36
|
const ROTATION_THRESHOLD = 10000;
|
|
37
37
|
const ROTATION_KEEP = 5000;
|
|
38
38
|
|
|
39
|
+
// Model Intelligence constants (hardcoded, not configurable)
|
|
40
|
+
const MI_WEIGHT_CPS = 0.60;
|
|
41
|
+
const MI_WEIGHT_ES = 0.25;
|
|
42
|
+
const MI_WEIGHT_PS = 0.15;
|
|
43
|
+
const MI_GREEN_THRESHOLD = 0.65;
|
|
44
|
+
const MI_YELLOW_THRESHOLD = 0.35;
|
|
45
|
+
const MI_PRODUCTIVITY_TARGET = 0.2;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compute Model Intelligence score.
|
|
49
|
+
* Returns { mi, cps, es, ps }.
|
|
50
|
+
*/
|
|
51
|
+
function computeMI(usedTokens, contextWindowSize, cacheRead, totalContext,
|
|
52
|
+
deltaLines, deltaOutput, beta = 1.5) {
|
|
53
|
+
// Guard clause
|
|
54
|
+
if (contextWindowSize === 0) {
|
|
55
|
+
return { mi: 1.0, cps: 1.0, es: 1.0, ps: 0.5 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// CPS
|
|
59
|
+
const u = usedTokens / contextWindowSize;
|
|
60
|
+
const cps = u > 0 ? Math.max(0, 1 - Math.pow(u, beta)) : 1.0;
|
|
61
|
+
|
|
62
|
+
// ES
|
|
63
|
+
let es;
|
|
64
|
+
if (totalContext === 0) {
|
|
65
|
+
es = 1.0;
|
|
66
|
+
} else {
|
|
67
|
+
const cacheHitRatio = cacheRead / totalContext;
|
|
68
|
+
es = 0.3 + 0.7 * cacheHitRatio;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// PS
|
|
72
|
+
let ps;
|
|
73
|
+
if (deltaOutput === null || deltaOutput === undefined || deltaOutput <= 0) {
|
|
74
|
+
ps = 0.5;
|
|
75
|
+
} else {
|
|
76
|
+
const ratio = deltaLines / deltaOutput;
|
|
77
|
+
const normalized = Math.min(1.0, ratio / MI_PRODUCTIVITY_TARGET);
|
|
78
|
+
ps = 0.2 + 0.8 * normalized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const mi = MI_WEIGHT_CPS * cps + MI_WEIGHT_ES * es + MI_WEIGHT_PS * ps;
|
|
82
|
+
return { mi, cps, es, ps };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return ANSI color code for MI score.
|
|
87
|
+
*/
|
|
88
|
+
function getMIColor(mi, greenColor, yellowColor, redColor) {
|
|
89
|
+
if (mi > MI_GREEN_THRESHOLD) return greenColor || GREEN;
|
|
90
|
+
if (mi > MI_YELLOW_THRESHOLD) return yellowColor || YELLOW;
|
|
91
|
+
return redColor || RED;
|
|
92
|
+
}
|
|
93
|
+
|
|
39
94
|
/**
|
|
40
95
|
* Rotate a state file if it exceeds ROTATION_THRESHOLD lines.
|
|
41
96
|
* Keeps the most recent ROTATION_KEEP lines via atomic temp-file + rename.
|
|
@@ -72,7 +127,7 @@ function maybeRotateStateFile(stateFile) {
|
|
|
72
127
|
}
|
|
73
128
|
}
|
|
74
129
|
|
|
75
|
-
// ANSI Colors
|
|
130
|
+
// ANSI Colors (defaults, overridable via config)
|
|
76
131
|
const BLUE = '\x1b[0;34m';
|
|
77
132
|
const MAGENTA = '\x1b[0;35m';
|
|
78
133
|
const CYAN = '\x1b[0;36m';
|
|
@@ -82,6 +137,54 @@ const RED = '\x1b[0;31m';
|
|
|
82
137
|
const DIM = '\x1b[2m';
|
|
83
138
|
const RESET = '\x1b[0m';
|
|
84
139
|
|
|
140
|
+
// Named colors for config parsing
|
|
141
|
+
const COLOR_NAMES = {
|
|
142
|
+
black: '\x1b[0;30m',
|
|
143
|
+
red: '\x1b[0;31m',
|
|
144
|
+
green: '\x1b[0;32m',
|
|
145
|
+
yellow: '\x1b[0;33m',
|
|
146
|
+
blue: '\x1b[0;34m',
|
|
147
|
+
magenta: '\x1b[0;35m',
|
|
148
|
+
cyan: '\x1b[0;36m',
|
|
149
|
+
white: '\x1b[0;37m',
|
|
150
|
+
bright_black: '\x1b[0;90m',
|
|
151
|
+
bright_red: '\x1b[0;91m',
|
|
152
|
+
bright_green: '\x1b[0;92m',
|
|
153
|
+
bright_yellow: '\x1b[0;93m',
|
|
154
|
+
bright_blue: '\x1b[0;94m',
|
|
155
|
+
bright_magenta: '\x1b[0;95m',
|
|
156
|
+
bright_cyan: '\x1b[0;96m',
|
|
157
|
+
bright_white: '\x1b[0;97m',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse a color name or #rrggbb hex into an ANSI escape code.
|
|
162
|
+
* Returns null if unrecognized.
|
|
163
|
+
*/
|
|
164
|
+
function parseColor(value) {
|
|
165
|
+
value = value.trim().toLowerCase();
|
|
166
|
+
if (COLOR_NAMES[value]) {
|
|
167
|
+
return COLOR_NAMES[value];
|
|
168
|
+
}
|
|
169
|
+
const m = value.match(/^#([0-9a-f]{6})$/);
|
|
170
|
+
if (m) {
|
|
171
|
+
const r = parseInt(m[1].slice(0, 2), 16);
|
|
172
|
+
const g = parseInt(m[1].slice(2, 4), 16);
|
|
173
|
+
const b = parseInt(m[1].slice(4, 6), 16);
|
|
174
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const COLOR_CONFIG_KEYS = {
|
|
180
|
+
color_green: 'green',
|
|
181
|
+
color_yellow: 'yellow',
|
|
182
|
+
color_red: 'red',
|
|
183
|
+
color_blue: 'blue',
|
|
184
|
+
color_magenta: 'magenta',
|
|
185
|
+
color_cyan: 'cyan',
|
|
186
|
+
};
|
|
187
|
+
|
|
85
188
|
/**
|
|
86
189
|
* Return the visible width of a string after stripping ANSI escape sequences.
|
|
87
190
|
*/
|
|
@@ -93,8 +196,24 @@ function visibleWidth(s) {
|
|
|
93
196
|
/**
|
|
94
197
|
* Return the terminal width in columns, defaulting to 80.
|
|
95
198
|
*/
|
|
199
|
+
/**
|
|
200
|
+
* Return the terminal width in columns.
|
|
201
|
+
*
|
|
202
|
+
* When running inside Claude Code's statusline subprocess, neither $COLUMNS
|
|
203
|
+
* nor process.stdout.columns can detect the real terminal width (they return
|
|
204
|
+
* undefined or 80). If COLUMNS is not explicitly set and we'd fall back to 80,
|
|
205
|
+
* use a generous default of 200 so that no parts are unnecessarily dropped;
|
|
206
|
+
* Claude Code's own UI handles any overflow/truncation.
|
|
207
|
+
*/
|
|
96
208
|
function getTerminalWidth() {
|
|
97
|
-
|
|
209
|
+
if (process.env.COLUMNS) {
|
|
210
|
+
return parseInt(process.env.COLUMNS, 10) || 200;
|
|
211
|
+
}
|
|
212
|
+
const cols = process.stdout.columns;
|
|
213
|
+
if (cols && cols !== 80) {
|
|
214
|
+
return cols;
|
|
215
|
+
}
|
|
216
|
+
return 200;
|
|
98
217
|
}
|
|
99
218
|
|
|
100
219
|
/**
|
|
@@ -125,7 +244,9 @@ function fitToWidth(parts, maxWidth) {
|
|
|
125
244
|
return result;
|
|
126
245
|
}
|
|
127
246
|
|
|
128
|
-
function getGitInfo(projectDir) {
|
|
247
|
+
function getGitInfo(projectDir, magentaColor, cyanColor) {
|
|
248
|
+
const mg = magentaColor || MAGENTA;
|
|
249
|
+
const cy = cyanColor || CYAN;
|
|
129
250
|
const gitDir = path.join(projectDir, '.git');
|
|
130
251
|
if (!fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) {
|
|
131
252
|
return '';
|
|
@@ -154,9 +275,9 @@ function getGitInfo(projectDir) {
|
|
|
154
275
|
const changes = status.split('\n').filter(l => l.trim()).length;
|
|
155
276
|
|
|
156
277
|
if (changes > 0) {
|
|
157
|
-
return ` | ${
|
|
278
|
+
return ` | ${mg}${branch}${RESET} ${cy}[${changes}]${RESET}`;
|
|
158
279
|
}
|
|
159
|
-
return ` | ${
|
|
280
|
+
return ` | ${mg}${branch}${RESET}`;
|
|
160
281
|
} catch {
|
|
161
282
|
return '';
|
|
162
283
|
}
|
|
@@ -170,6 +291,9 @@ function readConfig() {
|
|
|
170
291
|
showSession: true,
|
|
171
292
|
showIoTokens: true,
|
|
172
293
|
reducedMotion: false,
|
|
294
|
+
showMI: true,
|
|
295
|
+
miCurveBeta: 1.5,
|
|
296
|
+
colors: {},
|
|
173
297
|
};
|
|
174
298
|
const configPath = path.join(os.homedir(), '.claude', 'statusline.conf');
|
|
175
299
|
|
|
@@ -192,6 +316,12 @@ show_delta=true
|
|
|
192
316
|
|
|
193
317
|
# Show session_id in status line
|
|
194
318
|
show_session=true
|
|
319
|
+
|
|
320
|
+
# Custom colors - use named colors or hex (#rrggbb)
|
|
321
|
+
# Available: color_green, color_yellow, color_red, color_blue, color_magenta, color_cyan
|
|
322
|
+
# Examples:
|
|
323
|
+
# color_green=#7dcfff
|
|
324
|
+
# color_red=#f7768e
|
|
195
325
|
`;
|
|
196
326
|
fs.writeFileSync(configPath, defaultConfig);
|
|
197
327
|
} catch (e) {
|
|
@@ -207,9 +337,10 @@ show_session=true
|
|
|
207
337
|
if (trimmed.startsWith('#') || !trimmed.includes('=')) {
|
|
208
338
|
continue;
|
|
209
339
|
}
|
|
210
|
-
const
|
|
211
|
-
const keyTrimmed =
|
|
212
|
-
const
|
|
340
|
+
const eqIdx = trimmed.indexOf('=');
|
|
341
|
+
const keyTrimmed = trimmed.slice(0, eqIdx).trim();
|
|
342
|
+
const rawValue = trimmed.slice(eqIdx + 1).trim();
|
|
343
|
+
const valueTrimmed = rawValue.toLowerCase();
|
|
213
344
|
if (keyTrimmed === 'autocompact') {
|
|
214
345
|
config.autocompact = valueTrimmed !== 'false';
|
|
215
346
|
} else if (keyTrimmed === 'token_detail') {
|
|
@@ -222,6 +353,18 @@ show_session=true
|
|
|
222
353
|
config.showIoTokens = valueTrimmed !== 'false';
|
|
223
354
|
} else if (keyTrimmed === 'reduced_motion') {
|
|
224
355
|
config.reducedMotion = valueTrimmed !== 'false';
|
|
356
|
+
} else if (keyTrimmed === 'show_mi') {
|
|
357
|
+
config.showMI = valueTrimmed !== 'false';
|
|
358
|
+
} else if (keyTrimmed === 'mi_curve_beta') {
|
|
359
|
+
const parsed = parseFloat(rawValue);
|
|
360
|
+
if (!isNaN(parsed)) {
|
|
361
|
+
config.miCurveBeta = parsed;
|
|
362
|
+
}
|
|
363
|
+
} else if (COLOR_CONFIG_KEYS[keyTrimmed]) {
|
|
364
|
+
const ansi = parseColor(rawValue);
|
|
365
|
+
if (ansi) {
|
|
366
|
+
config.colors[COLOR_CONFIG_KEYS[keyTrimmed]] = ansi;
|
|
367
|
+
}
|
|
225
368
|
}
|
|
226
369
|
}
|
|
227
370
|
} catch (e) {
|
|
@@ -250,9 +393,6 @@ process.stdin.on('end', () => {
|
|
|
250
393
|
const model = data.model?.display_name || 'Claude';
|
|
251
394
|
const dirName = path.basename(cwd) || '~';
|
|
252
395
|
|
|
253
|
-
// Git info
|
|
254
|
-
const gitInfo = getGitInfo(projectDir);
|
|
255
|
-
|
|
256
396
|
// Read settings from config file
|
|
257
397
|
const config = readConfig();
|
|
258
398
|
const autocompactEnabled = config.autocompact;
|
|
@@ -261,6 +401,18 @@ process.stdin.on('end', () => {
|
|
|
261
401
|
const showSession = config.showSession;
|
|
262
402
|
// Note: showIoTokens setting is read but not yet implemented
|
|
263
403
|
|
|
404
|
+
// Apply color overrides from config
|
|
405
|
+
const c = config.colors || {};
|
|
406
|
+
const cGreen = c.green || GREEN;
|
|
407
|
+
const cYellow = c.yellow || YELLOW;
|
|
408
|
+
const cRed = c.red || RED;
|
|
409
|
+
const cBlue = c.blue || BLUE;
|
|
410
|
+
const cMagenta = c.magenta || MAGENTA;
|
|
411
|
+
const cCyan = c.cyan || CYAN;
|
|
412
|
+
|
|
413
|
+
// Git info (pass configurable colors)
|
|
414
|
+
const gitInfo = getGitInfo(projectDir, cMagenta, cCyan);
|
|
415
|
+
|
|
264
416
|
// Extract session_id once for reuse
|
|
265
417
|
const sessionId = data.session_id;
|
|
266
418
|
|
|
@@ -268,7 +420,10 @@ process.stdin.on('end', () => {
|
|
|
268
420
|
let contextInfo = '';
|
|
269
421
|
let acInfo = '';
|
|
270
422
|
let deltaInfo = '';
|
|
423
|
+
let miInfo = '';
|
|
271
424
|
let sessionInfo = '';
|
|
425
|
+
const showMI = config.showMI;
|
|
426
|
+
const miCurveBeta = config.miCurveBeta;
|
|
272
427
|
const totalSize = data.context_window?.context_window_size || 0;
|
|
273
428
|
const currentUsage = data.context_window?.current_usage;
|
|
274
429
|
const totalInputTokens = data.context_window?.total_input_tokens || 0;
|
|
@@ -320,17 +475,17 @@ process.stdin.on('end', () => {
|
|
|
320
475
|
// Color based on free percentage
|
|
321
476
|
let ctxColor;
|
|
322
477
|
if (freePctInt > 50) {
|
|
323
|
-
ctxColor =
|
|
478
|
+
ctxColor = cGreen;
|
|
324
479
|
} else if (freePctInt > 25) {
|
|
325
|
-
ctxColor =
|
|
480
|
+
ctxColor = cYellow;
|
|
326
481
|
} else {
|
|
327
|
-
ctxColor =
|
|
482
|
+
ctxColor = cRed;
|
|
328
483
|
}
|
|
329
484
|
|
|
330
|
-
contextInfo = ` | ${ctxColor}${freeDisplay}
|
|
485
|
+
contextInfo = ` | ${ctxColor}${freeDisplay} (${freePct.toFixed(1)}%)${RESET}`;
|
|
331
486
|
|
|
332
|
-
//
|
|
333
|
-
if (showDelta) {
|
|
487
|
+
// Read previous entry if needed for delta OR MI
|
|
488
|
+
if (showDelta || showMI) {
|
|
334
489
|
const stateDir = path.join(os.homedir(), '.claude', 'statusline');
|
|
335
490
|
if (!fs.existsSync(stateDir)) {
|
|
336
491
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
@@ -360,10 +515,13 @@ process.stdin.on('end', () => {
|
|
|
360
515
|
const stateFile = path.join(stateDir, stateFileName);
|
|
361
516
|
let hasPrev = false;
|
|
362
517
|
let prevTokens = 0;
|
|
518
|
+
let prevOutputTokens = 0;
|
|
519
|
+
let prevLinesAdded = 0;
|
|
520
|
+
let prevLinesRemoved = 0;
|
|
363
521
|
try {
|
|
364
522
|
if (fs.existsSync(stateFile)) {
|
|
365
523
|
hasPrev = true;
|
|
366
|
-
// Read last line to get previous
|
|
524
|
+
// Read last line to get previous state
|
|
367
525
|
const content = fs.readFileSync(stateFile, 'utf8').trim();
|
|
368
526
|
const lines = content.split('\n');
|
|
369
527
|
const lastLine = lines[lines.length - 1];
|
|
@@ -376,6 +534,10 @@ process.stdin.on('end', () => {
|
|
|
376
534
|
const prevCacheCreation = parseInt(parts[5], 10) || 0;
|
|
377
535
|
const prevCacheRead = parseInt(parts[6], 10) || 0;
|
|
378
536
|
prevTokens = prevCurInput + prevCacheCreation + prevCacheRead;
|
|
537
|
+
// For MI productivity score
|
|
538
|
+
prevOutputTokens = parseInt(parts[2], 10) || 0;
|
|
539
|
+
prevLinesAdded = parseInt(parts[8], 10) || 0;
|
|
540
|
+
prevLinesRemoved = parseInt(parts[9], 10) || 0;
|
|
379
541
|
} else {
|
|
380
542
|
// Old format - single value
|
|
381
543
|
prevTokens = parseInt(lastLine, 10) || 0;
|
|
@@ -387,20 +549,40 @@ process.stdin.on('end', () => {
|
|
|
387
549
|
);
|
|
388
550
|
prevTokens = 0;
|
|
389
551
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
552
|
+
|
|
553
|
+
// Calculate and display token delta if enabled
|
|
554
|
+
if (showDelta) {
|
|
555
|
+
const delta = usedTokens - prevTokens;
|
|
556
|
+
if (hasPrev && delta > 0) {
|
|
557
|
+
const deltaDisplay = tokenDetail
|
|
558
|
+
? delta.toLocaleString('en-US')
|
|
559
|
+
: `${(delta / 1000).toFixed(1)}k`;
|
|
560
|
+
deltaInfo = ` ${DIM}[+${deltaDisplay}]${RESET}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Calculate and display MI score if enabled
|
|
565
|
+
if (showMI) {
|
|
566
|
+
let deltaLines, deltaOutput;
|
|
567
|
+
if (hasPrev) {
|
|
568
|
+
const deltaLA = linesAdded - prevLinesAdded;
|
|
569
|
+
const deltaLR = linesRemoved - prevLinesRemoved;
|
|
570
|
+
deltaLines = deltaLA + deltaLR;
|
|
571
|
+
deltaOutput = totalOutputTokens - prevOutputTokens;
|
|
572
|
+
} else {
|
|
573
|
+
deltaLines = 0;
|
|
574
|
+
deltaOutput = null;
|
|
575
|
+
}
|
|
576
|
+
const miResult = computeMI(
|
|
577
|
+
usedTokens, totalSize, cacheRead, usedTokens,
|
|
578
|
+
deltaLines, deltaOutput, miCurveBeta
|
|
579
|
+
);
|
|
580
|
+
const miColor = getMIColor(miResult.mi, cGreen, cYellow, cRed);
|
|
581
|
+
miInfo = ` ${miColor}MI:${miResult.mi.toFixed(2)}${RESET}`;
|
|
398
582
|
}
|
|
583
|
+
|
|
399
584
|
// Only append if context usage changed (avoid duplicates from multiple refreshes)
|
|
400
585
|
if (!hasPrev || usedTokens !== prevTokens) {
|
|
401
|
-
// Append current usage with comprehensive format
|
|
402
|
-
// Format: ts,total_in,total_out,cur_in,cur_out,cache_create,cache_read,
|
|
403
|
-
// cost_usd,lines_added,lines_removed,session_id,model_id,project_dir
|
|
404
586
|
try {
|
|
405
587
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
406
588
|
const curInputTokens = currentUsage.input_tokens || 0;
|
|
@@ -438,13 +620,13 @@ process.stdin.on('end', () => {
|
|
|
438
620
|
}
|
|
439
621
|
|
|
440
622
|
// Output: [Model] dir | branch [n] | free (%) [+delta] [AC] session
|
|
441
|
-
const base = `${DIM}[${model}]${RESET} ${
|
|
623
|
+
const base = `${DIM}[${model}]${RESET} ${cBlue}${dirName}${RESET}`;
|
|
442
624
|
const maxWidth = getTerminalWidth();
|
|
443
|
-
const parts = [base, gitInfo, contextInfo, deltaInfo, acInfo, sessionInfo];
|
|
625
|
+
const parts = [base, gitInfo, contextInfo, deltaInfo, miInfo, acInfo, sessionInfo];
|
|
444
626
|
console.log(fitToWidth(parts, maxWidth));
|
|
445
627
|
});
|
|
446
628
|
|
|
447
629
|
// Export for testing
|
|
448
630
|
if (typeof module !== 'undefined' && module.exports) {
|
|
449
|
-
module.exports = { maybeRotateStateFile, ROTATION_THRESHOLD, ROTATION_KEEP };
|
|
631
|
+
module.exports = { maybeRotateStateFile, ROTATION_THRESHOLD, ROTATION_KEEP, computeMI };
|
|
450
632
|
}
|