@kolmopdf/mcp-server 1.0.2 → 1.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/index.cjs CHANGED
@@ -29,6 +29,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
29
29
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
30
30
 
31
31
  // src/client.ts
32
+ var import_node_crypto = require("crypto");
32
33
  var import_node_stream = require("stream");
33
34
  var import_promises = require("stream/promises");
34
35
 
@@ -228,6 +229,12 @@ function errorFromApiBody(body, httpStatus) {
228
229
  }
229
230
 
230
231
  // src/client.ts
232
+ function normalizeStatus(status) {
233
+ if (!status) return "processing";
234
+ if (status === "completed") return "succeeded";
235
+ if (status === "pending" || status === "waiting") return "queued";
236
+ return status;
237
+ }
231
238
  var KolmoPdfClient = class {
232
239
  apiKey;
233
240
  baseUrl;
@@ -239,18 +246,38 @@ var KolmoPdfClient = class {
239
246
  this.httpTimeoutMs = opts.httpTimeoutMs;
240
247
  this.uploadTimeoutMs = opts.uploadTimeoutMs;
241
248
  }
242
- get apiBase() {
243
- return `${this.baseUrl}/api/pdf-to-markdown-proxy`;
249
+ get jobsBase() {
250
+ return `${this.baseUrl}/api/v1/jobs`;
244
251
  }
245
252
  headers() {
246
- return { "X-API-Key": this.apiKey };
253
+ return {
254
+ "X-API-Key": this.apiKey,
255
+ Authorization: `Bearer ${this.apiKey}`
256
+ };
247
257
  }
248
258
  async jsonRequest(url, init) {
249
259
  const res = await fetch(url, init);
250
- const body = await res.json();
260
+ let body = {};
261
+ const text = await res.text();
262
+ try {
263
+ body = text ? JSON.parse(text) : {};
264
+ } catch {
265
+ if (!res.ok) {
266
+ throw new KolmoPdfError("api_task_error", {
267
+ message: `HTTP ${res.status}: non-JSON body`,
268
+ httpStatus: res.status
269
+ });
270
+ }
271
+ }
251
272
  if (!res.ok || body.success === false) {
273
+ const errObj = body.error;
252
274
  throw errorFromApiBody(
253
- body,
275
+ {
276
+ error_code: body.error_code || errObj?.code,
277
+ message: body.message || errObj?.message,
278
+ points_required: body.points_required,
279
+ current_points: body.current_points
280
+ },
254
281
  res.status
255
282
  );
256
283
  }
@@ -271,6 +298,20 @@ var KolmoPdfClient = class {
271
298
  form.append("file", blob, filename);
272
299
  return form;
273
300
  }
301
+ normalizeSubmit(body) {
302
+ const id = String(body.id ?? body.task_id ?? body.legacy_task_id ?? "");
303
+ if (!id) {
304
+ throw new KolmoPdfError("task_creation_failed", { message: "No job id in create response" });
305
+ }
306
+ const queue = body.queue;
307
+ return {
308
+ task_id: id,
309
+ status: normalizeStatus(String(body.status ?? "queued")),
310
+ points_deducted: Number(body.points_deducted ?? 0),
311
+ remaining_points: Number(body.remaining_points ?? 0),
312
+ queue_info: queue && typeof queue.ahead === "number" ? { position: queue.position ?? 0, ahead_tasks: queue.ahead } : void 0
313
+ };
314
+ }
274
315
  async parse(file, form, filename) {
275
316
  const fd = await this.buildFileForm(file, filename);
276
317
  if (form.table_mode) fd.append("table_mode", form.table_mode);
@@ -284,13 +325,14 @@ var KolmoPdfClient = class {
284
325
  fd.append("skip_rotation_detection", String(form.skip_rotation_detection));
285
326
  if (form.enable_cross_page_merge !== void 0)
286
327
  fd.append("enable_cross_page_merge", String(form.enable_cross_page_merge));
287
- const body = await this.jsonRequest(`${this.apiBase}/parse`, {
328
+ if (form.enrichment !== void 0) fd.append("enrichment", form.enrichment);
329
+ const body = await this.jsonRequest(`${this.jobsBase}/parse`, {
288
330
  method: "POST",
289
- headers: this.headers(),
331
+ headers: { ...this.headers(), "Idempotency-Key": (0, import_node_crypto.randomUUID)() },
290
332
  body: fd,
291
333
  signal: AbortSignal.timeout(this.uploadTimeoutMs)
292
334
  });
293
- return body;
335
+ return this.normalizeSubmit(body);
294
336
  }
295
337
  async translatePdf(file, form, filename) {
296
338
  const fd = await this.buildFileForm(file, filename);
@@ -301,35 +343,77 @@ var KolmoPdfClient = class {
301
343
  fd.append("enableImageTranslation", String(form.enable_image_translation));
302
344
  if (form.enable_table_translation !== void 0)
303
345
  fd.append("enableTableTranslation", String(form.enable_table_translation));
304
- const body = await this.jsonRequest(`${this.apiBase}/translate-pdf`, {
346
+ const body = await this.jsonRequest(`${this.jobsBase}/translate-pdf`, {
305
347
  method: "POST",
306
- headers: this.headers(),
348
+ headers: { ...this.headers(), "Idempotency-Key": (0, import_node_crypto.randomUUID)() },
307
349
  body: fd,
308
350
  signal: AbortSignal.timeout(this.uploadTimeoutMs)
309
351
  });
310
- return body;
352
+ return this.normalizeSubmit(body);
311
353
  }
312
354
  async convert(file, form, filename) {
313
355
  const fd = await this.buildFileForm(file, filename);
314
356
  if (form.target_format) fd.append("targetFormat", form.target_format);
315
- const body = await this.jsonRequest(`${this.apiBase}/convert`, {
357
+ const body = await this.jsonRequest(`${this.jobsBase}/convert`, {
316
358
  method: "POST",
317
- headers: this.headers(),
359
+ headers: { ...this.headers(), "Idempotency-Key": (0, import_node_crypto.randomUUID)() },
318
360
  body: fd,
319
361
  signal: AbortSignal.timeout(this.uploadTimeoutMs)
320
362
  });
321
- return body;
363
+ return this.normalizeSubmit(body);
322
364
  }
323
365
  async getStatus(taskId) {
324
- const body = await this.jsonRequest(`${this.apiBase}/status/${taskId}`, {
366
+ const body = await this.jsonRequest(`${this.jobsBase}/${encodeURIComponent(taskId)}`, {
325
367
  method: "GET",
326
368
  headers: this.headers(),
327
369
  signal: AbortSignal.timeout(this.httpTimeoutMs)
328
370
  });
329
- return body;
371
+ const status = normalizeStatus(String(body.status ?? "processing"));
372
+ const err = body.error;
373
+ const queue = body.queue;
374
+ const result = body.result;
375
+ const ok = status === "succeeded" || status === "completed";
376
+ return {
377
+ success: ok,
378
+ status,
379
+ message: body.message || err?.message,
380
+ error_code: err?.code,
381
+ queue_info: queue && typeof queue.ahead === "number" ? { position: queue.position ?? 0, ahead_tasks: queue.ahead } : void 0,
382
+ result: result ? {
383
+ task_id: taskId,
384
+ download_url: result.download_url,
385
+ filename: result.filename ?? null,
386
+ kind: result.kind ?? null,
387
+ content_type: result.content_type ?? null,
388
+ sha256: result.sha256 ?? null,
389
+ bytes: result.bytes ?? null,
390
+ files: result.files ?? null
391
+ } : void 0
392
+ };
393
+ }
394
+ /** SSE stream for a job. Caller must abort/cancel the response body. */
395
+ async openEvents(taskId, signal) {
396
+ const res = await fetch(`${this.jobsBase}/${encodeURIComponent(taskId)}/events`, {
397
+ method: "GET",
398
+ headers: {
399
+ ...this.headers(),
400
+ Accept: "text/event-stream"
401
+ },
402
+ ...signal === void 0 ? {} : { signal }
403
+ });
404
+ if (!res.ok) {
405
+ throw new KolmoPdfError("api_task_error", {
406
+ message: `SSE failed with HTTP ${res.status}`,
407
+ httpStatus: res.status
408
+ });
409
+ }
410
+ return res;
330
411
  }
331
- async download(taskId, dest) {
332
- const res = await fetch(`${this.apiBase}/download/${taskId}`, {
412
+ /**
413
+ * Stream download to a Writable, or to destPath (preferred — allows ZIP sniff after write).
414
+ */
415
+ async download(taskId, dest, opts) {
416
+ const res = await fetch(`${this.jobsBase}/${encodeURIComponent(taskId)}/download`, {
333
417
  method: "GET",
334
418
  headers: this.headers(),
335
419
  signal: AbortSignal.timeout(this.uploadTimeoutMs)
@@ -341,32 +425,51 @@ var KolmoPdfClient = class {
341
425
  });
342
426
  }
343
427
  const contentType = res.headers.get("content-type");
344
- const isZip = contentType?.includes("zip") || contentType?.includes("octet-stream") || false;
428
+ let isZip = !!contentType && (contentType.includes("zip") || contentType.includes("application/octet-stream") || contentType.includes("application/x-zip"));
345
429
  const body = res.body;
346
430
  if (!body) {
347
431
  throw new KolmoPdfError("api_task_error", { message: "Empty download response body" });
348
432
  }
349
433
  const reader = body.getReader();
350
434
  let bytesWritten = 0;
435
+ const firstChunks = [];
436
+ let sniffed = false;
351
437
  async function* generate() {
352
438
  while (true) {
353
439
  const { done, value } = await reader.read();
354
440
  if (done) break;
355
- bytesWritten += value.byteLength;
356
- yield Buffer.from(value);
441
+ const buf = Buffer.from(value);
442
+ bytesWritten += buf.byteLength;
443
+ if (!sniffed) {
444
+ firstChunks.push(buf);
445
+ const head = Buffer.concat(firstChunks);
446
+ if (head.byteLength >= 4) {
447
+ if (head[0] === 80 && head[1] === 75 && (head[2] === 3 || head[2] === 5 || head[2] === 7)) {
448
+ isZip = true;
449
+ } else if (!contentType?.includes("zip")) {
450
+ isZip = false;
451
+ }
452
+ sniffed = true;
453
+ }
454
+ }
455
+ yield buf;
357
456
  }
358
457
  }
359
458
  const readable = import_node_stream.Readable.from(generate());
360
459
  await (0, import_promises.pipeline)(readable, dest);
361
- return { contentType, isZip, bytesWritten };
460
+ return { contentType, isZip, bytesWritten, destPath: opts?.destPath };
362
461
  }
363
462
  async getBalance() {
364
- const body = await this.jsonRequest(`${this.apiBase}/balance`, {
463
+ const body = await this.jsonRequest(`${this.baseUrl}/api/v1/balance`, {
365
464
  method: "GET",
366
465
  headers: this.headers(),
367
466
  signal: AbortSignal.timeout(this.httpTimeoutMs)
368
467
  });
369
- return body;
468
+ return {
469
+ success: body.success !== false,
470
+ points: Number(body.points ?? 0),
471
+ api_key: String(body.api_key ?? "")
472
+ };
370
473
  }
371
474
  };
372
475
 
@@ -429,8 +532,8 @@ async function checkBalanceHandler(_args, ctx) {
429
532
 
430
533
  // src/tools/convert.ts
431
534
  var import_node_fs = require("fs");
432
- var import_promises3 = require("fs/promises");
433
- var import_node_path = require("path");
535
+ var import_promises4 = require("fs/promises");
536
+ var import_node_path2 = require("path");
434
537
  var import_zod2 = require("zod");
435
538
 
436
539
  // src/pages.ts
@@ -457,8 +560,8 @@ function humanizeStatus(status, aheadTasks) {
457
560
  }
458
561
 
459
562
  // src/polling.ts
460
- var TERMINAL_OK = "completed";
461
- var TERMINAL_FAIL = "failed";
563
+ var TERMINAL_OK = /* @__PURE__ */ new Set(["succeeded", "completed"]);
564
+ var TERMINAL_FAIL = /* @__PURE__ */ new Set(["failed", "cancelled"]);
462
565
  var RETRY_POLICY = {
463
566
  maxAttempts: 3,
464
567
  baseDelayMs: 1e3,
@@ -488,19 +591,119 @@ async function fetchStatusWithRetry(client, taskId) {
488
591
  }
489
592
  throw new KolmoPdfError("client_network_error");
490
593
  }
594
+ function nextSseFrame(buf) {
595
+ const lf = buf.indexOf("\n\n");
596
+ const crlf = buf.indexOf("\r\n\r\n");
597
+ if (lf < 0 && crlf < 0) return null;
598
+ if (crlf >= 0 && (lf < 0 || crlf < lf)) {
599
+ return { frame: buf.slice(0, crlf), rest: buf.slice(crlf + 4) };
600
+ }
601
+ return { frame: buf.slice(0, lf), rest: buf.slice(lf + 2) };
602
+ }
603
+ function eventNameFromFrame(raw) {
604
+ let eventName = "message";
605
+ for (const line of raw.split(/\r?\n/)) {
606
+ if (line.startsWith("event:")) eventName = line.slice(6).trim();
607
+ }
608
+ return eventName;
609
+ }
610
+ async function waitViaSse(ctx, deadline) {
611
+ const { client, taskId, progress, options } = ctx;
612
+ const remaining = Math.max(1e3, deadline - Date.now());
613
+ const timeout = AbortSignal.timeout(remaining);
614
+ const parent = options.signal;
615
+ const combined = parent === void 0 ? timeout : AbortSignal.any([parent, timeout]);
616
+ let reader;
617
+ try {
618
+ const res = await client.openEvents(taskId, combined);
619
+ const body = res.body;
620
+ if (!body) return null;
621
+ reader = body.getReader();
622
+ const decoder = new TextDecoder();
623
+ let buf = "";
624
+ const handleEvent = async (eventName) => {
625
+ if (eventName === "job.succeeded") {
626
+ const status = await fetchStatusWithRetry(client, taskId);
627
+ if (TERMINAL_OK.has(String(status.status || ""))) {
628
+ await progress?.report(`[completed] Task ${taskId} done`);
629
+ return status;
630
+ }
631
+ return "continue";
632
+ }
633
+ if (eventName === "job.failed" || eventName === "job.cancelled") {
634
+ const failed = await fetchStatusWithRetry(client, taskId);
635
+ throw new KolmoPdfError(failed.error_code || eventName.slice("job.".length), {
636
+ message: failed.message || "Task failed"
637
+ });
638
+ }
639
+ if (eventName === "job.progress" || eventName === "job.snapshot") {
640
+ await progress?.report(humanizeStatus("processing"));
641
+ }
642
+ return "continue";
643
+ };
644
+ while (!timeout.aborted) {
645
+ if (parent?.aborted === true) throw new KolmoPdfError("client_polling_timeout");
646
+ const { done, value } = await reader.read();
647
+ if (done) {
648
+ buf += decoder.decode();
649
+ const last = nextSseFrame(`${buf}
650
+
651
+ `);
652
+ if (last) {
653
+ const result = await handleEvent(eventNameFromFrame(last.frame));
654
+ if (result !== "continue") return result;
655
+ }
656
+ break;
657
+ }
658
+ buf += decoder.decode(value, { stream: true });
659
+ let next = nextSseFrame(buf);
660
+ while (next) {
661
+ buf = next.rest;
662
+ const result = await handleEvent(eventNameFromFrame(next.frame));
663
+ if (result !== "continue") return result;
664
+ next = nextSseFrame(buf);
665
+ }
666
+ }
667
+ return null;
668
+ } catch (err) {
669
+ if (err instanceof KolmoPdfError) {
670
+ const code = err.errorCode;
671
+ if (code !== "api_task_error" && code !== "client_network_error" && code !== "client_polling_timeout") {
672
+ throw err;
673
+ }
674
+ }
675
+ return null;
676
+ } finally {
677
+ try {
678
+ await reader?.cancel();
679
+ } catch {
680
+ }
681
+ }
682
+ }
491
683
  async function pollUntilComplete(ctx) {
492
684
  const { client, taskId, options, progress } = ctx;
493
685
  const deadline = Date.now() + options.maxPollMinutes * 6e4;
686
+ const viaSse = await waitViaSse(ctx, deadline);
687
+ if (viaSse && TERMINAL_OK.has(String(viaSse.status || ""))) return viaSse;
688
+ if (viaSse && TERMINAL_FAIL.has(String(viaSse.status || ""))) {
689
+ throw new KolmoPdfError(viaSse.error_code || "api_task_error", {
690
+ message: viaSse.message || "Task failed"
691
+ });
692
+ }
494
693
  while (true) {
694
+ if (options.signal?.aborted === true) {
695
+ throw new KolmoPdfError("client_polling_timeout");
696
+ }
495
697
  if (Date.now() > deadline) {
496
698
  throw new KolmoPdfError("client_polling_timeout");
497
699
  }
498
700
  const result = await fetchStatusWithRetry(client, taskId);
499
- if (result.status === TERMINAL_OK) {
701
+ const status = String(result.status || "");
702
+ if (TERMINAL_OK.has(status)) {
500
703
  await progress?.report(`[completed] Task ${taskId} done`);
501
704
  return result;
502
705
  }
503
- if (result.status === TERMINAL_FAIL) {
706
+ if (TERMINAL_FAIL.has(status)) {
504
707
  throw new KolmoPdfError(result.error_code || "api_task_error", {
505
708
  message: result.message || "Task failed"
506
709
  });
@@ -511,6 +714,52 @@ async function pollUntilComplete(ctx) {
511
714
  }
512
715
  }
513
716
 
717
+ // src/sniff.ts
718
+ var import_promises3 = require("fs/promises");
719
+ var import_node_path = require("path");
720
+ var EXT = {
721
+ zip: ".zip",
722
+ pdf: ".pdf",
723
+ markdown: ".md",
724
+ docx: ".docx",
725
+ html: ".html",
726
+ latex: ".tex",
727
+ binary: ".bin"
728
+ };
729
+ function extensionForKind(kind) {
730
+ return EXT[kind];
731
+ }
732
+ function sniffBytes(buf) {
733
+ if (buf.length >= 4 && buf[0] === 80 && buf[1] === 75 && (buf[2] === 3 || buf[2] === 5 || buf[2] === 7)) {
734
+ const hay = Buffer.from(buf.subarray(0, Math.min(buf.length, 65536))).toString("latin1");
735
+ if (hay.includes("word/document.xml") || hay.includes("wordprocessingml.document") || hay.includes("[Content_Types].xml") && hay.toLowerCase().includes("word/")) {
736
+ return "docx";
737
+ }
738
+ return "zip";
739
+ }
740
+ if (buf.length >= 4 && buf[0] === 37 && buf[1] === 80 && buf[2] === 68 && buf[3] === 70) {
741
+ return "pdf";
742
+ }
743
+ const head = Buffer.from(buf.subarray(0, Math.min(buf.length, 800))).toString("utf8");
744
+ const trimmed = head.trimStart().toLowerCase();
745
+ if (trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html")) return "html";
746
+ if (trimmed.startsWith("\\documentclass") || trimmed.startsWith("\\begin{document}"))
747
+ return "latex";
748
+ if (head.trimStart().startsWith("#") || head.includes("\n# ") || head.includes("\n```"))
749
+ return "markdown";
750
+ return "binary";
751
+ }
752
+ async function sniffFile(filePath) {
753
+ const handle = await (0, import_promises3.open)(filePath, "r");
754
+ try {
755
+ const bytes = Buffer.alloc(65536);
756
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
757
+ return sniffBytes(bytes.subarray(0, bytesRead));
758
+ } finally {
759
+ await handle.close();
760
+ }
761
+ }
762
+
514
763
  // src/tools/convert.ts
515
764
  var convertName = "kolmopdf_convert_markdown";
516
765
  var convertDescription = "Convert a Markdown file (or a ZIP of markdown + images) to DOCX, HTML, PDF, or LaTeX via KolmoPDF.";
@@ -519,22 +768,6 @@ var convertInputSchema = import_zod2.z.object({
519
768
  target_format: import_zod2.z.enum(["word", "docx", "html", "pdf", "latex", "tex"]).optional().default("word"),
520
769
  output_subdir: import_zod2.z.string().optional()
521
770
  });
522
- function formatToExtension(targetFormat) {
523
- switch (targetFormat) {
524
- case "word":
525
- case "docx":
526
- return ".docx";
527
- case "html":
528
- return ".html";
529
- case "pdf":
530
- return ".pdf";
531
- case "latex":
532
- case "tex":
533
- return ".tex";
534
- default:
535
- return ".out";
536
- }
537
- }
538
771
  function normalizeFormat(targetFormat) {
539
772
  switch (targetFormat) {
540
773
  case "word":
@@ -549,8 +782,8 @@ function normalizeFormat(targetFormat) {
549
782
  }
550
783
  async function convertHandler(args, ctx) {
551
784
  const client = ctx.getClient();
552
- const filePath = (0, import_node_path.resolve)(args.file_path);
553
- const filename = (0, import_node_path.basename)(filePath);
785
+ const filePath = (0, import_node_path2.resolve)(args.file_path);
786
+ const filename = (0, import_node_path2.basename)(filePath);
554
787
  const fileSize = await readFileSize(filePath);
555
788
  if (fileSize > MAX_FILE_BYTES) {
556
789
  throw new KolmoPdfError("convert_file_too_large");
@@ -560,7 +793,7 @@ async function convertHandler(args, ctx) {
560
793
  throw new KolmoPdfError("convert_file_type_unsupported");
561
794
  }
562
795
  await ctx.progress?.report("[uploading] Sending file for conversion...");
563
- const fileBuffer = await (0, import_promises3.readFile)(filePath);
796
+ const fileBuffer = await (0, import_promises4.readFile)(filePath);
564
797
  const submitResult = await client.convert(
565
798
  fileBuffer,
566
799
  {
@@ -581,26 +814,29 @@ async function convertHandler(args, ctx) {
581
814
  });
582
815
  await ctx.progress?.report("[downloading] Fetching converted file...");
583
816
  const subdir = args.output_subdir || taskId;
584
- const outputRoot = (0, import_node_path.resolve)(ctx.config.outputDir, subdir);
817
+ const outputRoot = (0, import_node_path2.resolve)(ctx.config.outputDir, subdir);
585
818
  (0, import_node_fs.mkdirSync)(outputRoot, { recursive: true });
586
- const outExt = formatToExtension(args.target_format);
587
- const outputPath = (0, import_node_path.join)(outputRoot, `result${outExt}`);
588
- const ws = (0, import_node_fs.createWriteStream)(outputPath);
589
- await client.download(taskId, ws);
819
+ const tempPath = (0, import_node_path2.join)(outputRoot, "download.bin");
820
+ const ws = (0, import_node_fs.createWriteStream)(tempPath);
821
+ await client.download(taskId, ws, { destPath: tempPath });
822
+ const kind = await sniffFile(tempPath);
823
+ const outputPath = (0, import_node_path2.join)(outputRoot, `result${extensionForKind(kind)}`);
824
+ await (0, import_promises4.rename)(tempPath, outputPath);
590
825
  const output = {
591
826
  task_id: taskId,
592
827
  points_deducted: submitResult.points_deducted,
593
828
  remaining_points: submitResult.remaining_points,
594
829
  output: {
595
830
  output_path: outputPath,
596
- target_format: normalizeFormat(args.target_format)
831
+ target_format: normalizeFormat(args.target_format),
832
+ kind
597
833
  }
598
834
  };
599
835
  return jsonResult(output);
600
836
  }
601
837
 
602
838
  // src/tools/estimate-cost.ts
603
- var import_node_path2 = require("path");
839
+ var import_node_path3 = require("path");
604
840
  var import_zod3 = require("zod");
605
841
  var estimateCostName = "kolmopdf_estimate_cost";
606
842
  var estimateCostDescription = "Estimate the credit cost of a KolmoPDF operation before running it. Reads page count locally and checks the current balance. Does not spend credits.";
@@ -630,7 +866,7 @@ async function estimateCostHandler(args, ctx) {
630
866
  const client = ctx.getClient();
631
867
  let pages = null;
632
868
  if (args.operation !== "convert") {
633
- const filePath = (0, import_node_path2.resolve)(args.file_path);
869
+ const filePath = (0, import_node_path3.resolve)(args.file_path);
634
870
  pages = await readPageCount(filePath);
635
871
  }
636
872
  const estimatedCredits = estimateCredits(args.operation, pages ?? 1);
@@ -663,23 +899,40 @@ async function getTaskStatusHandler(args, ctx) {
663
899
 
664
900
  // src/tools/parse-pdf.ts
665
901
  var import_node_fs3 = require("fs");
666
- var import_promises5 = require("fs/promises");
667
- var import_node_path4 = require("path");
902
+ var import_promises6 = require("fs/promises");
903
+ var import_node_path5 = require("path");
668
904
  var import_zod5 = require("zod");
669
905
 
670
906
  // src/extract.ts
671
907
  var import_node_fs2 = require("fs");
672
- var import_node_path3 = require("path");
673
- var import_promises4 = require("stream/promises");
908
+ var import_node_path4 = require("path");
909
+ var import_promises5 = require("stream/promises");
674
910
  var import_yauzl = require("yauzl");
911
+ function pickPrimaryMarkdownPath(candidates) {
912
+ if (candidates.length === 0) return null;
913
+ const scored = candidates.map((c) => {
914
+ const base = (c.entryName.split("/").pop() || c.entryName).toLowerCase();
915
+ let score = c.size;
916
+ if (/^(outline|summary|verification_report|enrichment_meta|tables_changelog|tables_normalized)(\.|$)/i.test(
917
+ base
918
+ ) || /outline|summary|verification|enrichment|tables_/.test(base)) {
919
+ score -= 1e12;
920
+ }
921
+ if (base === "readme.md") score -= 1e9;
922
+ if (/translated|bilingual/.test(base)) score -= 1e6;
923
+ return { path: c.path, score };
924
+ });
925
+ scored.sort((a, b) => b.score - a.score);
926
+ return scored[0]?.path ?? null;
927
+ }
675
928
  async function extractZip(zipPath, destDir) {
676
929
  (0, import_node_fs2.mkdirSync)(destDir, { recursive: true });
677
930
  const zipFile = await openZip(zipPath);
678
931
  const files = [];
679
- let markdownPath = null;
932
+ const mdCandidates = [];
680
933
  let imagesDir = null;
681
934
  for await (const entry of iterEntries(zipFile)) {
682
- const entryPath = (0, import_node_path3.join)(destDir, entry.fileName);
935
+ const entryPath = (0, import_node_path4.join)(destDir, entry.fileName);
683
936
  if (entry.fileName.endsWith("/")) {
684
937
  (0, import_node_fs2.mkdirSync)(entryPath, { recursive: true });
685
938
  if (entry.fileName.includes("images")) {
@@ -687,19 +940,25 @@ async function extractZip(zipPath, destDir) {
687
940
  }
688
941
  continue;
689
942
  }
690
- (0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(entryPath), { recursive: true });
943
+ (0, import_node_fs2.mkdirSync)((0, import_node_path4.dirname)(entryPath), { recursive: true });
691
944
  const readStream = await openReadStream(zipFile, entry);
692
945
  const writeStream = (0, import_node_fs2.createWriteStream)(entryPath);
693
- await (0, import_promises4.pipeline)(readStream, writeStream);
946
+ await (0, import_promises5.pipeline)(readStream, writeStream);
694
947
  files.push(entryPath);
695
- if (!markdownPath && /\.md$/i.test(entry.fileName)) {
696
- markdownPath = entryPath;
948
+ if (/\.md$/i.test(entry.fileName)) {
949
+ let size = entry.uncompressedSize || 0;
950
+ try {
951
+ size = (0, import_node_fs2.readFileSync)(entryPath).byteLength;
952
+ } catch {
953
+ }
954
+ mdCandidates.push({ path: entryPath, entryName: entry.fileName, size });
697
955
  }
698
956
  if (!imagesDir && /images\//i.test(entry.fileName)) {
699
957
  const prefix = entry.fileName.split("images/")[0] ?? "";
700
- imagesDir = (0, import_node_path3.join)(destDir, prefix, "images");
958
+ imagesDir = (0, import_node_path4.join)(destDir, prefix, "images");
701
959
  }
702
960
  }
961
+ const markdownPath = pickPrimaryMarkdownPath(mdCandidates);
703
962
  return { markdownPath, imagesDir, outputRoot: destDir, files };
704
963
  }
705
964
  function openZip(path) {
@@ -752,7 +1011,7 @@ function openReadStream(zipFile, entry) {
752
1011
 
753
1012
  // src/tools/parse-pdf.ts
754
1013
  var parsePdfName = "kolmopdf_parse_pdf";
755
- var parsePdfDescription = "Parse a local PDF into Markdown via KolmoPDF. Handles formulas, tables, multi-column layouts, and code blocks. Optionally translates while parsing.";
1014
+ var parsePdfDescription = "Parse a local PDF into Markdown via KolmoPDF. Handles formulas, tables, multi-column layouts, and code blocks. Optionally translates while parsing. Server may attach outline.md/summary.md sidecars (ZIP download).";
756
1015
  var parsePdfInputSchema = import_zod5.z.object({
757
1016
  file_path: import_zod5.z.string().describe("Absolute or cwd-relative path to a local PDF file."),
758
1017
  table_mode: import_zod5.z.enum(["markdown", "image"]).optional(),
@@ -763,12 +1022,15 @@ var parsePdfInputSchema = import_zod5.z.object({
763
1022
  images_as_url: import_zod5.z.boolean().optional(),
764
1023
  skip_rotation_detection: import_zod5.z.boolean().optional(),
765
1024
  enable_cross_page_merge: import_zod5.z.boolean().optional(),
1025
+ enrichment: import_zod5.z.string().optional().describe(
1026
+ "Parse-time AI sidecars. Omit for server default outline,summary. Use 'none' to disable. Examples: outline,summary,verification"
1027
+ ),
766
1028
  output_subdir: import_zod5.z.string().optional().describe("Subdirectory name under KOLMOPDF_OUTPUT_DIR. Defaults to <task_id>.")
767
1029
  });
768
1030
  async function parsePdfHandler(args, ctx) {
769
1031
  const client = ctx.getClient();
770
- const filePath = (0, import_node_path4.resolve)(args.file_path);
771
- const filename = (0, import_node_path4.basename)(filePath);
1032
+ const filePath = (0, import_node_path5.resolve)(args.file_path);
1033
+ const filename = (0, import_node_path5.basename)(filePath);
772
1034
  const fileSize = await readFileSize(filePath);
773
1035
  if (fileSize > MAX_FILE_BYTES) {
774
1036
  throw new KolmoPdfError("parse_file_too_large");
@@ -778,7 +1040,7 @@ async function parsePdfHandler(args, ctx) {
778
1040
  throw new KolmoPdfError("parse_page_limit_exceeded");
779
1041
  }
780
1042
  await ctx.progress?.report("[uploading] Sending PDF to KolmoPDF...");
781
- const fileBuffer = await (0, import_promises5.readFile)(filePath);
1043
+ const fileBuffer = await (0, import_promises6.readFile)(filePath);
782
1044
  const submitResult = await client.parse(
783
1045
  fileBuffer,
784
1046
  {
@@ -789,7 +1051,8 @@ async function parsePdfHandler(args, ctx) {
789
1051
  output_options: args.output_options,
790
1052
  images_as_url: args.images_as_url,
791
1053
  skip_rotation_detection: args.skip_rotation_detection,
792
- enable_cross_page_merge: args.enable_cross_page_merge
1054
+ enable_cross_page_merge: args.enable_cross_page_merge,
1055
+ enrichment: args.enrichment
793
1056
  },
794
1057
  filename
795
1058
  );
@@ -806,27 +1069,28 @@ async function parsePdfHandler(args, ctx) {
806
1069
  });
807
1070
  await ctx.progress?.report("[downloading] Fetching result...");
808
1071
  const subdir = args.output_subdir || taskId;
809
- const outputRoot = (0, import_node_path4.resolve)(ctx.config.outputDir, subdir);
1072
+ const outputRoot = (0, import_node_path5.resolve)(ctx.config.outputDir, subdir);
810
1073
  (0, import_node_fs3.mkdirSync)(outputRoot, { recursive: true });
811
- const isUrlMode = args.images_as_url === true;
1074
+ const downloadPath = (0, import_node_path5.join)(outputRoot, "download.bin");
1075
+ const ws = (0, import_node_fs3.createWriteStream)(downloadPath);
1076
+ await client.download(taskId, ws, { destPath: downloadPath });
812
1077
  let markdownPath;
813
1078
  let imagesDir = null;
814
1079
  let outputType;
815
- if (isUrlMode) {
816
- markdownPath = (0, import_node_path4.join)(outputRoot, "result.md");
817
- const ws = (0, import_node_fs3.createWriteStream)(markdownPath);
818
- await client.download(taskId, ws);
819
- outputType = "markdown_file";
820
- } else {
821
- const zipPath = (0, import_node_path4.join)(outputRoot, "result.zip");
822
- const ws = (0, import_node_fs3.createWriteStream)(zipPath);
823
- await client.download(taskId, ws);
1080
+ const kind = await sniffFile(downloadPath);
1081
+ if (kind === "zip") {
1082
+ const zipPath = (0, import_node_path5.join)(outputRoot, "result.zip");
1083
+ await (0, import_promises6.rename)(downloadPath, zipPath);
824
1084
  const extracted = await extractZip(zipPath, outputRoot);
825
- markdownPath = extracted.markdownPath || (0, import_node_path4.join)(outputRoot, "result.md");
1085
+ markdownPath = extracted.markdownPath || (0, import_node_path5.join)(outputRoot, "result.md");
826
1086
  imagesDir = extracted.imagesDir;
827
1087
  outputType = "zip_extracted";
1088
+ } else {
1089
+ markdownPath = (0, import_node_path5.join)(outputRoot, "result.md");
1090
+ await (0, import_promises6.rename)(downloadPath, markdownPath);
1091
+ outputType = "markdown_file";
828
1092
  }
829
- const mdContent = await (0, import_promises5.readFile)(markdownPath, "utf-8").catch(() => "");
1093
+ const mdContent = await (0, import_promises6.readFile)(markdownPath, "utf-8").catch(() => "");
830
1094
  const preview = mdContent.slice(0, 500);
831
1095
  const ptsPerPage = args.enable_translation ? 3 : 2;
832
1096
  const pagesParsed = Math.round(submitResult.points_deducted / ptsPerPage);
@@ -848,11 +1112,11 @@ async function parsePdfHandler(args, ctx) {
848
1112
 
849
1113
  // src/tools/translate-pdf.ts
850
1114
  var import_node_fs4 = require("fs");
851
- var import_promises6 = require("fs/promises");
852
- var import_node_path5 = require("path");
1115
+ var import_promises7 = require("fs/promises");
1116
+ var import_node_path6 = require("path");
853
1117
  var import_zod6 = require("zod");
854
1118
  var translatePdfName = "kolmopdf_translate_pdf";
855
- var translatePdfDescription = "Translate a PDF while preserving its original layout via KolmoPDF. Produces a translated PDF (optionally side-by-side bilingual).";
1119
+ var translatePdfDescription = "Translate a PDF while preserving its original layout via KolmoPDF. Produces a translated PDF, or a ZIP of PDFs when multiple layout modes are requested.";
856
1120
  var translatePdfInputSchema = import_zod6.z.object({
857
1121
  file_path: import_zod6.z.string(),
858
1122
  source_language: import_zod6.z.string().optional().default("en"),
@@ -864,8 +1128,8 @@ var translatePdfInputSchema = import_zod6.z.object({
864
1128
  });
865
1129
  async function translatePdfHandler(args, ctx) {
866
1130
  const client = ctx.getClient();
867
- const filePath = (0, import_node_path5.resolve)(args.file_path);
868
- const filename = (0, import_node_path5.basename)(filePath);
1131
+ const filePath = (0, import_node_path6.resolve)(args.file_path);
1132
+ const filename = (0, import_node_path6.basename)(filePath);
869
1133
  const fileSize = await readFileSize(filePath);
870
1134
  if (fileSize > MAX_FILE_BYTES) {
871
1135
  throw new KolmoPdfError("translate_pdf_file_too_large");
@@ -875,7 +1139,7 @@ async function translatePdfHandler(args, ctx) {
875
1139
  throw new KolmoPdfError("translate_pdf_page_limit_exceeded");
876
1140
  }
877
1141
  await ctx.progress?.report("[uploading] Sending PDF for translation...");
878
- const fileBuffer = await (0, import_promises6.readFile)(filePath);
1142
+ const fileBuffer = await (0, import_promises7.readFile)(filePath);
879
1143
  const submitResult = await client.translatePdf(
880
1144
  fileBuffer,
881
1145
  {
@@ -898,13 +1162,23 @@ async function translatePdfHandler(args, ctx) {
898
1162
  },
899
1163
  progress: ctx.progress
900
1164
  });
901
- await ctx.progress?.report("[downloading] Fetching translated PDF...");
1165
+ await ctx.progress?.report("[downloading] Fetching translated result...");
902
1166
  const subdir = args.output_subdir || taskId;
903
- const outputRoot = (0, import_node_path5.resolve)(ctx.config.outputDir, subdir);
1167
+ const outputRoot = (0, import_node_path6.resolve)(ctx.config.outputDir, subdir);
904
1168
  (0, import_node_fs4.mkdirSync)(outputRoot, { recursive: true });
905
- const pdfPath = (0, import_node_path5.join)(outputRoot, "translated.pdf");
906
- const ws = (0, import_node_fs4.createWriteStream)(pdfPath);
907
- await client.download(taskId, ws);
1169
+ const tempPath = (0, import_node_path6.join)(outputRoot, "download.bin");
1170
+ const ws = (0, import_node_fs4.createWriteStream)(tempPath);
1171
+ await client.download(taskId, ws, { destPath: tempPath });
1172
+ const kind = await sniffFile(tempPath);
1173
+ let translatedPdfPath = (0, import_node_path6.join)(outputRoot, `translated${extensionForKind(kind)}`);
1174
+ let archivePath;
1175
+ await (0, import_promises7.rename)(tempPath, translatedPdfPath);
1176
+ if (kind === "zip") {
1177
+ archivePath = translatedPdfPath;
1178
+ const extracted = await extractZip(archivePath, outputRoot);
1179
+ const pdfs = extracted.files.filter((f) => f.toLowerCase().endsWith(".pdf"));
1180
+ if (pdfs[0]) translatedPdfPath = pdfs[0];
1181
+ }
908
1182
  const pagesTranslated = Math.round(submitResult.points_deducted / 2);
909
1183
  const output = {
910
1184
  task_id: taskId,
@@ -912,14 +1186,16 @@ async function translatePdfHandler(args, ctx) {
912
1186
  points_deducted: submitResult.points_deducted,
913
1187
  remaining_points: submitResult.remaining_points,
914
1188
  output: {
915
- translated_pdf_path: pdfPath
1189
+ kind,
1190
+ translated_pdf_path: translatedPdfPath,
1191
+ ...archivePath ? { archive_path: archivePath } : {}
916
1192
  }
917
1193
  };
918
1194
  return jsonResult(output);
919
1195
  }
920
1196
 
921
1197
  // src/index.ts
922
- var VERSION = "1.0.0";
1198
+ var VERSION = "1.1.0";
923
1199
  function buildContext() {
924
1200
  const config = loadConfig();
925
1201
  return {