@clazic/kordoc 2.1.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/dist/mcp.js ADDED
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ blocksToMarkdown,
4
+ compare,
5
+ detectFormat,
6
+ extractFormFields,
7
+ extractHwp5MetadataOnly,
8
+ extractHwpxMetadataOnly,
9
+ extractPdfMetadataOnly,
10
+ parse
11
+ } from "./chunk-R6J5ZSDL.js";
12
+ import {
13
+ KordocError,
14
+ VERSION,
15
+ sanitizeError,
16
+ toArrayBuffer
17
+ } from "./chunk-PKIJLEV6.js";
18
+ import "./chunk-MOL7MDBG.js";
19
+
20
+ // src/mcp.ts
21
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
+ import { z } from "zod";
24
+ import { readFileSync, realpathSync, openSync, readSync, closeSync, statSync } from "fs";
25
+ import { resolve, isAbsolute, extname } from "path";
26
+ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".hwp", ".hwpx", ".pdf", ".xlsx", ".docx"]);
27
+ var MAX_FILE_SIZE = 500 * 1024 * 1024;
28
+ function safePath(filePath) {
29
+ if (!filePath) throw new KordocError("\uD30C\uC77C \uACBD\uB85C\uAC00 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4");
30
+ const resolved = resolve(filePath);
31
+ const real = realpathSync(resolved);
32
+ if (!isAbsolute(real)) throw new KordocError("\uC808\uB300 \uACBD\uB85C\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4");
33
+ const ext = extname(real).toLowerCase();
34
+ if (!ALLOWED_EXTENSIONS.has(ext)) throw new KordocError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD655\uC7A5\uC790\uC785\uB2C8\uB2E4: ${ext} (\uD5C8\uC6A9: ${[...ALLOWED_EXTENSIONS].join(", ")})`);
35
+ return real;
36
+ }
37
+ var MAX_METADATA_FILE_SIZE = 50 * 1024 * 1024;
38
+ function readValidatedFile(filePath, maxSize = MAX_FILE_SIZE) {
39
+ const resolved = safePath(filePath);
40
+ const fileSize = statSync(resolved).size;
41
+ if (fileSize > maxSize) {
42
+ throw new KordocError(`\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${(fileSize / 1024 / 1024).toFixed(1)}MB (\uCD5C\uB300 ${maxSize / 1024 / 1024}MB)`);
43
+ }
44
+ const raw = readFileSync(resolved);
45
+ return { buffer: toArrayBuffer(raw), resolved };
46
+ }
47
+ function detectFormatFromHeader(resolved) {
48
+ const fd = openSync(resolved, "r");
49
+ try {
50
+ const headerBuf = Buffer.alloc(16);
51
+ readSync(fd, headerBuf, 0, 16, 0);
52
+ return detectFormat(toArrayBuffer(headerBuf));
53
+ } finally {
54
+ closeSync(fd);
55
+ }
56
+ }
57
+ var server = new McpServer({
58
+ name: "kordoc",
59
+ version: VERSION
60
+ });
61
+ server.tool(
62
+ "parse_document",
63
+ "\uD55C\uAD6D \uBB38\uC11C \uD30C\uC77C(HWP, HWPX, PDF, XLSX, DOCX)\uC744 \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. \uD30C\uC77C \uACBD\uB85C\uB97C \uC785\uB825\uD558\uBA74 \uD3EC\uB9F7\uC744 \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD569\uB2C8\uB2E4.",
64
+ {
65
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX)")
66
+ },
67
+ async ({ file_path }) => {
68
+ try {
69
+ const { buffer } = readValidatedFile(file_path);
70
+ const format = detectFormat(buffer);
71
+ if (format === "unknown") {
72
+ return {
73
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
74
+ isError: true
75
+ };
76
+ }
77
+ const result = await parse(buffer);
78
+ if (!result.success) {
79
+ return {
80
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
81
+ isError: true
82
+ };
83
+ }
84
+ const meta = [
85
+ `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
86
+ result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null,
87
+ result.metadata?.title ? `\uC81C\uBAA9: ${result.metadata.title}` : null,
88
+ result.metadata?.author ? `\uC791\uC131\uC790: ${result.metadata.author}` : null,
89
+ result.isImageBased ? "\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uBD88\uAC00)" : null
90
+ ].filter(Boolean).join(" | ");
91
+ const parts = [`[${meta}]`];
92
+ if (result.outline && result.outline.length > 0) {
93
+ const outlineText = result.outline.map((o) => `${" ".repeat(o.level - 1)}- ${o.text}`).join("\n");
94
+ parts.push(`
95
+ \u{1F4D1} \uBB38\uC11C \uAD6C\uC870:
96
+ ${outlineText}`);
97
+ }
98
+ if (result.warnings && result.warnings.length > 0) {
99
+ const warnText = result.warnings.map((w) => `- [p${w.page || "?"}] ${w.message}`).join("\n");
100
+ parts.push(`
101
+ \u26A0\uFE0F \uACBD\uACE0:
102
+ ${warnText}`);
103
+ }
104
+ parts.push(`
105
+
106
+ ${result.markdown}`);
107
+ return {
108
+ content: [{ type: "text", text: parts.join("") }]
109
+ };
110
+ } catch (err) {
111
+ return {
112
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
113
+ isError: true
114
+ };
115
+ }
116
+ }
117
+ );
118
+ server.tool(
119
+ "detect_format",
120
+ "\uD30C\uC77C\uC758 \uD3EC\uB9F7\uC744 \uB9E4\uC9C1 \uBC14\uC774\uD2B8\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4 (hwpx, hwp, pdf, unknown).",
121
+ {
122
+ file_path: z.string().min(1).describe("\uAC10\uC9C0\uD560 \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
123
+ },
124
+ async ({ file_path }) => {
125
+ try {
126
+ const resolved = safePath(file_path);
127
+ const format = detectFormatFromHeader(resolved);
128
+ return {
129
+ content: [{ type: "text", text: `${file_path}: ${format}` }]
130
+ };
131
+ } catch (err) {
132
+ return {
133
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
134
+ isError: true
135
+ };
136
+ }
137
+ }
138
+ );
139
+ server.tool(
140
+ "parse_metadata",
141
+ "\uBB38\uC11C\uC758 \uBA54\uD0C0\uB370\uC774\uD130(\uC81C\uBAA9, \uC791\uC131\uC790, \uB0A0\uC9DC \uB4F1)\uB9CC \uBE60\uB974\uAC8C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC804\uCCB4 \uD30C\uC2F1 \uC5C6\uC774 \uD5E4\uB354/\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.",
142
+ {
143
+ file_path: z.string().min(1).describe("\uBA54\uD0C0\uB370\uC774\uD130\uB97C \uCD94\uCD9C\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
144
+ },
145
+ async ({ file_path }) => {
146
+ try {
147
+ const resolved = safePath(file_path);
148
+ const format = detectFormatFromHeader(resolved);
149
+ if (format === "unknown") {
150
+ return {
151
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
152
+ isError: true
153
+ };
154
+ }
155
+ const { buffer } = readValidatedFile(file_path, MAX_METADATA_FILE_SIZE);
156
+ let metadata;
157
+ switch (format) {
158
+ case "hwp":
159
+ metadata = extractHwp5MetadataOnly(Buffer.from(buffer));
160
+ break;
161
+ case "hwpx":
162
+ metadata = await extractHwpxMetadataOnly(buffer);
163
+ break;
164
+ case "pdf":
165
+ metadata = await extractPdfMetadataOnly(buffer);
166
+ break;
167
+ }
168
+ return {
169
+ content: [{ type: "text", text: JSON.stringify({ format, ...metadata }, null, 2) }]
170
+ };
171
+ } catch (err) {
172
+ return {
173
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
174
+ isError: true
175
+ };
176
+ }
177
+ }
178
+ );
179
+ server.tool(
180
+ "parse_pages",
181
+ "\uBB38\uC11C\uC758 \uD2B9\uC815 \uD398\uC774\uC9C0/\uC139\uC158 \uBC94\uC704\uB9CC \uD30C\uC2F1\uD569\uB2C8\uB2E4. PDF\uB294 \uC815\uD655\uD55C \uD398\uC774\uC9C0, HWP/HWPX\uB294 \uC139\uC158 \uB2E8\uC704 \uADFC\uC0AC\uCE58\uC785\uB2C8\uB2E4.",
182
+ {
183
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
184
+ pages: z.string().min(1).describe("\uD398\uC774\uC9C0 \uBC94\uC704 (\uC608: '1-3', '1,3,5-7')")
185
+ },
186
+ async ({ file_path, pages }) => {
187
+ try {
188
+ const { buffer } = readValidatedFile(file_path);
189
+ const format = detectFormat(buffer);
190
+ if (format === "unknown") {
191
+ return {
192
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
193
+ isError: true
194
+ };
195
+ }
196
+ const result = await parse(buffer, { pages });
197
+ if (!result.success) {
198
+ return {
199
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
200
+ isError: true
201
+ };
202
+ }
203
+ const meta = [
204
+ `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
205
+ `\uBC94\uC704: ${pages}`,
206
+ result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null
207
+ ].filter(Boolean).join(" | ");
208
+ return {
209
+ content: [{ type: "text", text: `[${meta}]
210
+
211
+ ${result.markdown}` }]
212
+ };
213
+ } catch (err) {
214
+ return {
215
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
216
+ isError: true
217
+ };
218
+ }
219
+ }
220
+ );
221
+ server.tool(
222
+ "parse_table",
223
+ "\uBB38\uC11C\uC5D0\uC11C N\uBC88\uC9F8 \uD14C\uC774\uBE14\uB9CC \uCD94\uCD9C\uD569\uB2C8\uB2E4 (0-based index). \uD14C\uC774\uBE14\uC774 \uC5C6\uAC70\uB098 \uC778\uB371\uC2A4 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uBA74 \uC624\uB958\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.",
224
+ {
225
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
226
+ table_index: z.number().int().min(0).describe("\uCD94\uCD9C\uD560 \uD14C\uC774\uBE14 \uC778\uB371\uC2A4 (0\uBD80\uD130 \uC2DC\uC791)")
227
+ },
228
+ async ({ file_path, table_index }) => {
229
+ try {
230
+ const { buffer } = readValidatedFile(file_path);
231
+ const format = detectFormat(buffer);
232
+ if (format === "unknown") {
233
+ return {
234
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
235
+ isError: true
236
+ };
237
+ }
238
+ const result = await parse(buffer);
239
+ if (!result.success) {
240
+ return {
241
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
242
+ isError: true
243
+ };
244
+ }
245
+ const tableBlocks = result.blocks.filter((b) => b.type === "table" && b.table);
246
+ if (tableBlocks.length === 0) {
247
+ return {
248
+ content: [{ type: "text", text: `\uBB38\uC11C\uC5D0 \uD14C\uC774\uBE14\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.` }],
249
+ isError: true
250
+ };
251
+ }
252
+ if (table_index >= tableBlocks.length) {
253
+ return {
254
+ content: [{ type: "text", text: `\uD14C\uC774\uBE14 \uC778\uB371\uC2A4 \uCD08\uACFC: ${table_index} (\uCD1D ${tableBlocks.length}\uAC1C \uD14C\uC774\uBE14)` }],
255
+ isError: true
256
+ };
257
+ }
258
+ const tableBlock = tableBlocks[table_index];
259
+ const tableMarkdown = blocksToMarkdown([tableBlock]);
260
+ return {
261
+ content: [{ type: "text", text: `[\uD14C\uC774\uBE14 #${table_index} / \uCD1D ${tableBlocks.length}\uAC1C]
262
+
263
+ ${tableMarkdown}` }]
264
+ };
265
+ } catch (err) {
266
+ return {
267
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
268
+ isError: true
269
+ };
270
+ }
271
+ }
272
+ );
273
+ server.tool(
274
+ "compare_documents",
275
+ "\uB450 \uD55C\uAD6D \uBB38\uC11C \uD30C\uC77C\uC744 \uBE44\uAD50\uD558\uC5EC \uCD94\uAC00/\uC0AD\uC81C/\uBCC0\uACBD\uB41C \uBE14\uB85D\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC2E0\uAD6C\uB300\uC870\uD45C \uC0DD\uC131\uC5D0 \uD65C\uC6A9\uB429\uB2C8\uB2E4. \uD06C\uB85C\uC2A4 \uD3EC\uB9F7(HWP\u2194HWPX) \uBE44\uAD50 \uAC00\uB2A5.",
276
+ {
277
+ file_path_a: z.string().min(1).describe("\uBE44\uAD50 \uC6D0\uBCF8 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
278
+ file_path_b: z.string().min(1).describe("\uBE44\uAD50 \uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C")
279
+ },
280
+ async ({ file_path_a, file_path_b }) => {
281
+ try {
282
+ const { buffer: bufA } = readValidatedFile(file_path_a);
283
+ const { buffer: bufB } = readValidatedFile(file_path_b);
284
+ const result = await compare(bufA, bufB);
285
+ const { stats, diffs } = result;
286
+ const lines = [
287
+ `## \uBB38\uC11C \uBE44\uAD50 \uACB0\uACFC`,
288
+ `\uCD94\uAC00: ${stats.added} | \uC0AD\uC81C: ${stats.removed} | \uBCC0\uACBD: ${stats.modified} | \uB3D9\uC77C: ${stats.unchanged}`,
289
+ ""
290
+ ];
291
+ for (const d of diffs) {
292
+ const prefix = d.type === "added" ? "+" : d.type === "removed" ? "-" : d.type === "modified" ? "~" : " ";
293
+ const text = d.after?.text || d.before?.text || (d.after?.table ? "[\uD14C\uC774\uBE14]" : d.before?.table ? "[\uD14C\uC774\uBE14]" : "");
294
+ const sim = d.similarity !== void 0 ? ` (${(d.similarity * 100).toFixed(0)}%)` : "";
295
+ lines.push(`${prefix} ${text.substring(0, 200)}${sim}`);
296
+ }
297
+ return {
298
+ content: [{ type: "text", text: lines.join("\n") }]
299
+ };
300
+ } catch (err) {
301
+ return {
302
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
303
+ isError: true
304
+ };
305
+ }
306
+ }
307
+ );
308
+ server.tool(
309
+ "parse_form",
310
+ "\uD55C\uAD6D \uC11C\uC2DD \uBB38\uC11C\uC5D0\uC11C \uB808\uC774\uBE14-\uAC12 \uC30D\uC744 \uAD6C\uC870\uD654\uB41C JSON\uC73C\uB85C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC591\uC2DD/\uC11C\uC2DD \uBB38\uC11C\uC5D0 \uCD5C\uC801\uD654.",
311
+ {
312
+ file_path: z.string().min(1).describe("\uC11C\uC2DD \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
313
+ },
314
+ async ({ file_path }) => {
315
+ try {
316
+ const { buffer } = readValidatedFile(file_path);
317
+ const result = await parse(buffer);
318
+ if (!result.success) {
319
+ return {
320
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${result.error}` }],
321
+ isError: true
322
+ };
323
+ }
324
+ const form = extractFormFields(result.blocks);
325
+ return {
326
+ content: [{ type: "text", text: JSON.stringify(form, null, 2) }]
327
+ };
328
+ } catch (err) {
329
+ return {
330
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
331
+ isError: true
332
+ };
333
+ }
334
+ }
335
+ );
336
+ async function main() {
337
+ const transport = new StdioServerTransport();
338
+ await server.connect(transport);
339
+ }
340
+ main().catch((err) => {
341
+ console.error(err);
342
+ process.exit(1);
343
+ });
344
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["/** kordoc MCP 서버 — Claude/Cursor에서 문서 파싱 도구로 사용 */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { z } from \"zod\"\nimport { readFileSync, realpathSync, openSync, readSync, closeSync, statSync } from \"fs\"\nimport { resolve, isAbsolute, extname } from \"path\"\nimport { parse, detectFormat, blocksToMarkdown, compare, extractFormFields } from \"./index.js\"\nimport { VERSION, toArrayBuffer, sanitizeError, KordocError } from \"./utils.js\"\nimport { extractHwp5MetadataOnly } from \"./hwp5/parser.js\"\nimport { extractHwpxMetadataOnly } from \"./hwpx/parser.js\"\nimport { extractPdfMetadataOnly } from \"./pdf/parser.js\"\n\n/** 허용 파일 확장자 */\nconst ALLOWED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\n/** 최대 파일 크기 (500MB) */\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/** 경로 정규화 및 보안 검증 */\nfunction safePath(filePath: string): string {\n if (!filePath) throw new KordocError(\"파일 경로가 비어있습니다\")\n const resolved = resolve(filePath)\n const real = realpathSync(resolved)\n if (!isAbsolute(real)) throw new KordocError(\"절대 경로만 허용됩니다\")\n const ext = extname(real).toLowerCase()\n if (!ALLOWED_EXTENSIONS.has(ext)) throw new KordocError(`지원하지 않는 확장자입니다: ${ext} (허용: ${[...ALLOWED_EXTENSIONS].join(\", \")})`)\n return real\n}\n\n/** 최대 파일 크기 — metadata 전용 (50MB, 전체 파싱보다 보수적) */\nconst MAX_METADATA_FILE_SIZE = 50 * 1024 * 1024\n\n/** 파일 읽기 + 크기 검증 공통 로직 */\nfunction readValidatedFile(filePath: string, maxSize = MAX_FILE_SIZE): { buffer: ArrayBuffer; resolved: string } {\n const resolved = safePath(filePath)\n const fileSize = statSync(resolved).size\n if (fileSize > maxSize) {\n throw new KordocError(`파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${maxSize / 1024 / 1024}MB)`)\n }\n const raw = readFileSync(resolved)\n return { buffer: toArrayBuffer(raw), resolved }\n}\n\n/** 파일 헤더(16바이트)만 읽어 포맷 감지 — 전체 파일 로드 불필요 */\nfunction detectFormatFromHeader(resolved: string): ReturnType<typeof detectFormat> {\n const fd = openSync(resolved, \"r\")\n try {\n const headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n return detectFormat(toArrayBuffer(headerBuf))\n } finally {\n closeSync(fd)\n }\n}\n\nconst server = new McpServer({\n name: \"kordoc\",\n version: VERSION,\n})\n\n// ─── 도구: parse_document ────────────────────────────\n\nserver.tool(\n \"parse_document\",\n \"한국 문서 파일(HWP, HWPX, PDF, XLSX, DOCX)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF, XLSX, DOCX)\"),\n },\n async ({ file_path }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n result.metadata?.title ? `제목: ${result.metadata.title}` : null,\n result.metadata?.author ? `작성자: ${result.metadata.author}` : null,\n result.isImageBased ? \"이미지 기반 PDF (텍스트 추출 불가)\" : null,\n ].filter(Boolean).join(\" | \")\n\n // outline/warnings 부가 정보 추가\n const parts: string[] = [`[${meta}]`]\n\n if (result.outline && result.outline.length > 0) {\n const outlineText = result.outline.map(o => `${\" \".repeat(o.level - 1)}- ${o.text}`).join(\"\\n\")\n parts.push(`\\n📑 문서 구조:\\n${outlineText}`)\n }\n\n if (result.warnings && result.warnings.length > 0) {\n const warnText = result.warnings.map(w => `- [p${w.page || \"?\"}] ${w.message}`).join(\"\\n\")\n parts.push(`\\n⚠️ 경고:\\n${warnText}`)\n }\n\n parts.push(`\\n\\n${result.markdown}`)\n\n return {\n content: [{ type: \"text\", text: parts.join(\"\") }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: detect_format ─────────────────────────────\n\nserver.tool(\n \"detect_format\",\n \"파일의 포맷을 매직 바이트로 감지합니다 (hwpx, hwp, pdf, unknown).\",\n {\n file_path: z.string().min(1).describe(\"감지할 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n return {\n content: [{ type: \"text\", text: `${file_path}: ${format}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_metadata ────────────────────────────\n\nserver.tool(\n \"parse_metadata\",\n \"문서의 메타데이터(제목, 작성자, 날짜 등)만 빠르게 추출합니다. 전체 파싱 없이 헤더/매니페스트만 읽습니다.\",\n {\n file_path: z.string().min(1).describe(\"메타데이터를 추출할 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const format = detectFormatFromHeader(resolved)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n // metadata 전용 크기 제한 (50MB)\n const { buffer } = readValidatedFile(file_path, MAX_METADATA_FILE_SIZE)\n\n let metadata\n switch (format) {\n case \"hwp\":\n metadata = extractHwp5MetadataOnly(Buffer.from(buffer))\n break\n case \"hwpx\":\n metadata = await extractHwpxMetadataOnly(buffer)\n break\n case \"pdf\":\n metadata = await extractPdfMetadataOnly(buffer)\n break\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify({ format, ...metadata }, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_pages ──────────────────────────────\n\nserver.tool(\n \"parse_pages\",\n \"문서의 특정 페이지/섹션 범위만 파싱합니다. PDF는 정확한 페이지, HWP/HWPX는 섹션 단위 근사치입니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n pages: z.string().min(1).describe(\"페이지 범위 (예: '1-3', '1,3,5-7')\"),\n },\n async ({ file_path, pages }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer, { pages })\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const meta = [\n `포맷: ${result.fileType.toUpperCase()}`,\n `범위: ${pages}`,\n result.pageCount ? `페이지: ${result.pageCount}` : null,\n ].filter(Boolean).join(\" | \")\n\n return {\n content: [{ type: \"text\", text: `[${meta}]\\n\\n${result.markdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_table ──────────────────────────────\n\nserver.tool(\n \"parse_table\",\n \"문서에서 N번째 테이블만 추출합니다 (0-based index). 테이블이 없거나 인덱스 범위를 초과하면 오류를 반환합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로\"),\n table_index: z.number().int().min(0).describe(\"추출할 테이블 인덱스 (0부터 시작)\"),\n },\n async ({ file_path, table_index }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\n\n if (format === \"unknown\") {\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\n }\n\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패 (${result.fileType}): ${result.error}` }],\n isError: true,\n }\n }\n\n const tableBlocks = result.blocks.filter(b => b.type === \"table\" && b.table)\n if (tableBlocks.length === 0) {\n return {\n content: [{ type: \"text\", text: `문서에 테이블이 없습니다.` }],\n isError: true,\n }\n }\n\n if (table_index >= tableBlocks.length) {\n return {\n content: [{ type: \"text\", text: `테이블 인덱스 초과: ${table_index} (총 ${tableBlocks.length}개 테이블)` }],\n isError: true,\n }\n }\n\n const tableBlock = tableBlocks[table_index]\n const tableMarkdown = blocksToMarkdown([tableBlock])\n\n return {\n content: [{ type: \"text\", text: `[테이블 #${table_index} / 총 ${tableBlocks.length}개]\\n\\n${tableMarkdown}` }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: compare_documents ─────────────────────────\n\nserver.tool(\n \"compare_documents\",\n \"두 한국 문서 파일을 비교하여 추가/삭제/변경된 블록을 표시합니다. 신구대조표 생성에 활용됩니다. 크로스 포맷(HWP↔HWPX) 비교 가능.\",\n {\n file_path_a: z.string().min(1).describe(\"비교 원본 문서의 절대 경로\"),\n file_path_b: z.string().min(1).describe(\"비교 대상 문서의 절대 경로\"),\n },\n async ({ file_path_a, file_path_b }) => {\n try {\n const { buffer: bufA } = readValidatedFile(file_path_a)\n const { buffer: bufB } = readValidatedFile(file_path_b)\n\n const result = await compare(bufA, bufB)\n const { stats, diffs } = result\n\n const lines: string[] = [\n `## 문서 비교 결과`,\n `추가: ${stats.added} | 삭제: ${stats.removed} | 변경: ${stats.modified} | 동일: ${stats.unchanged}`,\n \"\",\n ]\n\n for (const d of diffs) {\n const prefix = d.type === \"added\" ? \"+\" : d.type === \"removed\" ? \"-\" : d.type === \"modified\" ? \"~\" : \" \"\n const text = d.after?.text || d.before?.text || (d.after?.table ? \"[테이블]\" : d.before?.table ? \"[테이블]\" : \"\")\n const sim = d.similarity !== undefined ? ` (${(d.similarity * 100).toFixed(0)}%)` : \"\"\n lines.push(`${prefix} ${text.substring(0, 200)}${sim}`)\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 도구: parse_form ───────────────────────────────\n\nserver.tool(\n \"parse_form\",\n \"한국 서식 문서에서 레이블-값 쌍을 구조화된 JSON으로 추출합니다. 양식/서식 문서에 최적화.\",\n {\n file_path: z.string().min(1).describe(\"서식 문서 파일의 절대 경로\"),\n },\n async ({ file_path }) => {\n try {\n const { buffer } = readValidatedFile(file_path)\n const result = await parse(buffer)\n\n if (!result.success) {\n return {\n content: [{ type: \"text\", text: `파싱 실패: ${result.error}` }],\n isError: true,\n }\n }\n\n const form = extractFormFields(result.blocks)\n return {\n content: [{ type: \"text\", text: JSON.stringify(form, null, 2) }],\n }\n } catch (err) {\n return {\n content: [{ type: \"text\", text: `오류: ${sanitizeError(err)}` }],\n isError: true,\n }\n }\n }\n)\n\n// ─── 서버 시작 ───────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport()\n await server.connect(transport)\n}\n\nmain().catch((err) => { console.error(err); process.exit(1) })\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAEA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,SAAS;AAClB,SAAS,cAAc,cAAc,UAAU,UAAU,WAAW,gBAAgB;AACpF,SAAS,SAAS,YAAY,eAAe;AAQ7C,IAAM,qBAAqB,oBAAI,IAAI,CAAC,QAAQ,SAAS,QAAQ,SAAS,OAAO,CAAC;AAE9E,IAAM,gBAAgB,MAAM,OAAO;AAGnC,SAAS,SAAS,UAA0B;AAC1C,MAAI,CAAC,SAAU,OAAM,IAAI,YAAY,sEAAe;AACpD,QAAM,WAAW,QAAQ,QAAQ;AACjC,QAAM,OAAO,aAAa,QAAQ;AAClC,MAAI,CAAC,WAAW,IAAI,EAAG,OAAM,IAAI,YAAY,gEAAc;AAC3D,QAAM,MAAM,QAAQ,IAAI,EAAE,YAAY;AACtC,MAAI,CAAC,mBAAmB,IAAI,GAAG,EAAG,OAAM,IAAI,YAAY,+EAAmB,GAAG,mBAAS,CAAC,GAAG,kBAAkB,EAAE,KAAK,IAAI,CAAC,GAAG;AAC5H,SAAO;AACT;AAGA,IAAM,yBAAyB,KAAK,OAAO;AAG3C,SAAS,kBAAkB,UAAkB,UAAU,eAA0D;AAC/G,QAAM,WAAW,SAAS,QAAQ;AAClC,QAAM,WAAW,SAAS,QAAQ,EAAE;AACpC,MAAI,WAAW,SAAS;AACtB,UAAM,IAAI,YAAY,wDAAgB,WAAW,OAAO,MAAM,QAAQ,CAAC,CAAC,oBAAU,UAAU,OAAO,IAAI,KAAK;AAAA,EAC9G;AACA,QAAM,MAAM,aAAa,QAAQ;AACjC,SAAO,EAAE,QAAQ,cAAc,GAAG,GAAG,SAAS;AAChD;AAGA,SAAS,uBAAuB,UAAmD;AACjF,QAAM,KAAK,SAAS,UAAU,GAAG;AACjC,MAAI;AACF,UAAM,YAAY,OAAO,MAAM,EAAE;AACjC,aAAS,IAAI,WAAW,GAAG,IAAI,CAAC;AAChC,WAAO,aAAa,cAAc,SAAS,CAAC;AAAA,EAC9C,UAAE;AACA,cAAU,EAAE;AAAA,EACd;AACF;AAEA,IAAM,SAAS,IAAI,UAAU;AAAA,EAC3B,MAAM;AAAA,EACN,SAAS;AACX,CAAC;AAID,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,2GAA+C;AAAA,EACvF;AAAA,EACA,OAAO,EAAE,UAAU,MAAM;AACvB,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,kBAAkB,SAAS;AAC9C,YAAM,SAAS,aAAa,MAAM;AAElC,UAAI,WAAW,WAAW;AACxB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,sFAAqB,SAAS,GAAG,CAAC;AAAA,UAClE,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,MAAM,MAAM;AAEjC,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,8BAAU,OAAO,QAAQ,MAAM,OAAO,KAAK,GAAG,CAAC;AAAA,UAC/E,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,OAAO;AAAA,QACX,iBAAO,OAAO,SAAS,YAAY,CAAC;AAAA,QACpC,OAAO,YAAY,uBAAQ,OAAO,SAAS,KAAK;AAAA,QAChD,OAAO,UAAU,QAAQ,iBAAO,OAAO,SAAS,KAAK,KAAK;AAAA,QAC1D,OAAO,UAAU,SAAS,uBAAQ,OAAO,SAAS,MAAM,KAAK;AAAA,QAC7D,OAAO,eAAe,uFAA2B;AAAA,MACnD,EAAE,OAAO,OAAO,EAAE,KAAK,KAAK;AAG5B,YAAM,QAAkB,CAAC,IAAI,IAAI,GAAG;AAEpC,UAAI,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG;AAC/C,cAAM,cAAc,OAAO,QAAQ,IAAI,OAAK,GAAG,KAAK,OAAO,EAAE,QAAQ,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI;AAC/F,cAAM,KAAK;AAAA;AAAA,EAAgB,WAAW,EAAE;AAAA,MAC1C;AAEA,UAAI,OAAO,YAAY,OAAO,SAAS,SAAS,GAAG;AACjD,cAAM,WAAW,OAAO,SAAS,IAAI,OAAK,OAAO,EAAE,QAAQ,GAAG,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AACzF,cAAM,KAAK;AAAA;AAAA,EAAa,QAAQ,EAAE;AAAA,MACpC;AAEA,YAAM,KAAK;AAAA;AAAA,EAAO,OAAO,QAAQ,EAAE;AAEnC,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,MAAM,KAAK,EAAE,EAAE,CAAC;AAAA,MAClD;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,iEAAe;AAAA,EACvD;AAAA,EACA,OAAO,EAAE,UAAU,MAAM;AACvB,QAAI;AACF,YAAM,WAAW,SAAS,SAAS;AACnC,YAAM,SAAS,uBAAuB,QAAQ;AAC9C,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,CAAC;AAAA,MAC7D;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,mHAAyB;AAAA,EACjE;AAAA,EACA,OAAO,EAAE,UAAU,MAAM;AACvB,QAAI;AACF,YAAM,WAAW,SAAS,SAAS;AACnC,YAAM,SAAS,uBAAuB,QAAQ;AAE9C,UAAI,WAAW,WAAW;AACxB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,sFAAqB,SAAS,GAAG,CAAC;AAAA,UAClE,SAAS;AAAA,QACX;AAAA,MACF;AAGA,YAAM,EAAE,OAAO,IAAI,kBAAkB,WAAW,sBAAsB;AAEtE,UAAI;AACJ,cAAQ,QAAQ;AAAA,QACd,KAAK;AACH,qBAAW,wBAAwB,OAAO,KAAK,MAAM,CAAC;AACtD;AAAA,QACF,KAAK;AACH,qBAAW,MAAM,wBAAwB,MAAM;AAC/C;AAAA,QACF,KAAK;AACH,qBAAW,MAAM,uBAAuB,MAAM;AAC9C;AAAA,MACJ;AAEA,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC;AAAA,MACpF;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,8EAAkB;AAAA,IACxD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,4DAA8B;AAAA,EAClE;AAAA,EACA,OAAO,EAAE,WAAW,MAAM,MAAM;AAC9B,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,kBAAkB,SAAS;AAC9C,YAAM,SAAS,aAAa,MAAM;AAElC,UAAI,WAAW,WAAW;AACxB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,sFAAqB,SAAS,GAAG,CAAC;AAAA,UAClE,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,MAAM,QAAQ,EAAE,MAAM,CAAC;AAE5C,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,8BAAU,OAAO,QAAQ,MAAM,OAAO,KAAK,GAAG,CAAC;AAAA,UAC/E,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,OAAO;AAAA,QACX,iBAAO,OAAO,SAAS,YAAY,CAAC;AAAA,QACpC,iBAAO,KAAK;AAAA,QACZ,OAAO,YAAY,uBAAQ,OAAO,SAAS,KAAK;AAAA,MAClD,EAAE,OAAO,OAAO,EAAE,KAAK,KAAK;AAE5B,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,IAAI,IAAI;AAAA;AAAA,EAAQ,OAAO,QAAQ,GAAG,CAAC;AAAA,MACrE;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,8EAAkB;AAAA,IACxD,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS,uFAAsB;AAAA,EACtE;AAAA,EACA,OAAO,EAAE,WAAW,YAAY,MAAM;AACpC,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,kBAAkB,SAAS;AAC9C,YAAM,SAAS,aAAa,MAAM;AAElC,UAAI,WAAW,WAAW;AACxB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,sFAAqB,SAAS,GAAG,CAAC;AAAA,UAClE,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,MAAM,MAAM;AAEjC,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,8BAAU,OAAO,QAAQ,MAAM,OAAO,KAAK,GAAG,CAAC;AAAA,UAC/E,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,cAAc,OAAO,OAAO,OAAO,OAAK,EAAE,SAAS,WAAW,EAAE,KAAK;AAC3E,UAAI,YAAY,WAAW,GAAG;AAC5B,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,wEAAiB,CAAC;AAAA,UAClD,SAAS;AAAA,QACX;AAAA,MACF;AAEA,UAAI,eAAe,YAAY,QAAQ;AACrC,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,uDAAe,WAAW,YAAO,YAAY,MAAM,6BAAS,CAAC;AAAA,UAC7F,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,aAAa,YAAY,WAAW;AAC1C,YAAM,gBAAgB,iBAAiB,CAAC,UAAU,CAAC;AAEnD,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,wBAAS,WAAW,aAAQ,YAAY,MAAM;AAAA;AAAA,EAAS,aAAa,GAAG,CAAC;AAAA,MAC1G;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,wEAAiB;AAAA,IACzD,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,wEAAiB;AAAA,EAC3D;AAAA,EACA,OAAO,EAAE,aAAa,YAAY,MAAM;AACtC,QAAI;AACF,YAAM,EAAE,QAAQ,KAAK,IAAI,kBAAkB,WAAW;AACtD,YAAM,EAAE,QAAQ,KAAK,IAAI,kBAAkB,WAAW;AAEtD,YAAM,SAAS,MAAM,QAAQ,MAAM,IAAI;AACvC,YAAM,EAAE,OAAO,MAAM,IAAI;AAEzB,YAAM,QAAkB;AAAA,QACtB;AAAA,QACA,iBAAO,MAAM,KAAK,oBAAU,MAAM,OAAO,oBAAU,MAAM,QAAQ,oBAAU,MAAM,SAAS;AAAA,QAC1F;AAAA,MACF;AAEA,iBAAW,KAAK,OAAO;AACrB,cAAM,SAAS,EAAE,SAAS,UAAU,MAAM,EAAE,SAAS,YAAY,MAAM,EAAE,SAAS,aAAa,MAAM;AACrG,cAAM,OAAO,EAAE,OAAO,QAAQ,EAAE,QAAQ,SAAS,EAAE,OAAO,QAAQ,yBAAU,EAAE,QAAQ,QAAQ,yBAAU;AACxG,cAAM,MAAM,EAAE,eAAe,SAAY,MAAM,EAAE,aAAa,KAAK,QAAQ,CAAC,CAAC,OAAO;AACpF,cAAM,KAAK,GAAG,MAAM,IAAI,KAAK,UAAU,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE;AAAA,MACxD;AAEA,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,MAAM,KAAK,IAAI,EAAE,CAAC;AAAA,MACpD;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,IACE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,wEAAiB;AAAA,EACzD;AAAA,EACA,OAAO,EAAE,UAAU,MAAM;AACvB,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,kBAAkB,SAAS;AAC9C,YAAM,SAAS,MAAM,MAAM,MAAM;AAEjC,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO;AAAA,UACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,8BAAU,OAAO,KAAK,GAAG,CAAC;AAAA,UAC1D,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,OAAO,kBAAkB,OAAO,MAAM;AAC5C,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC,EAAE,CAAC;AAAA,MACjE;AAAA,IACF,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAO,cAAc,GAAG,CAAC,GAAG,CAAC;AAAA,QAC7D,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACF;AAIA,eAAe,OAAO;AACpB,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAChC;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AAAE,UAAQ,MAAM,GAAG;AAAG,UAAQ,KAAK,CAAC;AAAE,CAAC;","names":[]}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parsePageRange
4
+ } from "./chunk-MOL7MDBG.js";
5
+ export {
6
+ parsePageRange
7
+ };
8
+ //# sourceMappingURL=page-range-737B4EZW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/ocr/provider.ts
4
+ async function ocrPages(doc, provider, pageFilter, effectivePageCount) {
5
+ const blocks = [];
6
+ for (let i = 1; i <= effectivePageCount; i++) {
7
+ if (pageFilter && !pageFilter.has(i)) continue;
8
+ const page = await doc.getPage(i);
9
+ try {
10
+ const imageData = await renderPageToPng(page);
11
+ const text = await provider(imageData, i, "image/png");
12
+ if (text.trim()) {
13
+ blocks.push({ type: "paragraph", text: text.trim(), pageNumber: i });
14
+ }
15
+ } catch {
16
+ }
17
+ }
18
+ return blocks;
19
+ }
20
+ async function renderPageToPng(page) {
21
+ let createCanvas;
22
+ try {
23
+ const canvasModule = await import("canvas");
24
+ createCanvas = canvasModule.createCanvas;
25
+ } catch {
26
+ throw new Error("OCR\uC744 \uC0AC\uC6A9\uD558\uB824\uBA74 'canvas' \uD328\uD0A4\uC9C0\uB97C \uC124\uCE58\uD558\uC138\uC694: npm install canvas");
27
+ }
28
+ const scale = 2;
29
+ const viewport = page.getViewport({ scale });
30
+ const canvas = createCanvas(Math.floor(viewport.width), Math.floor(viewport.height));
31
+ const ctx = canvas.getContext("2d");
32
+ await page.render({ canvasContext: ctx, viewport }).promise;
33
+ return new Uint8Array(canvas.toBuffer("image/png"));
34
+ }
35
+ export {
36
+ ocrPages
37
+ };
38
+ //# sourceMappingURL=provider-A4FHJSID.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ocr/provider.ts"],"sourcesContent":["/**\n * OCR 프로바이더 브릿지 — PDF 페이지를 이미지로 렌더링하여 OCR 호출\n *\n * kordoc은 OCR 라이브러리를 번들하지 않음.\n * 사용자가 OcrProvider 함수를 제공하면 이미지 기반 PDF도 텍스트 추출 가능.\n *\n * @example\n * ```ts\n * import { parse } from \"kordoc\"\n *\n * const result = await parse(buffer, {\n * ocr: async (pageImage, pageNumber, mimeType) => {\n * // Tesseract, Claude Vision, Google Vision 등 사용\n * return await myOcrService.recognize(pageImage)\n * }\n * })\n * ```\n */\n\nimport type { OcrProvider, IRBlock } from \"../types.js\"\n\n/**\n * 이미지 기반 PDF 페이지에 OCR을 적용하여 IRBlock[] 반환.\n *\n * pdfjs page 객체에서 viewport + render를 통해 PNG 생성 후\n * 사용자 제공 OcrProvider 호출.\n *\n * canvas 미설치 시 pdfjs render 불가하므로 에러 반환.\n */\nexport async function ocrPages(\n doc: { numPages: number; getPage(n: number): Promise<PdfPageProxy> },\n provider: OcrProvider,\n pageFilter: Set<number> | null,\n effectivePageCount: number\n): Promise<IRBlock[]> {\n const blocks: IRBlock[] = []\n\n for (let i = 1; i <= effectivePageCount; i++) {\n if (pageFilter && !pageFilter.has(i)) continue\n const page = await doc.getPage(i)\n try {\n const imageData = await renderPageToPng(page)\n const text = await provider(imageData, i, \"image/png\")\n if (text.trim()) {\n blocks.push({ type: \"paragraph\", text: text.trim(), pageNumber: i })\n }\n } catch {\n // OCR 실패한 페이지는 건너뜀\n }\n }\n\n return blocks\n}\n\ninterface PdfPageProxy {\n getViewport(params: { scale: number }): { width: number; height: number }\n render(params: { canvasContext: unknown; viewport: unknown }): { promise: Promise<void> }\n}\n\n/**\n * PDF 페이지를 PNG로 렌더링.\n * node-canvas가 설치되어 있어야 동작.\n * 미설치 시 에러 throw → 호출측에서 catch.\n */\nasync function renderPageToPng(page: PdfPageProxy): Promise<Uint8Array> {\n // node-canvas 동적 로드 (선택적 의존성)\n let createCanvas: (w: number, h: number) => { getContext(t: string): unknown; toBuffer(t: string): Buffer }\n try {\n const canvasModule = await import(\"canvas\")\n createCanvas = canvasModule.createCanvas\n } catch {\n throw new Error(\"OCR을 사용하려면 'canvas' 패키지를 설치하세요: npm install canvas\")\n }\n\n const scale = 2.0 // 300 DPI 근사\n const viewport = page.getViewport({ scale })\n const canvas = createCanvas(Math.floor(viewport.width), Math.floor(viewport.height))\n const ctx = canvas.getContext(\"2d\")\n\n await page.render({ canvasContext: ctx, viewport }).promise\n return new Uint8Array(canvas.toBuffer(\"image/png\"))\n}\n"],"mappings":";;;AA6BA,eAAsB,SACpB,KACA,UACA,YACA,oBACoB;AACpB,QAAM,SAAoB,CAAC;AAE3B,WAAS,IAAI,GAAG,KAAK,oBAAoB,KAAK;AAC5C,QAAI,cAAc,CAAC,WAAW,IAAI,CAAC,EAAG;AACtC,UAAM,OAAO,MAAM,IAAI,QAAQ,CAAC;AAChC,QAAI;AACF,YAAM,YAAY,MAAM,gBAAgB,IAAI;AAC5C,YAAM,OAAO,MAAM,SAAS,WAAW,GAAG,WAAW;AACrD,UAAI,KAAK,KAAK,GAAG;AACf,eAAO,KAAK,EAAE,MAAM,aAAa,MAAM,KAAK,KAAK,GAAG,YAAY,EAAE,CAAC;AAAA,MACrE;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO;AACT;AAYA,eAAe,gBAAgB,MAAyC;AAEtE,MAAI;AACJ,MAAI;AACF,UAAM,eAAe,MAAM,OAAO,QAAQ;AAC1C,mBAAe,aAAa;AAAA,EAC9B,QAAQ;AACN,UAAM,IAAI,MAAM,+HAAoD;AAAA,EACtE;AAEA,QAAM,QAAQ;AACd,QAAM,WAAW,KAAK,YAAY,EAAE,MAAM,CAAC;AAC3C,QAAM,SAAS,aAAa,KAAK,MAAM,SAAS,KAAK,GAAG,KAAK,MAAM,SAAS,MAAM,CAAC;AACnF,QAAM,MAAM,OAAO,WAAW,IAAI;AAElC,QAAM,KAAK,OAAO,EAAE,eAAe,KAAK,SAAS,CAAC,EAAE;AACpD,SAAO,IAAI,WAAW,OAAO,SAAS,WAAW,CAAC;AACpD;","names":[]}
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ KordocError,
4
+ VERSION,
5
+ classifyError,
6
+ isPathTraversal,
7
+ precheckZipSize,
8
+ sanitizeError,
9
+ sanitizeHref,
10
+ toArrayBuffer
11
+ } from "./chunk-PKIJLEV6.js";
12
+ export {
13
+ KordocError,
14
+ VERSION,
15
+ classifyError,
16
+ isPathTraversal,
17
+ precheckZipSize,
18
+ sanitizeError,
19
+ sanitizeHref,
20
+ toArrayBuffer
21
+ };
22
+ //# sourceMappingURL=utils-BWQ2RGUD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ detectFormat,
4
+ parse
5
+ } from "./chunk-R6J5ZSDL.js";
6
+ import {
7
+ toArrayBuffer
8
+ } from "./chunk-PKIJLEV6.js";
9
+ import "./chunk-MOL7MDBG.js";
10
+
11
+ // src/watch.ts
12
+ import { watch, readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from "fs";
13
+ import { basename, resolve, extname } from "path";
14
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".hwp", ".hwpx", ".pdf", ".xlsx", ".docx"]);
15
+ var DEBOUNCE_MS = 1e3;
16
+ var STABLE_CHECK_MS = 300;
17
+ var MAX_FILE_SIZE = 500 * 1024 * 1024;
18
+ async function watchDirectory(options) {
19
+ const { dir, outDir, webhook, format = "markdown", pages, silent } = options;
20
+ if (!existsSync(dir)) throw new Error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
21
+ if (webhook) validateWebhookUrl(webhook);
22
+ if (outDir) mkdirSync(outDir, { recursive: true });
23
+ const log = silent ? () => {
24
+ } : (msg) => process.stderr.write(msg + "\n");
25
+ log(`[kordoc watch] \uAC10\uC2DC \uC2DC\uC791: ${resolve(dir)}`);
26
+ if (outDir) log(`[kordoc watch] \uCD9C\uB825: ${resolve(outDir)}`);
27
+ if (webhook) log(`[kordoc watch] \uC6F9\uD6C5: ${webhook}`);
28
+ const pending = /* @__PURE__ */ new Map();
29
+ const waitForStableSize = async (absPath) => {
30
+ let prevSize = statSync(absPath).size;
31
+ await new Promise((r) => setTimeout(r, STABLE_CHECK_MS));
32
+ if (!existsSync(absPath)) return 0;
33
+ const currSize = statSync(absPath).size;
34
+ if (currSize !== prevSize) {
35
+ await new Promise((r) => setTimeout(r, STABLE_CHECK_MS));
36
+ if (!existsSync(absPath)) return 0;
37
+ return statSync(absPath).size;
38
+ }
39
+ return currSize;
40
+ };
41
+ const processFile = async (filePath) => {
42
+ const ext = extname(filePath).toLowerCase();
43
+ if (!SUPPORTED_EXTENSIONS.has(ext)) return;
44
+ const fileName = basename(filePath);
45
+ try {
46
+ const absPath = resolve(dir, filePath);
47
+ const realDir = resolve(dir);
48
+ if (!absPath.startsWith(realDir)) return;
49
+ if (!existsSync(absPath)) return;
50
+ const fileSize = await waitForStableSize(absPath);
51
+ if (fileSize > MAX_FILE_SIZE || fileSize === 0) return;
52
+ log(`[kordoc watch] \uBCC0\uD658 \uC911: ${fileName}`);
53
+ const buffer = readFileSync(absPath);
54
+ const arrayBuffer = toArrayBuffer(buffer);
55
+ const parseOptions = pages ? { pages } : void 0;
56
+ const result = await parse(arrayBuffer, parseOptions);
57
+ if (!result.success) {
58
+ log(`[kordoc watch] \uC2E4\uD328: ${fileName} \u2014 ${result.error}`);
59
+ await sendWebhook(webhook, { file: fileName, format: detectFormat(arrayBuffer), success: false, error: result.error });
60
+ return;
61
+ }
62
+ const output = format === "json" ? JSON.stringify(result, null, 2) : result.markdown;
63
+ if (outDir) {
64
+ const outExt = format === "json" ? ".json" : ".md";
65
+ const outPath = resolve(outDir, fileName.replace(/\.[^.]+$/, outExt));
66
+ writeFileSync(outPath, output, "utf-8");
67
+ log(`[kordoc watch] \uC644\uB8CC: ${fileName} \u2192 ${basename(outPath)}`);
68
+ } else {
69
+ process.stdout.write(output + "\n");
70
+ }
71
+ await sendWebhook(webhook, {
72
+ file: fileName,
73
+ format: result.fileType,
74
+ success: true,
75
+ markdown: format === "markdown" ? output.substring(0, 1e3) : void 0
76
+ });
77
+ } catch (err) {
78
+ log(`[kordoc watch] \uC5D0\uB7EC: ${fileName} \u2014 ${err instanceof Error ? err.message : err}`);
79
+ }
80
+ };
81
+ watch(dir, { recursive: true }, (event, filename) => {
82
+ if (!filename) return;
83
+ const filePath = filename.toString();
84
+ const existing = pending.get(filePath);
85
+ if (existing) clearTimeout(existing);
86
+ pending.set(filePath, setTimeout(() => {
87
+ pending.delete(filePath);
88
+ processFile(filePath).catch(() => {
89
+ });
90
+ }, DEBOUNCE_MS));
91
+ });
92
+ return new Promise(() => {
93
+ });
94
+ }
95
+ function validateWebhookUrl(url) {
96
+ let parsed;
97
+ try {
98
+ parsed = new URL(url);
99
+ } catch {
100
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 webhook URL: ${url}`);
101
+ }
102
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
103
+ throw new Error(`\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 webhook \uD504\uB85C\uD1A0\uCF5C: ${parsed.protocol}`);
104
+ }
105
+ const hostname = parsed.hostname.toLowerCase();
106
+ if (hostname === "localhost" || hostname === "[::1]" || hostname.startsWith("127.") || hostname.startsWith("10.") || hostname.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || hostname === "0.0.0.0" || hostname.startsWith("169.254.") || hostname.endsWith(".local") || // IPv6 사설 대역
107
+ hostname.startsWith("[fc") || hostname.startsWith("[fd") || hostname.startsWith("[fe80:") || hostname === "[::0]" || hostname === "[::]" || // 클라우드 메타데이터 엔드포인트
108
+ hostname === "metadata.google.internal" || hostname === "metadata.google" || // 16진수/8진수 IP 인코딩 우회 방지
109
+ /^0x[0-9a-f]+$/i.test(hostname) || /^0[0-7]+$/.test(hostname)) {
110
+ throw new Error(`\uB0B4\uBD80 \uB124\uD2B8\uC6CC\uD06C \uB300\uC0C1 webhook\uC740 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${hostname}`);
111
+ }
112
+ }
113
+ async function sendWebhook(url, payload) {
114
+ if (!url) return;
115
+ try {
116
+ validateWebhookUrl(url);
117
+ await fetch(url, {
118
+ method: "POST",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ ...payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() })
121
+ });
122
+ } catch {
123
+ }
124
+ }
125
+ export {
126
+ watchDirectory
127
+ };
128
+ //# sourceMappingURL=watch-U4AVI5RY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/watch.ts"],"sourcesContent":["/** 디렉토리 감시 모드 — 새 문서 자동 변환 + Webhook 알림 */\n\nimport { watch, readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from \"fs\"\nimport { basename, resolve, extname } from \"path\"\nimport { parse, detectFormat } from \"./index.js\"\nimport { toArrayBuffer } from \"./utils.js\"\nimport type { WatchOptions } from \"./types.js\"\n\nconst SUPPORTED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\", \".xlsx\", \".docx\"])\nconst DEBOUNCE_MS = 1000\n/** 파일 쓰기 완료 판정: 연속 2회 동일 크기 확인 간격 */\nconst STABLE_CHECK_MS = 300\nconst MAX_FILE_SIZE = 500 * 1024 * 1024\n\n/**\n * 디렉토리를 감시하여 새 문서 파일을 자동 변환.\n *\n * @example\n * ```bash\n * kordoc watch ./incoming -d ./output --webhook https://api.example.com/docs\n * ```\n */\nexport async function watchDirectory(options: WatchOptions): Promise<void> {\n const { dir, outDir, webhook, format = \"markdown\", pages, silent } = options\n\n if (!existsSync(dir)) throw new Error(`디렉토리를 찾을 수 없습니다: ${dir}`)\n if (webhook) validateWebhookUrl(webhook)\n if (outDir) mkdirSync(outDir, { recursive: true })\n\n const log = silent ? () => {} : (msg: string) => process.stderr.write(msg + \"\\n\")\n log(`[kordoc watch] 감시 시작: ${resolve(dir)}`)\n if (outDir) log(`[kordoc watch] 출력: ${resolve(outDir)}`)\n if (webhook) log(`[kordoc watch] 웹훅: ${webhook}`)\n\n // 디바운스 맵\n const pending = new Map<string, ReturnType<typeof setTimeout>>()\n\n /** 파일 크기가 안정화될 때까지 대기 (쓰기 완료 감지) */\n const waitForStableSize = async (absPath: string): Promise<number> => {\n let prevSize = statSync(absPath).size\n await new Promise(r => setTimeout(r, STABLE_CHECK_MS))\n if (!existsSync(absPath)) return 0\n const currSize = statSync(absPath).size\n if (currSize !== prevSize) {\n // 크기가 변했으면 한 번 더 대기\n await new Promise(r => setTimeout(r, STABLE_CHECK_MS))\n if (!existsSync(absPath)) return 0\n return statSync(absPath).size\n }\n return currSize\n }\n\n const processFile = async (filePath: string) => {\n const ext = extname(filePath).toLowerCase()\n if (!SUPPORTED_EXTENSIONS.has(ext)) return\n\n const fileName = basename(filePath)\n try {\n const absPath = resolve(dir, filePath)\n // 경로 순회 방지 — 감시 디렉토리 외부 파일 차단\n const realDir = resolve(dir)\n if (!absPath.startsWith(realDir)) return\n if (!existsSync(absPath)) return\n\n const fileSize = await waitForStableSize(absPath)\n if (fileSize > MAX_FILE_SIZE || fileSize === 0) return\n\n log(`[kordoc watch] 변환 중: ${fileName}`)\n\n const buffer = readFileSync(absPath)\n const arrayBuffer = toArrayBuffer(buffer)\n const parseOptions = pages ? { pages } : undefined\n const result = await parse(arrayBuffer, parseOptions)\n\n if (!result.success) {\n log(`[kordoc watch] 실패: ${fileName} — ${result.error}`)\n await sendWebhook(webhook, { file: fileName, format: detectFormat(arrayBuffer), success: false, error: result.error })\n return\n }\n\n const output = format === \"json\" ? JSON.stringify(result, null, 2) : result.markdown\n\n if (outDir) {\n const outExt = format === \"json\" ? \".json\" : \".md\"\n const outPath = resolve(outDir, fileName.replace(/\\.[^.]+$/, outExt))\n writeFileSync(outPath, output, \"utf-8\")\n log(`[kordoc watch] 완료: ${fileName} → ${basename(outPath)}`)\n } else {\n process.stdout.write(output + \"\\n\")\n }\n\n await sendWebhook(webhook, {\n file: fileName,\n format: result.fileType,\n success: true,\n markdown: format === \"markdown\" ? output.substring(0, 1000) : undefined,\n })\n } catch (err) {\n log(`[kordoc watch] 에러: ${fileName} — ${err instanceof Error ? err.message : err}`)\n }\n }\n\n // fs.watch recursive (Node 18+ Windows/macOS, Node 19+ Linux)\n watch(dir, { recursive: true }, (event, filename) => {\n if (!filename) return\n const filePath = filename.toString()\n\n // 디바운스\n const existing = pending.get(filePath)\n if (existing) clearTimeout(existing)\n pending.set(filePath, setTimeout(() => {\n pending.delete(filePath)\n processFile(filePath).catch(() => {})\n }, DEBOUNCE_MS))\n })\n\n // 프로세스 종료 방지 (Ctrl+C로 종료)\n return new Promise(() => {})\n}\n\n/** Webhook URL 검증 — SSRF 방지: http/https만 허용, localhost/private IP 차단 */\nfunction validateWebhookUrl(url: string): void {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`유효하지 않은 webhook URL: ${url}`)\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new Error(`허용되지 않는 webhook 프로토콜: ${parsed.protocol}`)\n }\n const hostname = parsed.hostname.toLowerCase()\n if (\n hostname === \"localhost\" ||\n hostname === \"[::1]\" ||\n hostname.startsWith(\"127.\") ||\n hostname.startsWith(\"10.\") ||\n hostname.startsWith(\"192.168.\") ||\n /^172\\.(1[6-9]|2\\d|3[01])\\./.test(hostname) ||\n hostname === \"0.0.0.0\" ||\n hostname.startsWith(\"169.254.\") ||\n hostname.endsWith(\".local\") ||\n // IPv6 사설 대역\n hostname.startsWith(\"[fc\") ||\n hostname.startsWith(\"[fd\") ||\n hostname.startsWith(\"[fe80:\") ||\n hostname === \"[::0]\" ||\n hostname === \"[::]\" ||\n // 클라우드 메타데이터 엔드포인트\n hostname === \"metadata.google.internal\" ||\n hostname === \"metadata.google\" ||\n // 16진수/8진수 IP 인코딩 우회 방지\n /^0x[0-9a-f]+$/i.test(hostname) ||\n /^0[0-7]+$/.test(hostname)\n ) {\n throw new Error(`내부 네트워크 대상 webhook은 허용되지 않습니다: ${hostname}`)\n }\n}\n\nasync function sendWebhook(url: string | undefined, payload: Record<string, unknown>): Promise<void> {\n if (!url) return\n try {\n validateWebhookUrl(url)\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ ...payload, timestamp: new Date().toISOString() }),\n })\n } catch {\n // webhook 실패는 조용히 무시\n }\n}\n"],"mappings":"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","names":[]}