@clazic/kordoc 2.4.10 → 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
@@ -17,7 +17,7 @@ var OCR_PROMPT = `\uC774 PDF \uD398\uC774\uC9C0 \uC774\uBBF8\uC9C0\uC5D0\uC11C \
17
17
  var _tempDir = null;
18
18
  function getTempDir() {
19
19
  if (!_tempDir) {
20
- _tempDir = join(process.cwd(), "_kordoc_ocr_tmp");
20
+ _tempDir = join(process.cwd(), ".kordoc_ocr_tmp");
21
21
  mkdirSync(_tempDir, { recursive: true });
22
22
  }
23
23
  return _tempDir;
@@ -164,4 +164,4 @@ function stripCodeFence(text) {
164
164
  export {
165
165
  createCliOcrProvider
166
166
  };
167
- //# sourceMappingURL=chunk-KZ2OG25G.js.map
167
+ //# sourceMappingURL=chunk-34WIGIQC.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ocr/cli-provider.ts"],"sourcesContent":["/**\n * CLI 기반 OCR 프로바이더\n *\n * gemini / claude / codex / ollama CLI를 subprocess로 호출하여\n * PDF 페이지 이미지를 Markdown으로 변환.\n *\n * 이미지 전달 방식:\n * - gemini: -p \"프롬프트 @이미지경로\" (@ 파일 참조)\n * - claude: -p \"프롬프트 @이미지경로\" (@ 파일 참조, --print 모드)\n * - codex: exec -i 이미지경로 \"프롬프트\" (-i/--image 플래그)\n * - ollama: REST API (localhost:11434) — CLI는 이미지 입력 미지원\n */\n\nimport { spawnSync } from \"child_process\"\nimport { writeFileSync, readFileSync, unlinkSync, mkdirSync } from \"fs\"\nimport { join } from \"path\"\nimport { tmpdir } from \"os\"\nimport type { OcrMode, StructuredOcrResult } from \"../types.js\"\n\n/** OCR 프롬프트 — 모든 CLI 공통 */\nconst OCR_PROMPT = `이 PDF 페이지 이미지에서 텍스트와 테이블을 추출하여 순수 Markdown으로 변환하세요.\n규칙:\n- 테이블은 Markdown 테이블 문법 사용 (| 구분, |---|---| 헤더 구분선 포함)\n- 병합된 셀은 해당 위치에 내용 기재\n- 헤딩은 글자 크기에 따라 ## ~ ###### 사용\n- 리스트는 - 또는 1. 사용\n- 이미지, 도형 등 비텍스트 요소는 무시\n- 원문의 읽기 순서와 구조를 유지\n- \\`\\`\\`로 감싸지 말고 순수 Markdown만 출력`\n\n/** 임시 디렉토리 (프로세스당 1회 생성)\n *\n * gemini CLI는 /tmp/ 등 시스템 임시 디렉토리를 워크스페이스 외부로 간주하여\n * @파일참조 시 접근을 거부할 수 있음. cwd 하위 폴더를 사용하면 모든 CLI에서 접근 가능.\n *\n * ⚠️ .gitignore에 포함된 경로(예: .kordoc-tmp/)는 gemini CLI가 무시하므로\n * 반드시 gitignore되지 않는 이름 사용. 파일은 try/finally로 즉시 정리.\n */\nlet _tempDir: string | null = null\nfunction getTempDir(): string {\n if (!_tempDir) {\n _tempDir = join(process.cwd(), \"_kordoc_ocr_tmp\")\n mkdirSync(_tempDir, { recursive: true })\n }\n return _tempDir\n}\n\n/**\n * CLI OcrProvider 생성.\n *\n * @param mode - 사용할 CLI (gemini, claude, codex, ollama)\n * @returns OcrProvider 함수 (StructuredOcrResult 반환)\n */\nexport function createCliOcrProvider(\n mode: Exclude<OcrMode, \"auto\" | \"off\" | \"tesseract\">\n): (pageImage: Uint8Array, pageNumber: number, mimeType: \"image/png\") => Promise<StructuredOcrResult> {\n return async (pageImage: Uint8Array, pageNumber: number): Promise<StructuredOcrResult> => {\n const tempPath = join(getTempDir(), `page-${pageNumber}.png`)\n\n try {\n writeFileSync(tempPath, pageImage)\n\n let output: string\n if (mode === \"ollama\") {\n output = await callOllamaApi(tempPath)\n } else {\n output = callCli(mode, tempPath)\n }\n\n return { markdown: stripCodeFence(output.trim()) }\n } finally {\n try { unlinkSync(tempPath) } catch { /* 임시 파일 정리 실패 무시 */ }\n }\n }\n}\n\n/**\n * 출력 텍스트에서 사용량·속도 제한 에러 감지.\n * 해당 메시지가 포함된 경우 throw하여 다음 엔진으로 fallback 트리거.\n */\nfunction checkForLimitError(output: string, mode: string): void {\n const lower = output.toLowerCase()\n if (lower.includes(\"usage limit\") || lower.includes(\"rate limit\")) {\n throw new Error(`${mode} 사용량/속도 제한: ${output.trim().slice(0, 200)}`)\n }\n}\n\n/**\n * CLI 실행 — gemini / claude / codex\n *\n * @throws CLI 실행 실패 또는 타임아웃(180초) 시 Error\n */\nfunction callCli(mode: string, imagePath: string): string {\n // codex는 --output-last-message로 대화 헤더 없는 깔끔한 출력 사용\n if (mode === \"codex\") {\n return callCodexCli(imagePath)\n }\n\n const args = buildCliArgs(mode, imagePath)\n\n const result = spawnSync(mode, args, {\n encoding: \"utf-8\",\n timeout: 600_000,\n maxBuffer: 10 * 1024 * 1024,\n shell: process.platform === \"win32\",\n // claude: /tmp에서 실행하여 프로젝트 CLAUDE.md의 규칙 간섭 방지\n ...(mode === \"claude\" ? { cwd: tmpdir() } : {}),\n })\n\n if (result.error) {\n throw new Error(`${mode} CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`${mode} OCR 실패: ${errMsg}`)\n }\n\n const output = result.stdout || \"\"\n checkForLimitError(output, mode)\n return output\n}\n\n/**\n * codex exec 실행 — --output-last-message로 대화 헤더 없는 깔끔한 출력.\n * 인자 순서: `codex exec <prompt> --image <file> --output-last-message <outfile>`\n */\nfunction callCodexCli(imagePath: string): string {\n // 출력 파일은 /tmp/ 사용 — codex sandbox는 cwd 내 쓰기를 막을 수 있음\n const outPath = join(tmpdir(), `kordoc-codex-out-${Date.now()}.txt`)\n try {\n const args = [\"exec\", OCR_PROMPT, \"--image\", imagePath, \"--output-last-message\", outPath]\n const model = process.env.KORDOC_CODEX_MODEL\n if (model) args.push(\"--model\", model)\n\n const result = spawnSync(\"codex\", args, {\n encoding: \"utf-8\",\n timeout: 180_000,\n maxBuffer: 10 * 1024 * 1024,\n input: \"\", // stdin EOF 즉시 전달 (대화형 입력 차단)\n shell: process.platform === \"win32\",\n })\n\n if (result.error) {\n throw new Error(`codex CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`codex OCR 실패: ${errMsg}`)\n }\n\n // --output-last-message 파일에서 읽기 (없으면 stdout 폴백)\n let text: string\n try {\n text = readFileSync(outPath, \"utf-8\")\n } catch {\n text = result.stdout || \"\"\n }\n checkForLimitError(text, \"codex\")\n return text\n } finally {\n try { unlinkSync(outPath) } catch { /* 무시 */ }\n }\n}\n\n/**\n * CLI별 인자 배열 생성.\n *\n * gemini: [\"--prompt\", \"프롬프트 @이미지경로\", \"--yolo\"]\n * - -y/--yolo: 자동 승인 (OCR은 도구 사용 없으므로 실질적 영향 없음)\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * claude: [\"--print\", \"프롬프트 @이미지경로\"]\n * - --print(-p): 비대화형 출력 모드\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * codex: callCodexCli()에서 별도 처리\n * - `codex exec <prompt> --image <file> --output-last-message <outfile>`\n * - 프롬프트가 --image보다 앞에 위치해야 함 (인자 순서 중요)\n *\n * ⚠️ CLI 버전에 따라 문법이 다를 수 있음. 업데이트 시 --help 재확인 필요.\n */\nfunction buildCliArgs(mode: string, imagePath: string): string[] {\n const normalizedPath = imagePath.replace(/\\\\/g, \"/\")\n const promptWithImage = `${OCR_PROMPT}\n\n이미지: @${normalizedPath}`\n\n switch (mode) {\n case \"gemini\": {\n const args = [\"--prompt\", promptWithImage, \"--yolo\"]\n const model = process.env.KORDOC_GEMINI_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n case \"claude\": {\n const args = [\"--print\", promptWithImage]\n const model = process.env.KORDOC_CLAUDE_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n default:\n throw new Error(`지원하지 않는 CLI: ${mode}`)\n }\n}\n\n/**\n * Ollama REST API 호출 — CLI는 이미지 입력을 지원하지 않으므로 API 직접 사용.\n *\n * 기본 모델: KORDOC_OLLAMA_MODEL 환경변수 또는 \"gemma4:27b\"\n * 기본 호스트: KORDOC_OLLAMA_HOST 환경변수 또는 \"http://localhost:11434\"\n *\n * @throws Ollama 서버 미실행 또는 응답 오류 시 Error\n */\nasync function callOllamaApi(imagePath: string): Promise<string> {\n const { readFileSync } = await import(\"fs\")\n const imageBase64 = readFileSync(imagePath).toString(\"base64\")\n\n const model = process.env.KORDOC_OLLAMA_MODEL || \"qwen3-vl:8b\"\n const host = process.env.KORDOC_OLLAMA_HOST || \"http://localhost:11434\"\n const timeoutMs = Number(process.env.KORDOC_OLLAMA_TIMEOUT) || 120_000\n\n const response = await fetch(`${host}/api/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{\n role: \"user\",\n content: OCR_PROMPT,\n images: [imageBase64],\n }],\n stream: false,\n }),\n signal: AbortSignal.timeout(timeoutMs),\n })\n\n if (!response.ok) {\n throw new Error(`Ollama API 오류: ${response.status} ${response.statusText}`)\n }\n\n const data = await response.json() as { message?: { content?: string } }\n return data.message?.content || \"\"\n}\n\n/**\n * LLM 출력에서 코드 펜스 제거.\n * LLM이 가끔 결과를 ```markdown ... ``` 으로 감싸는 경우 처리.\n */\nfunction stripCodeFence(text: string): string {\n const match = text.match(/^```(?:markdown|md)?\\s*([\\s\\S]*?)```\\s*$/m)\n return match ? match[1].trim() : text\n}\n"],"mappings":"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","names":["readFileSync"]}
1
+ {"version":3,"sources":["../src/ocr/cli-provider.ts"],"sourcesContent":["/**\n * CLI 기반 OCR 프로바이더\n *\n * gemini / claude / codex / ollama CLI를 subprocess로 호출하여\n * PDF 페이지 이미지를 Markdown으로 변환.\n *\n * 이미지 전달 방식:\n * - gemini: -p \"프롬프트 @이미지경로\" (@ 파일 참조)\n * - claude: -p \"프롬프트 @이미지경로\" (@ 파일 참조, --print 모드)\n * - codex: exec -i 이미지경로 \"프롬프트\" (-i/--image 플래그)\n * - ollama: REST API (localhost:11434) — CLI는 이미지 입력 미지원\n */\n\nimport { spawnSync } from \"child_process\"\nimport { writeFileSync, readFileSync, unlinkSync, mkdirSync } from \"fs\"\nimport { join } from \"path\"\nimport { tmpdir } from \"os\"\nimport type { OcrMode, StructuredOcrResult } from \"../types.js\"\n\n/** OCR 프롬프트 — 모든 CLI 공통 */\nconst OCR_PROMPT = `이 PDF 페이지 이미지에서 텍스트와 테이블을 추출하여 순수 Markdown으로 변환하세요.\n규칙:\n- 테이블은 Markdown 테이블 문법 사용 (| 구분, |---|---| 헤더 구분선 포함)\n- 병합된 셀은 해당 위치에 내용 기재\n- 헤딩은 글자 크기에 따라 ## ~ ###### 사용\n- 리스트는 - 또는 1. 사용\n- 이미지, 도형 등 비텍스트 요소는 무시\n- 원문의 읽기 순서와 구조를 유지\n- \\`\\`\\`로 감싸지 말고 순수 Markdown만 출력`\n\n/** 임시 디렉토리 (프로세스당 1회 생성)\n *\n * gemini CLI는 /tmp/ 등 시스템 임시 디렉토리를 워크스페이스 외부로 간주하여\n * @파일참조 시 접근을 거부할 수 있음. cwd 하위 폴더를 사용하면 모든 CLI에서 접근 가능.\n *\n * ⚠️ .gitignore에 포함된 경로(예: .kordoc-tmp/)는 gemini CLI가 무시하므로\n * 반드시 gitignore되지 않는 이름 사용. 파일은 try/finally로 즉시 정리.\n */\nlet _tempDir: string | null = null\nfunction getTempDir(): string {\n if (!_tempDir) {\n _tempDir = join(process.cwd(), \".kordoc_ocr_tmp\")\n mkdirSync(_tempDir, { recursive: true })\n }\n return _tempDir\n}\n\n/**\n * CLI OcrProvider 생성.\n *\n * @param mode - 사용할 CLI (gemini, claude, codex, ollama)\n * @returns OcrProvider 함수 (StructuredOcrResult 반환)\n */\nexport function createCliOcrProvider(\n mode: Exclude<OcrMode, \"auto\" | \"off\" | \"tesseract\">\n): (pageImage: Uint8Array, pageNumber: number, mimeType: \"image/png\") => Promise<StructuredOcrResult> {\n return async (pageImage: Uint8Array, pageNumber: number): Promise<StructuredOcrResult> => {\n const tempPath = join(getTempDir(), `page-${pageNumber}.png`)\n\n try {\n writeFileSync(tempPath, pageImage)\n\n let output: string\n if (mode === \"ollama\") {\n output = await callOllamaApi(tempPath)\n } else {\n output = callCli(mode, tempPath)\n }\n\n return { markdown: stripCodeFence(output.trim()) }\n } finally {\n try { unlinkSync(tempPath) } catch { /* 임시 파일 정리 실패 무시 */ }\n }\n }\n}\n\n/**\n * 출력 텍스트에서 사용량·속도 제한 에러 감지.\n * 해당 메시지가 포함된 경우 throw하여 다음 엔진으로 fallback 트리거.\n */\nfunction checkForLimitError(output: string, mode: string): void {\n const lower = output.toLowerCase()\n if (lower.includes(\"usage limit\") || lower.includes(\"rate limit\")) {\n throw new Error(`${mode} 사용량/속도 제한: ${output.trim().slice(0, 200)}`)\n }\n}\n\n/**\n * CLI 실행 — gemini / claude / codex\n *\n * @throws CLI 실행 실패 또는 타임아웃(180초) 시 Error\n */\nfunction callCli(mode: string, imagePath: string): string {\n // codex는 --output-last-message로 대화 헤더 없는 깔끔한 출력 사용\n if (mode === \"codex\") {\n return callCodexCli(imagePath)\n }\n\n const args = buildCliArgs(mode, imagePath)\n\n const result = spawnSync(mode, args, {\n encoding: \"utf-8\",\n timeout: 600_000,\n maxBuffer: 10 * 1024 * 1024,\n shell: process.platform === \"win32\",\n // claude: /tmp에서 실행하여 프로젝트 CLAUDE.md의 규칙 간섭 방지\n ...(mode === \"claude\" ? { cwd: tmpdir() } : {}),\n })\n\n if (result.error) {\n throw new Error(`${mode} CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`${mode} OCR 실패: ${errMsg}`)\n }\n\n const output = result.stdout || \"\"\n checkForLimitError(output, mode)\n return output\n}\n\n/**\n * codex exec 실행 — --output-last-message로 대화 헤더 없는 깔끔한 출력.\n * 인자 순서: `codex exec <prompt> --image <file> --output-last-message <outfile>`\n */\nfunction callCodexCli(imagePath: string): string {\n // 출력 파일은 /tmp/ 사용 — codex sandbox는 cwd 내 쓰기를 막을 수 있음\n const outPath = join(tmpdir(), `kordoc-codex-out-${Date.now()}.txt`)\n try {\n const args = [\"exec\", OCR_PROMPT, \"--image\", imagePath, \"--output-last-message\", outPath]\n const model = process.env.KORDOC_CODEX_MODEL\n if (model) args.push(\"--model\", model)\n\n const result = spawnSync(\"codex\", args, {\n encoding: \"utf-8\",\n timeout: 180_000,\n maxBuffer: 10 * 1024 * 1024,\n input: \"\", // stdin EOF 즉시 전달 (대화형 입력 차단)\n shell: process.platform === \"win32\",\n })\n\n if (result.error) {\n throw new Error(`codex CLI 실행 실패: ${result.error.message}`)\n }\n if (result.status !== 0) {\n const errMsg = result.stderr?.trim() || `exit code ${result.status}`\n throw new Error(`codex OCR 실패: ${errMsg}`)\n }\n\n // --output-last-message 파일에서 읽기 (없으면 stdout 폴백)\n let text: string\n try {\n text = readFileSync(outPath, \"utf-8\")\n } catch {\n text = result.stdout || \"\"\n }\n checkForLimitError(text, \"codex\")\n return text\n } finally {\n try { unlinkSync(outPath) } catch { /* 무시 */ }\n }\n}\n\n/**\n * CLI별 인자 배열 생성.\n *\n * gemini: [\"--prompt\", \"프롬프트 @이미지경로\", \"--yolo\"]\n * - -y/--yolo: 자동 승인 (OCR은 도구 사용 없으므로 실질적 영향 없음)\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * claude: [\"--print\", \"프롬프트 @이미지경로\"]\n * - --print(-p): 비대화형 출력 모드\n * - @ 파일 참조로 이미지를 컨텍스트에 포함\n *\n * codex: callCodexCli()에서 별도 처리\n * - `codex exec <prompt> --image <file> --output-last-message <outfile>`\n * - 프롬프트가 --image보다 앞에 위치해야 함 (인자 순서 중요)\n *\n * ⚠️ CLI 버전에 따라 문법이 다를 수 있음. 업데이트 시 --help 재확인 필요.\n */\nfunction buildCliArgs(mode: string, imagePath: string): string[] {\n const normalizedPath = imagePath.replace(/\\\\/g, \"/\")\n const promptWithImage = `${OCR_PROMPT}\n\n이미지: @${normalizedPath}`\n\n switch (mode) {\n case \"gemini\": {\n const args = [\"--prompt\", promptWithImage, \"--yolo\"]\n const model = process.env.KORDOC_GEMINI_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n case \"claude\": {\n const args = [\"--print\", promptWithImage]\n const model = process.env.KORDOC_CLAUDE_MODEL\n if (model) args.push(\"--model\", model)\n return args\n }\n\n default:\n throw new Error(`지원하지 않는 CLI: ${mode}`)\n }\n}\n\n/**\n * Ollama REST API 호출 — CLI는 이미지 입력을 지원하지 않으므로 API 직접 사용.\n *\n * 기본 모델: KORDOC_OLLAMA_MODEL 환경변수 또는 \"gemma4:27b\"\n * 기본 호스트: KORDOC_OLLAMA_HOST 환경변수 또는 \"http://localhost:11434\"\n *\n * @throws Ollama 서버 미실행 또는 응답 오류 시 Error\n */\nasync function callOllamaApi(imagePath: string): Promise<string> {\n const { readFileSync } = await import(\"fs\")\n const imageBase64 = readFileSync(imagePath).toString(\"base64\")\n\n const model = process.env.KORDOC_OLLAMA_MODEL || \"qwen3-vl:8b\"\n const host = process.env.KORDOC_OLLAMA_HOST || \"http://localhost:11434\"\n const timeoutMs = Number(process.env.KORDOC_OLLAMA_TIMEOUT) || 120_000\n\n const response = await fetch(`${host}/api/chat`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{\n role: \"user\",\n content: OCR_PROMPT,\n images: [imageBase64],\n }],\n stream: false,\n }),\n signal: AbortSignal.timeout(timeoutMs),\n })\n\n if (!response.ok) {\n throw new Error(`Ollama API 오류: ${response.status} ${response.statusText}`)\n }\n\n const data = await response.json() as { message?: { content?: string } }\n return data.message?.content || \"\"\n}\n\n/**\n * LLM 출력에서 코드 펜스 제거.\n * LLM이 가끔 결과를 ```markdown ... ``` 으로 감싸는 경우 처리.\n */\nfunction stripCodeFence(text: string): string {\n const match = text.match(/^```(?:markdown|md)?\\s*([\\s\\S]*?)```\\s*$/m)\n return match ? match[1].trim() : text\n}\n"],"mappings":"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","names":["readFileSync"]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/utils.ts
4
- var VERSION = true ? "2.4.10" : "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-GKN4GHFO.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":"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","names":[]}