@echofiles/echo-pdf 0.4.3 → 0.6.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.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +85 -562
  3. package/bin/echo-pdf.js +130 -525
  4. package/dist/file-utils.d.ts +0 -3
  5. package/dist/file-utils.js +0 -18
  6. package/dist/local/document.d.ts +10 -0
  7. package/dist/local/document.js +133 -0
  8. package/dist/local/index.d.ts +3 -135
  9. package/dist/local/index.js +2 -555
  10. package/dist/local/semantic.d.ts +2 -0
  11. package/dist/local/semantic.js +231 -0
  12. package/dist/local/shared.d.ts +50 -0
  13. package/dist/local/shared.js +173 -0
  14. package/dist/local/types.d.ts +183 -0
  15. package/dist/local/types.js +2 -0
  16. package/dist/node/pdfium-local.js +30 -6
  17. package/dist/pdf-config.js +2 -65
  18. package/dist/pdf-types.d.ts +1 -58
  19. package/dist/types.d.ts +1 -87
  20. package/echo-pdf.config.json +1 -21
  21. package/package.json +25 -22
  22. package/bin/lib/http.js +0 -97
  23. package/bin/lib/mcp-stdio.js +0 -99
  24. package/dist/auth.d.ts +0 -18
  25. package/dist/auth.js +0 -36
  26. package/dist/core/index.d.ts +0 -50
  27. package/dist/core/index.js +0 -7
  28. package/dist/file-ops.d.ts +0 -11
  29. package/dist/file-ops.js +0 -36
  30. package/dist/file-store-do.d.ts +0 -36
  31. package/dist/file-store-do.js +0 -298
  32. package/dist/http-error.d.ts +0 -9
  33. package/dist/http-error.js +0 -14
  34. package/dist/index.d.ts +0 -1
  35. package/dist/index.js +0 -1
  36. package/dist/mcp-server.d.ts +0 -3
  37. package/dist/mcp-server.js +0 -124
  38. package/dist/node/semantic-local.d.ts +0 -16
  39. package/dist/node/semantic-local.js +0 -113
  40. package/dist/pdf-agent.d.ts +0 -18
  41. package/dist/pdf-agent.js +0 -217
  42. package/dist/pdf-storage.d.ts +0 -8
  43. package/dist/pdf-storage.js +0 -86
  44. package/dist/pdfium-engine.d.ts +0 -9
  45. package/dist/pdfium-engine.js +0 -180
  46. package/dist/r2-file-store.d.ts +0 -20
  47. package/dist/r2-file-store.js +0 -176
  48. package/dist/response-schema.d.ts +0 -15
  49. package/dist/response-schema.js +0 -159
  50. package/dist/tool-registry.d.ts +0 -16
  51. package/dist/tool-registry.js +0 -175
  52. package/dist/worker.d.ts +0 -7
  53. package/dist/worker.js +0 -386
  54. package/scripts/export-fixtures.sh +0 -204
  55. package/wrangler.toml +0 -19
@@ -1,6 +1,9 @@
1
1
  /// <reference path="./compat.d.ts" />
2
2
  import { encode as encodePng } from "@cf-wasm/png";
3
3
  import { init } from "@embedpdf/pdfium";
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+ import path from "node:path";
4
7
  let moduleInstance = null;
5
8
  let libraryInitialized = false;
6
9
  const isNodeRuntime = () => typeof process !== "undefined" && Boolean(process.versions?.node);
@@ -10,13 +13,34 @@ const ensureWasmFunctionShim = () => {
10
13
  return;
11
14
  wasmApi.Function = (_sig, fn) => fn;
12
15
  };
