@kaynooo/cli-out 0.6.0 → 0.7.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 +3 -3
- package/dist/cli.js +162 -28
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ out --stats --since 7d
|
|
|
11
11
|
|
|
12
12
|
## Behaviour
|
|
13
13
|
|
|
14
|
-
- Output of at most 150 lines (`-n <lines>` changes it) passes through byte for byte, stdout and stderr to their own streams.
|
|
14
|
+
- Output of at most 150 lines (`-n <lines>` changes it) and about 8 KB (55 bytes per allowed line) passes through byte for byte, stdout and stderr to their own streams.
|
|
15
15
|
- Longer output is captured (both streams, in arrival order) and printed folded, after a header with the line count, how many lines were left out and the path of the full log.
|
|
16
16
|
- Lines alike in shape (ANSI codes stripped; numbers, hashes, durations, percentages, paths and file names normalized) fold into one line with a count and the last line: `[1/300] compiling a.ts (x300, last: [300/300] compiling z.ts)`.
|
|
17
17
|
- A block of 2 to 20 lines repeated back to back is shown once, followed by `(block repeated N times)`.
|
|
18
18
|
- A line or block seen earlier is replaced by `(same as line N)`, N being a line of the full log, and the header lists the most repeated messages with their counts.
|
|
19
19
|
- Carriage-return progress (spinners, bars) keeps only the final state of each line.
|
|
20
|
-
- The head and tail are kept, and lines with `error`, `fail`, `warn`, `panic`, `exception`, `fatal` or a stack frame stay even in the folded middle.
|
|
20
|
+
- The head and tail are kept, and lines with `error`, `fail`, `warn`, `panic`, `exception`, `fatal` or a stack frame stay even in the folded middle, the first 40 of them. Lines wider than 300 characters are cut in the folded output (the full log keeps them), and a few-lines output that is still heavy keeps a shorter head and tail.
|
|
21
21
|
- The exit code is always the wrapped command's. Piped into with no command, `out` folds what it reads and exits 0, the writer's code left to `set -o pipefail`; only stdout is piped, so `2>&1` brings stderr along. A terminal or `/dev/null` on stdin with no command is a usage error.
|
|
22
22
|
- The output is streamed to a full log as it arrives once it outgrows the 4 MiB held in memory (the first and last 2 MiB), otherwise the log is written when folding. Logs go to `$XDG_RUNTIME_DIR/kaynooo-cli/out` (`~/.cache/kaynooo-cli/out` without it), a folder only the user can enter, files at mode 0600, and logs older than a day are removed on the next run.
|
|
23
23
|
- SIGINT, SIGTERM and SIGHUP go to the command's whole process group, and `out` exits with its code. A watch command (`--watch`, `tsc -w`, `just dev`) is never wrapped by the hook, as its output would sit buffered.
|
|
@@ -26,7 +26,7 @@ out --stats --since 7d
|
|
|
26
26
|
|
|
27
27
|
- Each run appends a line to `$XDG_STATE_HOME/kaynooo-cli/out/stats.jsonl` (`~/.local/state/...` without it, folder 0700, file 0600): the time, the folder, the words naming the command, the bytes it printed, the bytes the session would have read without `out` and the bytes `out` showed. Only lowercase words without digits after the program are kept (`bun run build`, `docker compose exec`), so no argument, assignment or token reaches the file; a `sh -c` chain is named by its last command that is not a `cd`.
|
|
28
28
|
- `--was '<stage>'` (repeatable, in pipeline order) names a `tail`, `head` or `grep` stage `out` stands in for, as the hook passes them when it drops one: the session would have read what that stage printed, so the saving counts against it. Output that folding would not make shorter is printed untouched, so `out` never shows more than the command printed. A stage it cannot read (`tail -f`, a long grep option it does not know) counts the whole output.
|
|
29
|
-
- `out --stats [--since 7d] [--json]` prints the runs, how many were folded, the bytes printed and shown, and about how many tokens were saved (bytes / 4, an estimate), then the ten commands that saved the most.
|
|
29
|
+
- `out --stats [--since 7d] [--json]` prints the runs, how many were folded, the bytes printed and shown, and about how many tokens were saved (bytes / 4, an estimate), then the ten commands that saved the most, with the share of runs folded and, per command, the median share of the output a folded run cut.
|
|
30
30
|
|
|
31
31
|
## Wrapping from a hook
|
|
32
32
|
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import process2 from "process";
|
|
|
15
15
|
// ../kit/src/style.ts
|
|
16
16
|
import process from "process";
|
|
17
17
|
|
|
18
|
-
// ../../node_modules/.bun/@kaynooo+utils@2.31.
|
|
18
|
+
// ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-mtetydvc.js
|
|
19
19
|
function x(s) {
|
|
20
20
|
let r = s.startsWith("#") ? s.slice(1).toLowerCase() : s.toLowerCase();
|
|
21
21
|
if (!/^[a-f0-9]+$/.test(r))
|
|
@@ -235,7 +235,7 @@ class e {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
// ../../node_modules/.bun/@kaynooo+utils@2.31.
|
|
238
|
+
// ../../node_modules/.bun/@kaynooo+utils@2.31.2+d0769e28dc4dad16/node_modules/@kaynooo/utils/dist/chunk-y1msbssb.js
|
|
239
239
|
var r = 39;
|
|
240
240
|
var o = 49;
|
|
241
241
|
var f2 = "\x1B[";
|
|
@@ -402,8 +402,12 @@ class HelpShown extends Exit {
|
|
|
402
402
|
|
|
403
403
|
class Completing {
|
|
404
404
|
options;
|
|
405
|
-
|
|
405
|
+
positionals;
|
|
406
|
+
candidates;
|
|
407
|
+
constructor(options, positionals = [], candidates = {}) {
|
|
406
408
|
this.options = options;
|
|
409
|
+
this.positionals = positionals;
|
|
410
|
+
this.candidates = candidates;
|
|
407
411
|
}
|
|
408
412
|
}
|
|
409
413
|
var completion = { active: false };
|
|
@@ -446,8 +450,10 @@ function exampleRows(examples, colors = paint(process3.stdout)) {
|
|
|
446
450
|
function optionRows(options, colors = paint(process3.stdout)) {
|
|
447
451
|
const rows = Object.entries(options).map(([name, option]) => {
|
|
448
452
|
const short = option.short === undefined ? " " : `${colors.cyan(`-${option.short}`)}, `;
|
|
449
|
-
const
|
|
450
|
-
|
|
453
|
+
const named = option.choices === undefined ? "<value>" : `<${option.choices.join("|")}>`;
|
|
454
|
+
const placeholder = option.optional === undefined ? named : `[${named}]`;
|
|
455
|
+
const value = option.type === "string" ? ` ${colors.dim(placeholder)}` : "";
|
|
456
|
+
return { flag: `${short}${colors.cyan(`--${name}`)}${value}`, width: `-x, --${name}`.length + (option.type === "string" ? placeholder.length + 1 : 0), description: option.description ?? "" };
|
|
451
457
|
});
|
|
452
458
|
const width = Math.max(0, ...rows.map((row) => row.width));
|
|
453
459
|
return rows.map((row) => row.description === "" ? ` ${row.flag}` : ` ${row.flag}${" ".repeat(width - row.width)} ${colors.dim(row.description)}`);
|
|
@@ -473,14 +479,13 @@ function applyOutput(color, plain, env = process3.env) {
|
|
|
473
479
|
function parse(args, usage, options, settings = {}) {
|
|
474
480
|
const output = Object.fromEntries(Object.entries(OUTPUT_OPTIONS).filter(([name]) => !(name in options)));
|
|
475
481
|
const all = { ...options, ...output, help: { type: "boolean", short: "h", description: "show this help" } };
|
|
482
|
+
const listing = listed(all, settings.allowNegative === true);
|
|
476
483
|
if (completion.active)
|
|
477
|
-
throw new Completing(
|
|
478
|
-
const
|
|
479
|
-
const own = separator === -1 ? args : args.slice(0, separator);
|
|
480
|
-
const rest = separator === -1 ? undefined : args.slice(separator + 1);
|
|
484
|
+
throw new Completing(listing, settings.positionals, settings.candidates);
|
|
485
|
+
const { own, rest } = settings.trailing === true ? splitTrailing(args, all, settings.positionals?.length ?? 0) : splitRest(args, settings.rest === true);
|
|
481
486
|
let parsed;
|
|
482
487
|
try {
|
|
483
|
-
parsed = parseArgs({ args: joinDashValues(own, all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
488
|
+
parsed = parseArgs({ args: joinDashValues(withBare(own, all), all), options: all, allowPositionals: true, allowNegative: settings.allowNegative === true, strict: true });
|
|
484
489
|
} catch (error) {
|
|
485
490
|
const message = parseError(error);
|
|
486
491
|
throw new UsageError(message, nearestOption(message, all, settings.allowNegative === true));
|
|
@@ -488,12 +493,84 @@ function parse(args, usage, options, settings = {}) {
|
|
|
488
493
|
const values = parsed.values;
|
|
489
494
|
applyOutput("color" in output ? values.color : undefined, "plain" in output ? values.plain : undefined);
|
|
490
495
|
if (values.help === true) {
|
|
491
|
-
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage,
|
|
496
|
+
console.log(wantsHelpJson() ? JSON.stringify(description(usage, settings)) : helpText(usage, listing, undefined, settings));
|
|
492
497
|
throw new HelpShown;
|
|
493
498
|
}
|
|
499
|
+
checkChoices(parsed.values, all);
|
|
500
|
+
checkConflicts(parsed.values, all);
|
|
494
501
|
if (settings.positionals !== undefined)
|
|
495
502
|
checkPositionals(parsed.positionals, settings.positionals);
|
|
496
|
-
return { ...parsed, positionals: parsed.positionals, rest };
|
|
503
|
+
return { ...parsed, values: parsed.values, positionals: parsed.positionals, rest };
|
|
504
|
+
}
|
|
505
|
+
function splitRest(args, passes) {
|
|
506
|
+
const separator = passes ? args.indexOf("--") : -1;
|
|
507
|
+
return { own: separator === -1 ? args : args.slice(0, separator), rest: separator === -1 ? undefined : args.slice(separator + 1) };
|
|
508
|
+
}
|
|
509
|
+
function splitTrailing(args, options, count) {
|
|
510
|
+
const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
|
|
511
|
+
let seen = 0;
|
|
512
|
+
if (count === 0)
|
|
513
|
+
return { own: args, rest: undefined };
|
|
514
|
+
for (let at = 0;at < args.length; at++) {
|
|
515
|
+
const word = args[at];
|
|
516
|
+
if (word === "--")
|
|
517
|
+
break;
|
|
518
|
+
if (word.startsWith("-")) {
|
|
519
|
+
if (takesValue(word))
|
|
520
|
+
at++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
seen++;
|
|
524
|
+
if (seen === count)
|
|
525
|
+
return { own: args.slice(0, at + 1), rest: args.slice(at + 1) };
|
|
526
|
+
}
|
|
527
|
+
return { own: args, rest: undefined };
|
|
528
|
+
}
|
|
529
|
+
function listed(options, allowNegative) {
|
|
530
|
+
if (!allowNegative)
|
|
531
|
+
return options;
|
|
532
|
+
return Object.fromEntries(Object.entries(options).flatMap(([name, option]) => {
|
|
533
|
+
if (option.type !== "boolean" || option.negative === undefined)
|
|
534
|
+
return [[name, option]];
|
|
535
|
+
return [...option.description === undefined ? [] : [[name, option]], [`no-${name}`, { type: "boolean", description: option.negative }]];
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
function withBare(args, options) {
|
|
539
|
+
const bare = new Map(Object.entries(options).flatMap(([name, { short, optional }]) => optional === undefined ? [] : [[`--${name}`, optional], ...short === undefined ? [] : [[`-${short}`, optional]]]));
|
|
540
|
+
if (bare.size === 0)
|
|
541
|
+
return args;
|
|
542
|
+
const separator = args.indexOf("--");
|
|
543
|
+
const own = separator === -1 ? args : args.slice(0, separator);
|
|
544
|
+
return [...own.flatMap((arg, index) => {
|
|
545
|
+
const optional = bare.get(arg);
|
|
546
|
+
const next = own[index + 1];
|
|
547
|
+
const value = next !== undefined && (optional?.accepts === undefined ? !next.startsWith("-") : optional.accepts.test(next));
|
|
548
|
+
return optional === undefined || value ? [arg] : [arg, optional.bare];
|
|
549
|
+
}), ...separator === -1 ? [] : args.slice(separator)];
|
|
550
|
+
}
|
|
551
|
+
function checkConflicts(values, options) {
|
|
552
|
+
const given = (name) => values[name] !== undefined && values[name] !== false;
|
|
553
|
+
for (const [name, { conflicts = [] }] of Object.entries(options)) {
|
|
554
|
+
const other = given(name) ? conflicts.find(given) : undefined;
|
|
555
|
+
if (other !== undefined)
|
|
556
|
+
badUsage(`--${name} cannot be used with --${other}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function checkChoices(values, options) {
|
|
560
|
+
for (const [name, option] of Object.entries(options)) {
|
|
561
|
+
if (option.choices === undefined)
|
|
562
|
+
continue;
|
|
563
|
+
const given = values[name];
|
|
564
|
+
for (const value of Array.isArray(given) ? given : [given]) {
|
|
565
|
+
if (typeof value === "string")
|
|
566
|
+
oneOf(value, name, option.choices);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function oneOf(value, name, choices) {
|
|
571
|
+
if (!choices.includes(value))
|
|
572
|
+
badUsage(`${name.startsWith("<") ? name : `--${name}`} takes one of ${choices.join(", ")}, not ${value}`);
|
|
573
|
+
return value;
|
|
497
574
|
}
|
|
498
575
|
function checkPositionals(given, names) {
|
|
499
576
|
const variadic = names.at(-1)?.replace("]", "").endsWith("...") === true;
|
|
@@ -727,22 +804,55 @@ function reportThrown(prefix, error, redact = (text) => text, json = false) {
|
|
|
727
804
|
}
|
|
728
805
|
var COMPLETE = "__complete";
|
|
729
806
|
var FILES = "__files__";
|
|
730
|
-
async function
|
|
807
|
+
async function completingOf(run) {
|
|
731
808
|
completion.active = true;
|
|
732
809
|
try {
|
|
733
810
|
await run([]);
|
|
734
811
|
} catch (error) {
|
|
735
812
|
if (error instanceof Completing)
|
|
736
|
-
return error
|
|
813
|
+
return error;
|
|
737
814
|
} finally {
|
|
738
815
|
completion.active = false;
|
|
739
816
|
}
|
|
740
|
-
return {};
|
|
817
|
+
return new Completing({});
|
|
818
|
+
}
|
|
819
|
+
function candidateLine(candidate) {
|
|
820
|
+
return typeof candidate === "string" ? candidate : candidate.join("\t");
|
|
821
|
+
}
|
|
822
|
+
function positionalBeingTyped(before, { options, positionals }) {
|
|
823
|
+
const takesValue = (word) => !word.includes("=") && Object.entries(options).some(([name, { short, type }]) => type === "string" && (word === `--${name}` || word === `-${short}`));
|
|
824
|
+
let index = 0;
|
|
825
|
+
let at = 0;
|
|
826
|
+
while (at < before.length) {
|
|
827
|
+
const word = before[at];
|
|
828
|
+
if (word === "--") {
|
|
829
|
+
index += before.length - at - 1;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
if (!word.startsWith("-"))
|
|
833
|
+
index++;
|
|
834
|
+
else if (takesValue(word))
|
|
835
|
+
at++;
|
|
836
|
+
at++;
|
|
837
|
+
}
|
|
838
|
+
const last = positionals.at(-1);
|
|
839
|
+
if (index < positionals.length)
|
|
840
|
+
return positionals[index];
|
|
841
|
+
return last?.replace("]", "").endsWith("...") === true ? last : undefined;
|
|
741
842
|
}
|
|
742
843
|
async function leafCandidates(run, words) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
844
|
+
const completing = await completingOf(run);
|
|
845
|
+
if ((words.at(-1) ?? "").startsWith("-"))
|
|
846
|
+
return Object.entries(completing.options).map(([name, option]) => option.description === undefined ? `--${name}` : `--${name} ${option.description}`);
|
|
847
|
+
const flag = words.at(-2);
|
|
848
|
+
const option = flag?.startsWith("-") === true ? Object.entries(completing.options).find(([name, { short }]) => flag === `--${name}` || flag === `-${short}`)?.[1] : undefined;
|
|
849
|
+
if (option?.type === "string") {
|
|
850
|
+
const values = option.choices === undefined ? await option.candidates?.() : option.choices;
|
|
851
|
+
return values === undefined ? [FILES] : values.map(candidateLine);
|
|
852
|
+
}
|
|
853
|
+
const name = positionalBeingTyped(words.slice(0, -1), completing);
|
|
854
|
+
const values = name === undefined ? undefined : await completing.candidates[name]?.();
|
|
855
|
+
return values === undefined ? [FILES] : values.map(candidateLine);
|
|
746
856
|
}
|
|
747
857
|
async function printCandidates(found) {
|
|
748
858
|
for (const line of await found)
|
|
@@ -819,7 +929,7 @@ var {Glob } = globalThis.Bun;
|
|
|
819
929
|
var SESSION_FILES = new Glob("*.json");
|
|
820
930
|
var ACCOUNT_DIRS = new Glob("*/sessions");
|
|
821
931
|
// package.json
|
|
822
|
-
var version = "0.
|
|
932
|
+
var version = "0.7.0";
|
|
823
933
|
|
|
824
934
|
// src/shape.ts
|
|
825
935
|
var HEX_LITERAL = /\b0x[0-9a-f]+\b/gi;
|
|
@@ -844,6 +954,9 @@ var HEAD = 40;
|
|
|
844
954
|
var TAIL = 60;
|
|
845
955
|
var MAX_BLOCK = 20;
|
|
846
956
|
var MIN_REFERENCED = 20;
|
|
957
|
+
var BYTES_PER_LINE = 55;
|
|
958
|
+
var MAX_WIDTH = 300;
|
|
959
|
+
var MAX_KEPT_IMPORTANT = 40;
|
|
847
960
|
var TOP_REPEATED = 3;
|
|
848
961
|
var LINE_BREAK = /\r\n|\r|\n/;
|
|
849
962
|
var LINE_END = /(?:\r\n|\r|\n)$/;
|
|
@@ -854,7 +967,10 @@ function lineCount(text) {
|
|
|
854
967
|
return LINE_END.test(text) ? count - 1 : count;
|
|
855
968
|
}
|
|
856
969
|
function shouldFold(text, threshold) {
|
|
857
|
-
return lineCount(text) > threshold;
|
|
970
|
+
return lineCount(text) > threshold || Buffer.byteLength(text) > threshold * BYTES_PER_LINE;
|
|
971
|
+
}
|
|
972
|
+
function cutWide(line) {
|
|
973
|
+
return line.length > MAX_WIDTH ? `${line.slice(0, MAX_WIDTH)}... [+${line.length - MAX_WIDTH} chars]` : line;
|
|
858
974
|
}
|
|
859
975
|
function finalState(line) {
|
|
860
976
|
const segments = line.split("\r").filter((segment) => segment !== "");
|
|
@@ -948,11 +1064,16 @@ function toEntries(units) {
|
|
|
948
1064
|
function fold(text, { threshold, logPath }) {
|
|
949
1065
|
const lines = splitLines(text);
|
|
950
1066
|
const { entries, references, repeated } = toEntries(toUnits(lines, lines.map((line) => shape(line))));
|
|
951
|
-
|
|
952
|
-
|
|
1067
|
+
for (const entry of entries)
|
|
1068
|
+
entry.text = cutWide(entry.text);
|
|
1069
|
+
const heavy = entries.reduce((sum, { text }) => sum + text.length + 1, 0) > threshold * BYTES_PER_LINE;
|
|
1070
|
+
const shrink = heavy && entries.length <= threshold ? 2 : 1;
|
|
1071
|
+
const head = Math.floor(Math.min(HEAD, Math.floor(threshold / 3)) / shrink);
|
|
1072
|
+
const tail = Math.floor(Math.min(TAIL, Math.floor(threshold / 2)) / shrink);
|
|
953
1073
|
const out = [];
|
|
954
1074
|
let dropped = 0;
|
|
955
|
-
|
|
1075
|
+
let kept = 0;
|
|
1076
|
+
if (entries.length <= threshold && !heavy) {
|
|
956
1077
|
out.push(...entries.map(({ text }) => text));
|
|
957
1078
|
} else {
|
|
958
1079
|
const middleEnd = entries.length - tail;
|
|
@@ -969,7 +1090,8 @@ function fold(text, { threshold, logPath }) {
|
|
|
969
1090
|
out.push(entry.text);
|
|
970
1091
|
continue;
|
|
971
1092
|
}
|
|
972
|
-
if (entry.important) {
|
|
1093
|
+
if (entry.important && kept < MAX_KEPT_IMPORTANT) {
|
|
1094
|
+
kept++;
|
|
973
1095
|
closeGap();
|
|
974
1096
|
out.push(entry.text);
|
|
975
1097
|
continue;
|
|
@@ -1307,29 +1429,41 @@ async function readEntries(path = statsPath(), since = 0) {
|
|
|
1307
1429
|
}
|
|
1308
1430
|
});
|
|
1309
1431
|
}
|
|
1432
|
+
function median(values) {
|
|
1433
|
+
const sorted = values.toSorted((a, b) => a - b);
|
|
1434
|
+
const middle = Math.floor(sorted.length / 2);
|
|
1435
|
+
if (sorted.length === 0)
|
|
1436
|
+
return;
|
|
1437
|
+
return sorted.length % 2 === 1 ? sorted[middle] : ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
1438
|
+
}
|
|
1310
1439
|
function tokens(bytes) {
|
|
1311
1440
|
return Math.round(bytes / BYTES_PER_TOKEN);
|
|
1312
1441
|
}
|
|
1313
1442
|
function formatNumber(value) {
|
|
1314
1443
|
return value.toLocaleString("en-US");
|
|
1315
1444
|
}
|
|
1445
|
+
function cut2(runs) {
|
|
1446
|
+
const value = median(runs.filter((run) => run.folded && run.baseline > 0).map((run) => (1 - run.shown / run.baseline) * 100));
|
|
1447
|
+
return value === undefined ? undefined : Math.round(value);
|
|
1448
|
+
}
|
|
1316
1449
|
function summarize(entries) {
|
|
1317
1450
|
const commands = Map.groupBy(entries, (entry) => entry.command).entries().map(([command, runs]) => ({
|
|
1318
1451
|
command,
|
|
1319
1452
|
runs: runs.length,
|
|
1320
1453
|
folded: runs.filter((run) => run.folded).length,
|
|
1321
|
-
tokensSaved: tokens(runs.reduce((sum, run) => sum + run.baseline - run.shown, 0))
|
|
1454
|
+
tokensSaved: tokens(runs.reduce((sum, run) => sum + run.baseline - run.shown, 0)),
|
|
1455
|
+
medianCut: cut2(runs)
|
|
1322
1456
|
}));
|
|
1323
1457
|
const sum = (field) => entries.reduce((total, entry) => total + entry[field], 0);
|
|
1324
|
-
return { runs: entries.length, folded: entries.filter((entry) => entry.folded).length, bytes: sum("bytes"), shown: sum("shown"), tokensSaved: tokens(sum("baseline") - sum("shown")), commands: [...commands].sort((a, b) => b.tokensSaved - a.tokensSaved) };
|
|
1458
|
+
return { runs: entries.length, folded: entries.filter((entry) => entry.folded).length, bytes: sum("bytes"), shown: sum("shown"), tokensSaved: tokens(sum("baseline") - sum("shown")), foldedShare: entries.length === 0 ? 0 : Math.round(entries.filter((entry) => entry.folded).length / entries.length * 100), commands: [...commands].sort((a, b) => b.tokensSaved - a.tokensSaved) };
|
|
1325
1459
|
}
|
|
1326
1460
|
function renderSummary(summary, period) {
|
|
1327
1461
|
if (summary.runs === 0)
|
|
1328
1462
|
return `no run of out recorded ${period}`;
|
|
1329
1463
|
const top = summary.commands.slice(0, TOP).filter((each) => each.tokensSaved !== 0);
|
|
1330
1464
|
return [
|
|
1331
|
-
`${formatNumber(summary.runs)} runs ${period}, ${formatNumber(summary.folded)} folded: ${formatNumber(summary.bytes)} bytes printed, ${formatNumber(summary.shown)} shown, about ${formatNumber(summary.tokensSaved)} tokens saved (bytes / ${BYTES_PER_TOKEN})`,
|
|
1332
|
-
...top.length === 0 ? [] : ["", table([["command", "runs", "folded", "tokens saved"], ...top.map((each) => [each.command, formatNumber(each.runs), formatNumber(each.folded), formatNumber(each.tokensSaved)])])]
|
|
1465
|
+
`${formatNumber(summary.runs)} runs ${period}, ${formatNumber(summary.folded)} folded (${summary.foldedShare}%): ${formatNumber(summary.bytes)} bytes printed, ${formatNumber(summary.shown)} shown, about ${formatNumber(summary.tokensSaved)} tokens saved (bytes / ${BYTES_PER_TOKEN})`,
|
|
1466
|
+
...top.length === 0 ? [] : ["", table([["command", "runs", "folded", "median cut", "tokens saved"], ...top.map((each) => [each.command, formatNumber(each.runs), formatNumber(each.folded), each.medianCut === undefined ? "-" : `${each.medianCut}%`, formatNumber(each.tokensSaved)])])]
|
|
1333
1467
|
].join(`
|
|
1334
1468
|
`);
|
|
1335
1469
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/cli-out",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "out: shortens noisy command output for Claude Code, folding long logs and keeping errors, with the full log saved to a file.",
|
|
6
6
|
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"out": "$npm_execpath src/cli.ts"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@kaynooo/cli-kit": "0.
|
|
31
|
+
"@kaynooo/cli-kit": "0.18.0"
|
|
32
32
|
}
|
|
33
33
|
}
|