kordoc 1.2.0 → 1.4.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 CHANGED
@@ -2,11 +2,17 @@
2
2
  import {
3
3
  KordocError,
4
4
  VERSION,
5
+ blocksToMarkdown,
6
+ compare,
5
7
  detectFormat,
8
+ extractFormFields,
9
+ extractHwp5MetadataOnly,
10
+ extractHwpxMetadataOnly,
11
+ extractPdfMetadataOnly,
6
12
  parse,
7
13
  sanitizeError,
8
14
  toArrayBuffer
9
- } from "./chunk-4BKNDXGU.js";
15
+ } from "./chunk-BWZW234S.js";
10
16
 
11
17
  // src/mcp.ts
12
18
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -25,6 +31,15 @@ function safePath(filePath) {
25
31
  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(", ")})`);
26
32
  return real;
27
33
  }
34
+ function readValidatedFile(filePath) {
35
+ const resolved = safePath(filePath);
36
+ const fileSize = statSync(resolved).size;
37
+ if (fileSize > MAX_FILE_SIZE) {
38
+ throw new KordocError(`\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${(fileSize / 1024 / 1024).toFixed(1)}MB (\uCD5C\uB300 ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
39
+ }
40
+ const raw = readFileSync(resolved);
41
+ return { buffer: toArrayBuffer(raw), resolved };
42
+ }
28
43
  var server = new McpServer({
29
44
  name: "kordoc",
30
45
  version: VERSION
@@ -37,24 +52,15 @@ server.tool(
37
52
  },
38
53
  async ({ file_path }) => {
39
54
  try {
40
- const resolved = safePath(file_path);
41
- const fileSize = statSync(resolved).size;
42
- if (fileSize > MAX_FILE_SIZE) {
43
- return {
44
- content: [{ type: "text", text: `\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4: ${(fileSize / 1024 / 1024).toFixed(1)}MB (\uCD5C\uB300 ${MAX_FILE_SIZE / 1024 / 1024}MB)` }],
45
- isError: true
46
- };
47
- }
48
- const buffer = readFileSync(resolved);
49
- const arrayBuffer = toArrayBuffer(buffer);
50
- const format = detectFormat(arrayBuffer);
55
+ const { buffer } = readValidatedFile(file_path);
56
+ const format = detectFormat(buffer);
51
57
  if (format === "unknown") {
52
58
  return {
53
59
  content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
54
60
  isError: true
55
61
  };
56
62
  }
57
- const result = await parse(arrayBuffer);
63
+ const result = await parse(buffer);
58
64
  if (!result.success) {
59
65
  return {
60
66
  content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
@@ -64,6 +70,8 @@ server.tool(
64
70
  const meta = [
65
71
  `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
66
72
  result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null,
73
+ result.metadata?.title ? `\uC81C\uBAA9: ${result.metadata.title}` : null,
74
+ result.metadata?.author ? `\uC791\uC131\uC790: ${result.metadata.author}` : null,
67
75
  result.isImageBased ? "\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uBD88\uAC00)" : null
68
76
  ].filter(Boolean).join(" | ");
69
77
  return {
@@ -109,6 +117,201 @@ server.tool(
109
117
  }
110
118
  }
111
119
  );
120
+ server.tool(
121
+ "parse_metadata",
122
+ "\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.",
123
+ {
124
+ 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")
125
+ },
126
+ async ({ file_path }) => {
127
+ try {
128
+ const { buffer } = readValidatedFile(file_path);
129
+ const format = detectFormat(buffer);
130
+ let metadata;
131
+ switch (format) {
132
+ case "hwp":
133
+ metadata = extractHwp5MetadataOnly(Buffer.from(buffer));
134
+ break;
135
+ case "hwpx":
136
+ metadata = await extractHwpxMetadataOnly(buffer);
137
+ break;
138
+ case "pdf":
139
+ metadata = await extractPdfMetadataOnly(buffer);
140
+ break;
141
+ default:
142
+ return {
143
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
144
+ isError: true
145
+ };
146
+ }
147
+ return {
148
+ content: [{ type: "text", text: JSON.stringify({ format, ...metadata }, null, 2) }]
149
+ };
150
+ } catch (err) {
151
+ return {
152
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
153
+ isError: true
154
+ };
155
+ }
156
+ }
157
+ );
158
+ server.tool(
159
+ "parse_pages",
160
+ "\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.",
161
+ {
162
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
163
+ pages: z.string().min(1).describe("\uD398\uC774\uC9C0 \uBC94\uC704 (\uC608: '1-3', '1,3,5-7')")
164
+ },
165
+ async ({ file_path, pages }) => {
166
+ try {
167
+ const { buffer } = readValidatedFile(file_path);
168
+ const format = detectFormat(buffer);
169
+ if (format === "unknown") {
170
+ return {
171
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
172
+ isError: true
173
+ };
174
+ }
175
+ const result = await parse(buffer, { pages });
176
+ if (!result.success) {
177
+ return {
178
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
179
+ isError: true
180
+ };
181
+ }
182
+ const meta = [
183
+ `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
184
+ `\uBC94\uC704: ${pages}`,
185
+ result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null
186
+ ].filter(Boolean).join(" | ");
187
+ return {
188
+ content: [{ type: "text", text: `[${meta}]
189
+
190
+ ${result.markdown}` }]
191
+ };
192
+ } catch (err) {
193
+ return {
194
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
195
+ isError: true
196
+ };
197
+ }
198
+ }
199
+ );
200
+ server.tool(
201
+ "parse_table",
202
+ "\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.",
203
+ {
204
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
205
+ table_index: z.number().int().min(0).describe("\uCD94\uCD9C\uD560 \uD14C\uC774\uBE14 \uC778\uB371\uC2A4 (0\uBD80\uD130 \uC2DC\uC791)")
206
+ },
207
+ async ({ file_path, table_index }) => {
208
+ try {
209
+ const { buffer } = readValidatedFile(file_path);
210
+ const format = detectFormat(buffer);
211
+ if (format === "unknown") {
212
+ return {
213
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
214
+ isError: true
215
+ };
216
+ }
217
+ const result = await parse(buffer);
218
+ if (!result.success) {
219
+ return {
220
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
221
+ isError: true
222
+ };
223
+ }
224
+ const tableBlocks = result.blocks.filter((b) => b.type === "table" && b.table);
225
+ if (tableBlocks.length === 0) {
226
+ return {
227
+ content: [{ type: "text", text: `\uBB38\uC11C\uC5D0 \uD14C\uC774\uBE14\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.` }],
228
+ isError: true
229
+ };
230
+ }
231
+ if (table_index >= tableBlocks.length) {
232
+ return {
233
+ content: [{ type: "text", text: `\uD14C\uC774\uBE14 \uC778\uB371\uC2A4 \uCD08\uACFC: ${table_index} (\uCD1D ${tableBlocks.length}\uAC1C \uD14C\uC774\uBE14)` }],
234
+ isError: true
235
+ };
236
+ }
237
+ const tableBlock = tableBlocks[table_index];
238
+ const tableMarkdown = blocksToMarkdown([tableBlock]);
239
+ return {
240
+ content: [{ type: "text", text: `[\uD14C\uC774\uBE14 #${table_index} / \uCD1D ${tableBlocks.length}\uAC1C]
241
+
242
+ ${tableMarkdown}` }]
243
+ };
244
+ } catch (err) {
245
+ return {
246
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
247
+ isError: true
248
+ };
249
+ }
250
+ }
251
+ );
252
+ server.tool(
253
+ "compare_documents",
254
+ "\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.",
255
+ {
256
+ file_path_a: z.string().min(1).describe("\uBE44\uAD50 \uC6D0\uBCF8 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
257
+ file_path_b: z.string().min(1).describe("\uBE44\uAD50 \uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C")
258
+ },
259
+ async ({ file_path_a, file_path_b }) => {
260
+ try {
261
+ const { buffer: bufA } = readValidatedFile(file_path_a);
262
+ const { buffer: bufB } = readValidatedFile(file_path_b);
263
+ const result = await compare(bufA, bufB);
264
+ const { stats, diffs } = result;
265
+ const lines = [
266
+ `## \uBB38\uC11C \uBE44\uAD50 \uACB0\uACFC`,
267
+ `\uCD94\uAC00: ${stats.added} | \uC0AD\uC81C: ${stats.removed} | \uBCC0\uACBD: ${stats.modified} | \uB3D9\uC77C: ${stats.unchanged}`,
268
+ ""
269
+ ];
270
+ for (const d of diffs) {
271
+ const prefix = d.type === "added" ? "+" : d.type === "removed" ? "-" : d.type === "modified" ? "~" : " ";
272
+ const text = d.after?.text || d.before?.text || (d.after?.table ? "[\uD14C\uC774\uBE14]" : d.before?.table ? "[\uD14C\uC774\uBE14]" : "");
273
+ const sim = d.similarity !== void 0 ? ` (${(d.similarity * 100).toFixed(0)}%)` : "";
274
+ lines.push(`${prefix} ${text.substring(0, 200)}${sim}`);
275
+ }
276
+ return {
277
+ content: [{ type: "text", text: lines.join("\n") }]
278
+ };
279
+ } catch (err) {
280
+ return {
281
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
282
+ isError: true
283
+ };
284
+ }
285
+ }
286
+ );
287
+ server.tool(
288
+ "parse_form",
289
+ "\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.",
290
+ {
291
+ file_path: z.string().min(1).describe("\uC11C\uC2DD \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
292
+ },
293
+ async ({ file_path }) => {
294
+ try {
295
+ const { buffer } = readValidatedFile(file_path);
296
+ const result = await parse(buffer);
297
+ if (!result.success) {
298
+ return {
299
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${result.error}` }],
300
+ isError: true
301
+ };
302
+ }
303
+ const form = extractFormFields(result.blocks);
304
+ return {
305
+ content: [{ type: "text", text: JSON.stringify(form, null, 2) }]
306
+ };
307
+ } catch (err) {
308
+ return {
309
+ content: [{ type: "text", text: `\uC624\uB958: ${sanitizeError(err)}` }],
310
+ isError: true
311
+ };
312
+ }
313
+ }
314
+ );
112
315
  async function main() {
113
316
  const transport = new StdioServerTransport();
114
317
  await server.connect(transport);
package/dist/mcp.js.map CHANGED
@@ -1 +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 } from \"./index.js\"\nimport { VERSION, toArrayBuffer, sanitizeError, KordocError } from \"./utils.js\"\n\n/** 허용 파일 확장자 */\nconst ALLOWED_EXTENSIONS = new Set([\".hwp\", \".hwpx\", \".pdf\"])\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\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)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF)\"),\n },\n async ({ file_path }) => {\n try {\n const resolved = safePath(file_path)\n const fileSize = statSync(resolved).size\n if (fileSize > MAX_FILE_SIZE) {\n return {\n content: [{ type: \"text\", text: `파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${MAX_FILE_SIZE / 1024 / 1024}MB)` }],\n isError: true,\n }\n }\n const buffer = readFileSync(resolved)\n const arrayBuffer = toArrayBuffer(buffer)\n const format = detectFormat(arrayBuffer)\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(arrayBuffer)\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.isImageBased ? \"이미지 기반 PDF (텍스트 추출 불가)\" : 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// ─── 도구: 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 // 전체 파일 대신 첫 16바이트만 읽기 — 대용량 파일 OOM 방지\n const fd = openSync(resolved, \"r\")\n let headerBuf: Buffer\n try {\n headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n } finally {\n closeSync(fd)\n }\n const header = toArrayBuffer(headerBuf)\n const format = detectFormat(header)\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// ─── 서버 시작 ───────────────────────────────────────\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":"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","names":[]}
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\"])\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/** 파일 읽기 + 크기 검증 공통 로직 */\nfunction readValidatedFile(filePath: string): { buffer: ArrayBuffer; resolved: string } {\n const resolved = safePath(filePath)\n const fileSize = statSync(resolved).size\n if (fileSize > MAX_FILE_SIZE) {\n throw new KordocError(`파일이 너무 큽니다: ${(fileSize / 1024 / 1024).toFixed(1)}MB (최대 ${MAX_FILE_SIZE / 1024 / 1024}MB)`)\n }\n const raw = readFileSync(resolved)\n return { buffer: toArrayBuffer(raw), resolved }\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)을 마크다운으로 변환합니다. 파일 경로를 입력하면 포맷을 자동 감지하여 텍스트를 추출합니다.\",\n {\n file_path: z.string().min(1).describe(\"파싱할 문서 파일의 절대 경로 (HWP, HWPX, PDF)\"),\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 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// ─── 도구: 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 // 전체 파일 대신 첫 16바이트만 읽기 — 대용량 파일 OOM 방지\n const fd = openSync(resolved, \"r\")\n let headerBuf: Buffer\n try {\n headerBuf = Buffer.alloc(16)\n readSync(fd, headerBuf, 0, 16, 0)\n } finally {\n closeSync(fd)\n }\n const header = toArrayBuffer(headerBuf)\n const format = detectFormat(header)\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 { buffer } = readValidatedFile(file_path)\n const format = detectFormat(buffer)\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 default:\n return {\n content: [{ type: \"text\", text: `지원하지 않는 파일 형식입니다: ${file_path}` }],\n isError: true,\n }\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":"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","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() });
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-JB7SY74K.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() })\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,EAAE,CAAC;AAAA,MACtD;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,90 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ detectFormat,
4
+ parse,
5
+ toArrayBuffer
6
+ } from "./chunk-BWZW234S.js";
7
+
8
+ // src/watch.ts
9
+ import { watch, readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from "fs";
10
+ import { basename, resolve, extname } from "path";
11
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".hwp", ".hwpx", ".pdf"]);
12
+ var DEBOUNCE_MS = 500;
13
+ var MAX_FILE_SIZE = 500 * 1024 * 1024;
14
+ async function watchDirectory(options) {
15
+ const { dir, outDir, webhook, format = "markdown", pages, silent } = options;
16
+ if (!existsSync(dir)) throw new Error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
17
+ if (outDir) mkdirSync(outDir, { recursive: true });
18
+ const log = silent ? () => {
19
+ } : (msg) => process.stderr.write(msg + "\n");
20
+ log(`[kordoc watch] \uAC10\uC2DC \uC2DC\uC791: ${resolve(dir)}`);
21
+ if (outDir) log(`[kordoc watch] \uCD9C\uB825: ${resolve(outDir)}`);
22
+ if (webhook) log(`[kordoc watch] \uC6F9\uD6C5: ${webhook}`);
23
+ const pending = /* @__PURE__ */ new Map();
24
+ const processFile = async (filePath) => {
25
+ const ext = extname(filePath).toLowerCase();
26
+ if (!SUPPORTED_EXTENSIONS.has(ext)) return;
27
+ const fileName = basename(filePath);
28
+ try {
29
+ const absPath = resolve(dir, filePath);
30
+ if (!existsSync(absPath)) return;
31
+ const fileSize = statSync(absPath).size;
32
+ if (fileSize > MAX_FILE_SIZE || fileSize === 0) return;
33
+ log(`[kordoc watch] \uBCC0\uD658 \uC911: ${fileName}`);
34
+ const buffer = readFileSync(absPath);
35
+ const arrayBuffer = toArrayBuffer(buffer);
36
+ const parseOptions = pages ? { pages } : void 0;
37
+ const result = await parse(arrayBuffer, parseOptions);
38
+ if (!result.success) {
39
+ log(`[kordoc watch] \uC2E4\uD328: ${fileName} \u2014 ${result.error}`);
40
+ await sendWebhook(webhook, { file: fileName, format: detectFormat(arrayBuffer), success: false, error: result.error });
41
+ return;
42
+ }
43
+ const output = format === "json" ? JSON.stringify(result, null, 2) : result.markdown;
44
+ if (outDir) {
45
+ const outExt = format === "json" ? ".json" : ".md";
46
+ const outPath = resolve(outDir, fileName.replace(/\.[^.]+$/, outExt));
47
+ writeFileSync(outPath, output, "utf-8");
48
+ log(`[kordoc watch] \uC644\uB8CC: ${fileName} \u2192 ${basename(outPath)}`);
49
+ } else {
50
+ process.stdout.write(output + "\n");
51
+ }
52
+ await sendWebhook(webhook, {
53
+ file: fileName,
54
+ format: result.fileType,
55
+ success: true,
56
+ markdown: format === "markdown" ? output.substring(0, 1e3) : void 0
57
+ });
58
+ } catch (err) {
59
+ log(`[kordoc watch] \uC5D0\uB7EC: ${fileName} \u2014 ${err instanceof Error ? err.message : err}`);
60
+ }
61
+ };
62
+ watch(dir, { recursive: true }, (event, filename) => {
63
+ if (!filename) return;
64
+ const filePath = filename.toString();
65
+ const existing = pending.get(filePath);
66
+ if (existing) clearTimeout(existing);
67
+ pending.set(filePath, setTimeout(() => {
68
+ pending.delete(filePath);
69
+ processFile(filePath).catch(() => {
70
+ });
71
+ }, DEBOUNCE_MS));
72
+ });
73
+ return new Promise(() => {
74
+ });
75
+ }
76
+ async function sendWebhook(url, payload) {
77
+ if (!url) return;
78
+ try {
79
+ await fetch(url, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ ...payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() })
83
+ });
84
+ } catch {
85
+ }
86
+ }
87
+ export {
88
+ watchDirectory
89
+ };
90
+ //# sourceMappingURL=watch-LIGKH3QS.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\"])\nconst DEBOUNCE_MS = 500\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 (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 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 if (!existsSync(absPath)) return\n\n const fileSize = statSync(absPath).size\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\nasync function sendWebhook(url: string | undefined, payload: Record<string, unknown>): Promise<void> {\n if (!url) return\n try {\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kordoc",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Parse Korean documents (HWP, HWPX, PDF) to Markdown",
5
5
  "type": "module",
6
6
  "exports": {