@clazic/kordoc 2.4.11 → 2.4.12

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 CHANGED
@@ -218,6 +218,26 @@ const result = await parse(buffer, {
218
218
  const result = await parse(buffer, { ocrMode: "auto" })
219
219
  ```
220
220
 
221
+ ### 디버그/관측 로그
222
+
223
+ ```bash
224
+ # 콘솔 + 파일(JSONL) 로그
225
+ KORDOC_LOG_LEVEL=debug \
226
+ KORDOC_LOG_FILE=./logs/kordoc.jsonl \
227
+ KORDOC_LOG_STACK=1 \
228
+ KORDOC_LOG_PROGRESS_SAMPLE_MS=1000 \
229
+ kordoc sample.pdf --ocr auto
230
+ ```
231
+
232
+ - `KORDOC_LOG_LEVEL`: `error|warn|info|debug|trace`
233
+ - `KORDOC_LOG_FILE`: JSONL 파일 경로
234
+ - `KORDOC_LOG_STACK`: `1`이면 stack 포함
235
+ - `KORDOC_LOG_PROGRESS_SAMPLE_MS`: progress 로그 샘플링(ms)
236
+ - `KORDOC_LOG_BASENAME_PATHS`: `1`이면 path/file/dir meta를 basename으로 축약
237
+ - `KORDOC_LOG_TEXT_LIMIT`: 로그 문자열 최대 길이
238
+
239
+ 상세 가이드는 [docs/2026-04-22-debug-observability-guide.md](./docs/2026-04-22-debug-observability-guide.md) 참고.
240
+
221
241
  ## CLI
222
242
 
223
243
  ```bash
@@ -240,6 +260,11 @@ kordoc scan.pdf --ocr codex # codex CLI 지정
240
260
  kordoc scan.pdf --ocr ollama # Ollama (KORDOC_OLLAMA_MODEL 환경변수로 모델 지정)
241
261
  kordoc scan.pdf --ocr tesseract # 내장 tesseract.js (별도 설치 불필요)
242
262
 
263
+ # .env 템플릿 생성 (명시 실행)
264
+ kordoc init-env # .env.kordoc.example 생성
265
+ kordoc init-env --create-dotenv # .env 없을 때 .env도 생성
266
+ kordoc init-env --force # 기존 파일 덮어쓰기
267
+
243
268
  # Markdown → 문서 변환
244
269
  kordoc convert 보고서.md # → 보고서.hwpx (기본)
245
270
  kordoc convert 보고서.md -f xlsx # → 보고서.xlsx
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/utils.ts
4
- var VERSION = true ? "2.4.11" : "0.0.0-dev";
4
+ var VERSION = true ? "2.4.12" : "0.0.0-dev";
5
5
  function toArrayBuffer(buf) {
6
6
  if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
7
7
  return buf.buffer;
@@ -9,9 +9,13 @@ function toArrayBuffer(buf) {
9
9
  return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
10
10
  }
11
11
  var KordocError = class extends Error {
12
- constructor(message) {
12
+ code;
13
+ stage;
14
+ constructor(message, opts = {}) {
13
15
  super(message);
14
16
  this.name = "KordocError";
17
+ this.code = opts.code;
18
+ this.stage = opts.stage;
15
19
  }
16
20
  };
17
21
  function sanitizeError(err) {
@@ -79,6 +83,16 @@ function classifyError(err) {
79
83
  if (msg.includes("\uC2DC\uADF8\uB2C8\uCC98") || msg.includes("\uBCF5\uAD6C\uD560 \uC218 \uC5C6")) return "CORRUPTED";
80
84
  return "PARSE_ERROR";
81
85
  }
86
+ function normalizeKordocError(err, fallbackMessage, stage = "unknown", fallbackCode = "PARSE_ERROR") {
87
+ if (err instanceof KordocError) {
88
+ if (!err.stage) err.stage = stage;
89
+ if (!err.code) err.code = fallbackCode;
90
+ return err;
91
+ }
92
+ const message = err instanceof Error ? err.message : fallbackMessage;
93
+ const code = err instanceof Error ? classifyError(err) : fallbackCode;
94
+ return new KordocError(message || fallbackMessage, { code, stage });
95
+ }
82
96
 
83
97
  export {
84
98
  VERSION,
@@ -88,6 +102,7 @@ export {
88
102
  isPathTraversal,
89
103
  precheckZipSize,
90
104
  sanitizeHref,
91
- classifyError
105
+ classifyError,
106
+ normalizeKordocError
92
107
  };
93
- //# sourceMappingURL=chunk-PJSXZBZB.js.map
108
+ //# sourceMappingURL=chunk-5R37N6KE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.ts"],"sourcesContent":["/** kordoc 공용 유틸리티 */\nimport type { ErrorCode } from \"./types.js\"\nimport type { LogStage } from \"./logging/logger.js\"\n\n/** 빌드 타임에 tsup define으로 주입되는 버전 */\ndeclare const __KORDOC_VERSION__: string\nexport const VERSION: string = typeof __KORDOC_VERSION__ !== \"undefined\" ? __KORDOC_VERSION__ : \"0.0.0-dev\"\n\n/**\n * Node.js Buffer → ArrayBuffer 변환\n * pool Buffer의 공유 ArrayBuffer 문제를 안전하게 처리.\n * offset=0이고 전체 ArrayBuffer를 차지하면 복사 없이 직접 반환.\n */\nexport function toArrayBuffer(buf: Buffer): ArrayBuffer {\n if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {\n return buf.buffer as ArrayBuffer\n }\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer\n}\n\n/**\n * kordoc 내부 에러 클래스 — 사용자에게 노출해도 안전한 메시지만 포함.\n * MCP 에러 정제에서 instanceof로 판별하여 allowlist 패턴 매칭 없이 안전하게 통과.\n */\nexport class KordocError extends Error {\n code?: ErrorCode | \"UNKNOWN\"\n stage?: LogStage\n constructor(message: string, opts: { code?: ErrorCode | \"UNKNOWN\"; stage?: LogStage } = {}) {\n super(message)\n this.name = \"KordocError\"\n this.code = opts.code\n this.stage = opts.stage\n }\n}\n\n/**\n * 에러 메시지 정제 — KordocError는 그대로, 나머지는 일반 메시지로 대체.\n * 파일시스템 경로, 스택 트레이스 등 내부 정보 노출 방지.\n */\nexport function sanitizeError(err: unknown): string {\n if (err instanceof KordocError) return err.message\n return \"문서 처리 중 오류가 발생했습니다\"\n}\n\n/**\n * ZIP 엔트리 경로의 경로 순회 여부 판별.\n * 백슬래시 정규화, .., 절대경로, Windows 드라이브 문자 모두 차단.\n */\nexport function isPathTraversal(name: string): boolean {\n if (name.includes(\"\\x00\")) return true\n const normalized = name.replace(/\\\\/g, \"/\")\n return normalized.includes(\"..\") || normalized.startsWith(\"/\") || /^[A-Za-z]:/.test(normalized)\n}\n\n// ─── ZIP 안전 로딩 (ZIP bomb 방지) ────────────────────\n\n/**\n * ZIP bomb 사전 검사 — Central Directory에서 비압축 합계와 엔트리 수 확인.\n * HWPX/XLSX/DOCX 등 모든 ZIP 기반 포맷에서 공통 사용.\n */\nexport function precheckZipSize(\n buffer: ArrayBuffer,\n maxUncompressedSize = 100 * 1024 * 1024,\n maxEntries = 500,\n): { totalUncompressed: number; entryCount: number } {\n try {\n const data = new DataView(buffer)\n const len = buffer.byteLength\n // EOCD 시그니처 역방향 스캔\n let eocdOffset = -1\n for (let i = len - 22; i >= Math.max(0, len - 65557); i--) {\n if (data.getUint32(i, true) === 0x06054b50) { eocdOffset = i; break }\n }\n if (eocdOffset < 0) return { totalUncompressed: 0, entryCount: 0 }\n\n const entryCount = data.getUint16(eocdOffset + 10, true)\n if (entryCount > maxEntries) {\n throw new KordocError(`ZIP 엔트리 수 초과: ${entryCount} (최대 ${maxEntries})`)\n }\n\n const cdSize = data.getUint32(eocdOffset + 12, true)\n const cdOffset = data.getUint32(eocdOffset + 16, true)\n if (cdOffset + cdSize > len) return { totalUncompressed: 0, entryCount }\n\n let totalUncompressed = 0\n let pos = cdOffset\n for (let i = 0; i < entryCount && pos + 46 <= cdOffset + cdSize; i++) {\n if (data.getUint32(pos, true) !== 0x02014b50) break\n totalUncompressed += data.getUint32(pos + 24, true)\n const nameLen = data.getUint16(pos + 28, true)\n const extraLen = data.getUint16(pos + 30, true)\n const commentLen = data.getUint16(pos + 32, true)\n pos += 46 + nameLen + extraLen + commentLen\n }\n\n if (totalUncompressed > maxUncompressedSize) {\n throw new KordocError(`ZIP 비압축 크기 초과: ${(totalUncompressed / 1024 / 1024).toFixed(1)}MB (최대 ${maxUncompressedSize / 1024 / 1024}MB)`)\n }\n\n return { totalUncompressed, entryCount }\n } catch (err) {\n if (err instanceof KordocError) throw err\n return { totalUncompressed: 0, entryCount: 0 }\n }\n}\n\n/** 하이퍼링크 URL 살균 — javascript: 등 XSS 위험 스킴 차단 */\nconst SAFE_HREF_RE = /^(?:https?:|mailto:|tel:|#)/i\nexport function sanitizeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed || !SAFE_HREF_RE.test(trimmed)) return null\n return trimmed\n}\n\n// ─── 에러 분류 ──────────────────────────────────────\n\n/** 에러를 구조화된 ErrorCode로 분류 — KordocError 메시지 패턴 매칭 */\nexport function classifyError(err: unknown): ErrorCode {\n if (!(err instanceof Error)) return \"PARSE_ERROR\"\n const msg = err.message\n if (msg.includes(\"암호화\")) return \"ENCRYPTED\"\n if (msg.includes(\"DRM\")) return \"DRM_PROTECTED\"\n if (msg.includes(\"ZIP bomb\") || msg.includes(\"ZIP 비압축 크기 초과\") || msg.includes(\"ZIP 엔트리 수 초과\")) return \"ZIP_BOMB\"\n if (msg.includes(\"bomb\") || msg.includes(\"크기 초과\") || msg.includes(\"압축 해제\")) return \"DECOMPRESSION_BOMB\"\n if (msg.includes(\"이미지 기반\")) return \"IMAGE_BASED_PDF\"\n if (msg.includes(\"섹션\") && (msg.includes(\"찾을 수 없\") || msg.includes(\"없음\"))) return \"NO_SECTIONS\"\n if (msg.includes(\"시그니처\") || msg.includes(\"복구할 수 없\")) return \"CORRUPTED\"\n return \"PARSE_ERROR\"\n}\n\n/** unknown 에러를 KordocError(code/stage 포함)로 정규화 */\nexport function normalizeKordocError(\n err: unknown,\n fallbackMessage: string,\n stage: LogStage = \"unknown\",\n fallbackCode: ErrorCode | \"UNKNOWN\" = \"PARSE_ERROR\",\n): KordocError {\n if (err instanceof KordocError) {\n if (!err.stage) err.stage = stage\n if (!err.code) err.code = fallbackCode\n return err\n }\n const message = err instanceof Error ? err.message : fallbackMessage\n const code = err instanceof Error ? classifyError(err) : fallbackCode\n return new KordocError(message || fallbackMessage, { code, stage })\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/logging/logger.ts
4
+ import { mkdirSync, appendFileSync } from "fs";
5
+ import { appendFile } from "fs/promises";
6
+ import { basename, dirname, resolve } from "path";
7
+ import { randomUUID } from "crypto";
8
+ var LEVEL_ORDER = {
9
+ error: 0,
10
+ warn: 1,
11
+ info: 2,
12
+ debug: 3,
13
+ trace: 4
14
+ };
15
+ var BaseLogger = class _BaseLogger {
16
+ constructor(config, context = {}) {
17
+ this.config = config;
18
+ this.context = context;
19
+ }
20
+ static progressSeenAt = /* @__PURE__ */ new Map();
21
+ shouldLog(level) {
22
+ return LEVEL_ORDER[level] <= LEVEL_ORDER[this.config.level];
23
+ }
24
+ shouldEmitProgress(ev) {
25
+ if (this.config.progressSampleMs <= 0) return true;
26
+ if ((ev.event ?? "message") !== "progress") return true;
27
+ if (ev.level === "error" || ev.level === "warn") return true;
28
+ const key = [
29
+ this.context.runId ?? ev.runId ?? "no-run",
30
+ this.context.component ?? ev.component ?? "no-component",
31
+ this.context.stage ?? ev.stage ?? "unknown",
32
+ ev.message
33
+ ].join("|");
34
+ const now = Date.now();
35
+ const prev = _BaseLogger.progressSeenAt.get(key) ?? 0;
36
+ if (now - prev < this.config.progressSampleMs) return false;
37
+ _BaseLogger.progressSeenAt.set(key, now);
38
+ return true;
39
+ }
40
+ merge(ev) {
41
+ const out = {
42
+ ...this.context,
43
+ ...ev,
44
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
45
+ level: ev.level,
46
+ stage: ev.stage ?? this.context.stage ?? "unknown",
47
+ event: ev.event ?? "message",
48
+ message: ev.message
49
+ };
50
+ if (!this.config.includeStack && out.error?.stack) {
51
+ out.error = { ...out.error, stack: void 0 };
52
+ }
53
+ if (out.meta) out.meta = sanitizeMeta(out.meta, this.config);
54
+ if (out.error?.message) out.error.message = maskSecrets(out.error.message);
55
+ if (out.message) out.message = limitText(maskSecrets(out.message), this.config.textLimit);
56
+ return out;
57
+ }
58
+ child(context) {
59
+ return new _BaseLogger(this.config, { ...this.context, ...context });
60
+ }
61
+ withRun(runId) {
62
+ return this.child({ runId });
63
+ }
64
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
65
+ log(event) {
66
+ }
67
+ };
68
+ var ConsoleLogger = class extends BaseLogger {
69
+ log(event) {
70
+ if (!this.shouldLog(event.level)) return;
71
+ if (!this.shouldEmitProgress(event)) return;
72
+ const e = this.merge(event);
73
+ const prefix = `[${e.ts}] [${e.level.toUpperCase()}]${e.runId ? ` [${e.runId}]` : ""}${e.stage ? ` [${e.stage}]` : ""}`;
74
+ const line = `${prefix} ${e.message}${e.component ? ` (${e.component})` : ""}`;
75
+ if (e.level === "error") {
76
+ process.stderr.write(line + "\n");
77
+ if (e.error?.stack) process.stderr.write(e.error.stack + "\n");
78
+ } else {
79
+ process.stdout.write(line + "\n");
80
+ }
81
+ }
82
+ };
83
+ var JsonlLogger = class _JsonlLogger extends BaseLogger {
84
+ constructor(config, filePath, context = {}) {
85
+ super(config, context);
86
+ this.filePath = filePath;
87
+ mkdirSync(dirname(filePath), { recursive: true });
88
+ _JsonlLogger.ensureState(filePath);
89
+ }
90
+ static states = /* @__PURE__ */ new Map();
91
+ static ensureState(path) {
92
+ let state = _JsonlLogger.states.get(path);
93
+ if (!state) {
94
+ state = { queue: [], flushing: false };
95
+ _JsonlLogger.states.set(path, state);
96
+ const flushSync = () => {
97
+ const s = _JsonlLogger.states.get(path);
98
+ if (!s || s.queue.length === 0) return;
99
+ const payload = s.queue.join("");
100
+ s.queue = [];
101
+ if (!payload) return;
102
+ appendFileSync(path, payload, "utf-8");
103
+ };
104
+ process.on("beforeExit", flushSync);
105
+ process.on("exit", flushSync);
106
+ }
107
+ return state;
108
+ }
109
+ scheduleFlush(path) {
110
+ const state = _JsonlLogger.ensureState(path);
111
+ if (state.timer || state.flushing) return;
112
+ state.timer = setTimeout(() => {
113
+ state.timer = void 0;
114
+ void this.flush(path);
115
+ }, 200);
116
+ }
117
+ async flush(path) {
118
+ const state = _JsonlLogger.ensureState(path);
119
+ if (state.flushing) return;
120
+ if (state.queue.length === 0) return;
121
+ state.flushing = true;
122
+ const payload = state.queue.join("");
123
+ state.queue = [];
124
+ try {
125
+ await appendFile(path, payload, "utf-8");
126
+ } finally {
127
+ state.flushing = false;
128
+ if (state.queue.length > 0) this.scheduleFlush(path);
129
+ }
130
+ }
131
+ log(event) {
132
+ if (!this.shouldLog(event.level)) return;
133
+ if (!this.shouldEmitProgress(event)) return;
134
+ const e = this.merge(event);
135
+ const state = _JsonlLogger.ensureState(this.filePath);
136
+ state.queue.push(JSON.stringify(e) + "\n");
137
+ this.scheduleFlush(this.filePath);
138
+ }
139
+ child(context) {
140
+ return new _JsonlLogger(this.config, this.filePath, { ...this.context, ...context });
141
+ }
142
+ };
143
+ var CompositeLogger = class _CompositeLogger extends BaseLogger {
144
+ constructor(config, sinks, context = {}) {
145
+ super(config, context);
146
+ this.sinks = sinks;
147
+ }
148
+ log(event) {
149
+ if (!this.shouldLog(event.level)) return;
150
+ if (!this.shouldEmitProgress(event)) return;
151
+ for (const sink of this.sinks) sink.log(event);
152
+ }
153
+ child(context) {
154
+ const nextSinks = this.sinks.map((s) => s.child(context));
155
+ return new _CompositeLogger(this.config, nextSinks, { ...this.context, ...context });
156
+ }
157
+ };
158
+ function createLoggerFromEnv(env = process.env) {
159
+ const level = parseLevel(env.KORDOC_LOG_LEVEL);
160
+ const includeStack = env.KORDOC_LOG_STACK === "1";
161
+ const filePath = env.KORDOC_LOG_FILE ? resolve(env.KORDOC_LOG_FILE) : "";
162
+ const config = {
163
+ level,
164
+ includeStack,
165
+ progressSampleMs: parsePositiveInt(env.KORDOC_LOG_PROGRESS_SAMPLE_MS, 1e3),
166
+ basenamePaths: env.KORDOC_LOG_BASENAME_PATHS === "1",
167
+ textLimit: parsePositiveInt(env.KORDOC_LOG_TEXT_LIMIT, 400)
168
+ };
169
+ const consoleSink = new ConsoleLogger(config);
170
+ const sinks = [consoleSink];
171
+ if (filePath) sinks.push(new JsonlLogger(config, filePath));
172
+ return new CompositeLogger(config, sinks);
173
+ }
174
+ function generateRunId(prefix = "run") {
175
+ return `${prefix}_${randomUUID().slice(0, 8)}`;
176
+ }
177
+ function parseLevel(input) {
178
+ const v = (input || "").toLowerCase();
179
+ if (v === "error" || v === "warn" || v === "info" || v === "debug" || v === "trace") return v;
180
+ return "error";
181
+ }
182
+ function maskSecrets(input) {
183
+ return input.replace(/nvapi-[A-Za-z0-9_\-]+/g, "nvapi-***").replace(/Bearer\s+[A-Za-z0-9_\-\.]+/gi, "Bearer ***");
184
+ }
185
+ function sanitizeMeta(meta, cfg) {
186
+ const out = {};
187
+ for (const [k, v] of Object.entries(meta)) {
188
+ if (/authorization|api[_-]?key|token/i.test(k)) {
189
+ out[k] = "***";
190
+ continue;
191
+ }
192
+ if (typeof v === "string") {
193
+ let next = maskSecrets(v);
194
+ if (cfg.basenamePaths && /path|file|dir/i.test(k)) {
195
+ next = basename(next);
196
+ }
197
+ out[k] = limitText(next, cfg.textLimit);
198
+ } else {
199
+ out[k] = v;
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+ function parsePositiveInt(input, fallback) {
205
+ const n = Number(input);
206
+ if (!Number.isFinite(n) || n < 0) return fallback;
207
+ return Math.floor(n);
208
+ }
209
+ function limitText(input, maxLen) {
210
+ if (maxLen <= 0) return input;
211
+ if (input.length <= maxLen) return input;
212
+ return `${input.slice(0, maxLen)}...(+${input.length - maxLen})`;
213
+ }
214
+
215
+ export {
216
+ createLoggerFromEnv,
217
+ generateRunId
218
+ };
219
+ //# sourceMappingURL=chunk-I6YC6ZGK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logging/logger.ts"],"sourcesContent":["import { mkdirSync, appendFileSync } from \"fs\"\nimport { appendFile } from \"fs/promises\"\nimport { basename, dirname, resolve } from \"path\"\nimport { randomUUID } from \"crypto\"\n\nexport type LogLevel = \"error\" | \"warn\" | \"info\" | \"debug\" | \"trace\"\nexport type LogEventType = \"start\" | \"progress\" | \"done\" | \"error\" | \"message\"\nexport type LogStage = \"detect\" | \"convert\" | \"render\" | \"probe\" | \"ocr\" | \"proofread\" | \"merge\" | \"finalize\" | \"unknown\"\n\nexport interface LogEvent {\n ts?: string\n level: LogLevel\n runId?: string\n stage?: LogStage\n event?: LogEventType\n component?: string\n message: string\n meta?: Record<string, unknown>\n error?: {\n code?: string\n name?: string\n message?: string\n stack?: string\n }\n}\n\nexport interface Logger {\n log(event: LogEvent): void\n child(context: Partial<LogEvent>): Logger\n withRun(runId: string): Logger\n}\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n trace: 4,\n}\n\ninterface LoggerConfig {\n level: LogLevel\n includeStack: boolean\n progressSampleMs: number\n basenamePaths: boolean\n textLimit: number\n}\n\nclass BaseLogger implements Logger {\n private static progressSeenAt = new Map<string, number>()\n\n constructor(\n protected readonly config: LoggerConfig,\n protected readonly context: Partial<LogEvent> = {},\n ) {}\n\n protected shouldLog(level: LogLevel): boolean {\n return LEVEL_ORDER[level] <= LEVEL_ORDER[this.config.level]\n }\n\n protected shouldEmitProgress(ev: LogEvent): boolean {\n if (this.config.progressSampleMs <= 0) return true\n if ((ev.event ?? \"message\") !== \"progress\") return true\n if (ev.level === \"error\" || ev.level === \"warn\") return true\n\n const key = [\n this.context.runId ?? ev.runId ?? \"no-run\",\n this.context.component ?? ev.component ?? \"no-component\",\n this.context.stage ?? ev.stage ?? \"unknown\",\n ev.message,\n ].join(\"|\")\n const now = Date.now()\n const prev = BaseLogger.progressSeenAt.get(key) ?? 0\n if (now - prev < this.config.progressSampleMs) return false\n BaseLogger.progressSeenAt.set(key, now)\n return true\n }\n\n protected merge(ev: LogEvent): LogEvent {\n const out: LogEvent = {\n ...this.context,\n ...ev,\n ts: new Date().toISOString(),\n level: ev.level,\n stage: ev.stage ?? this.context.stage ?? \"unknown\",\n event: ev.event ?? \"message\",\n message: ev.message,\n }\n if (!this.config.includeStack && out.error?.stack) {\n out.error = { ...out.error, stack: undefined }\n }\n if (out.meta) out.meta = sanitizeMeta(out.meta, this.config)\n if (out.error?.message) out.error.message = maskSecrets(out.error.message)\n if (out.message) out.message = limitText(maskSecrets(out.message), this.config.textLimit)\n return out\n }\n\n child(context: Partial<LogEvent>): Logger {\n return new BaseLogger(this.config, { ...this.context, ...context })\n }\n\n withRun(runId: string): Logger {\n return this.child({ runId })\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n log(event: LogEvent): void {}\n}\n\nexport class ConsoleLogger extends BaseLogger {\n override log(event: LogEvent): void {\n if (!this.shouldLog(event.level)) return\n if (!this.shouldEmitProgress(event)) return\n const e = this.merge(event)\n const prefix = `[${e.ts}] [${e.level.toUpperCase()}]${e.runId ? ` [${e.runId}]` : \"\"}${e.stage ? ` [${e.stage}]` : \"\"}`\n const line = `${prefix} ${e.message}${e.component ? ` (${e.component})` : \"\"}`\n if (e.level === \"error\") {\n process.stderr.write(line + \"\\n\")\n if (e.error?.stack) process.stderr.write(e.error.stack + \"\\n\")\n } else {\n process.stdout.write(line + \"\\n\")\n }\n }\n}\n\ntype JsonlState = {\n queue: string[]\n flushing: boolean\n timer?: NodeJS.Timeout\n}\n\nexport class JsonlLogger extends BaseLogger {\n private static states = new Map<string, JsonlState>()\n\n constructor(\n config: LoggerConfig,\n private readonly filePath: string,\n context: Partial<LogEvent> = {},\n ) {\n super(config, context)\n mkdirSync(dirname(filePath), { recursive: true })\n JsonlLogger.ensureState(filePath)\n }\n\n private static ensureState(path: string): JsonlState {\n let state = JsonlLogger.states.get(path)\n if (!state) {\n state = { queue: [], flushing: false }\n JsonlLogger.states.set(path, state)\n const flushSync = () => {\n const s = JsonlLogger.states.get(path)\n if (!s || s.queue.length === 0) return\n const payload = s.queue.join(\"\")\n s.queue = []\n if (!payload) return\n appendFileSync(path, payload, \"utf-8\")\n }\n process.on(\"beforeExit\", flushSync)\n process.on(\"exit\", flushSync)\n }\n return state\n }\n\n private scheduleFlush(path: string): void {\n const state = JsonlLogger.ensureState(path)\n if (state.timer || state.flushing) return\n state.timer = setTimeout(() => {\n state.timer = undefined\n void this.flush(path)\n }, 200)\n }\n\n private async flush(path: string): Promise<void> {\n const state = JsonlLogger.ensureState(path)\n if (state.flushing) return\n if (state.queue.length === 0) return\n state.flushing = true\n const payload = state.queue.join(\"\")\n state.queue = []\n try {\n await appendFile(path, payload, \"utf-8\")\n } finally {\n state.flushing = false\n if (state.queue.length > 0) this.scheduleFlush(path)\n }\n }\n\n override log(event: LogEvent): void {\n if (!this.shouldLog(event.level)) return\n if (!this.shouldEmitProgress(event)) return\n const e = this.merge(event)\n const state = JsonlLogger.ensureState(this.filePath)\n state.queue.push(JSON.stringify(e) + \"\\n\")\n this.scheduleFlush(this.filePath)\n }\n\n override child(context: Partial<LogEvent>): Logger {\n return new JsonlLogger(this.config, this.filePath, { ...this.context, ...context })\n }\n}\n\nexport class CompositeLogger extends BaseLogger {\n constructor(\n config: LoggerConfig,\n private readonly sinks: Logger[],\n context: Partial<LogEvent> = {},\n ) {\n super(config, context)\n }\n\n override log(event: LogEvent): void {\n if (!this.shouldLog(event.level)) return\n if (!this.shouldEmitProgress(event)) return\n for (const sink of this.sinks) sink.log(event)\n }\n\n override child(context: Partial<LogEvent>): Logger {\n const nextSinks = this.sinks.map(s => s.child(context))\n return new CompositeLogger(this.config, nextSinks, { ...this.context, ...context })\n }\n}\n\nexport function createLoggerFromEnv(env: NodeJS.ProcessEnv = process.env): Logger {\n const level = parseLevel(env.KORDOC_LOG_LEVEL)\n const includeStack = env.KORDOC_LOG_STACK === \"1\"\n const filePath = env.KORDOC_LOG_FILE ? resolve(env.KORDOC_LOG_FILE) : \"\"\n const config: LoggerConfig = {\n level,\n includeStack,\n progressSampleMs: parsePositiveInt(env.KORDOC_LOG_PROGRESS_SAMPLE_MS, 1000),\n basenamePaths: env.KORDOC_LOG_BASENAME_PATHS === \"1\",\n textLimit: parsePositiveInt(env.KORDOC_LOG_TEXT_LIMIT, 400),\n }\n\n const consoleSink: Logger = new ConsoleLogger(config)\n const sinks: Logger[] = [consoleSink]\n if (filePath) sinks.push(new JsonlLogger(config, filePath))\n\n return new CompositeLogger(config, sinks)\n}\n\nexport function createNoopLogger(): Logger {\n return {\n log: () => {},\n child: () => createNoopLogger(),\n withRun: () => createNoopLogger(),\n }\n}\n\nexport function generateRunId(prefix = \"run\"): string {\n return `${prefix}_${randomUUID().slice(0, 8)}`\n}\n\nexport function parseLevel(input?: string | null): LogLevel {\n const v = (input || \"\").toLowerCase()\n if (v === \"error\" || v === \"warn\" || v === \"info\" || v === \"debug\" || v === \"trace\") return v\n return \"error\"\n}\n\nexport function maskSecrets(input: string): string {\n return input\n .replace(/nvapi-[A-Za-z0-9_\\-]+/g, \"nvapi-***\")\n .replace(/Bearer\\s+[A-Za-z0-9_\\-\\.]+/gi, \"Bearer ***\")\n}\n\nfunction sanitizeMeta(meta: Record<string, unknown>, cfg: LoggerConfig): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(meta)) {\n if (/authorization|api[_-]?key|token/i.test(k)) {\n out[k] = \"***\"\n continue\n }\n if (typeof v === \"string\") {\n let next = maskSecrets(v)\n if (cfg.basenamePaths && /path|file|dir/i.test(k)) {\n next = basename(next)\n }\n out[k] = limitText(next, cfg.textLimit)\n } else {\n out[k] = v\n }\n }\n return out\n}\n\nfunction parsePositiveInt(input: string | undefined, fallback: number): number {\n const n = Number(input)\n if (!Number.isFinite(n) || n < 0) return fallback\n return Math.floor(n)\n}\n\nfunction limitText(input: string, maxLen: number): string {\n if (maxLen <= 0) return input\n if (input.length <= maxLen) return input\n return `${input.slice(0, maxLen)}...(+${input.length - maxLen})`\n}\n"],"mappings":";;;AAAA,SAAS,WAAW,sBAAsB;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,UAAU,SAAS,eAAe;AAC3C,SAAS,kBAAkB;AA6B3B,IAAM,cAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AACT;AAUA,IAAM,aAAN,MAAM,YAA6B;AAAA,EAGjC,YACqB,QACA,UAA6B,CAAC,GACjD;AAFmB;AACA;AAAA,EAClB;AAAA,EALH,OAAe,iBAAiB,oBAAI,IAAoB;AAAA,EAO9C,UAAU,OAA0B;AAC5C,WAAO,YAAY,KAAK,KAAK,YAAY,KAAK,OAAO,KAAK;AAAA,EAC5D;AAAA,EAEU,mBAAmB,IAAuB;AAClD,QAAI,KAAK,OAAO,oBAAoB,EAAG,QAAO;AAC9C,SAAK,GAAG,SAAS,eAAe,WAAY,QAAO;AACnD,QAAI,GAAG,UAAU,WAAW,GAAG,UAAU,OAAQ,QAAO;AAExD,UAAM,MAAM;AAAA,MACV,KAAK,QAAQ,SAAS,GAAG,SAAS;AAAA,MAClC,KAAK,QAAQ,aAAa,GAAG,aAAa;AAAA,MAC1C,KAAK,QAAQ,SAAS,GAAG,SAAS;AAAA,MAClC,GAAG;AAAA,IACL,EAAE,KAAK,GAAG;AACV,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,OAAO,YAAW,eAAe,IAAI,GAAG,KAAK;AACnD,QAAI,MAAM,OAAO,KAAK,OAAO,iBAAkB,QAAO;AACtD,gBAAW,eAAe,IAAI,KAAK,GAAG;AACtC,WAAO;AAAA,EACT;AAAA,EAEU,MAAM,IAAwB;AACtC,UAAM,MAAgB;AAAA,MACpB,GAAG,KAAK;AAAA,MACR,GAAG;AAAA,MACH,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,MAC3B,OAAO,GAAG;AAAA,MACV,OAAO,GAAG,SAAS,KAAK,QAAQ,SAAS;AAAA,MACzC,OAAO,GAAG,SAAS;AAAA,MACnB,SAAS,GAAG;AAAA,IACd;AACA,QAAI,CAAC,KAAK,OAAO,gBAAgB,IAAI,OAAO,OAAO;AACjD,UAAI,QAAQ,EAAE,GAAG,IAAI,OAAO,OAAO,OAAU;AAAA,IAC/C;AACA,QAAI,IAAI,KAAM,KAAI,OAAO,aAAa,IAAI,MAAM,KAAK,MAAM;AAC3D,QAAI,IAAI,OAAO,QAAS,KAAI,MAAM,UAAU,YAAY,IAAI,MAAM,OAAO;AACzE,QAAI,IAAI,QAAS,KAAI,UAAU,UAAU,YAAY,IAAI,OAAO,GAAG,KAAK,OAAO,SAAS;AACxF,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAoC;AACxC,WAAO,IAAI,YAAW,KAAK,QAAQ,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,CAAC;AAAA,EACpE;AAAA,EAEA,QAAQ,OAAuB;AAC7B,WAAO,KAAK,MAAM,EAAE,MAAM,CAAC;AAAA,EAC7B;AAAA;AAAA,EAGA,IAAI,OAAuB;AAAA,EAAC;AAC9B;AAEO,IAAM,gBAAN,cAA4B,WAAW;AAAA,EACnC,IAAI,OAAuB;AAClC,QAAI,CAAC,KAAK,UAAU,MAAM,KAAK,EAAG;AAClC,QAAI,CAAC,KAAK,mBAAmB,KAAK,EAAG;AACrC,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,UAAM,SAAS,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,EAAE,KAAK,MAAM,EAAE,GAAG,EAAE,QAAQ,KAAK,EAAE,KAAK,MAAM,EAAE;AACrH,UAAM,OAAO,GAAG,MAAM,IAAI,EAAE,OAAO,GAAG,EAAE,YAAY,KAAK,EAAE,SAAS,MAAM,EAAE;AAC5E,QAAI,EAAE,UAAU,SAAS;AACvB,cAAQ,OAAO,MAAM,OAAO,IAAI;AAChC,UAAI,EAAE,OAAO,MAAO,SAAQ,OAAO,MAAM,EAAE,MAAM,QAAQ,IAAI;AAAA,IAC/D,OAAO;AACL,cAAQ,OAAO,MAAM,OAAO,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAQO,IAAM,cAAN,MAAM,qBAAoB,WAAW;AAAA,EAG1C,YACE,QACiB,UACjB,UAA6B,CAAC,GAC9B;AACA,UAAM,QAAQ,OAAO;AAHJ;AAIjB,cAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,iBAAY,YAAY,QAAQ;AAAA,EAClC;AAAA,EAVA,OAAe,SAAS,oBAAI,IAAwB;AAAA,EAYpD,OAAe,YAAY,MAA0B;AACnD,QAAI,QAAQ,aAAY,OAAO,IAAI,IAAI;AACvC,QAAI,CAAC,OAAO;AACV,cAAQ,EAAE,OAAO,CAAC,GAAG,UAAU,MAAM;AACrC,mBAAY,OAAO,IAAI,MAAM,KAAK;AAClC,YAAM,YAAY,MAAM;AACtB,cAAM,IAAI,aAAY,OAAO,IAAI,IAAI;AACrC,YAAI,CAAC,KAAK,EAAE,MAAM,WAAW,EAAG;AAChC,cAAM,UAAU,EAAE,MAAM,KAAK,EAAE;AAC/B,UAAE,QAAQ,CAAC;AACX,YAAI,CAAC,QAAS;AACd,uBAAe,MAAM,SAAS,OAAO;AAAA,MACvC;AACA,cAAQ,GAAG,cAAc,SAAS;AAClC,cAAQ,GAAG,QAAQ,SAAS;AAAA,IAC9B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,cAAc,MAAoB;AACxC,UAAM,QAAQ,aAAY,YAAY,IAAI;AAC1C,QAAI,MAAM,SAAS,MAAM,SAAU;AACnC,UAAM,QAAQ,WAAW,MAAM;AAC7B,YAAM,QAAQ;AACd,WAAK,KAAK,MAAM,IAAI;AAAA,IACtB,GAAG,GAAG;AAAA,EACR;AAAA,EAEA,MAAc,MAAM,MAA6B;AAC/C,UAAM,QAAQ,aAAY,YAAY,IAAI;AAC1C,QAAI,MAAM,SAAU;AACpB,QAAI,MAAM,MAAM,WAAW,EAAG;AAC9B,UAAM,WAAW;AACjB,UAAM,UAAU,MAAM,MAAM,KAAK,EAAE;AACnC,UAAM,QAAQ,CAAC;AACf,QAAI;AACF,YAAM,WAAW,MAAM,SAAS,OAAO;AAAA,IACzC,UAAE;AACA,YAAM,WAAW;AACjB,UAAI,MAAM,MAAM,SAAS,EAAG,MAAK,cAAc,IAAI;AAAA,IACrD;AAAA,EACF;AAAA,EAES,IAAI,OAAuB;AAClC,QAAI,CAAC,KAAK,UAAU,MAAM,KAAK,EAAG;AAClC,QAAI,CAAC,KAAK,mBAAmB,KAAK,EAAG;AACrC,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,UAAM,QAAQ,aAAY,YAAY,KAAK,QAAQ;AACnD,UAAM,MAAM,KAAK,KAAK,UAAU,CAAC,IAAI,IAAI;AACzC,SAAK,cAAc,KAAK,QAAQ;AAAA,EAClC;AAAA,EAES,MAAM,SAAoC;AACjD,WAAO,IAAI,aAAY,KAAK,QAAQ,KAAK,UAAU,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,CAAC;AAAA,EACpF;AACF;AAEO,IAAM,kBAAN,MAAM,yBAAwB,WAAW;AAAA,EAC9C,YACE,QACiB,OACjB,UAA6B,CAAC,GAC9B;AACA,UAAM,QAAQ,OAAO;AAHJ;AAAA,EAInB;AAAA,EAES,IAAI,OAAuB;AAClC,QAAI,CAAC,KAAK,UAAU,MAAM,KAAK,EAAG;AAClC,QAAI,CAAC,KAAK,mBAAmB,KAAK,EAAG;AACrC,eAAW,QAAQ,KAAK,MAAO,MAAK,IAAI,KAAK;AAAA,EAC/C;AAAA,EAES,MAAM,SAAoC;AACjD,UAAM,YAAY,KAAK,MAAM,IAAI,OAAK,EAAE,MAAM,OAAO,CAAC;AACtD,WAAO,IAAI,iBAAgB,KAAK,QAAQ,WAAW,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,CAAC;AAAA,EACpF;AACF;AAEO,SAAS,oBAAoB,MAAyB,QAAQ,KAAa;AAChF,QAAM,QAAQ,WAAW,IAAI,gBAAgB;AAC7C,QAAM,eAAe,IAAI,qBAAqB;AAC9C,QAAM,WAAW,IAAI,kBAAkB,QAAQ,IAAI,eAAe,IAAI;AACtE,QAAM,SAAuB;AAAA,IAC3B;AAAA,IACA;AAAA,IACA,kBAAkB,iBAAiB,IAAI,+BAA+B,GAAI;AAAA,IAC1E,eAAe,IAAI,8BAA8B;AAAA,IACjD,WAAW,iBAAiB,IAAI,uBAAuB,GAAG;AAAA,EAC5D;AAEA,QAAM,cAAsB,IAAI,cAAc,MAAM;AACpD,QAAM,QAAkB,CAAC,WAAW;AACpC,MAAI,SAAU,OAAM,KAAK,IAAI,YAAY,QAAQ,QAAQ,CAAC;AAE1D,SAAO,IAAI,gBAAgB,QAAQ,KAAK;AAC1C;AAUO,SAAS,cAAc,SAAS,OAAe;AACpD,SAAO,GAAG,MAAM,IAAI,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AAC9C;AAEO,SAAS,WAAW,OAAiC;AAC1D,QAAM,KAAK,SAAS,IAAI,YAAY;AACpC,MAAI,MAAM,WAAW,MAAM,UAAU,MAAM,UAAU,MAAM,WAAW,MAAM,QAAS,QAAO;AAC5F,SAAO;AACT;AAEO,SAAS,YAAY,OAAuB;AACjD,SAAO,MACJ,QAAQ,0BAA0B,WAAW,EAC7C,QAAQ,gCAAgC,YAAY;AACzD;AAEA,SAAS,aAAa,MAA+B,KAA4C;AAC/F,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,GAAG;AACzC,QAAI,mCAAmC,KAAK,CAAC,GAAG;AAC9C,UAAI,CAAC,IAAI;AACT;AAAA,IACF;AACA,QAAI,OAAO,MAAM,UAAU;AACzB,UAAI,OAAO,YAAY,CAAC;AACxB,UAAI,IAAI,iBAAiB,iBAAiB,KAAK,CAAC,GAAG;AACjD,eAAO,SAAS,IAAI;AAAA,MACtB;AACA,UAAI,CAAC,IAAI,UAAU,MAAM,IAAI,SAAS;AAAA,IACxC,OAAO;AACL,UAAI,CAAC,IAAI;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,OAA2B,UAA0B;AAC7E,QAAM,IAAI,OAAO,KAAK;AACtB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,EAAG,QAAO;AACzC,SAAO,KAAK,MAAM,CAAC;AACrB;AAEA,SAAS,UAAU,OAAe,QAAwB;AACxD,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,MAAM,UAAU,OAAQ,QAAO;AACnC,SAAO,GAAG,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,MAAM,SAAS,MAAM;AAC/D;","names":[]}