@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.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
|
|
243
|
-
return `${this.baseUrl}/api/
|
|
249
|
+
get jobsBase() {
|
|
250
|
+
return `${this.baseUrl}/api/v1/jobs`;
|
|
244
251
|
}
|
|
245
252
|
headers() {
|
|
246
|
-
return {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
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.
|
|
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
|
|
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
|
|
433
|
-
var
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
553
|
-
const filename = (0,
|
|
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,
|
|
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,
|
|
817
|
+
const outputRoot = (0, import_node_path2.resolve)(ctx.config.outputDir, subdir);
|
|
585
818
|
(0, import_node_fs.mkdirSync)(outputRoot, { recursive: true });
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
await
|
|
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
|
|
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,
|
|
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
|
|
667
|
-
var
|
|
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
|
|
673
|
-
var
|
|
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
|
-
|
|
932
|
+
const mdCandidates = [];
|
|
680
933
|
let imagesDir = null;
|
|
681
934
|
for await (const entry of iterEntries(zipFile)) {
|
|
682
|
-
const entryPath = (0,
|
|
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,
|
|
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,
|
|
946
|
+
await (0, import_promises5.pipeline)(readStream, writeStream);
|
|
694
947
|
files.push(entryPath);
|
|
695
|
-
if (
|
|
696
|
-
|
|
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,
|
|
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,
|
|
771
|
-
const filename = (0,
|
|
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,
|
|
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,
|
|
1072
|
+
const outputRoot = (0, import_node_path5.resolve)(ctx.config.outputDir, subdir);
|
|
810
1073
|
(0, import_node_fs3.mkdirSync)(outputRoot, { recursive: true });
|
|
811
|
-
const
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
await
|
|
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,
|
|
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,
|
|
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
|
|
852
|
-
var
|
|
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
|
|
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,
|
|
868
|
-
const filename = (0,
|
|
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,
|
|
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
|
|
1165
|
+
await ctx.progress?.report("[downloading] Fetching translated result...");
|
|
902
1166
|
const subdir = args.output_subdir || taskId;
|
|
903
|
-
const outputRoot = (0,
|
|
1167
|
+
const outputRoot = (0, import_node_path6.resolve)(ctx.config.outputDir, subdir);
|
|
904
1168
|
(0, import_node_fs4.mkdirSync)(outputRoot, { recursive: true });
|
|
905
|
-
const
|
|
906
|
-
const ws = (0, import_node_fs4.createWriteStream)(
|
|
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
|
-
|
|
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.
|
|
1198
|
+
var VERSION = "1.1.0";
|
|
923
1199
|
function buildContext() {
|
|
924
1200
|
const config = loadConfig();
|
|
925
1201
|
return {
|