logstrip 1.0.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/LICENSE +21 -0
- package/README.md +341 -0
- package/action.yml +19 -0
- package/dist/action/index.d.ts +5 -0
- package/dist/action/index.js +26980 -0
- package/dist/cli/index.d.ts +31 -0
- package/dist/cli/index.js +224 -0
- package/dist/core/logstrip-config.d.ts +19 -0
- package/dist/core/logstrip-config.js +268 -0
- package/dist/core/logstrip-parser.d.ts +58 -0
- package/dist/core/logstrip-parser.js +1302 -0
- package/package.json +86 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
import { type Aggressiveness, type LogStripResult } from '../core/logstrip-parser';
|
|
4
|
+
export declare const CLI_VERSION = "1.0.0";
|
|
5
|
+
export declare const HELP_TEXT = "Usage: logstrip [INPUT] [options]\n\nStream-based log compression that trims noisy server logs, build\npipelines, vulnerability scanners, and container workloads down to the\ndiagnostic context an LLM actually needs.\n\nArguments:\n INPUT Path to the raw log. When omitted, reads from stdin.\n\nOptions:\n -o, --output <path> Write the compressed log to <path>. Defaults to stdout.\n -a, --aggressiveness <l> Compression preset: low | medium | high | aggressive.\n Default: high.\n -s, --stats Print compression statistics to stderr.\n -j, --json Print LogStripResult as JSON to stdout. Requires --output.\n --config <path> Path to .logstrip.yml config file. Auto-detects from cwd.\n -h, --help Show this help text and exit.\n -v, --version Print the CLI version and exit.\n\nExamples:\n logstrip raw.log -o clean.log\n cat raw.log | logstrip > clean.log\n logstrip raw.log --stats > clean.log\n logstrip raw.log -o clean.log --json\n";
|
|
6
|
+
export interface CliOptions {
|
|
7
|
+
input?: string;
|
|
8
|
+
output?: string;
|
|
9
|
+
aggressiveness: Aggressiveness;
|
|
10
|
+
stats: boolean;
|
|
11
|
+
json: boolean;
|
|
12
|
+
config?: string;
|
|
13
|
+
help: boolean;
|
|
14
|
+
version: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface CliIo {
|
|
17
|
+
stdin: NodeJS.ReadableStream;
|
|
18
|
+
stdout: NodeJS.WritableStream;
|
|
19
|
+
stderr: NodeJS.WritableStream;
|
|
20
|
+
stdinIsTTY: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class CliError extends Error {
|
|
23
|
+
readonly exitCode: number;
|
|
24
|
+
constructor(message: string, exitCode?: number);
|
|
25
|
+
}
|
|
26
|
+
export declare function messageOf(error: unknown): string;
|
|
27
|
+
export declare function parseCliOptions(argv: readonly string[]): CliOptions;
|
|
28
|
+
export declare function formatStats(result: LogStripResult): string;
|
|
29
|
+
export declare function writeAll(stream: NodeJS.WritableStream, chunk: string): Promise<void>;
|
|
30
|
+
export declare function endStream(stream: Writable): Promise<void>;
|
|
31
|
+
export declare function runCli(argv: readonly string[], io: CliIo): Promise<number>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.CliError = exports.HELP_TEXT = exports.CLI_VERSION = void 0;
|
|
5
|
+
exports.messageOf = messageOf;
|
|
6
|
+
exports.parseCliOptions = parseCliOptions;
|
|
7
|
+
exports.formatStats = formatStats;
|
|
8
|
+
exports.writeAll = writeAll;
|
|
9
|
+
exports.endStream = endStream;
|
|
10
|
+
exports.runCli = runCli;
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_util_1 = require("node:util");
|
|
13
|
+
const logstrip_parser_1 = require("../core/logstrip-parser");
|
|
14
|
+
exports.CLI_VERSION = '1.0.0'; // x-release-please-version
|
|
15
|
+
exports.HELP_TEXT = `Usage: logstrip [INPUT] [options]
|
|
16
|
+
|
|
17
|
+
Stream-based log compression that trims noisy server logs, build
|
|
18
|
+
pipelines, vulnerability scanners, and container workloads down to the
|
|
19
|
+
diagnostic context an LLM actually needs.
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
INPUT Path to the raw log. When omitted, reads from stdin.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
-o, --output <path> Write the compressed log to <path>. Defaults to stdout.
|
|
26
|
+
-a, --aggressiveness <l> Compression preset: low | medium | high | aggressive.
|
|
27
|
+
Default: high.
|
|
28
|
+
-s, --stats Print compression statistics to stderr.
|
|
29
|
+
-j, --json Print LogStripResult as JSON to stdout. Requires --output.
|
|
30
|
+
--config <path> Path to .logstrip.yml config file. Auto-detects from cwd.
|
|
31
|
+
-h, --help Show this help text and exit.
|
|
32
|
+
-v, --version Print the CLI version and exit.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
logstrip raw.log -o clean.log
|
|
36
|
+
cat raw.log | logstrip > clean.log
|
|
37
|
+
logstrip raw.log --stats > clean.log
|
|
38
|
+
logstrip raw.log -o clean.log --json
|
|
39
|
+
`;
|
|
40
|
+
class CliError extends Error {
|
|
41
|
+
exitCode;
|
|
42
|
+
constructor(message, exitCode = 1) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = 'CliError';
|
|
45
|
+
this.exitCode = exitCode;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.CliError = CliError;
|
|
49
|
+
function messageOf(error) {
|
|
50
|
+
return error instanceof Error ? error.message : String(error);
|
|
51
|
+
}
|
|
52
|
+
function parseCliOptions(argv) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = (0, node_util_1.parseArgs)({
|
|
56
|
+
args: argv,
|
|
57
|
+
allowPositionals: true,
|
|
58
|
+
strict: true,
|
|
59
|
+
options: {
|
|
60
|
+
output: { type: 'string', short: 'o' },
|
|
61
|
+
aggressiveness: { type: 'string', short: 'a' },
|
|
62
|
+
stats: { type: 'boolean', short: 's', default: false },
|
|
63
|
+
json: { type: 'boolean', short: 'j', default: false },
|
|
64
|
+
config: { type: 'string' },
|
|
65
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
66
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new CliError(messageOf(error), 2);
|
|
72
|
+
}
|
|
73
|
+
if (parsed.positionals.length > 1) {
|
|
74
|
+
throw new CliError(`Unexpected positional argument: ${parsed.positionals[1]}`, 2);
|
|
75
|
+
}
|
|
76
|
+
const aggressivenessInput = typeof parsed.values.aggressiveness === 'string'
|
|
77
|
+
? parsed.values.aggressiveness
|
|
78
|
+
: 'high';
|
|
79
|
+
let aggressiveness;
|
|
80
|
+
try {
|
|
81
|
+
aggressiveness = (0, logstrip_parser_1.parseAggressiveness)(aggressivenessInput);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw new CliError(messageOf(error), 2);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
input: parsed.positionals[0],
|
|
88
|
+
output: typeof parsed.values.output === 'string' ? parsed.values.output : undefined,
|
|
89
|
+
aggressiveness,
|
|
90
|
+
stats: parsed.values.stats === true,
|
|
91
|
+
json: parsed.values.json === true,
|
|
92
|
+
config: typeof parsed.values.config === 'string'
|
|
93
|
+
? parsed.values.config
|
|
94
|
+
: undefined,
|
|
95
|
+
help: parsed.values.help === true,
|
|
96
|
+
version: parsed.values.version === true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function formatStats(result) {
|
|
100
|
+
const lines = [
|
|
101
|
+
'LogStrip compression report',
|
|
102
|
+
` input lines : ${result.stats.inputLines}`,
|
|
103
|
+
` output lines : ${result.stats.outputLines}`,
|
|
104
|
+
` dropped lines : ${result.stats.droppedLines}`,
|
|
105
|
+
` duplicate lines : ${result.stats.duplicateLines}`,
|
|
106
|
+
` hidden internal : ${result.stats.hiddenInternalStackLines}`,
|
|
107
|
+
` input tokens : ${result.inputTokens}`,
|
|
108
|
+
` output tokens : ${result.outputTokens}`,
|
|
109
|
+
` saved tokens : ${result.savedTokens}`,
|
|
110
|
+
` savings : ${result.savingsPercent.toFixed(2)}%`,
|
|
111
|
+
];
|
|
112
|
+
if (result.outputPath !== undefined) {
|
|
113
|
+
lines.push(` output path : ${result.outputPath}`);
|
|
114
|
+
}
|
|
115
|
+
return `${lines.join('\n')}\n`;
|
|
116
|
+
}
|
|
117
|
+
function writeAll(stream, chunk) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
stream.write(chunk, (error) => {
|
|
120
|
+
if (error) {
|
|
121
|
+
reject(error);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
resolve();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function endStream(stream) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
stream.end((error) => {
|
|
132
|
+
if (error) {
|
|
133
|
+
reject(error);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function runCli(argv, io) {
|
|
142
|
+
let options;
|
|
143
|
+
try {
|
|
144
|
+
options = parseCliOptions(argv);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const cliError = error;
|
|
148
|
+
await writeAll(io.stderr, `logstrip: ${cliError.message}\n`);
|
|
149
|
+
return cliError.exitCode;
|
|
150
|
+
}
|
|
151
|
+
if (options.help) {
|
|
152
|
+
await writeAll(io.stdout, exports.HELP_TEXT);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
if (options.version) {
|
|
156
|
+
await writeAll(io.stdout, `${exports.CLI_VERSION}\n`);
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
if (options.json && options.output === undefined) {
|
|
160
|
+
await writeAll(io.stderr, 'logstrip: --json requires --output so the compressed log does not collide with the JSON report on stdout\n');
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
if (options.input === undefined && io.stdinIsTTY) {
|
|
164
|
+
await writeAll(io.stderr, 'logstrip: no INPUT given and stdin is a terminal. Pass a file path or pipe a log.\n');
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
if (options.input !== undefined &&
|
|
168
|
+
options.output !== undefined &&
|
|
169
|
+
(0, logstrip_parser_1.pathsReferToSameFile)(options.input, options.output)) {
|
|
170
|
+
await writeAll(io.stderr, 'logstrip: INPUT and --output must be different paths; refusing to overwrite the input log\n');
|
|
171
|
+
return 2;
|
|
172
|
+
}
|
|
173
|
+
const input = options.input !== undefined
|
|
174
|
+
? (0, node_fs_1.createReadStream)(options.input, { encoding: 'utf8' })
|
|
175
|
+
: io.stdin;
|
|
176
|
+
const output = options.output !== undefined
|
|
177
|
+
? (0, node_fs_1.createWriteStream)(options.output, { encoding: 'utf8' })
|
|
178
|
+
: io.stdout;
|
|
179
|
+
let result;
|
|
180
|
+
try {
|
|
181
|
+
result = await (0, logstrip_parser_1.processLogStream)(input, output, {
|
|
182
|
+
aggressiveness: options.aggressiveness,
|
|
183
|
+
configPath: options.config,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (options.input !== undefined) {
|
|
188
|
+
input.destroy();
|
|
189
|
+
}
|
|
190
|
+
if (options.output !== undefined) {
|
|
191
|
+
output.destroy();
|
|
192
|
+
}
|
|
193
|
+
await writeAll(io.stderr, `logstrip: ${messageOf(error)}\n`);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
if (options.output !== undefined) {
|
|
197
|
+
result = { ...result, outputPath: options.output };
|
|
198
|
+
await endStream(output);
|
|
199
|
+
}
|
|
200
|
+
if (options.json) {
|
|
201
|
+
await writeAll(io.stdout, `${JSON.stringify(result, null, 2)}\n`);
|
|
202
|
+
}
|
|
203
|
+
else if (options.stats) {
|
|
204
|
+
await writeAll(io.stderr, formatStats(result));
|
|
205
|
+
}
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
/* v8 ignore start */
|
|
209
|
+
if (require.main === module) {
|
|
210
|
+
runCli(process.argv.slice(2), {
|
|
211
|
+
stdin: process.stdin,
|
|
212
|
+
stdout: process.stdout,
|
|
213
|
+
stderr: process.stderr,
|
|
214
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
215
|
+
})
|
|
216
|
+
.then((code) => {
|
|
217
|
+
process.exitCode = code;
|
|
218
|
+
})
|
|
219
|
+
.catch((error) => {
|
|
220
|
+
process.stderr.write(`logstrip: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/* v8 ignore stop */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LogStripSourceSignature {
|
|
2
|
+
name: string;
|
|
3
|
+
markers: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export interface LogStripCustomConfig {
|
|
6
|
+
sources: readonly LogStripSourceSignature[];
|
|
7
|
+
diagnosticPatterns: readonly string[];
|
|
8
|
+
ignorePatterns: readonly string[];
|
|
9
|
+
sanitizePatterns: readonly SanitizeRule[];
|
|
10
|
+
internalStackPatterns: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface SanitizeRule {
|
|
13
|
+
pattern: string;
|
|
14
|
+
replacement: string;
|
|
15
|
+
flags?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseLogStripConfig(content: string): LogStripCustomConfig;
|
|
18
|
+
export declare function loadLogStripConfig(explicitPath?: string, startDir?: string): LogStripCustomConfig;
|
|
19
|
+
export declare function resolveConfigPath(explicitPath?: string, startDir?: string): string | undefined;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseLogStripConfig = parseLogStripConfig;
|
|
7
|
+
exports.loadLogStripConfig = loadLogStripConfig;
|
|
8
|
+
exports.resolveConfigPath = resolveConfigPath;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const CONFIG_FILENAME = '.logstrip.yml';
|
|
12
|
+
const EMPTY_CONFIG = {
|
|
13
|
+
sources: [],
|
|
14
|
+
diagnosticPatterns: [],
|
|
15
|
+
ignorePatterns: [],
|
|
16
|
+
sanitizePatterns: [],
|
|
17
|
+
internalStackPatterns: [],
|
|
18
|
+
};
|
|
19
|
+
function parseLogStripConfig(content) {
|
|
20
|
+
const parsed = parseMinimalYaml(content);
|
|
21
|
+
return normalizeConfig(parsed);
|
|
22
|
+
}
|
|
23
|
+
function loadLogStripConfig(explicitPath, startDir) {
|
|
24
|
+
const configPath = resolveConfigPath(explicitPath, startDir);
|
|
25
|
+
if (configPath === undefined)
|
|
26
|
+
return EMPTY_CONFIG;
|
|
27
|
+
const content = (0, node_fs_1.readFileSync)(configPath, 'utf8');
|
|
28
|
+
return parseLogStripConfig(content);
|
|
29
|
+
}
|
|
30
|
+
function resolveConfigPath(explicitPath, startDir) {
|
|
31
|
+
if (explicitPath !== undefined) {
|
|
32
|
+
if (!(0, node_fs_1.existsSync)(explicitPath))
|
|
33
|
+
return undefined;
|
|
34
|
+
return explicitPath;
|
|
35
|
+
}
|
|
36
|
+
const dir = startDir ?? process.cwd();
|
|
37
|
+
const candidate = node_path_1.default.join(dir, CONFIG_FILENAME);
|
|
38
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function normalizeConfig(raw) {
|
|
43
|
+
return {
|
|
44
|
+
sources: normalizeSources(raw.sources),
|
|
45
|
+
diagnosticPatterns: normalizeStringArray(raw.diagnosticPatterns),
|
|
46
|
+
ignorePatterns: normalizeStringArray(raw.ignorePatterns),
|
|
47
|
+
sanitizePatterns: normalizeSanitizeRules(raw.sanitizePatterns),
|
|
48
|
+
internalStackPatterns: normalizeStringArray(raw.internalStackPatterns),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function normalizeSources(raw) {
|
|
52
|
+
if (!Array.isArray(raw))
|
|
53
|
+
return [];
|
|
54
|
+
return raw
|
|
55
|
+
.filter((entry) => typeof entry === 'object' && entry !== null)
|
|
56
|
+
.map((entry) => ({
|
|
57
|
+
name: String(entry.name ?? ''),
|
|
58
|
+
markers: normalizeStringArray(entry.markers),
|
|
59
|
+
}))
|
|
60
|
+
.filter((entry) => entry.name !== '' && entry.markers.length > 0);
|
|
61
|
+
}
|
|
62
|
+
function normalizeStringArray(raw) {
|
|
63
|
+
if (!Array.isArray(raw))
|
|
64
|
+
return [];
|
|
65
|
+
return raw.filter((v) => typeof v === 'string');
|
|
66
|
+
}
|
|
67
|
+
function normalizeSanitizeRules(raw) {
|
|
68
|
+
if (!Array.isArray(raw))
|
|
69
|
+
return [];
|
|
70
|
+
return raw
|
|
71
|
+
.filter((entry) => typeof entry === 'object' && entry !== null)
|
|
72
|
+
.map((entry) => {
|
|
73
|
+
const flagsRaw = entry.flags;
|
|
74
|
+
const flags = flagsRaw !== undefined ? String(flagsRaw) : undefined;
|
|
75
|
+
return {
|
|
76
|
+
pattern: String(entry.pattern ?? ''),
|
|
77
|
+
replacement: String(entry.replacement ?? ''),
|
|
78
|
+
flags,
|
|
79
|
+
};
|
|
80
|
+
})
|
|
81
|
+
.filter((entry) => entry.pattern !== '');
|
|
82
|
+
}
|
|
83
|
+
function parseMinimalYaml(content) {
|
|
84
|
+
const result = {};
|
|
85
|
+
const lines = content.split(/\r?\n/u);
|
|
86
|
+
// State machine: track which block we are building
|
|
87
|
+
let topKey = '';
|
|
88
|
+
let topValue = null;
|
|
89
|
+
// For array-of-objects (like sources:)
|
|
90
|
+
let objList = null;
|
|
91
|
+
let currentObj = null;
|
|
92
|
+
// For plain arrays (like diagnosticPatterns:)
|
|
93
|
+
let scalarList = null;
|
|
94
|
+
// For sub-array inside currentObj (like markers: inside a source)
|
|
95
|
+
let subArrayKey = '';
|
|
96
|
+
let subArray = null;
|
|
97
|
+
const flushTop = () => {
|
|
98
|
+
if (topKey === '')
|
|
99
|
+
return;
|
|
100
|
+
// Flush any pending sub-array into the current object
|
|
101
|
+
if (subArrayKey !== '' && subArray !== null && currentObj !== null) {
|
|
102
|
+
currentObj[subArrayKey] = subArray;
|
|
103
|
+
}
|
|
104
|
+
if (objList !== null) {
|
|
105
|
+
result[topKey] = objList;
|
|
106
|
+
}
|
|
107
|
+
else if (scalarList !== null) {
|
|
108
|
+
result[topKey] = scalarList;
|
|
109
|
+
}
|
|
110
|
+
else if (topValue !== null) {
|
|
111
|
+
result[topKey] = topValue;
|
|
112
|
+
}
|
|
113
|
+
topKey = '';
|
|
114
|
+
topValue = null;
|
|
115
|
+
objList = null;
|
|
116
|
+
currentObj = null;
|
|
117
|
+
scalarList = null;
|
|
118
|
+
subArrayKey = '';
|
|
119
|
+
subArray = null;
|
|
120
|
+
};
|
|
121
|
+
for (const rawLine of lines) {
|
|
122
|
+
const line = rawLine.replace(/#.*$/u, '').trimEnd();
|
|
123
|
+
if (line.trim().length === 0)
|
|
124
|
+
continue;
|
|
125
|
+
const indent = rawLine.match(/^(\s*)/u)[1].length;
|
|
126
|
+
// ── Top-level key ──
|
|
127
|
+
const topMatch = line.match(/^([\w-]+):\s*(.*)$/u);
|
|
128
|
+
if (topMatch && indent === 0) {
|
|
129
|
+
flushTop();
|
|
130
|
+
topKey = topMatch[1];
|
|
131
|
+
const inlineVal = topMatch[2].trim();
|
|
132
|
+
if (inlineVal !== '') {
|
|
133
|
+
topValue = parseYamlValue(inlineVal);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// ── Array item: "- value" or "- name: foo" at indent 2 ──
|
|
138
|
+
const arrMatch = line.match(/^(\s{2})-\s+(.*)$/u);
|
|
139
|
+
if (arrMatch) {
|
|
140
|
+
const rest = arrMatch[2];
|
|
141
|
+
// Is this "- name: value" (object in array)?
|
|
142
|
+
const objPropMatch = rest.match(/^([\w-]+):\s*(.*)$/u);
|
|
143
|
+
if (objPropMatch) {
|
|
144
|
+
// Flush sub-array if we were building one
|
|
145
|
+
if (subArrayKey !== '' && subArray !== null && currentObj !== null) {
|
|
146
|
+
currentObj[subArrayKey] = subArray;
|
|
147
|
+
subArrayKey = '';
|
|
148
|
+
subArray = null;
|
|
149
|
+
}
|
|
150
|
+
// Start a new object in the array
|
|
151
|
+
if (objList === null)
|
|
152
|
+
objList = [];
|
|
153
|
+
currentObj = { [objPropMatch[1]]: parseYamlValue(objPropMatch[2]) };
|
|
154
|
+
objList.push(currentObj);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Plain scalar array item
|
|
158
|
+
if (scalarList === null)
|
|
159
|
+
scalarList = [];
|
|
160
|
+
scalarList.push(parseYamlValue(rest));
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// ── Sub-array item: " - value" at indent 4+ ──
|
|
165
|
+
const subArrMatch = line.match(/^(\s{4,})-\s+(.*)$/u);
|
|
166
|
+
if (subArrMatch && currentObj !== null) {
|
|
167
|
+
if (subArray === null)
|
|
168
|
+
subArray = [];
|
|
169
|
+
subArray.push(parseYamlValue(subArrMatch[2]));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// ── Property inside currentObj: " key: value" at indent 4+ ──
|
|
173
|
+
const propMatch = line.match(/^(\s{4,})([\w-]+):\s*(.*)$/u);
|
|
174
|
+
if (propMatch && currentObj !== null) {
|
|
175
|
+
// Flush previous sub-array
|
|
176
|
+
if (subArrayKey !== '') {
|
|
177
|
+
if (subArray !== null) {
|
|
178
|
+
currentObj[subArrayKey] = subArray;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const propKey = propMatch[2];
|
|
182
|
+
const propVal = propMatch[3].trim();
|
|
183
|
+
if (propVal === '') {
|
|
184
|
+
// Property with no inline value → next lines will be sub-array
|
|
185
|
+
subArrayKey = propKey;
|
|
186
|
+
subArray = null;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
currentObj[propKey] = parseYamlValue(propVal);
|
|
190
|
+
subArrayKey = '';
|
|
191
|
+
subArray = null;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Flush final state
|
|
197
|
+
if (subArrayKey !== '' && subArray !== null && currentObj !== null) {
|
|
198
|
+
currentObj[subArrayKey] = subArray;
|
|
199
|
+
}
|
|
200
|
+
flushTop();
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
function parseYamlValue(value) {
|
|
204
|
+
if (value === 'true')
|
|
205
|
+
return true;
|
|
206
|
+
if (value === 'false')
|
|
207
|
+
return false;
|
|
208
|
+
if (value === 'null' || value === '~')
|
|
209
|
+
return null;
|
|
210
|
+
// Quoted string
|
|
211
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
212
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
213
|
+
const inner = value.slice(1, -1);
|
|
214
|
+
// Double-quoted YAML strings unescape \\ → \ and \" → "
|
|
215
|
+
if (value.startsWith('"')) {
|
|
216
|
+
return inner.replace(/\\\\/gu, '\\').replace(/\\"/gu, '"');
|
|
217
|
+
}
|
|
218
|
+
return inner;
|
|
219
|
+
}
|
|
220
|
+
// Inline array: [a, b, c]
|
|
221
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
222
|
+
const inner = value.slice(1, -1).trim();
|
|
223
|
+
if (inner === '')
|
|
224
|
+
return [];
|
|
225
|
+
return splitInlineArray(inner).map((s) => parseYamlValue(s.trim()));
|
|
226
|
+
}
|
|
227
|
+
// Number
|
|
228
|
+
if (/^-?\d+(\.\d+)?$/u.test(value)) {
|
|
229
|
+
return Number(value);
|
|
230
|
+
}
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
function splitInlineArray(inner) {
|
|
234
|
+
const result = [];
|
|
235
|
+
let current = '';
|
|
236
|
+
let inQuote = null;
|
|
237
|
+
let depth = 0;
|
|
238
|
+
for (let i = 0; i < inner.length; i++) {
|
|
239
|
+
const ch = inner[i];
|
|
240
|
+
if (inQuote !== null) {
|
|
241
|
+
current += ch;
|
|
242
|
+
if (ch === inQuote)
|
|
243
|
+
inQuote = null;
|
|
244
|
+
}
|
|
245
|
+
else if (ch === '"' || ch === "'") {
|
|
246
|
+
inQuote = ch;
|
|
247
|
+
current += ch;
|
|
248
|
+
}
|
|
249
|
+
else if (ch === '[') {
|
|
250
|
+
depth++;
|
|
251
|
+
current += ch;
|
|
252
|
+
}
|
|
253
|
+
else if (ch === ']') {
|
|
254
|
+
depth--;
|
|
255
|
+
current += ch;
|
|
256
|
+
}
|
|
257
|
+
else if (ch === ',' && depth === 0) {
|
|
258
|
+
result.push(current);
|
|
259
|
+
current = '';
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
current += ch;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (current.trim() !== '')
|
|
266
|
+
result.push(current);
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { type LogStripCustomConfig } from './logstrip-config.js';
|
|
3
|
+
export type Aggressiveness = 'low' | 'medium' | 'high' | 'aggressive';
|
|
4
|
+
export interface LogStripOptions {
|
|
5
|
+
aggressiveness?: Aggressiveness;
|
|
6
|
+
configPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LogStripStats {
|
|
9
|
+
inputLines: number;
|
|
10
|
+
outputLines: number;
|
|
11
|
+
inputWords: number;
|
|
12
|
+
outputWords: number;
|
|
13
|
+
inputBytes: number;
|
|
14
|
+
outputBytes: number;
|
|
15
|
+
droppedLines: number;
|
|
16
|
+
duplicateLines: number;
|
|
17
|
+
hiddenInternalStackLines: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LogStripResult {
|
|
20
|
+
stats: LogStripStats;
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
savedTokens: number;
|
|
24
|
+
savingsPercent: number;
|
|
25
|
+
detectedSources?: readonly string[];
|
|
26
|
+
outputPath?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare const INTERNAL_STACK_MARKER = "[... hidden internal library frames ...]";
|
|
29
|
+
export declare const CONTEXT_WINDOW_BEFORE = 3;
|
|
30
|
+
export declare const CONTEXT_WINDOW_AFTER = 2;
|
|
31
|
+
export declare const SCORE_KEEP_THRESHOLD = 40;
|
|
32
|
+
export declare const TFIDF_REPEAT_THRESHOLD = 3;
|
|
33
|
+
export declare const TFIDF_PENALTY = 8;
|
|
34
|
+
export declare const TFIDF_MAP_LIMIT = 50000;
|
|
35
|
+
export declare const MAX_REPEAT_DELTA_VALUES = 3;
|
|
36
|
+
export declare const LOG_SOURCE_SIGNATURES: ReadonlyArray<readonly [source: string, markers: readonly string[]]>;
|
|
37
|
+
export declare const KNOWN_LOG_SOURCES: readonly string[];
|
|
38
|
+
export declare function detectLogSources(input: string | readonly string[], limit?: number): readonly string[];
|
|
39
|
+
export declare function parseAggressiveness(value: string | undefined): Aggressiveness;
|
|
40
|
+
export declare function sanitizeLine(line: string): string;
|
|
41
|
+
export declare function createRepeatSignature(line: string): string;
|
|
42
|
+
export declare function shouldKeepLine(line: string): boolean;
|
|
43
|
+
export declare function looksLikeDiagnosticLine(line: string): boolean;
|
|
44
|
+
export declare function isInternalStackTraceLine(line: string): boolean;
|
|
45
|
+
export declare function estimateTokens(wordCount: number): number;
|
|
46
|
+
/**
|
|
47
|
+
* Multi-signal relevance score for a single (sanitized) log line.
|
|
48
|
+
* >= SCORE_KEEP_THRESHOLD → emit immediately
|
|
49
|
+
* 0 to <SCORE_KEEP_THRESHOLD → buffer in context window
|
|
50
|
+
* < 0 or -Infinity → drop
|
|
51
|
+
*/
|
|
52
|
+
export declare function scoreLineRelevance(line: string, aggressiveness: Aggressiveness, seenCount?: number): number;
|
|
53
|
+
export declare function buildMergedConfig(options?: LogStripOptions): LogStripCustomConfig & {
|
|
54
|
+
mergedSources: readonly (readonly [string, readonly string[]])[];
|
|
55
|
+
};
|
|
56
|
+
export declare function processLogStream(input: NodeJS.ReadableStream, output: Writable, options?: LogStripOptions): Promise<LogStripResult>;
|
|
57
|
+
export declare function processLogFile(inputPath: string, outputPath: string, options?: LogStripOptions): Promise<LogStripResult>;
|
|
58
|
+
export declare function pathsReferToSameFile(inputPath: string, outputPath: string): boolean;
|