13
- const readLocalPdfiumWasm = async () => {
14
- const [{ readFile }, { createRequire }] = await Promise.all([
15
- import("node:fs/promises"),
16
- import("node:module"),
17
- ]);
16
+ const resolveLocalPdfiumWasmPath = async () => {
18
17
  const require = createRequire(import.meta.url);
19
- const bytes = await readFile(require.resolve("@embedpdf/pdfium/pdfium.wasm"));
18
+ const candidates = [];
19
+ try {
20
+ candidates.push(require.resolve("@embedpdf/pdfium/pdfium.wasm"));
21
+ }
22
+ catch (error) {
23
+ const code = error && typeof error === "object" && "code" in error ? error.code : "";
24
+ if (!["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"].includes(String(code))) {
25
+ throw error;
26
+ }
27
+ }
28
+ const pdfiumEntry = require.resolve("@embedpdf/pdfium");
29
+ candidates.push(path.join(path.dirname(pdfiumEntry), "pdfium.wasm"));
30
+ for (const candidate of candidates) {
31
+ try {
32
+ await stat(candidate);
33
+ return candidate;
34
+ }
35
+ catch {
36
+ // Keep trying known package-local locations before surfacing a hard runtime error.
37
+ }
38
+ }
39
+ throw new Error("Failed to resolve the packaged PDFium WebAssembly binary for the local runtime.");
40
+ };
41
+ const readLocalPdfiumWasm = async () => {
42
+ const wasmPath = await resolveLocalPdfiumWasmPath();
43
+ const bytes = await readFile(wasmPath);
20
44
  return new Uint8Array(bytes).slice().buffer;
21
45
  };
22
46
  const ensureLocalPdfium = async (_config) => {
@@ -20,37 +20,10 @@ const resolveEnvRefs = (value, env) => {
20
20
  return value;
21
21
  };
22
22
  const validateConfig = (config) => {
23
- if (!config.service?.name)
24
- throw new Error("service.name is required");
25
23
  if (!config.pdfium?.wasmUrl)
26
24
  throw new Error("pdfium.wasmUrl is required");
27
- if (!config.service?.storage)
28
- throw new Error("service.storage is required");
29
- if (typeof config.service.publicBaseUrl === "string" &&
30
- config.service.publicBaseUrl.length > 0 &&
31
- !/^https?:\/\//.test(config.service.publicBaseUrl)) {
32
- throw new Error("service.publicBaseUrl must start with http:// or https://");
33
- }
34
- if (typeof config.service.fileGet?.cacheTtlSeconds === "number" && config.service.fileGet.cacheTtlSeconds < 0) {
35
- throw new Error("service.fileGet.cacheTtlSeconds must be >= 0");
36
- }
37
- if (!Number.isFinite(config.service.storage.maxFileBytes) || config.service.storage.maxFileBytes <= 0) {
38
- throw new Error("service.storage.maxFileBytes must be positive");
39
- }
40
- if (config.service.storage.maxFileBytes < config.service.maxPdfBytes) {
41
- throw new Error("service.storage.maxFileBytes must be >= service.maxPdfBytes");
42
- }
43
- if (!Number.isFinite(config.service.storage.maxTotalBytes) || config.service.storage.maxTotalBytes <= 0) {
44
- throw new Error("service.storage.maxTotalBytes must be positive");
45
- }
46
- if (config.service.storage.maxTotalBytes < config.service.storage.maxFileBytes) {
47
- throw new Error("service.storage.maxTotalBytes must be >= maxFileBytes");
48
- }
49
- if (!Number.isFinite(config.service.storage.ttlHours) || config.service.storage.ttlHours <= 0) {
50
- throw new Error("service.storage.ttlHours must be positive");
51
- }
52
- if (!Number.isFinite(config.service.storage.cleanupBatchSize) || config.service.storage.cleanupBatchSize <= 0) {
53
- throw new Error("service.storage.cleanupBatchSize must be positive");
25
+ if (!Number.isFinite(config.service?.defaultRenderScale) || config.service.defaultRenderScale <= 0) {
26
+ throw new Error("service.defaultRenderScale must be positive");
54
27
  }
55
28
  if (!config.agent?.defaultProvider)
56
29
  throw new Error("agent.defaultProvider is required");
@@ -68,43 +41,8 @@ export const loadEchoPdfConfig = (env) => {
68
41
  const resolved = resolveEnvRefs(configJson, env);
69
42
  const providerOverride = env.ECHO_PDF_DEFAULT_PROVIDER;
70
43
  const modelOverride = env.ECHO_PDF_DEFAULT_MODEL;
71
- const publicBaseUrlOverride = env.ECHO_PDF_PUBLIC_BASE_URL;
72
- const computeAuthHeaderOverride = env.ECHO_PDF_COMPUTE_AUTH_HEADER;
73
- const computeAuthEnvOverride = env.ECHO_PDF_COMPUTE_AUTH_ENV;
74
- const fileGetAuthHeaderOverride = env.ECHO_PDF_FILE_GET_AUTH_HEADER;
75
- const fileGetAuthEnvOverride = env.ECHO_PDF_FILE_GET_AUTH_ENV;
76
- const fileGetCacheTtlOverride = env.ECHO_PDF_FILE_GET_CACHE_TTL_SECONDS;
77
44
  const withOverrides = {
78
45
  ...resolved,
79
- service: {
80
- ...resolved.service,
81
- publicBaseUrl: typeof publicBaseUrlOverride === "string" && publicBaseUrlOverride.trim().length > 0
82
- ? publicBaseUrlOverride.trim()
83
- : resolved.service.publicBaseUrl,
84
- computeAuth: {
85
- authHeader: typeof computeAuthHeaderOverride === "string" && computeAuthHeaderOverride.trim().length > 0
86
- ? computeAuthHeaderOverride.trim()
87
- : resolved.service.computeAuth?.authHeader,
88
- authEnv: typeof computeAuthEnvOverride === "string" && computeAuthEnvOverride.trim().length > 0
89
- ? computeAuthEnvOverride.trim()
90
- : resolved.service.computeAuth?.authEnv,
91
- },
92
- fileGet: {
93
- authHeader: typeof fileGetAuthHeaderOverride === "string" && fileGetAuthHeaderOverride.trim().length > 0
94
- ? fileGetAuthHeaderOverride.trim()
95
- : resolved.service.fileGet?.authHeader,
96
- authEnv: typeof fileGetAuthEnvOverride === "string" && fileGetAuthEnvOverride.trim().length > 0
97
- ? fileGetAuthEnvOverride.trim()
98
- : resolved.service.fileGet?.authEnv,
99
- cacheTtlSeconds: (() => {
100
- if (typeof fileGetCacheTtlOverride === "string" && fileGetCacheTtlOverride.trim().length > 0) {
101
- const value = Number(fileGetCacheTtlOverride);
102
- return Number.isFinite(value) && value >= 0 ? Math.floor(value) : resolved.service.fileGet?.cacheTtlSeconds;
103
- }
104
- return resolved.service.fileGet?.cacheTtlSeconds;
105
- })(),
106
- },
107
- },
108
46
  agent: {
109
47
  ...resolved.agent,
110
48
  defaultProvider: typeof providerOverride === "string" && providerOverride.trim().length > 0
@@ -125,7 +63,6 @@ export const readRequiredEnv = (env, key) => {
125
63
  const direct = read(key);
126
64
  if (direct)
127
65
  return direct;
128
- // Backward compatibility: allow *_KEY and *_API_KEY aliases.
129
66
  if (key.endsWith("_API_KEY")) {
130
67
  const alt = read(key.replace(/_API_KEY$/, "_KEY"));
131
68
  if (alt)
@@ -1,4 +1,4 @@
1
- import type { ProviderType, ReturnMode } from "./types.js";
1
+ import type { ProviderType } from "./types.js";
2
2
  export interface EchoPdfProviderConfig {
3
3
  readonly type: ProviderType;
4
4
  readonly apiKeyEnv: string;
@@ -10,29 +10,9 @@ export interface EchoPdfProviderConfig {
10
10
  readonly modelsPath?: string;
11
11
  };
12
12
  }
13
- export interface StoragePolicy {
14
- readonly maxFileBytes: number;
15
- readonly maxTotalBytes: number;
16
- readonly ttlHours: number;
17
- readonly cleanupBatchSize: number;
18
- }
19
13
  export interface EchoPdfConfig {
20
14
  readonly service: {
21
- readonly name: string;
22
- readonly publicBaseUrl?: string;
23
- readonly computeAuth?: {
24
- readonly authHeader?: string;
25
- readonly authEnv?: string;
26
- };
27
- readonly fileGet?: {
28
- readonly authHeader?: string;
29
- readonly authEnv?: string;
30
- readonly cacheTtlSeconds?: number;
31
- };
32
- readonly maxPdfBytes: number;
33
- readonly maxPagesPerRequest: number;
34
15
  readonly defaultRenderScale: number;
35
- readonly storage: StoragePolicy;
36
16
  };
37
17
  readonly pdfium: {
38
18
  readonly wasmUrl: string;
@@ -40,44 +20,7 @@ export interface EchoPdfConfig {
40
20
  readonly agent: {
41
21
  readonly defaultProvider: string;
42
22
  readonly defaultModel: string;
43
- readonly ocrPrompt: string;
44
23
  readonly tablePrompt: string;
45
24
  };
46
25
  readonly providers: Record<string, EchoPdfProviderConfig>;
47
- readonly mcp: {
48
- readonly serverName: string;
49
- readonly version: string;
50
- readonly authHeader?: string;
51
- readonly authEnv?: string;
52
- };
53
- }
54
- export interface AgentTraceEvent {
55
- readonly kind: "step";
56
- readonly phase: "start" | "end" | "log";
57
- readonly name: string;
58
- readonly level?: "info" | "error";
59
- readonly payload?: unknown;
60
- }
61
- export interface PdfOperationRequest {
62
- readonly operation: "extract_pages" | "ocr_pages" | "tables_to_latex";
63
- readonly fileId?: string;
64
- readonly url?: string;
65
- readonly base64?: string;
66
- readonly filename?: string;
67
- readonly pages: ReadonlyArray<number>;
68
- readonly renderScale?: number;
69
- readonly provider?: string;
70
- readonly model: string;
71
- readonly providerApiKeys?: Record<string, string>;
72
- readonly returnMode?: ReturnMode;
73
- readonly prompt?: string;
74
- }
75
- export interface ToolSchema {
76
- readonly name: string;
77
- readonly description: string;
78
- readonly inputSchema: Record<string, unknown>;
79
- readonly source: {
80
- readonly kind: "local";
81
- readonly toolName: string;
82
- };
83
26
  }
package/dist/types.d.ts CHANGED
@@ -1,91 +1,5 @@
1
- export type JsonPrimitive = string | number | boolean | null;
2
- export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
3
- export type JsonArray = JsonValue[];
4
- export interface JsonObject {
5
- [key: string]: JsonValue;
6
- }
7
1
  export type ProviderType = "openai" | "openrouter" | "vercel-ai-gateway";
8
- export type ReturnMode = "inline" | "file_id" | "url";
9
- export interface Fetcher {
10
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
11
- }
12
- export interface DurableObjectStub {
13
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
14
- }
15
- export interface DurableObjectId {
16
- }
17
- export interface DurableObjectNamespace {
18
- idFromName(name: string): DurableObjectId;
19
- get(id: DurableObjectId): DurableObjectStub;
20
- }
21
- export interface DurableObjectStorage {
22
- get<T>(key: string): Promise<T | undefined>;
23
- put<T>(key: string, value: T): Promise<void>;
24
- list<T>(options?: {
25
- prefix?: string;
26
- }): Promise<Map<string, T>>;
27
- delete(key: string): Promise<boolean>;
28
- }
29
- export interface DurableObjectState {
30
- storage: DurableObjectStorage;
31
- }
32
- export interface R2ObjectBody {
33
- key: string;
34
- size: number;
35
- uploaded: Date;
36
- httpMetadata?: {
37
- contentType?: string;
38
- };
39
- customMetadata?: Record<string, string>;
40
- arrayBuffer(): Promise<ArrayBuffer>;
41
- }
42
- export interface R2Bucket {
43
- put(key: string, value: ArrayBuffer | ArrayBufferView, options?: {
44
- httpMetadata?: {
45
- contentType?: string;
46
- };
47
- customMetadata?: Record<string, string>;
48
- }): Promise<unknown>;
49
- get(key: string): Promise<R2ObjectBody | null>;
50
- delete(key: string | string[]): Promise<void>;
51
- list(options?: {
52
- prefix?: string;
53
- limit?: number;
54
- cursor?: string;
55
- }): Promise<{
56
- objects: R2ObjectBody[];
57
- truncated: boolean;
58
- cursor?: string;
59
- }>;
60
- }
61
2
  export interface Env {
62
3
  readonly ECHO_PDF_CONFIG_JSON?: string;
63
- readonly ASSETS?: Fetcher;
64
- readonly FILE_STORE_BUCKET?: R2Bucket;
65
- readonly FILE_STORE_DO?: DurableObjectNamespace;
66
- readonly [key: string]: string | Fetcher | DurableObjectNamespace | R2Bucket | undefined;
67
- }
68
- export interface WorkerExecutionContext {
69
- waitUntil(promise: Promise<unknown>): void;
70
- passThroughOnException?(): void;
71
- }
72
- export interface StoredFileMeta {
73
- readonly id: string;
74
- readonly filename: string;
75
- readonly mimeType: string;
76
- readonly sizeBytes: number;
77
- readonly createdAt: string;
78
- }
79
- export interface StoredFileRecord extends StoredFileMeta {
80
- readonly bytes: Uint8Array;
81
- }
82
- export interface FileStore {
83
- put(input: {
84
- readonly filename: string;
85
- readonly mimeType: string;
86
- readonly bytes: Uint8Array;
87
- }): Promise<StoredFileMeta>;
88
- get(fileId: string): Promise<StoredFileRecord | null>;
89
- list(): Promise<ReadonlyArray<StoredFileMeta>>;
90
- delete(fileId: string): Promise<boolean>;
4
+ readonly [key: string]: string | undefined;
91
5
  }
@@ -1,19 +1,6 @@
1
1
  {
2
2
  "service": {
3
- "name": "echo-pdf",
4
- "publicBaseUrl": "https://echo-pdf.echofilesai.workers.dev",
5
- "fileGet": {
6
- "cacheTtlSeconds": 300
7
- },
8
- "maxPdfBytes": 10000000,
9
- "maxPagesPerRequest": 20,
10
- "defaultRenderScale": 2,
11
- "storage": {
12
- "maxFileBytes": 10000000,
13
- "maxTotalBytes": 52428800,
14
- "ttlHours": 24,
15
- "cleanupBatchSize": 50
16
- }
3
+ "defaultRenderScale": 2
17
4
  },
18
5
  "pdfium": {
19
6
  "wasmUrl": "https://cdn.jsdelivr.net/npm/@embedpdf/pdfium@2.7.0/dist/pdfium.wasm"
@@ -21,7 +8,6 @@
21
8
  "agent": {
22
9
  "defaultProvider": "openai",
23
10
  "defaultModel": "",
24
- "ocrPrompt": "Extract readable text from this PDF page image. Preserve line breaks and section structure.",
25
11
  "tablePrompt": "Detect all tabular structures from this PDF page image. Output only valid LaTeX tabular environments, no explanations, no markdown fences."
26
12
  },
27
13
  "providers": {
@@ -52,11 +38,5 @@
52
38
  "modelsPath": "/models"
53
39
  }
54
40
  }
55
- },
56
- "mcp": {
57
- "serverName": "echo-pdf-mcp",
58
- "version": "0.1.0",
59
- "authHeader": "x-mcp-key",
60
- "authEnv": "ECHO_PDF_MCP_KEY"
61
41
  }
62
42
  }
package/package.json CHANGED
@@ -1,32 +1,39 @@
1
1
  {
2
2
  "name": "@echofiles/echo-pdf",
3
3
  "description": "Local-first PDF document component core with CLI, workspace artifacts, and reusable page primitives.",
4
- "version": "0.4.3",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
+ "homepage": "https://pdf.echofile.ai/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/echofiles/echo-pdf.git"
10
+ },
11
+ "license": "Apache-2.0",
12
+ "keywords": [
13
+ "pdf",
14
+ "local-first",
15
+ "ai-agent",
16
+ "semantic-structure",
17
+ "workspace-artifacts",
18
+ "cli",
19
+ "vision-language"
20
+ ],
6
21
  "publishConfig": {
7
22
  "access": "public"
8
23
  },
9
24
  "bin": {
10
25
  "echo-pdf": "./bin/echo-pdf.js"
11
26
  },
12
- "main": "./dist/index.js",
13
- "types": "./dist/index.d.ts",
27
+ "main": "./dist/local/index.js",
28
+ "types": "./dist/local/index.d.ts",
14
29
  "exports": {
15
30
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js"
18
- },
19
- "./core": {
20
- "types": "./dist/core/index.d.ts",
21
- "import": "./dist/core/index.js"
31
+ "types": "./dist/local/index.d.ts",
32
+ "import": "./dist/local/index.js"
22
33
  },
23
34
  "./local": {
24
35
  "types": "./dist/local/index.d.ts",
25
36
  "import": "./dist/local/index.js"
26
- },
27
- "./worker": {
28
- "types": "./dist/worker.d.ts",
29
- "import": "./dist/worker.js"
30
37
  }
31
38
  },
32
39
  "files": [
@@ -34,15 +41,12 @@
34
41
  "dist",
35
42
  "scripts",
36
43
  "README.md",
37
- "wrangler.toml",
38
44
  "echo-pdf.config.json"
39
45
  ],
40
46
  "scripts": {
41
47
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
42
48
  "check:runtime": "bash ./scripts/check-runtime.sh",
43
- "dev": "wrangler dev",
44
- "deploy": "wrangler deploy",
45
- "document:dev": "ECHO_PDF_SOURCE_DEV=1 bun ./bin/echo-pdf.js",
49
+ "cli:dev": "ECHO_PDF_SOURCE_DEV=1 bun ./bin/echo-pdf.js",
46
50
  "eval": "node ./eval/run-local.mjs",
47
51
  "eval:smoke": "node ./eval/run-local.mjs --suite smoke",
48
52
  "eval:core": "node ./eval/run-local.mjs --suite core",
@@ -51,9 +55,10 @@
51
55
  "eval:fetch-public-samples": "node ./eval/fetch-public-samples.mjs",
52
56
  "typecheck": "npm run check:runtime && tsc --noEmit",
53
57
  "test:unit": "npm run check:runtime && vitest run tests/unit",
58
+ "test:acceptance": "npm run check:runtime && npm run build && vitest run tests/acceptance",
54
59
  "test:import-smoke": "npm run check:runtime && npm run build && vitest run tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
55
- "test:integration": "npm run check:runtime && npm run build && vitest run tests/integration",
56
- "test": "npm run test:unit && npm run test:integration",
60
+ "test:integration": "npm run check:runtime && npm run build && vitest run tests/integration/local-document-cli.integration.test.ts tests/integration/local-document.integration.test.ts tests/integration/local-semantic-structure.integration.test.ts tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
61
+ "test": "npm run test:unit && npm run test:acceptance && npm run test:integration",
57
62
  "smoke": "bash ./scripts/smoke.sh",
58
63
  "prepublishOnly": "npm run build && npm run typecheck && npm run test"
59
64
  },
@@ -61,10 +66,8 @@
61
66
  "node": ">=20.0.0"
62
67
  },
63
68
  "devDependencies": {
64
- "@cloudflare/workers-types": "^4.20260301.0",
65
69
  "typescript": "^5.7.3",
66
- "vitest": "^2.1.9",
67
- "wrangler": "^4.8.0"
70
+ "vitest": "^2.1.9"
68
71
  },
69
72
  "dependencies": {
70
73
  "@cf-wasm/png": "^0.3.2",
package/bin/lib/http.js DELETED
@@ -1,97 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
-
4
- export const postJson = async (url, payload, extraHeaders = {}) => {
5
- const response = await fetch(url, {
6
- method: "POST",
7
- headers: { "Content-Type": "application/json", ...extraHeaders },
8
- body: JSON.stringify(payload),
9
- })
10
- const text = await response.text()
11
- let data
12
- try {
13
- data = JSON.parse(text)
14
- } catch {
15
- data = { raw: text }
16
- }
17
- if (!response.ok) {
18
- throw new Error(`${response.status} ${JSON.stringify(data)}`)
19
- }
20
- return data
21
- }
22
-
23
- export const uploadFile = async (serviceUrl, filePath) => {
24
- const absPath = path.resolve(process.cwd(), filePath)
25
- const bytes = fs.readFileSync(absPath)
26
- const filename = path.basename(absPath)
27
- const form = new FormData()
28
- form.append("file", new Blob([bytes]), filename)
29
- const response = await fetch(`${serviceUrl}/api/files/upload`, { method: "POST", body: form })
30
- const text = await response.text()
31
- let data
32
- try {
33
- data = JSON.parse(text)
34
- } catch {
35
- data = { raw: text }
36
- }
37
- if (!response.ok) {
38
- throw new Error(`${response.status} ${JSON.stringify(data)}`)
39
- }
40
- return data
41
- }
42
-
43
- export const downloadFile = async (serviceUrl, fileId, outputPath) => {
44
- const response = await fetch(`${serviceUrl}/api/files/get?fileId=${encodeURIComponent(fileId)}&download=1`)
45
- if (!response.ok) {
46
- const text = await response.text()
47
- throw new Error(`${response.status} ${text}`)
48
- }
49
- const bytes = Buffer.from(await response.arrayBuffer())
50
- const absOut = path.resolve(process.cwd(), outputPath)
51
- fs.mkdirSync(path.dirname(absOut), { recursive: true })
52
- fs.writeFileSync(absOut, bytes)
53
- return absOut
54
- }
55
-
56
- const parseAutoUploadFlag = (value) => {
57
- if (value === true) return true
58
- if (typeof value === "string") {
59
- const normalized = value.trim().toLowerCase()
60
- return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"
61
- }
62
- return false
63
- }
64
-
65
- export const prepareArgsWithLocalUploads = async (serviceUrl, tool, args, options = {}) => {
66
- const nextArgs = { ...(args || {}) }
67
- const uploads = []
68
- const autoUploadEnabled = options.autoUpload !== false
69
- if (tool.startsWith("pdf_")) {
70
- const localPath = typeof nextArgs.path === "string"
71
- ? nextArgs.path
72
- : (typeof nextArgs.filePath === "string" ? nextArgs.filePath : "")
73
- if (localPath && !nextArgs.fileId && !nextArgs.url && !nextArgs.base64) {
74
- if (!autoUploadEnabled) {
75
- throw new Error(
76
- "Local file auto-upload is disabled for `echo-pdf call`. " +
77
- "Use --auto-upload, or upload first (`echo-pdf file upload`) and pass fileId, or use `echo-pdf mcp-stdio`."
78
- )
79
- }
80
- const upload = await uploadFile(serviceUrl, localPath)
81
- const fileId = upload?.file?.id
82
- if (!fileId) throw new Error(`upload failed for local path: ${localPath}`)
83
- nextArgs.fileId = fileId
84
- delete nextArgs.path
85
- delete nextArgs.filePath
86
- uploads.push({ tool, localPath, fileId })
87
- }
88
- }
89
- return { args: nextArgs, uploads }
90
- }
91
-
92
- export const withUploadedLocalFile = async (serviceUrl, tool, args, options = {}) => {
93
- const { args: nextArgs } = await prepareArgsWithLocalUploads(serviceUrl, tool, args, {
94
- autoUpload: parseAutoUploadFlag(options.autoUpload ?? true),
95
- })
96
- return nextArgs
97
- }
@@ -1,99 +0,0 @@
1
- const mcpReadLoop = (onMessage, onError) => {
2
- let buffer = Buffer.alloc(0)
3
- let expectedLength = null
4
- process.stdin.on("data", (chunk) => {
5
- buffer = Buffer.concat([buffer, chunk])
6
- while (true) {
7
- if (expectedLength === null) {
8
- const headerEnd = buffer.indexOf("\r\n\r\n")
9
- if (headerEnd === -1) break
10
- const headerRaw = buffer.slice(0, headerEnd).toString("utf-8")
11
- const lines = headerRaw.split("\r\n")
12
- const cl = lines.find((line) => line.toLowerCase().startsWith("content-length:"))
13
- if (!cl) {
14
- onError(new Error("Missing Content-Length"))
15
- buffer = buffer.slice(headerEnd + 4)
16
- continue
17
- }
18
- expectedLength = Number(cl.split(":")[1]?.trim() || "0")
19
- buffer = buffer.slice(headerEnd + 4)
20
- }
21
- if (!Number.isFinite(expectedLength) || expectedLength < 0) {
22
- onError(new Error("Invalid Content-Length"))
23
- expectedLength = null
24
- continue
25
- }
26
- if (buffer.length < expectedLength) break
27
- const body = buffer.slice(0, expectedLength).toString("utf-8")
28
- buffer = buffer.slice(expectedLength)
29
- expectedLength = null
30
- try {
31
- const maybePromise = onMessage(JSON.parse(body))
32
- if (maybePromise && typeof maybePromise.then === "function") {
33
- maybePromise.catch(onError)
34
- }
35
- } catch (error) {
36
- onError(error)
37
- }
38
- }
39
- })
40
- }
41
-
42
- const mcpWrite = (obj) => {
43
- const body = Buffer.from(JSON.stringify(obj))
44
- const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`)
45
- process.stdout.write(header)
46
- process.stdout.write(body)
47
- }
48
-
49
- export const runMcpStdio = async (deps) => {
50
- const {
51
- serviceUrl,
52
- headers,
53
- postJson,
54
- withUploadedLocalFile,
55
- } = deps
56
- mcpReadLoop(async (msg) => {
57
- const method = msg?.method
58
- const id = Object.hasOwn(msg || {}, "id") ? msg.id : null
59
- if (msg?.jsonrpc !== "2.0" || typeof method !== "string") {
60
- mcpWrite({ jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } })
61
- return
62
- }
63
- if (method === "notifications/initialized") return
64
- if (method === "initialize" || method === "tools/list") {
65
- const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
66
- mcpWrite(data)
67
- return
68
- }
69
- if (method === "tools/call") {
70
- try {
71
- const tool = String(msg?.params?.name || "")
72
- const args = (msg?.params?.arguments && typeof msg.params.arguments === "object")
73
- ? msg.params.arguments
74
- : {}
75
- const preparedArgs = await withUploadedLocalFile(serviceUrl, tool, args)
76
- const payload = {
77
- ...msg,
78
- params: {
79
- ...(msg.params || {}),
80
- arguments: preparedArgs,
81
- },
82
- }
83
- const data = await postJson(`${serviceUrl}/mcp`, payload, headers)
84
- mcpWrite(data)
85
- } catch (error) {
86
- mcpWrite({
87
- jsonrpc: "2.0",
88
- id,
89
- error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
90
- })
91
- }
92
- return
93
- }
94
- const data = await postJson(`${serviceUrl}/mcp`, msg, headers)
95
- mcpWrite(data)
96
- }, (error) => {
97
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
98
- })
99
- }
package/dist/auth.d.ts DELETED
@@ -1,18 +0,0 @@
1
- import type { Env } from "./types.js";
2
- export interface AuthCheckOptions {
3
- readonly authHeader?: string;
4
- readonly authEnv?: string;
5
- readonly allowMissingSecret?: boolean;
6
- readonly misconfiguredCode: string;
7
- readonly unauthorizedCode: string;
8
- readonly contextName: string;
9
- }
10
- export type AuthCheckResult = {
11
- readonly ok: true;
12
- } | {
13
- readonly ok: false;
14
- readonly status: number;
15
- readonly code: string;
16
- readonly message: string;
17
- };
18
- export declare const checkHeaderAuth: (request: Request, env: Env, options: AuthCheckOptions) => AuthCheckResult;