@kolmopdf/mcp-server 1.0.3 → 1.1.1
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 +375 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +358 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
8
9
|
import { Readable } from "stream";
|
|
9
10
|
import { pipeline } from "stream/promises";
|
|
10
11
|
|
|
@@ -204,6 +205,12 @@ function errorFromApiBody(body, httpStatus) {
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
// src/client.ts
|
|
208
|
+
function normalizeStatus(status) {
|
|
209
|
+
if (!status) return "processing";
|
|
210
|
+
if (status === "completed") return "succeeded";
|
|
211
|
+
if (status === "pending" || status === "waiting") return "queued";
|
|
212
|
+
return status;
|
|
213
|
+
}
|
|
207
214
|
var KolmoPdfClient = class {
|
|
208
215
|
apiKey;
|
|
209
216
|
baseUrl;
|
|
@@ -215,18 +222,38 @@ var KolmoPdfClient = class {
|
|
|
215
222
|
this.httpTimeoutMs = opts.httpTimeoutMs;
|
|
216
223
|
this.uploadTimeoutMs = opts.uploadTimeoutMs;
|
|
217
224
|
}
|
|
218
|
-
get
|
|
219
|
-
return `${this.baseUrl}/api/
|
|
225
|
+
get jobsBase() {
|
|
226
|
+
return `${this.baseUrl}/api/v1/jobs`;
|
|
220
227
|
}
|
|
221
228
|
headers() {
|
|
222
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
"X-API-Key": this.apiKey,
|
|
231
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
232
|
+
};
|
|
223
233
|
}
|
|
224
234
|
async jsonRequest(url, init) {
|
|
225
235
|
const res = await fetch(url, init);
|
|
226
|
-
|
|
236
|
+
let body = {};
|
|
237
|
+
const text = await res.text();
|
|
238
|
+
try {
|
|
239
|
+
body = text ? JSON.parse(text) : {};
|
|
240
|
+
} catch {
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
throw new KolmoPdfError("api_task_error", {
|
|
243
|
+
message: `HTTP ${res.status}: non-JSON body`,
|
|
244
|
+
httpStatus: res.status
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
227
248
|
if (!res.ok || body.success === false) {
|
|
249
|
+
const errObj = body.error;
|
|
228
250
|
throw errorFromApiBody(
|
|
229
|
-
|
|
251
|
+
{
|
|
252
|
+
error_code: body.error_code || errObj?.code,
|
|
253
|
+
message: body.message || errObj?.message,
|
|
254
|
+
points_required: body.points_required,
|
|
255
|
+
current_points: body.current_points
|
|
256
|
+
},
|
|
230
257
|
res.status
|
|
231
258
|
);
|
|
232
259
|
}
|
|
@@ -247,6 +274,20 @@ var KolmoPdfClient = class {
|
|
|
247
274
|
form.append("file", blob, filename);
|
|
248
275
|
return form;
|
|
249
276
|
}
|
|
277
|
+
normalizeSubmit(body) {
|
|
278
|
+
const id = String(body.id ?? body.task_id ?? body.legacy_task_id ?? "");
|
|
279
|
+
if (!id) {
|
|
280
|
+
throw new KolmoPdfError("task_creation_failed", { message: "No job id in create response" });
|
|
281
|
+
}
|
|
282
|
+
const queue = body.queue;
|
|
283
|
+
return {
|
|
284
|
+
task_id: id,
|
|
285
|
+
status: normalizeStatus(String(body.status ?? "queued")),
|
|
286
|
+
points_deducted: Number(body.points_deducted ?? 0),
|
|
287
|
+
remaining_points: Number(body.remaining_points ?? 0),
|
|
288
|
+
queue_info: queue && typeof queue.ahead === "number" ? { position: queue.position ?? 0, ahead_tasks: queue.ahead } : void 0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
250
291
|
async parse(file, form, filename) {
|
|
251
292
|
const fd = await this.buildFileForm(file, filename);
|
|
252
293
|
if (form.table_mode) fd.append("table_mode", form.table_mode);
|
|
@@ -260,13 +301,14 @@ var KolmoPdfClient = class {
|
|
|
260
301
|
fd.append("skip_rotation_detection", String(form.skip_rotation_detection));
|
|
261
302
|
if (form.enable_cross_page_merge !== void 0)
|
|
262
303
|
fd.append("enable_cross_page_merge", String(form.enable_cross_page_merge));
|
|
263
|
-
|
|
304
|
+
if (form.enrichment !== void 0) fd.append("enrichment", form.enrichment);
|
|
305
|
+
const body = await this.jsonRequest(`${this.jobsBase}/parse`, {
|
|
264
306
|
method: "POST",
|
|
265
|
-
headers: this.headers(),
|
|
307
|
+
headers: { ...this.headers(), "Idempotency-Key": randomUUID() },
|
|
266
308
|
body: fd,
|
|
267
309
|
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
268
310
|
});
|
|
269
|
-
return body;
|
|
311
|
+
return this.normalizeSubmit(body);
|
|
270
312
|
}
|
|
271
313
|
async translatePdf(file, form, filename) {
|
|
272
314
|
const fd = await this.buildFileForm(file, filename);
|
|
@@ -277,35 +319,77 @@ var KolmoPdfClient = class {
|
|
|
277
319
|
fd.append("enableImageTranslation", String(form.enable_image_translation));
|
|
278
320
|
if (form.enable_table_translation !== void 0)
|
|
279
321
|
fd.append("enableTableTranslation", String(form.enable_table_translation));
|
|
280
|
-
const body = await this.jsonRequest(`${this.
|
|
322
|
+
const body = await this.jsonRequest(`${this.jobsBase}/translate-pdf`, {
|
|
281
323
|
method: "POST",
|
|
282
|
-
headers: this.headers(),
|
|
324
|
+
headers: { ...this.headers(), "Idempotency-Key": randomUUID() },
|
|
283
325
|
body: fd,
|
|
284
326
|
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
285
327
|
});
|
|
286
|
-
return body;
|
|
328
|
+
return this.normalizeSubmit(body);
|
|
287
329
|
}
|
|
288
330
|
async convert(file, form, filename) {
|
|
289
331
|
const fd = await this.buildFileForm(file, filename);
|
|
290
332
|
if (form.target_format) fd.append("targetFormat", form.target_format);
|
|
291
|
-
const body = await this.jsonRequest(`${this.
|
|
333
|
+
const body = await this.jsonRequest(`${this.jobsBase}/convert`, {
|
|
292
334
|
method: "POST",
|
|
293
|
-
headers: this.headers(),
|
|
335
|
+
headers: { ...this.headers(), "Idempotency-Key": randomUUID() },
|
|
294
336
|
body: fd,
|
|
295
337
|
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
296
338
|
});
|
|
297
|
-
return body;
|
|
339
|
+
return this.normalizeSubmit(body);
|
|
298
340
|
}
|
|
299
341
|
async getStatus(taskId) {
|
|
300
|
-
const body = await this.jsonRequest(`${this.
|
|
342
|
+
const body = await this.jsonRequest(`${this.jobsBase}/${encodeURIComponent(taskId)}`, {
|
|
301
343
|
method: "GET",
|
|
302
344
|
headers: this.headers(),
|
|
303
345
|
signal: AbortSignal.timeout(this.httpTimeoutMs)
|
|
304
346
|
});
|
|
305
|
-
|
|
347
|
+
const status = normalizeStatus(String(body.status ?? "processing"));
|
|
348
|
+
const err = body.error;
|
|
349
|
+
const queue = body.queue;
|
|
350
|
+
const result = body.result;
|
|
351
|
+
const ok = status === "succeeded" || status === "completed";
|
|
352
|
+
return {
|
|
353
|
+
success: ok,
|
|
354
|
+
status,
|
|
355
|
+
message: body.message || err?.message,
|
|
356
|
+
error_code: err?.code,
|
|
357
|
+
queue_info: queue && typeof queue.ahead === "number" ? { position: queue.position ?? 0, ahead_tasks: queue.ahead } : void 0,
|
|
358
|
+
result: result ? {
|
|
359
|
+
task_id: taskId,
|
|
360
|
+
download_url: result.download_url,
|
|
361
|
+
filename: result.filename ?? null,
|
|
362
|
+
kind: result.kind ?? null,
|
|
363
|
+
content_type: result.content_type ?? null,
|
|
364
|
+
sha256: result.sha256 ?? null,
|
|
365
|
+
bytes: result.bytes ?? null,
|
|
366
|
+
files: result.files ?? null
|
|
367
|
+
} : void 0
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** SSE stream for a job. Caller must abort/cancel the response body. */
|
|
371
|
+
async openEvents(taskId, signal) {
|
|
372
|
+
const res = await fetch(`${this.jobsBase}/${encodeURIComponent(taskId)}/events`, {
|
|
373
|
+
method: "GET",
|
|
374
|
+
headers: {
|
|
375
|
+
...this.headers(),
|
|
376
|
+
Accept: "text/event-stream"
|
|
377
|
+
},
|
|
378
|
+
...signal === void 0 ? {} : { signal }
|
|
379
|
+
});
|
|
380
|
+
if (!res.ok) {
|
|
381
|
+
throw new KolmoPdfError("api_task_error", {
|
|
382
|
+
message: `SSE failed with HTTP ${res.status}`,
|
|
383
|
+
httpStatus: res.status
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return res;
|
|
306
387
|
}
|
|
307
|
-
|
|
308
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Stream download to a Writable, or to destPath (preferred — allows ZIP sniff after write).
|
|
390
|
+
*/
|
|
391
|
+
async download(taskId, dest, opts) {
|
|
392
|
+
const res = await fetch(`${this.jobsBase}/${encodeURIComponent(taskId)}/download`, {
|
|
309
393
|
method: "GET",
|
|
310
394
|
headers: this.headers(),
|
|
311
395
|
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
@@ -317,32 +401,51 @@ var KolmoPdfClient = class {
|
|
|
317
401
|
});
|
|
318
402
|
}
|
|
319
403
|
const contentType = res.headers.get("content-type");
|
|
320
|
-
|
|
404
|
+
let isZip = !!contentType && (contentType.includes("zip") || contentType.includes("application/octet-stream") || contentType.includes("application/x-zip"));
|
|
321
405
|
const body = res.body;
|
|
322
406
|
if (!body) {
|
|
323
407
|
throw new KolmoPdfError("api_task_error", { message: "Empty download response body" });
|
|
324
408
|
}
|
|
325
409
|
const reader = body.getReader();
|
|
326
410
|
let bytesWritten = 0;
|
|
411
|
+
const firstChunks = [];
|
|
412
|
+
let sniffed = false;
|
|
327
413
|
async function* generate() {
|
|
328
414
|
while (true) {
|
|
329
415
|
const { done, value } = await reader.read();
|
|
330
416
|
if (done) break;
|
|
331
|
-
|
|
332
|
-
|
|
417
|
+
const buf = Buffer.from(value);
|
|
418
|
+
bytesWritten += buf.byteLength;
|
|
419
|
+
if (!sniffed) {
|
|
420
|
+
firstChunks.push(buf);
|
|
421
|
+
const head = Buffer.concat(firstChunks);
|
|
422
|
+
if (head.byteLength >= 4) {
|
|
423
|
+
if (head[0] === 80 && head[1] === 75 && (head[2] === 3 || head[2] === 5 || head[2] === 7)) {
|
|
424
|
+
isZip = true;
|
|
425
|
+
} else if (!contentType?.includes("zip")) {
|
|
426
|
+
isZip = false;
|
|
427
|
+
}
|
|
428
|
+
sniffed = true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
yield buf;
|
|
333
432
|
}
|
|
334
433
|
}
|
|
335
434
|
const readable = Readable.from(generate());
|
|
336
435
|
await pipeline(readable, dest);
|
|
337
|
-
return { contentType, isZip, bytesWritten };
|
|
436
|
+
return { contentType, isZip, bytesWritten, destPath: opts?.destPath };
|
|
338
437
|
}
|
|
339
438
|
async getBalance() {
|
|
340
|
-
const body = await this.jsonRequest(`${this.
|
|
439
|
+
const body = await this.jsonRequest(`${this.baseUrl}/api/v1/balance`, {
|
|
341
440
|
method: "GET",
|
|
342
441
|
headers: this.headers(),
|
|
343
442
|
signal: AbortSignal.timeout(this.httpTimeoutMs)
|
|
344
443
|
});
|
|
345
|
-
return
|
|
444
|
+
return {
|
|
445
|
+
success: body.success !== false,
|
|
446
|
+
points: Number(body.points ?? 0),
|
|
447
|
+
api_key: String(body.api_key ?? "")
|
|
448
|
+
};
|
|
346
449
|
}
|
|
347
450
|
};
|
|
348
451
|
|
|
@@ -405,8 +508,8 @@ async function checkBalanceHandler(_args, ctx) {
|
|
|
405
508
|
|
|
406
509
|
// src/tools/convert.ts
|
|
407
510
|
import { createWriteStream, mkdirSync } from "fs";
|
|
408
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
409
|
-
import { basename, join, resolve } from "path";
|
|
511
|
+
import { readFile as readFile2, rename as rename2 } from "fs/promises";
|
|
512
|
+
import { basename, join as join2, resolve } from "path";
|
|
410
513
|
import { z as z2 } from "zod";
|
|
411
514
|
|
|
412
515
|
// src/pages.ts
|
|
@@ -433,8 +536,8 @@ function humanizeStatus(status, aheadTasks) {
|
|
|
433
536
|
}
|
|
434
537
|
|
|
435
538
|
// src/polling.ts
|
|
436
|
-
var TERMINAL_OK = "completed";
|
|
437
|
-
var TERMINAL_FAIL = "failed";
|
|
539
|
+
var TERMINAL_OK = /* @__PURE__ */ new Set(["succeeded", "completed"]);
|
|
540
|
+
var TERMINAL_FAIL = /* @__PURE__ */ new Set(["failed", "cancelled"]);
|
|
438
541
|
var RETRY_POLICY = {
|
|
439
542
|
maxAttempts: 3,
|
|
440
543
|
baseDelayMs: 1e3,
|
|
@@ -464,19 +567,119 @@ async function fetchStatusWithRetry(client, taskId) {
|
|
|
464
567
|
}
|
|
465
568
|
throw new KolmoPdfError("client_network_error");
|
|
466
569
|
}
|
|
570
|
+
function nextSseFrame(buf) {
|
|
571
|
+
const lf = buf.indexOf("\n\n");
|
|
572
|
+
const crlf = buf.indexOf("\r\n\r\n");
|
|
573
|
+
if (lf < 0 && crlf < 0) return null;
|
|
574
|
+
if (crlf >= 0 && (lf < 0 || crlf < lf)) {
|
|
575
|
+
return { frame: buf.slice(0, crlf), rest: buf.slice(crlf + 4) };
|
|
576
|
+
}
|
|
577
|
+
return { frame: buf.slice(0, lf), rest: buf.slice(lf + 2) };
|
|
578
|
+
}
|
|
579
|
+
function eventNameFromFrame(raw) {
|
|
580
|
+
let eventName = "message";
|
|
581
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
582
|
+
if (line.startsWith("event:")) eventName = line.slice(6).trim();
|
|
583
|
+
}
|
|
584
|
+
return eventName;
|
|
585
|
+
}
|
|
586
|
+
async function waitViaSse(ctx, deadline) {
|
|
587
|
+
const { client, taskId, progress, options } = ctx;
|
|
588
|
+
const remaining = Math.max(1e3, deadline - Date.now());
|
|
589
|
+
const timeout = AbortSignal.timeout(remaining);
|
|
590
|
+
const parent = options.signal;
|
|
591
|
+
const combined = parent === void 0 ? timeout : AbortSignal.any([parent, timeout]);
|
|
592
|
+
let reader;
|
|
593
|
+
try {
|
|
594
|
+
const res = await client.openEvents(taskId, combined);
|
|
595
|
+
const body = res.body;
|
|
596
|
+
if (!body) return null;
|
|
597
|
+
reader = body.getReader();
|
|
598
|
+
const decoder = new TextDecoder();
|
|
599
|
+
let buf = "";
|
|
600
|
+
const handleEvent = async (eventName) => {
|
|
601
|
+
if (eventName === "job.succeeded") {
|
|
602
|
+
const status = await fetchStatusWithRetry(client, taskId);
|
|
603
|
+
if (TERMINAL_OK.has(String(status.status || ""))) {
|
|
604
|
+
await progress?.report(`[completed] Task ${taskId} done`);
|
|
605
|
+
return status;
|
|
606
|
+
}
|
|
607
|
+
return "continue";
|
|
608
|
+
}
|
|
609
|
+
if (eventName === "job.failed" || eventName === "job.cancelled") {
|
|
610
|
+
const failed = await fetchStatusWithRetry(client, taskId);
|
|
611
|
+
throw new KolmoPdfError(failed.error_code || eventName.slice("job.".length), {
|
|
612
|
+
message: failed.message || "Task failed"
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (eventName === "job.progress" || eventName === "job.snapshot") {
|
|
616
|
+
await progress?.report(humanizeStatus("processing"));
|
|
617
|
+
}
|
|
618
|
+
return "continue";
|
|
619
|
+
};
|
|
620
|
+
while (!timeout.aborted) {
|
|
621
|
+
if (parent?.aborted === true) throw new KolmoPdfError("client_polling_timeout");
|
|
622
|
+
const { done, value } = await reader.read();
|
|
623
|
+
if (done) {
|
|
624
|
+
buf += decoder.decode();
|
|
625
|
+
const last = nextSseFrame(`${buf}
|
|
626
|
+
|
|
627
|
+
`);
|
|
628
|
+
if (last) {
|
|
629
|
+
const result = await handleEvent(eventNameFromFrame(last.frame));
|
|
630
|
+
if (result !== "continue") return result;
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
buf += decoder.decode(value, { stream: true });
|
|
635
|
+
let next = nextSseFrame(buf);
|
|
636
|
+
while (next) {
|
|
637
|
+
buf = next.rest;
|
|
638
|
+
const result = await handleEvent(eventNameFromFrame(next.frame));
|
|
639
|
+
if (result !== "continue") return result;
|
|
640
|
+
next = nextSseFrame(buf);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
} catch (err) {
|
|
645
|
+
if (err instanceof KolmoPdfError) {
|
|
646
|
+
const code = err.errorCode;
|
|
647
|
+
if (code !== "api_task_error" && code !== "client_network_error" && code !== "client_polling_timeout") {
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
} finally {
|
|
653
|
+
try {
|
|
654
|
+
await reader?.cancel();
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
467
659
|
async function pollUntilComplete(ctx) {
|
|
468
660
|
const { client, taskId, options, progress } = ctx;
|
|
469
661
|
const deadline = Date.now() + options.maxPollMinutes * 6e4;
|
|
662
|
+
const viaSse = await waitViaSse(ctx, deadline);
|
|
663
|
+
if (viaSse && TERMINAL_OK.has(String(viaSse.status || ""))) return viaSse;
|
|
664
|
+
if (viaSse && TERMINAL_FAIL.has(String(viaSse.status || ""))) {
|
|
665
|
+
throw new KolmoPdfError(viaSse.error_code || "api_task_error", {
|
|
666
|
+
message: viaSse.message || "Task failed"
|
|
667
|
+
});
|
|
668
|
+
}
|
|
470
669
|
while (true) {
|
|
670
|
+
if (options.signal?.aborted === true) {
|
|
671
|
+
throw new KolmoPdfError("client_polling_timeout");
|
|
672
|
+
}
|
|
471
673
|
if (Date.now() > deadline) {
|
|
472
674
|
throw new KolmoPdfError("client_polling_timeout");
|
|
473
675
|
}
|
|
474
676
|
const result = await fetchStatusWithRetry(client, taskId);
|
|
475
|
-
|
|
677
|
+
const status = String(result.status || "");
|
|
678
|
+
if (TERMINAL_OK.has(status)) {
|
|
476
679
|
await progress?.report(`[completed] Task ${taskId} done`);
|
|
477
680
|
return result;
|
|
478
681
|
}
|
|
479
|
-
if (
|
|
682
|
+
if (TERMINAL_FAIL.has(status)) {
|
|
480
683
|
throw new KolmoPdfError(result.error_code || "api_task_error", {
|
|
481
684
|
message: result.message || "Task failed"
|
|
482
685
|
});
|
|
@@ -487,6 +690,52 @@ async function pollUntilComplete(ctx) {
|
|
|
487
690
|
}
|
|
488
691
|
}
|
|
489
692
|
|
|
693
|
+
// src/sniff.ts
|
|
694
|
+
import { open, rename } from "fs/promises";
|
|
695
|
+
import { join } from "path";
|
|
696
|
+
var EXT = {
|
|
697
|
+
zip: ".zip",
|
|
698
|
+
pdf: ".pdf",
|
|
699
|
+
markdown: ".md",
|
|
700
|
+
docx: ".docx",
|
|
701
|
+
html: ".html",
|
|
702
|
+
latex: ".tex",
|
|
703
|
+
binary: ".bin"
|
|
704
|
+
};
|
|
705
|
+
function extensionForKind(kind) {
|
|
706
|
+
return EXT[kind];
|
|
707
|
+
}
|
|
708
|
+
function sniffBytes(buf) {
|
|
709
|
+
if (buf.length >= 4 && buf[0] === 80 && buf[1] === 75 && (buf[2] === 3 || buf[2] === 5 || buf[2] === 7)) {
|
|
710
|
+
const hay = Buffer.from(buf.subarray(0, Math.min(buf.length, 65536))).toString("latin1");
|
|
711
|
+
if (hay.includes("word/document.xml") || hay.includes("wordprocessingml.document") || hay.includes("[Content_Types].xml") && hay.toLowerCase().includes("word/")) {
|
|
712
|
+
return "docx";
|
|
713
|
+
}
|
|
714
|
+
return "zip";
|
|
715
|
+
}
|
|
716
|
+
if (buf.length >= 4 && buf[0] === 37 && buf[1] === 80 && buf[2] === 68 && buf[3] === 70) {
|
|
717
|
+
return "pdf";
|
|
718
|
+
}
|
|
719
|
+
const head = Buffer.from(buf.subarray(0, Math.min(buf.length, 800))).toString("utf8");
|
|
720
|
+
const trimmed = head.trimStart().toLowerCase();
|
|
721
|
+
if (trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html")) return "html";
|
|
722
|
+
if (trimmed.startsWith("\\documentclass") || trimmed.startsWith("\\begin{document}"))
|
|
723
|
+
return "latex";
|
|
724
|
+
if (head.trimStart().startsWith("#") || head.includes("\n# ") || head.includes("\n```"))
|
|
725
|
+
return "markdown";
|
|
726
|
+
return "binary";
|
|
727
|
+
}
|
|
728
|
+
async function sniffFile(filePath) {
|
|
729
|
+
const handle = await open(filePath, "r");
|
|
730
|
+
try {
|
|
731
|
+
const bytes = Buffer.alloc(65536);
|
|
732
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
733
|
+
return sniffBytes(bytes.subarray(0, bytesRead));
|
|
734
|
+
} finally {
|
|
735
|
+
await handle.close();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
490
739
|
// src/tools/convert.ts
|
|
491
740
|
var convertName = "kolmopdf_convert_markdown";
|
|
492
741
|
var convertDescription = "Convert a Markdown file (or a ZIP of markdown + images) to DOCX, HTML, PDF, or LaTeX via KolmoPDF.";
|
|
@@ -495,22 +744,6 @@ var convertInputSchema = z2.object({
|
|
|
495
744
|
target_format: z2.enum(["word", "docx", "html", "pdf", "latex", "tex"]).optional().default("word"),
|
|
496
745
|
output_subdir: z2.string().optional()
|
|
497
746
|
});
|
|
498
|
-
function formatToExtension(targetFormat) {
|
|
499
|
-
switch (targetFormat) {
|
|
500
|
-
case "word":
|
|
501
|
-
case "docx":
|
|
502
|
-
return ".docx";
|
|
503
|
-
case "html":
|
|
504
|
-
return ".html";
|
|
505
|
-
case "pdf":
|
|
506
|
-
return ".pdf";
|
|
507
|
-
case "latex":
|
|
508
|
-
case "tex":
|
|
509
|
-
return ".tex";
|
|
510
|
-
default:
|
|
511
|
-
return ".out";
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
747
|
function normalizeFormat(targetFormat) {
|
|
515
748
|
switch (targetFormat) {
|
|
516
749
|
case "word":
|
|
@@ -559,17 +792,20 @@ async function convertHandler(args, ctx) {
|
|
|
559
792
|
const subdir = args.output_subdir || taskId;
|
|
560
793
|
const outputRoot = resolve(ctx.config.outputDir, subdir);
|
|
561
794
|
mkdirSync(outputRoot, { recursive: true });
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
await
|
|
795
|
+
const tempPath = join2(outputRoot, "download.bin");
|
|
796
|
+
const ws = createWriteStream(tempPath);
|
|
797
|
+
await client.download(taskId, ws, { destPath: tempPath });
|
|
798
|
+
const kind = await sniffFile(tempPath);
|
|
799
|
+
const outputPath = join2(outputRoot, `result${extensionForKind(kind)}`);
|
|
800
|
+
await rename2(tempPath, outputPath);
|
|
566
801
|
const output = {
|
|
567
802
|
task_id: taskId,
|
|
568
803
|
points_deducted: submitResult.points_deducted,
|
|
569
804
|
remaining_points: submitResult.remaining_points,
|
|
570
805
|
output: {
|
|
571
806
|
output_path: outputPath,
|
|
572
|
-
target_format: normalizeFormat(args.target_format)
|
|
807
|
+
target_format: normalizeFormat(args.target_format),
|
|
808
|
+
kind
|
|
573
809
|
}
|
|
574
810
|
};
|
|
575
811
|
return jsonResult(output);
|
|
@@ -639,23 +875,40 @@ async function getTaskStatusHandler(args, ctx) {
|
|
|
639
875
|
|
|
640
876
|
// src/tools/parse-pdf.ts
|
|
641
877
|
import { createWriteStream as createWriteStream3, mkdirSync as mkdirSync3 } from "fs";
|
|
642
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
643
|
-
import { basename as basename2, join as
|
|
878
|
+
import { readFile as readFile3, rename as rename3 } from "fs/promises";
|
|
879
|
+
import { basename as basename2, join as join4, resolve as resolve3 } from "path";
|
|
644
880
|
import { z as z5 } from "zod";
|
|
645
881
|
|
|
646
882
|
// src/extract.ts
|
|
647
|
-
import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync2 } from "fs";
|
|
648
|
-
import { dirname, join as
|
|
883
|
+
import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync2, readFileSync } from "fs";
|
|
884
|
+
import { dirname, join as join3 } from "path";
|
|
649
885
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
650
886
|
import { open as yauzlOpen } from "yauzl";
|
|
887
|
+
function pickPrimaryMarkdownPath(candidates) {
|
|
888
|
+
if (candidates.length === 0) return null;
|
|
889
|
+
const scored = candidates.map((c) => {
|
|
890
|
+
const base = (c.entryName.split("/").pop() || c.entryName).toLowerCase();
|
|
891
|
+
let score = c.size;
|
|
892
|
+
if (/^(outline|summary|verification_report|enrichment_meta|tables_changelog|tables_normalized)(\.|$)/i.test(
|
|
893
|
+
base
|
|
894
|
+
) || /outline|summary|verification|enrichment|tables_/.test(base)) {
|
|
895
|
+
score -= 1e12;
|
|
896
|
+
}
|
|
897
|
+
if (base === "readme.md") score -= 1e9;
|
|
898
|
+
if (/translated|bilingual/.test(base)) score -= 1e6;
|
|
899
|
+
return { path: c.path, score };
|
|
900
|
+
});
|
|
901
|
+
scored.sort((a, b) => b.score - a.score);
|
|
902
|
+
return scored[0]?.path ?? null;
|
|
903
|
+
}
|
|
651
904
|
async function extractZip(zipPath, destDir) {
|
|
652
905
|
mkdirSync2(destDir, { recursive: true });
|
|
653
906
|
const zipFile = await openZip(zipPath);
|
|
654
907
|
const files = [];
|
|
655
|
-
|
|
908
|
+
const mdCandidates = [];
|
|
656
909
|
let imagesDir = null;
|
|
657
910
|
for await (const entry of iterEntries(zipFile)) {
|
|
658
|
-
const entryPath =
|
|
911
|
+
const entryPath = join3(destDir, entry.fileName);
|
|
659
912
|
if (entry.fileName.endsWith("/")) {
|
|
660
913
|
mkdirSync2(entryPath, { recursive: true });
|
|
661
914
|
if (entry.fileName.includes("images")) {
|
|
@@ -668,14 +921,20 @@ async function extractZip(zipPath, destDir) {
|
|
|
668
921
|
const writeStream = createWriteStream2(entryPath);
|
|
669
922
|
await pipeline2(readStream, writeStream);
|
|
670
923
|
files.push(entryPath);
|
|
671
|
-
if (
|
|
672
|
-
|
|
924
|
+
if (/\.md$/i.test(entry.fileName)) {
|
|
925
|
+
let size = entry.uncompressedSize || 0;
|
|
926
|
+
try {
|
|
927
|
+
size = readFileSync(entryPath).byteLength;
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
mdCandidates.push({ path: entryPath, entryName: entry.fileName, size });
|
|
673
931
|
}
|
|
674
932
|
if (!imagesDir && /images\//i.test(entry.fileName)) {
|
|
675
933
|
const prefix = entry.fileName.split("images/")[0] ?? "";
|
|
676
|
-
imagesDir =
|
|
934
|
+
imagesDir = join3(destDir, prefix, "images");
|
|
677
935
|
}
|
|
678
936
|
}
|
|
937
|
+
const markdownPath = pickPrimaryMarkdownPath(mdCandidates);
|
|
679
938
|
return { markdownPath, imagesDir, outputRoot: destDir, files };
|
|
680
939
|
}
|
|
681
940
|
function openZip(path) {
|
|
@@ -728,7 +987,7 @@ function openReadStream(zipFile, entry) {
|
|
|
728
987
|
|
|
729
988
|
// src/tools/parse-pdf.ts
|
|
730
989
|
var parsePdfName = "kolmopdf_parse_pdf";
|
|
731
|
-
var parsePdfDescription = "Parse a local PDF into Markdown via KolmoPDF. Handles formulas, tables, multi-column layouts, and code blocks. Optionally translates while parsing.";
|
|
990
|
+
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).";
|
|
732
991
|
var parsePdfInputSchema = z5.object({
|
|
733
992
|
file_path: z5.string().describe("Absolute or cwd-relative path to a local PDF file."),
|
|
734
993
|
table_mode: z5.enum(["markdown", "image"]).optional(),
|
|
@@ -739,6 +998,9 @@ var parsePdfInputSchema = z5.object({
|
|
|
739
998
|
images_as_url: z5.boolean().optional(),
|
|
740
999
|
skip_rotation_detection: z5.boolean().optional(),
|
|
741
1000
|
enable_cross_page_merge: z5.boolean().optional(),
|
|
1001
|
+
enrichment: z5.string().optional().describe(
|
|
1002
|
+
"Parse-time AI sidecars. Omit for server default outline,summary. Use 'none' to disable. Examples: outline,summary,verification"
|
|
1003
|
+
),
|
|
742
1004
|
output_subdir: z5.string().optional().describe("Subdirectory name under KOLMOPDF_OUTPUT_DIR. Defaults to <task_id>.")
|
|
743
1005
|
});
|
|
744
1006
|
async function parsePdfHandler(args, ctx) {
|
|
@@ -765,7 +1027,8 @@ async function parsePdfHandler(args, ctx) {
|
|
|
765
1027
|
output_options: args.output_options,
|
|
766
1028
|
images_as_url: args.images_as_url,
|
|
767
1029
|
skip_rotation_detection: args.skip_rotation_detection,
|
|
768
|
-
enable_cross_page_merge: args.enable_cross_page_merge
|
|
1030
|
+
enable_cross_page_merge: args.enable_cross_page_merge,
|
|
1031
|
+
enrichment: args.enrichment
|
|
769
1032
|
},
|
|
770
1033
|
filename
|
|
771
1034
|
);
|
|
@@ -784,23 +1047,24 @@ async function parsePdfHandler(args, ctx) {
|
|
|
784
1047
|
const subdir = args.output_subdir || taskId;
|
|
785
1048
|
const outputRoot = resolve3(ctx.config.outputDir, subdir);
|
|
786
1049
|
mkdirSync3(outputRoot, { recursive: true });
|
|
787
|
-
const
|
|
1050
|
+
const downloadPath = join4(outputRoot, "download.bin");
|
|
1051
|
+
const ws = createWriteStream3(downloadPath);
|
|
1052
|
+
await client.download(taskId, ws, { destPath: downloadPath });
|
|
788
1053
|
let markdownPath;
|
|
789
1054
|
let imagesDir = null;
|
|
790
1055
|
let outputType;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
await
|
|
795
|
-
outputType = "markdown_file";
|
|
796
|
-
} else {
|
|
797
|
-
const zipPath = join3(outputRoot, "result.zip");
|
|
798
|
-
const ws = createWriteStream3(zipPath);
|
|
799
|
-
await client.download(taskId, ws);
|
|
1056
|
+
const kind = await sniffFile(downloadPath);
|
|
1057
|
+
if (kind === "zip") {
|
|
1058
|
+
const zipPath = join4(outputRoot, "result.zip");
|
|
1059
|
+
await rename3(downloadPath, zipPath);
|
|
800
1060
|
const extracted = await extractZip(zipPath, outputRoot);
|
|
801
|
-
markdownPath = extracted.markdownPath ||
|
|
1061
|
+
markdownPath = extracted.markdownPath || join4(outputRoot, "result.md");
|
|
802
1062
|
imagesDir = extracted.imagesDir;
|
|
803
1063
|
outputType = "zip_extracted";
|
|
1064
|
+
} else {
|
|
1065
|
+
markdownPath = join4(outputRoot, "result.md");
|
|
1066
|
+
await rename3(downloadPath, markdownPath);
|
|
1067
|
+
outputType = "markdown_file";
|
|
804
1068
|
}
|
|
805
1069
|
const mdContent = await readFile3(markdownPath, "utf-8").catch(() => "");
|
|
806
1070
|
const preview = mdContent.slice(0, 500);
|
|
@@ -824,11 +1088,11 @@ async function parsePdfHandler(args, ctx) {
|
|
|
824
1088
|
|
|
825
1089
|
// src/tools/translate-pdf.ts
|
|
826
1090
|
import { createWriteStream as createWriteStream4, mkdirSync as mkdirSync4 } from "fs";
|
|
827
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
828
|
-
import { basename as basename3, join as
|
|
1091
|
+
import { readFile as readFile4, rename as rename4 } from "fs/promises";
|
|
1092
|
+
import { basename as basename3, join as join5, resolve as resolve4 } from "path";
|
|
829
1093
|
import { z as z6 } from "zod";
|
|
830
1094
|
var translatePdfName = "kolmopdf_translate_pdf";
|
|
831
|
-
var translatePdfDescription = "Translate a PDF while preserving its original layout via KolmoPDF. Produces a translated PDF
|
|
1095
|
+
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.";
|
|
832
1096
|
var translatePdfInputSchema = z6.object({
|
|
833
1097
|
file_path: z6.string(),
|
|
834
1098
|
source_language: z6.string().optional().default("en"),
|
|
@@ -874,13 +1138,23 @@ async function translatePdfHandler(args, ctx) {
|
|
|
874
1138
|
},
|
|
875
1139
|
progress: ctx.progress
|
|
876
1140
|
});
|
|
877
|
-
await ctx.progress?.report("[downloading] Fetching translated
|
|
1141
|
+
await ctx.progress?.report("[downloading] Fetching translated result...");
|
|
878
1142
|
const subdir = args.output_subdir || taskId;
|
|
879
1143
|
const outputRoot = resolve4(ctx.config.outputDir, subdir);
|
|
880
1144
|
mkdirSync4(outputRoot, { recursive: true });
|
|
881
|
-
const
|
|
882
|
-
const ws = createWriteStream4(
|
|
883
|
-
await client.download(taskId, ws);
|
|
1145
|
+
const tempPath = join5(outputRoot, "download.bin");
|
|
1146
|
+
const ws = createWriteStream4(tempPath);
|
|
1147
|
+
await client.download(taskId, ws, { destPath: tempPath });
|
|
1148
|
+
const kind = await sniffFile(tempPath);
|
|
1149
|
+
let translatedPdfPath = join5(outputRoot, `translated${extensionForKind(kind)}`);
|
|
1150
|
+
let archivePath;
|
|
1151
|
+
await rename4(tempPath, translatedPdfPath);
|
|
1152
|
+
if (kind === "zip") {
|
|
1153
|
+
archivePath = translatedPdfPath;
|
|
1154
|
+
const extracted = await extractZip(archivePath, outputRoot);
|
|
1155
|
+
const pdfs = extracted.files.filter((f) => f.toLowerCase().endsWith(".pdf"));
|
|
1156
|
+
if (pdfs[0]) translatedPdfPath = pdfs[0];
|
|
1157
|
+
}
|
|
884
1158
|
const pagesTranslated = Math.round(submitResult.points_deducted / 2);
|
|
885
1159
|
const output = {
|
|
886
1160
|
task_id: taskId,
|
|
@@ -888,14 +1162,16 @@ async function translatePdfHandler(args, ctx) {
|
|
|
888
1162
|
points_deducted: submitResult.points_deducted,
|
|
889
1163
|
remaining_points: submitResult.remaining_points,
|
|
890
1164
|
output: {
|
|
891
|
-
|
|
1165
|
+
kind,
|
|
1166
|
+
translated_pdf_path: translatedPdfPath,
|
|
1167
|
+
...archivePath ? { archive_path: archivePath } : {}
|
|
892
1168
|
}
|
|
893
1169
|
};
|
|
894
1170
|
return jsonResult(output);
|
|
895
1171
|
}
|
|
896
1172
|
|
|
897
1173
|
// src/index.ts
|
|
898
|
-
var VERSION = "1.
|
|
1174
|
+
var VERSION = "1.1.0";
|
|
899
1175
|
function buildContext() {
|
|
900
1176
|
const config = loadConfig();
|
|
901
1177
|
return {
|