somark-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +156 -0
- package/README.md +32 -0
- package/README_CN.md +31 -0
- package/dist/chunk-RWOUZTFQ.mjs +236 -0
- package/dist/cli.mjs +1503 -0
- package/dist/config-GOZJZCRT.mjs +26 -0
- package/dist/index.cjs +1304 -0
- package/dist/index.d.cts +258 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.mjs +1245 -0
- package/package.json +66 -0
- package/vendor/somarkdown-viewer/LICENSE +21 -0
- package/vendor/somarkdown-viewer/README.md +63 -0
- package/vendor/somarkdown-viewer/README_CN.md +63 -0
- package/vendor/somarkdown-viewer/index.html +112 -0
- package/vendor/somarkdown-viewer/lib/bi-direction-jump.js +161 -0
- package/vendor/somarkdown-viewer/lib/deps/highlight.js.default.min.css +11 -0
- package/vendor/somarkdown-viewer/lib/deps/katex.min.css +6 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/somarkdown-viewer/lib/deps/katex_fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/somarkdown-viewer/lib/i18n.js +81 -0
- package/vendor/somarkdown-viewer/lib/index.js +299 -0
- package/vendor/somarkdown-viewer/lib/somarkdown/VERSION +1 -0
- package/vendor/somarkdown-viewer/lib/somarkdown/somarkdown.css +591 -0
- package/vendor/somarkdown-viewer/lib/somarkdown/somarkdown.umd.min.js +59 -0
- package/vendor/somarkdown-viewer/lib/sync-scroll.js +147 -0
- package/vendor/somarkdown-viewer/style/index.css +527 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getConfigValue,
|
|
4
|
+
listResolvedConfig,
|
|
5
|
+
maskConfigValue,
|
|
6
|
+
resolveConfig,
|
|
7
|
+
saveConfig
|
|
8
|
+
} from "./chunk-RWOUZTFQ.mjs";
|
|
9
|
+
|
|
10
|
+
// src/cli/index.ts
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
|
|
13
|
+
// package.json
|
|
14
|
+
var package_default = {
|
|
15
|
+
name: "somark-js",
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
description: "SoMark SDK + CLI \u2014 Document parsing, PDF processing, and SoMarkDown preview",
|
|
18
|
+
type: "module",
|
|
19
|
+
main: "./dist/index.cjs",
|
|
20
|
+
module: "./dist/index.mjs",
|
|
21
|
+
types: "./dist/index.d.ts",
|
|
22
|
+
files: [
|
|
23
|
+
"dist",
|
|
24
|
+
"vendor/somarkdown-viewer/index.html",
|
|
25
|
+
"vendor/somarkdown-viewer/lib",
|
|
26
|
+
"vendor/somarkdown-viewer/style",
|
|
27
|
+
"README.md",
|
|
28
|
+
"README_CN.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
exports: {
|
|
32
|
+
".": {
|
|
33
|
+
types: "./dist/index.d.ts",
|
|
34
|
+
import: "./dist/index.mjs",
|
|
35
|
+
require: "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
bin: {
|
|
39
|
+
somark: "./dist/cli.mjs"
|
|
40
|
+
},
|
|
41
|
+
engines: {
|
|
42
|
+
node: ">=18"
|
|
43
|
+
},
|
|
44
|
+
scripts: {
|
|
45
|
+
build: "tsup",
|
|
46
|
+
test: "npm run build && vitest run",
|
|
47
|
+
"test:watch": "vitest"
|
|
48
|
+
},
|
|
49
|
+
keywords: [
|
|
50
|
+
"somark",
|
|
51
|
+
"pdf",
|
|
52
|
+
"document",
|
|
53
|
+
"parsing",
|
|
54
|
+
"cli",
|
|
55
|
+
"sdk"
|
|
56
|
+
],
|
|
57
|
+
author: "SoMark",
|
|
58
|
+
license: "Apache-2.0",
|
|
59
|
+
homepage: "https://somark.tech",
|
|
60
|
+
repository: {
|
|
61
|
+
type: "git",
|
|
62
|
+
url: "https://github.com/SoMarkAI/somark-js"
|
|
63
|
+
},
|
|
64
|
+
dependencies: {
|
|
65
|
+
"@hyzyla/pdfium": "^2.1.12",
|
|
66
|
+
"@types/pngjs": "^6.0.5",
|
|
67
|
+
chalk: "^5.3.0",
|
|
68
|
+
commander: "^12.0.0",
|
|
69
|
+
open: "^10.1.0",
|
|
70
|
+
ora: "^8.1.0",
|
|
71
|
+
pngjs: "^7.0.0"
|
|
72
|
+
},
|
|
73
|
+
devDependencies: {
|
|
74
|
+
"@types/node": "^20.0.0",
|
|
75
|
+
tsup: "^8.0.0",
|
|
76
|
+
typescript: "^5.4.0",
|
|
77
|
+
vitest: "^1.6.0"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/cli/commands/parse.ts
|
|
82
|
+
import chalk from "chalk";
|
|
83
|
+
import ora from "ora";
|
|
84
|
+
|
|
85
|
+
// src/errors.ts
|
|
86
|
+
var SoMarkError = class extends Error {
|
|
87
|
+
code;
|
|
88
|
+
constructor(message = "", code = -1) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "SoMarkError";
|
|
91
|
+
this.code = code;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var AuthenticationError = class extends SoMarkError {
|
|
95
|
+
constructor(message = "Authentication failed", code = -1) {
|
|
96
|
+
super(message, code);
|
|
97
|
+
this.name = "AuthenticationError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var InsufficientBalanceError = class extends SoMarkError {
|
|
101
|
+
constructor(message = "Insufficient balance", code = -1) {
|
|
102
|
+
super(message, code);
|
|
103
|
+
this.name = "InsufficientBalanceError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var RateLimitError = class extends SoMarkError {
|
|
107
|
+
constructor(message = "Rate limit exceeded", code = -1) {
|
|
108
|
+
super(message, code);
|
|
109
|
+
this.name = "RateLimitError";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var UnsupportedFileError = class extends SoMarkError {
|
|
113
|
+
constructor(message = "Unsupported file type", code = -1) {
|
|
114
|
+
super(message, code);
|
|
115
|
+
this.name = "UnsupportedFileError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var FileTooLargeError = class extends SoMarkError {
|
|
119
|
+
constructor(message = "File too large", code = -1) {
|
|
120
|
+
super(message, code);
|
|
121
|
+
this.name = "FileTooLargeError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var ParseTimeoutError = class extends SoMarkError {
|
|
125
|
+
constructor(message = "Parse timed out", code = -1) {
|
|
126
|
+
super(message, code);
|
|
127
|
+
this.name = "ParseTimeoutError";
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var ParseFailedError = class extends SoMarkError {
|
|
131
|
+
constructor(message = "Parse failed", code = -1) {
|
|
132
|
+
super(message, code);
|
|
133
|
+
this.name = "ParseFailedError";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var InvalidParamError = class extends SoMarkError {
|
|
137
|
+
constructor(message = "Invalid parameter", code = -1) {
|
|
138
|
+
super(message, code);
|
|
139
|
+
this.name = "InvalidParamError";
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var APIError = class extends SoMarkError {
|
|
143
|
+
constructor(message = "API error", code = -1) {
|
|
144
|
+
super(message, code);
|
|
145
|
+
this.name = "APIError";
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var ConnectionError = class extends SoMarkError {
|
|
149
|
+
constructor(message = "Connection failed", code = -1) {
|
|
150
|
+
super(message, code);
|
|
151
|
+
this.name = "ConnectionError";
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var RequestTimeoutError = class extends SoMarkError {
|
|
155
|
+
constructor(message = "Request timed out", code = -1) {
|
|
156
|
+
super(message, code);
|
|
157
|
+
this.name = "RequestTimeoutError";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var CODE_TO_ERROR = {
|
|
161
|
+
1101: RateLimitError,
|
|
162
|
+
1102: ParseTimeoutError,
|
|
163
|
+
1104: UnsupportedFileError,
|
|
164
|
+
1105: InvalidParamError,
|
|
165
|
+
1106: InvalidParamError,
|
|
166
|
+
1107: AuthenticationError,
|
|
167
|
+
1108: InsufficientBalanceError,
|
|
168
|
+
1111: APIError,
|
|
169
|
+
1118: FileTooLargeError,
|
|
170
|
+
1120: APIError,
|
|
171
|
+
1123: InsufficientBalanceError,
|
|
172
|
+
1124: RateLimitError,
|
|
173
|
+
1132: FileTooLargeError,
|
|
174
|
+
1133: InvalidParamError,
|
|
175
|
+
1137: ParseFailedError,
|
|
176
|
+
1138: APIError
|
|
177
|
+
};
|
|
178
|
+
function raiseForCode(code, message = "") {
|
|
179
|
+
const ErrorClass = CODE_TO_ERROR[code] ?? APIError;
|
|
180
|
+
throw new ErrorClass(message || `API error (code=${code})`, code);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/http.ts
|
|
184
|
+
var RETRY_CODES = /* @__PURE__ */ new Set([1101, 1124]);
|
|
185
|
+
var RETRY_HTTP_STATUSES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
|
|
186
|
+
var HttpClient = class {
|
|
187
|
+
apiKey;
|
|
188
|
+
baseUrl;
|
|
189
|
+
timeoutMs;
|
|
190
|
+
maxRetries;
|
|
191
|
+
constructor(opts) {
|
|
192
|
+
this.apiKey = opts.apiKey;
|
|
193
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
194
|
+
this.timeoutMs = opts.timeout * 1e3;
|
|
195
|
+
this.maxRetries = opts.maxRetries;
|
|
196
|
+
}
|
|
197
|
+
getApiKey() {
|
|
198
|
+
return this.apiKey;
|
|
199
|
+
}
|
|
200
|
+
buildFormData(data) {
|
|
201
|
+
const form = new FormData();
|
|
202
|
+
if (this.apiKey) form.append("api_key", this.apiKey);
|
|
203
|
+
for (const [k, v] of Object.entries(data)) {
|
|
204
|
+
if (v === void 0) continue;
|
|
205
|
+
if (Array.isArray(v)) {
|
|
206
|
+
for (const item of v) form.append(k, item);
|
|
207
|
+
} else {
|
|
208
|
+
form.append(k, v);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return form;
|
|
212
|
+
}
|
|
213
|
+
async post(path7, data = {}, file) {
|
|
214
|
+
const url = this.baseUrl + path7;
|
|
215
|
+
let lastError;
|
|
216
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
const form = this.buildFormData(data);
|
|
219
|
+
if (file) {
|
|
220
|
+
const blob = new Blob([file.buffer], { type: file.mimeType });
|
|
221
|
+
form.append("file", blob, file.name);
|
|
222
|
+
}
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
225
|
+
let response;
|
|
226
|
+
try {
|
|
227
|
+
response = await fetch(url, { method: "POST", body: form, signal: controller.signal });
|
|
228
|
+
} finally {
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
}
|
|
231
|
+
if (RETRY_HTTP_STATUSES.has(response.status) && attempt < this.maxRetries) {
|
|
232
|
+
await sleep(Math.pow(2, attempt) * 1e3);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new APIError(`HTTP ${response.status}: ${response.statusText}`);
|
|
237
|
+
}
|
|
238
|
+
const result = await response.json();
|
|
239
|
+
const code = result.code ?? -1;
|
|
240
|
+
if (code !== 0) {
|
|
241
|
+
if (RETRY_CODES.has(code) && attempt < this.maxRetries) {
|
|
242
|
+
await sleep(Math.pow(2, attempt) * 1e3);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
raiseForCode(code, (result.message ?? result.msg) || "");
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
250
|
+
lastError = new RequestTimeoutError("Request timed out");
|
|
251
|
+
} else if (err instanceof TypeError && err.message.includes("fetch")) {
|
|
252
|
+
lastError = new ConnectionError(err.message);
|
|
253
|
+
} else if (err instanceof Error && err.code === "ECONNREFUSED") {
|
|
254
|
+
lastError = new ConnectionError(err.message);
|
|
255
|
+
} else {
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
if (attempt < this.maxRetries) await sleep(Math.pow(2, attempt) * 1e3);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw lastError ?? new APIError("Request failed after retries");
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
function sleep(ms) {
|
|
265
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/resources/parser.ts
|
|
269
|
+
import * as fs3 from "fs";
|
|
270
|
+
import * as path3 from "path";
|
|
271
|
+
|
|
272
|
+
// src/models.ts
|
|
273
|
+
import * as fs from "fs";
|
|
274
|
+
import * as path from "path";
|
|
275
|
+
|
|
276
|
+
// src/warnings.ts
|
|
277
|
+
var warningsSuppressed = false;
|
|
278
|
+
function dedupe(messages) {
|
|
279
|
+
const seen = /* @__PURE__ */ new Set();
|
|
280
|
+
const result = [];
|
|
281
|
+
for (const message of messages) {
|
|
282
|
+
if (seen.has(message)) continue;
|
|
283
|
+
seen.add(message);
|
|
284
|
+
result.push(message);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
function setWarningsSuppressed(suppressed) {
|
|
289
|
+
warningsSuppressed = suppressed;
|
|
290
|
+
}
|
|
291
|
+
function collectApiWarnings(data) {
|
|
292
|
+
const raw = data.warnings;
|
|
293
|
+
if (!Array.isArray(raw)) return [];
|
|
294
|
+
return dedupe(raw.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean));
|
|
295
|
+
}
|
|
296
|
+
function emitWarnings(warnings) {
|
|
297
|
+
if (warningsSuppressed) return;
|
|
298
|
+
for (const warning of dedupe(warnings)) {
|
|
299
|
+
process.emitWarning(warning, { type: "SoMarkWarning" });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function parseMaxConcurrency(raw) {
|
|
303
|
+
const value = raw?.trim() ?? "";
|
|
304
|
+
if (!value) return { value: 1, warnings: [] };
|
|
305
|
+
const parsed = Number.parseInt(value, 10);
|
|
306
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
307
|
+
return {
|
|
308
|
+
value: 1,
|
|
309
|
+
warnings: [`Invalid SOMARK_PARSE_MAX_CONCURRENCY="${value}"; using 1.`]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (parsed >= 2) {
|
|
313
|
+
return {
|
|
314
|
+
value: parsed,
|
|
315
|
+
warnings: [
|
|
316
|
+
`SOMARK_PARSE_MAX_CONCURRENCY is set to ${parsed}. The official default quota is 1 for everyone; only users explicitly approved for higher concurrency should use 2 or above.`
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return { value: parsed, warnings: [] };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/models.ts
|
|
324
|
+
var ParseResponse = class _ParseResponse {
|
|
325
|
+
ok;
|
|
326
|
+
code;
|
|
327
|
+
message;
|
|
328
|
+
warnings;
|
|
329
|
+
taskId;
|
|
330
|
+
pages;
|
|
331
|
+
fileType;
|
|
332
|
+
imgs;
|
|
333
|
+
md;
|
|
334
|
+
jsonOutput;
|
|
335
|
+
zipUrl;
|
|
336
|
+
sourceFileName;
|
|
337
|
+
constructor(data) {
|
|
338
|
+
this.ok = data.ok;
|
|
339
|
+
this.code = data.code;
|
|
340
|
+
this.message = data.message;
|
|
341
|
+
this.warnings = data.warnings ?? [];
|
|
342
|
+
this.taskId = data.taskId;
|
|
343
|
+
this.pages = data.pages;
|
|
344
|
+
this.fileType = data.fileType;
|
|
345
|
+
this.imgs = data.imgs;
|
|
346
|
+
this.md = data.md;
|
|
347
|
+
this.jsonOutput = data.jsonOutput;
|
|
348
|
+
this.zipUrl = data.zipUrl;
|
|
349
|
+
this.sourceFileName = data.sourceFileName;
|
|
350
|
+
}
|
|
351
|
+
async save(targetPath, format) {
|
|
352
|
+
const plan = planOutput(this.sourceFileName, targetPath, format, { allowStdout: false });
|
|
353
|
+
for (const target of plan.targets) {
|
|
354
|
+
if (target.stdout || !target.path) throw new Error("save path is required");
|
|
355
|
+
await this.saveFormat(target.path, target.format);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
contentForFormat(format) {
|
|
359
|
+
const fmt = normalizeOutputFormat(format);
|
|
360
|
+
if (fmt === "json") return JSON.stringify(this.jsonOutput ?? {}, null, 2);
|
|
361
|
+
if (fmt === "md") return this.md ?? "";
|
|
362
|
+
if (fmt === "zip") {
|
|
363
|
+
if (!this.zipUrl) throw new Error("ZIP output URL was not returned by the API.");
|
|
364
|
+
return this.zipUrl;
|
|
365
|
+
}
|
|
366
|
+
throw new Error(`Unsupported output format: ${fmt}. Valid formats: md, markdown, json, zip.`);
|
|
367
|
+
}
|
|
368
|
+
async saveFormat(targetPath, format) {
|
|
369
|
+
const fmt = normalizeOutputFormat(format);
|
|
370
|
+
if (fmt === "zip") {
|
|
371
|
+
if (!this.zipUrl) throw new Error("ZIP output URL was not returned by the API.");
|
|
372
|
+
await downloadOutputFile(targetPath, this.zipUrl);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
fs.writeFileSync(targetPath, this.contentForFormat(fmt), "utf-8");
|
|
376
|
+
}
|
|
377
|
+
toJSON() {
|
|
378
|
+
return {
|
|
379
|
+
ok: this.ok,
|
|
380
|
+
code: this.code,
|
|
381
|
+
message: this.message,
|
|
382
|
+
warnings: this.warnings,
|
|
383
|
+
taskId: this.taskId,
|
|
384
|
+
pages: this.pages,
|
|
385
|
+
fileType: this.fileType,
|
|
386
|
+
imgs: this.imgs,
|
|
387
|
+
md: this.md,
|
|
388
|
+
jsonOutput: this.jsonOutput,
|
|
389
|
+
zipUrl: this.zipUrl,
|
|
390
|
+
sourceFileName: this.sourceFileName
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
static fromApi(data) {
|
|
394
|
+
const apiData = data.data ?? {};
|
|
395
|
+
const metadata = apiData.metadata ?? {};
|
|
396
|
+
const result = apiData.result ?? {};
|
|
397
|
+
const outputs = result.outputs ?? {};
|
|
398
|
+
const warnings = collectApiWarnings(data);
|
|
399
|
+
return new _ParseResponse({
|
|
400
|
+
ok: data.code === 0,
|
|
401
|
+
code: data.code ?? -1,
|
|
402
|
+
message: data.message ?? data.msg ?? "",
|
|
403
|
+
warnings,
|
|
404
|
+
taskId: apiData.task_id ?? "",
|
|
405
|
+
pages: metadata.page_num ?? 0,
|
|
406
|
+
fileType: metadata.file_type ?? "",
|
|
407
|
+
imgs: result.imgs ?? [],
|
|
408
|
+
md: outputs.markdown,
|
|
409
|
+
jsonOutput: outputs.json,
|
|
410
|
+
zipUrl: outputs.zip,
|
|
411
|
+
sourceFileName: result.file_name ?? apiData.file_name
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
function normalizeOutputFormat(format) {
|
|
416
|
+
const fmt = format.trim().toLowerCase();
|
|
417
|
+
return { markdown: "md" }[fmt] ?? fmt;
|
|
418
|
+
}
|
|
419
|
+
function normalizeSaveFormats(format, targetPath) {
|
|
420
|
+
const rawFormats = Array.isArray(format) ? format : typeof format === "string" ? format.split(",").map((item) => item.trim()) : [path.extname(targetPath ?? "").replace(/^\./, "") || "md"];
|
|
421
|
+
const formats = rawFormats.map(normalizeOutputFormat).filter(Boolean);
|
|
422
|
+
const normalized = formats.length ? formats : ["md"];
|
|
423
|
+
const invalid = normalized.filter((fmt) => !["md", "json", "zip"].includes(fmt));
|
|
424
|
+
if (invalid.length) {
|
|
425
|
+
throw new Error(`Unsupported output format: ${invalid.join(", ")}. Valid formats: md, markdown, json, zip.`);
|
|
426
|
+
}
|
|
427
|
+
return normalized;
|
|
428
|
+
}
|
|
429
|
+
function planOutput(sourceFileName, out, format, opts = {}) {
|
|
430
|
+
const formats = normalizeSaveFormats(format, out);
|
|
431
|
+
if (opts.batch) {
|
|
432
|
+
if (!out || !isExistingDirectory(out)) {
|
|
433
|
+
throw new Error("batch parse requires --out to be an existing directory");
|
|
434
|
+
}
|
|
435
|
+
return directoryOutputPlan(sourceFileName, out, formats);
|
|
436
|
+
}
|
|
437
|
+
if (!out) {
|
|
438
|
+
if (formats.length > 1) throw new Error("multiple formats require --out to be an existing directory");
|
|
439
|
+
if (!opts.allowStdout) throw new Error("output path is required");
|
|
440
|
+
return { formats, targets: [{ format: formats[0], stdout: true }], stdout: true };
|
|
441
|
+
}
|
|
442
|
+
if (isExistingDirectory(out)) return directoryOutputPlan(sourceFileName, out, formats);
|
|
443
|
+
if (formats.length > 1) throw new Error("multiple formats require --out to be an existing directory");
|
|
444
|
+
if (looksLikeDirectoryPath(out)) throw new Error(`output directory does not exist: ${out}`);
|
|
445
|
+
const parent = path.dirname(path.resolve(out));
|
|
446
|
+
if (!isExistingDirectory(parent)) throw new Error(`output parent directory does not exist: ${parent}`);
|
|
447
|
+
return { formats, targets: [{ format: formats[0], path: out, stdout: false }], stdout: false };
|
|
448
|
+
}
|
|
449
|
+
function directoryOutputPlan(sourceFileName, directory, formats) {
|
|
450
|
+
const stem = sourceStem(sourceFileName);
|
|
451
|
+
if (!stem) throw new Error("saving to a directory requires a source file name");
|
|
452
|
+
return {
|
|
453
|
+
formats,
|
|
454
|
+
targets: formats.map((fmt) => ({
|
|
455
|
+
format: fmt,
|
|
456
|
+
path: path.join(directory, `${stem}.${formatExt(fmt)}`),
|
|
457
|
+
stdout: false
|
|
458
|
+
})),
|
|
459
|
+
stdout: false
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function formatExt(format) {
|
|
463
|
+
return normalizeOutputFormat(format) === "md" ? "md" : normalizeOutputFormat(format);
|
|
464
|
+
}
|
|
465
|
+
function sourceStem(fileName) {
|
|
466
|
+
if (!fileName) return void 0;
|
|
467
|
+
return path.basename(fileName, path.extname(fileName));
|
|
468
|
+
}
|
|
469
|
+
function isExistingDirectory(target) {
|
|
470
|
+
return fs.existsSync(target) && fs.statSync(target).isDirectory();
|
|
471
|
+
}
|
|
472
|
+
function looksLikeDirectoryPath(target) {
|
|
473
|
+
return target.endsWith(path.sep) || target.endsWith("/");
|
|
474
|
+
}
|
|
475
|
+
async function downloadOutputFile(targetPath, url) {
|
|
476
|
+
let response;
|
|
477
|
+
try {
|
|
478
|
+
response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(6e4) });
|
|
479
|
+
} catch (err) {
|
|
480
|
+
throw new Error(`failed to download ZIP output: ${err.message}`);
|
|
481
|
+
}
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
throw new Error(`failed to download ZIP output: HTTP ${response.status}: ${response.statusText}`);
|
|
484
|
+
}
|
|
485
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
486
|
+
fs.writeFileSync(targetPath, bytes);
|
|
487
|
+
}
|
|
488
|
+
var Usage = class _Usage {
|
|
489
|
+
ok;
|
|
490
|
+
code;
|
|
491
|
+
message;
|
|
492
|
+
warnings;
|
|
493
|
+
remainingPaidPages;
|
|
494
|
+
remainingFreePagesToday;
|
|
495
|
+
remainingFreePagesThisMonth;
|
|
496
|
+
dashboardUrl;
|
|
497
|
+
constructor(data) {
|
|
498
|
+
this.ok = data.ok;
|
|
499
|
+
this.code = data.code;
|
|
500
|
+
this.message = data.message;
|
|
501
|
+
this.warnings = data.warnings ?? [];
|
|
502
|
+
this.remainingPaidPages = data.remainingPaidPages;
|
|
503
|
+
this.remainingFreePagesToday = data.remainingFreePagesToday;
|
|
504
|
+
this.remainingFreePagesThisMonth = data.remainingFreePagesThisMonth;
|
|
505
|
+
this.dashboardUrl = data.dashboardUrl;
|
|
506
|
+
}
|
|
507
|
+
toJSON() {
|
|
508
|
+
return {
|
|
509
|
+
ok: this.ok,
|
|
510
|
+
code: this.code,
|
|
511
|
+
message: this.message,
|
|
512
|
+
warnings: this.warnings,
|
|
513
|
+
remainingPaidPages: this.remainingPaidPages,
|
|
514
|
+
remainingFreePagesToday: this.remainingFreePagesToday,
|
|
515
|
+
remainingFreePagesThisMonth: this.remainingFreePagesThisMonth,
|
|
516
|
+
dashboardUrl: this.dashboardUrl
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
static fromApi(data) {
|
|
520
|
+
const apiData = data.data ?? {};
|
|
521
|
+
const warnings = collectApiWarnings(data);
|
|
522
|
+
return new _Usage({
|
|
523
|
+
ok: data.code === 0,
|
|
524
|
+
code: data.code ?? -1,
|
|
525
|
+
message: data.message ?? data.msg ?? "",
|
|
526
|
+
warnings,
|
|
527
|
+
remainingPaidPages: apiData.remaining_paid_pages ?? 0,
|
|
528
|
+
remainingFreePagesToday: apiData.remaining_free_pages_today ?? 0,
|
|
529
|
+
remainingFreePagesThisMonth: apiData.remaining_free_pages_this_month ?? 0,
|
|
530
|
+
dashboardUrl: apiData.dashboard_url ?? "https://somark.tech/workbench"
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
var Task = class {
|
|
535
|
+
id;
|
|
536
|
+
completed = false;
|
|
537
|
+
status = "queuing";
|
|
538
|
+
recordId;
|
|
539
|
+
fileName;
|
|
540
|
+
pages;
|
|
541
|
+
warnings;
|
|
542
|
+
refreshHandler;
|
|
543
|
+
_result;
|
|
544
|
+
constructor(taskId, refreshHandler, warnings = []) {
|
|
545
|
+
this.id = taskId;
|
|
546
|
+
this.refreshHandler = refreshHandler;
|
|
547
|
+
this.warnings = warnings;
|
|
548
|
+
}
|
|
549
|
+
async refresh() {
|
|
550
|
+
if (!this.refreshHandler) {
|
|
551
|
+
throw new Error("Task cannot refresh without a parser resource. Use client.parser.task(taskId).");
|
|
552
|
+
}
|
|
553
|
+
const data = await this.refreshHandler(this.id);
|
|
554
|
+
this.applyApi(data);
|
|
555
|
+
}
|
|
556
|
+
applyApi(data) {
|
|
557
|
+
this.warnings = collectApiWarnings(data);
|
|
558
|
+
const apiData = data.data ?? {};
|
|
559
|
+
const rawStatus = apiData.status ?? this.status;
|
|
560
|
+
this.status = rawStatus.toLowerCase();
|
|
561
|
+
this.recordId = apiData.record_id;
|
|
562
|
+
this.fileName = apiData.file_name;
|
|
563
|
+
const metadata = apiData.metadata ?? {};
|
|
564
|
+
this.pages = metadata.page_num;
|
|
565
|
+
if (this.status === "success" || this.status === "failed") {
|
|
566
|
+
this.completed = true;
|
|
567
|
+
if (this.status === "success") {
|
|
568
|
+
this._result = ParseResponse.fromApi(data);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async wait(opts = {}) {
|
|
573
|
+
const pollInterval = (opts.pollInterval ?? 3) * 1e3;
|
|
574
|
+
const timeout = (opts.timeout ?? 120) * 1e3;
|
|
575
|
+
const start = Date.now();
|
|
576
|
+
while (!this.completed) {
|
|
577
|
+
if (Date.now() - start >= timeout) {
|
|
578
|
+
throw new ParseTimeoutError(`Task ${this.id} timed out`);
|
|
579
|
+
}
|
|
580
|
+
await sleep2(pollInterval);
|
|
581
|
+
await this.refresh();
|
|
582
|
+
}
|
|
583
|
+
if (this.status === "failed") throw new ParseFailedError(`Task ${this.id} failed`);
|
|
584
|
+
return this._result;
|
|
585
|
+
}
|
|
586
|
+
result() {
|
|
587
|
+
if (!this._result) throw new Error("Task not completed. Call wait() first.");
|
|
588
|
+
return this._result;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
function sleep2(ms) {
|
|
592
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/resources/parse-core.ts
|
|
596
|
+
import * as fs2 from "fs";
|
|
597
|
+
import * as path2 from "path";
|
|
598
|
+
var FMT_MAP = { md: "markdown", zip: "zip", json: "json" };
|
|
599
|
+
var VALID_API_FORMATS = /* @__PURE__ */ new Set(["markdown", "json", "zip"]);
|
|
600
|
+
var VALID_FORMAT_HINT = "md, markdown, json, zip";
|
|
601
|
+
function buildParseData(opts) {
|
|
602
|
+
const data = {};
|
|
603
|
+
if (opts.formats?.length) {
|
|
604
|
+
const mapped = opts.formats.map((f) => {
|
|
605
|
+
const normalized = normalizeOutputFormat(f);
|
|
606
|
+
return FMT_MAP[normalized] ?? normalized;
|
|
607
|
+
});
|
|
608
|
+
const invalid = mapped.filter((f) => !VALID_API_FORMATS.has(f));
|
|
609
|
+
if (invalid.length) {
|
|
610
|
+
throw new InvalidParamError(`Unsupported output format: ${invalid.join(", ")}. Valid formats: ${VALID_FORMAT_HINT}.`);
|
|
611
|
+
}
|
|
612
|
+
data.output_formats = mapped;
|
|
613
|
+
}
|
|
614
|
+
if (opts.elementFormats) {
|
|
615
|
+
const ef = opts.elementFormats;
|
|
616
|
+
const efObj = {};
|
|
617
|
+
if (ef.image) efObj.image = ef.image;
|
|
618
|
+
if (ef.formula) efObj.formula = ef.formula;
|
|
619
|
+
if (ef.table) efObj.table = ef.table;
|
|
620
|
+
if (ef.cs) efObj.cs = ef.cs;
|
|
621
|
+
if (Object.keys(efObj).length) data.element_formats = JSON.stringify(efObj);
|
|
622
|
+
}
|
|
623
|
+
if (opts.extractConfig) {
|
|
624
|
+
const ec = opts.extractConfig;
|
|
625
|
+
const fcObj = {};
|
|
626
|
+
if (ec.enableTextCrossPage !== void 0) fcObj.enable_text_cross_page = ec.enableTextCrossPage;
|
|
627
|
+
if (ec.enableTableCrossPage !== void 0) fcObj.enable_table_cross_page = ec.enableTableCrossPage;
|
|
628
|
+
if (ec.enableTitleLevelRecognition !== void 0) fcObj.enable_title_level_recognition = ec.enableTitleLevelRecognition;
|
|
629
|
+
if (ec.enableInlineImage !== void 0) fcObj.enable_inline_image = ec.enableInlineImage;
|
|
630
|
+
if (ec.enableTableImage !== void 0) fcObj.enable_table_image = ec.enableTableImage;
|
|
631
|
+
if (ec.enableImageUnderstanding !== void 0) fcObj.enable_image_understanding = ec.enableImageUnderstanding;
|
|
632
|
+
if (ec.keepHeaderFooter !== void 0) fcObj.keep_header_footer = ec.keepHeaderFooter;
|
|
633
|
+
if (Object.keys(fcObj).length) data.feature_config = JSON.stringify(fcObj);
|
|
634
|
+
}
|
|
635
|
+
return data;
|
|
636
|
+
}
|
|
637
|
+
function readFileList(file) {
|
|
638
|
+
if (!fs2.existsSync(file)) throw new InvalidParamError(`File list not found: ${file}`);
|
|
639
|
+
return fs2.readFileSync(file, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((item) => path2.isAbsolute(item) ? item : path2.resolve(item));
|
|
640
|
+
}
|
|
641
|
+
function resolveFileInputs(file, fileList = false) {
|
|
642
|
+
if (fileList) {
|
|
643
|
+
const manifests = Array.isArray(file) ? file : [file];
|
|
644
|
+
return { files: manifests.flatMap(readFileList), isBatch: true };
|
|
645
|
+
}
|
|
646
|
+
if (Array.isArray(file)) return { files: file, isBatch: true };
|
|
647
|
+
return { files: [file], isBatch: false };
|
|
648
|
+
}
|
|
649
|
+
function resolveCliFileInputs(files, fileList) {
|
|
650
|
+
const resolved = [...files];
|
|
651
|
+
if (fileList) resolved.push(...readFileList(fileList));
|
|
652
|
+
return { files: resolved, isBatch: resolved.length > 1 || fileList !== void 0 };
|
|
653
|
+
}
|
|
654
|
+
function ensureFilesExist(files) {
|
|
655
|
+
const missing = files.filter((file) => !fs2.existsSync(file) || !fs2.statSync(file).isFile());
|
|
656
|
+
if (missing.length) throw new InvalidParamError(`File not found: ${missing.join(", ")}`);
|
|
657
|
+
}
|
|
658
|
+
function parseBatchSettings(itemCount) {
|
|
659
|
+
const { value, warnings } = parseMaxConcurrency(process.env.SOMARK_PARSE_MAX_CONCURRENCY);
|
|
660
|
+
return { concurrency: Math.min(value, itemCount) || 1, warnings };
|
|
661
|
+
}
|
|
662
|
+
async function runLimited(files, worker, onResult) {
|
|
663
|
+
const { concurrency, warnings } = parseBatchSettings(files.length);
|
|
664
|
+
const results = new Array(files.length);
|
|
665
|
+
let next = 0;
|
|
666
|
+
async function runWorker() {
|
|
667
|
+
while (next < files.length) {
|
|
668
|
+
const index = next++;
|
|
669
|
+
const result = await worker(files[index]);
|
|
670
|
+
results[index] = result;
|
|
671
|
+
onResult?.(index, result);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
await Promise.all(Array.from({ length: concurrency }, () => runWorker()));
|
|
675
|
+
return { results, warnings, concurrency };
|
|
676
|
+
}
|
|
677
|
+
function attachWarnings(items, warnings) {
|
|
678
|
+
if (!warnings.length) return;
|
|
679
|
+
for (const item of items) {
|
|
680
|
+
item.warnings = [...item.warnings ?? [], ...warnings];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/resources/parser.ts
|
|
685
|
+
var DocumentParser = class {
|
|
686
|
+
_http;
|
|
687
|
+
constructor(http2) {
|
|
688
|
+
this._http = http2;
|
|
689
|
+
}
|
|
690
|
+
checkApiKey() {
|
|
691
|
+
if (!this._http.getApiKey()) {
|
|
692
|
+
throw new AuthenticationError("apiKey is required for parser operations.");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async pollTask(taskId) {
|
|
696
|
+
this.checkApiKey();
|
|
697
|
+
const data = await this._http.post("/parse/async_check", { task_id: taskId });
|
|
698
|
+
emitWarnings(collectApiWarnings(data));
|
|
699
|
+
return data;
|
|
700
|
+
}
|
|
701
|
+
task(taskId) {
|
|
702
|
+
return new Task(taskId, (id) => this.pollTask(id));
|
|
703
|
+
}
|
|
704
|
+
async parseOne(opts) {
|
|
705
|
+
this.checkApiKey();
|
|
706
|
+
const data = buildParseData({ formats: opts.formats ?? ["md"], elementFormats: opts.elementFormats, extractConfig: opts.extractConfig });
|
|
707
|
+
const buffer = fs3.readFileSync(opts.file);
|
|
708
|
+
const fileName = path3.basename(opts.file);
|
|
709
|
+
const result = await this._http.post("/parse/sync", data, { name: fileName, buffer, mimeType: "application/octet-stream" });
|
|
710
|
+
const response = ParseResponse.fromApi(result);
|
|
711
|
+
emitWarnings(response.warnings);
|
|
712
|
+
response.sourceFileName = fileName;
|
|
713
|
+
return response;
|
|
714
|
+
}
|
|
715
|
+
async parse(opts) {
|
|
716
|
+
this.checkApiKey();
|
|
717
|
+
const { files, isBatch } = resolveFileInputs(opts.file, opts.fileList);
|
|
718
|
+
ensureFilesExist(files);
|
|
719
|
+
const { results: responses, warnings } = await runLimited(files, (file) => this.parseOne({ ...opts, file }));
|
|
720
|
+
if (isBatch) {
|
|
721
|
+
emitWarnings(warnings);
|
|
722
|
+
attachWarnings(responses, warnings);
|
|
723
|
+
}
|
|
724
|
+
return isBatch ? responses : responses[0];
|
|
725
|
+
}
|
|
726
|
+
async createOne(opts) {
|
|
727
|
+
this.checkApiKey();
|
|
728
|
+
const data = buildParseData({ formats: opts.formats ?? ["md"], elementFormats: opts.elementFormats, extractConfig: opts.extractConfig });
|
|
729
|
+
const buffer = fs3.readFileSync(opts.file);
|
|
730
|
+
const fileName = path3.basename(opts.file);
|
|
731
|
+
const result = await this._http.post("/parse/async", data, { name: fileName, buffer, mimeType: "application/octet-stream" });
|
|
732
|
+
const taskId = result.data?.task_id ?? "";
|
|
733
|
+
const warnings = collectApiWarnings(result);
|
|
734
|
+
emitWarnings(warnings);
|
|
735
|
+
return new Task(taskId, (id) => this.pollTask(id), warnings);
|
|
736
|
+
}
|
|
737
|
+
async create(opts) {
|
|
738
|
+
this.checkApiKey();
|
|
739
|
+
const { files, isBatch } = resolveFileInputs(opts.file, opts.fileList);
|
|
740
|
+
ensureFilesExist(files);
|
|
741
|
+
const { results: tasks, warnings } = await runLimited(files, (file) => this.createOne({ ...opts, file }));
|
|
742
|
+
if (isBatch) {
|
|
743
|
+
emitWarnings(warnings);
|
|
744
|
+
attachWarnings(tasks, warnings);
|
|
745
|
+
}
|
|
746
|
+
return isBatch ? tasks : tasks[0];
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/resources/usage.ts
|
|
751
|
+
var UsageClient = class {
|
|
752
|
+
_http;
|
|
753
|
+
constructor(http2) {
|
|
754
|
+
this._http = http2;
|
|
755
|
+
}
|
|
756
|
+
async get() {
|
|
757
|
+
if (!this._http.getApiKey()) {
|
|
758
|
+
throw new AuthenticationError("apiKey is required for usage queries.");
|
|
759
|
+
}
|
|
760
|
+
const result = await this._http.post("/usage", {});
|
|
761
|
+
const usage = Usage.fromApi(result);
|
|
762
|
+
emitWarnings(usage.warnings);
|
|
763
|
+
return usage;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/resources/pdf.ts
|
|
768
|
+
import * as fs4 from "fs";
|
|
769
|
+
import * as path4 from "path";
|
|
770
|
+
var PDFProcessor = class {
|
|
771
|
+
async toImages(opts) {
|
|
772
|
+
let pdfiumMod = null;
|
|
773
|
+
try {
|
|
774
|
+
pdfiumMod = await import("@hyzyla/pdfium");
|
|
775
|
+
} catch {
|
|
776
|
+
return { ok: false, files: [], error: "@hyzyla/pdfium is not installed. Run: npm install somark-js" };
|
|
777
|
+
}
|
|
778
|
+
let PNG = null;
|
|
779
|
+
try {
|
|
780
|
+
const pngjsMod = await import("pngjs");
|
|
781
|
+
PNG = pngjsMod.PNG;
|
|
782
|
+
} catch {
|
|
783
|
+
return { ok: false, files: [], error: "pngjs is not installed. Run: npm install pngjs" };
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const outDir = opts.out ?? "./";
|
|
787
|
+
if (!fs4.existsSync(outDir)) fs4.mkdirSync(outDir, { recursive: true });
|
|
788
|
+
const baseName = path4.basename(opts.file, path4.extname(opts.file));
|
|
789
|
+
const fmt = opts.filenameFormat ?? "{name}.page-{n}.png";
|
|
790
|
+
const dpi = opts.dpi ?? 150;
|
|
791
|
+
const scale = dpi / 72;
|
|
792
|
+
const pdfBuffer = fs4.readFileSync(opts.file);
|
|
793
|
+
const library = await pdfiumMod.PDFiumLibrary.init();
|
|
794
|
+
const document = await library.loadDocument(pdfBuffer);
|
|
795
|
+
const files = [];
|
|
796
|
+
const pageCount = document.getPageCount();
|
|
797
|
+
for (let i = 0; i < pageCount; i++) {
|
|
798
|
+
const page = document.getPage(i);
|
|
799
|
+
const image = await page.render({
|
|
800
|
+
scale,
|
|
801
|
+
render: async (renderOpts) => {
|
|
802
|
+
const png = new PNG({ width: renderOpts.width, height: renderOpts.height });
|
|
803
|
+
const src = renderOpts.data;
|
|
804
|
+
for (let j = 0; j < src.length; j += 4) {
|
|
805
|
+
png.data[j] = src[j + 2];
|
|
806
|
+
png.data[j + 1] = src[j + 1];
|
|
807
|
+
png.data[j + 2] = src[j];
|
|
808
|
+
png.data[j + 3] = src[j + 3];
|
|
809
|
+
}
|
|
810
|
+
return Buffer.from(PNG.sync.write(png));
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
const outName = fmt.replace("{name}", baseName).replace("{n}", String(i + 1));
|
|
814
|
+
const outPath = path4.join(outDir, outName);
|
|
815
|
+
fs4.writeFileSync(outPath, Buffer.from(image.data));
|
|
816
|
+
files.push(outPath);
|
|
817
|
+
}
|
|
818
|
+
document.destroy();
|
|
819
|
+
library.destroy();
|
|
820
|
+
return { ok: true, files };
|
|
821
|
+
} catch (err) {
|
|
822
|
+
return { ok: false, files: [], error: String(err) };
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// src/resources/preview.ts
|
|
828
|
+
import * as fs5 from "fs";
|
|
829
|
+
import * as http from "http";
|
|
830
|
+
import * as path5 from "path";
|
|
831
|
+
import { fileURLToPath } from "url";
|
|
832
|
+
var SUPPORTED_PREVIEW_EXTS = /* @__PURE__ */ new Set([".md", ".smd"]);
|
|
833
|
+
var VENDOR_FIX_HINT = "Run: git submodule update --init --recursive or somark doctor fix";
|
|
834
|
+
var previewHttp = {
|
|
835
|
+
createServer: http.createServer
|
|
836
|
+
};
|
|
837
|
+
var previewFs = {
|
|
838
|
+
createReadStream: fs5.createReadStream,
|
|
839
|
+
existsSync: fs5.existsSync,
|
|
840
|
+
statSync: fs5.statSync
|
|
841
|
+
};
|
|
842
|
+
var MIME_TYPES = {
|
|
843
|
+
".css": "text/css; charset=utf-8",
|
|
844
|
+
".gif": "image/gif",
|
|
845
|
+
".html": "text/html; charset=utf-8",
|
|
846
|
+
".jpeg": "image/jpeg",
|
|
847
|
+
".jpg": "image/jpeg",
|
|
848
|
+
".js": "application/javascript; charset=utf-8",
|
|
849
|
+
".json": "application/json; charset=utf-8",
|
|
850
|
+
".map": "application/json; charset=utf-8",
|
|
851
|
+
".md": "text/markdown; charset=utf-8",
|
|
852
|
+
".png": "image/png",
|
|
853
|
+
".smd": "text/plain; charset=utf-8",
|
|
854
|
+
".svg": "image/svg+xml",
|
|
855
|
+
".txt": "text/plain; charset=utf-8",
|
|
856
|
+
".wasm": "application/wasm",
|
|
857
|
+
".webp": "image/webp"
|
|
858
|
+
};
|
|
859
|
+
function currentModuleDir() {
|
|
860
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
861
|
+
return path5.dirname(fileURLToPath(import.meta.url));
|
|
862
|
+
}
|
|
863
|
+
var previewVendorPaths = {
|
|
864
|
+
packageVendorDir() {
|
|
865
|
+
return path5.resolve(currentModuleDir(), "../vendor/somarkdown-viewer");
|
|
866
|
+
},
|
|
867
|
+
developmentVendorDir() {
|
|
868
|
+
return path5.resolve(process.cwd(), "vendor/somarkdown-viewer");
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
function vendorCandidates() {
|
|
872
|
+
const candidates = [
|
|
873
|
+
{ dir: previewVendorPaths.packageVendorDir(), source: "package" },
|
|
874
|
+
{ dir: previewVendorPaths.developmentVendorDir(), source: "development" }
|
|
875
|
+
];
|
|
876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
877
|
+
return candidates.map((candidate) => ({ ...candidate, dir: path5.resolve(candidate.dir) })).filter((candidate) => {
|
|
878
|
+
if (seen.has(candidate.dir)) return false;
|
|
879
|
+
seen.add(candidate.dir);
|
|
880
|
+
return true;
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function describeVendorSource(source) {
|
|
884
|
+
return source === "package" ? "package assets" : "development checkout";
|
|
885
|
+
}
|
|
886
|
+
function findVendorLocation() {
|
|
887
|
+
for (const candidate of vendorCandidates()) {
|
|
888
|
+
if (previewFs.existsSync(candidate.dir) && previewFs.statSync(candidate.dir).isDirectory()) return candidate;
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
function missingVendorEntries(vendorDir) {
|
|
893
|
+
const required = [
|
|
894
|
+
path5.join(vendorDir, "index.html"),
|
|
895
|
+
path5.join(vendorDir, "lib")
|
|
896
|
+
];
|
|
897
|
+
return required.filter((candidate) => !previewFs.existsSync(candidate)).map((item) => path5.basename(item));
|
|
898
|
+
}
|
|
899
|
+
function validateVendorLocation(vendorLocation) {
|
|
900
|
+
if (!vendorLocation) {
|
|
901
|
+
throw new Error(`vendor/somarkdown-viewer not found. ${VENDOR_FIX_HINT}`);
|
|
902
|
+
}
|
|
903
|
+
const missing = missingVendorEntries(vendorLocation.dir);
|
|
904
|
+
if (missing.length > 0) {
|
|
905
|
+
if (vendorLocation.source === "package") {
|
|
906
|
+
throw new Error(`Installed preview assets are incomplete. Missing ${missing.join(", ")}. Reinstall the package or rebuild it with bundled viewer assets.`);
|
|
907
|
+
}
|
|
908
|
+
throw new Error(`vendor/somarkdown-viewer is incomplete. Missing ${missing.join(", ")}. ${VENDOR_FIX_HINT}`);
|
|
909
|
+
}
|
|
910
|
+
return vendorLocation;
|
|
911
|
+
}
|
|
912
|
+
function validatePreviewFile(file) {
|
|
913
|
+
const absolutePath = path5.resolve(file);
|
|
914
|
+
if (!previewFs.existsSync(absolutePath) || !previewFs.statSync(absolutePath).isFile()) {
|
|
915
|
+
throw new Error(`Preview file not found: ${absolutePath}`);
|
|
916
|
+
}
|
|
917
|
+
const ext = path5.extname(absolutePath).toLowerCase();
|
|
918
|
+
if (!SUPPORTED_PREVIEW_EXTS.has(ext)) {
|
|
919
|
+
throw new Error(`Unsupported preview file type: ${ext || "(no extension)"}. Use .md or .smd.`);
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
absolutePath,
|
|
923
|
+
rootDir: path5.dirname(absolutePath),
|
|
924
|
+
routePath: `/_preview/file/${encodeURIComponent(path5.basename(absolutePath))}`
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function normalizeRelativeRoute(routePathname) {
|
|
928
|
+
const prefix = "/_preview/file/";
|
|
929
|
+
if (!routePathname.startsWith(prefix)) return null;
|
|
930
|
+
const raw = routePathname.slice(prefix.length);
|
|
931
|
+
if (!raw) return null;
|
|
932
|
+
const decodedSegments = raw.split("/").filter(Boolean).map((segment) => {
|
|
933
|
+
try {
|
|
934
|
+
return decodeURIComponent(segment);
|
|
935
|
+
} catch {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
if (decodedSegments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
return decodedSegments.join(path5.sep);
|
|
943
|
+
}
|
|
944
|
+
function isPathInside(rootDir, candidatePath) {
|
|
945
|
+
const relativePath = path5.relative(rootDir, candidatePath);
|
|
946
|
+
return relativePath !== "" && !relativePath.startsWith("..") && !path5.isAbsolute(relativePath);
|
|
947
|
+
}
|
|
948
|
+
function sendFile(res, filePath) {
|
|
949
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
950
|
+
res.writeHead(200, { "Content-Type": MIME_TYPES[ext] ?? "application/octet-stream" });
|
|
951
|
+
previewFs.createReadStream(filePath).pipe(res);
|
|
952
|
+
}
|
|
953
|
+
function sendText(res, status, body) {
|
|
954
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
955
|
+
res.end(body);
|
|
956
|
+
}
|
|
957
|
+
var SoMarkDownPreview = class {
|
|
958
|
+
async start(opts = {}) {
|
|
959
|
+
const host = opts.host ?? "127.0.0.1";
|
|
960
|
+
const port = opts.port ?? 7878;
|
|
961
|
+
const openBrowser = opts.openBrowser ?? true;
|
|
962
|
+
const vendorLocation = validateVendorLocation(findVendorLocation());
|
|
963
|
+
const vendorDir = vendorLocation.dir;
|
|
964
|
+
const previewFile = opts.file ? validatePreviewFile(opts.file) : null;
|
|
965
|
+
const server = previewHttp.createServer((req, res) => {
|
|
966
|
+
const pathname = (req.url ?? "/").split("?")[0] || "/";
|
|
967
|
+
if (pathname === "/") {
|
|
968
|
+
sendFile(res, path5.join(vendorDir, "index.html"));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const relativePreviewPath = normalizeRelativeRoute(pathname);
|
|
972
|
+
if (pathname.startsWith("/_preview/file/")) {
|
|
973
|
+
if (!previewFile) {
|
|
974
|
+
sendText(res, 404, "No preview file loaded.");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (!relativePreviewPath) {
|
|
978
|
+
sendText(res, 403, "Preview access denied.");
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const candidatePath = path5.resolve(previewFile.rootDir, relativePreviewPath);
|
|
982
|
+
if (candidatePath !== previewFile.absolutePath && !isPathInside(previewFile.rootDir, candidatePath)) {
|
|
983
|
+
sendText(res, 403, "Preview access denied.");
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (!previewFs.existsSync(candidatePath) || !previewFs.statSync(candidatePath).isFile()) {
|
|
987
|
+
sendText(res, 404, "Preview file not found.");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
sendFile(res, candidatePath);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const vendorPath = path5.resolve(vendorDir, `.${pathname}`);
|
|
994
|
+
const relativeVendorPath = path5.relative(vendorDir, vendorPath);
|
|
995
|
+
if (relativeVendorPath.startsWith("..") || path5.isAbsolute(relativeVendorPath) || !previewFs.existsSync(vendorPath) || !previewFs.statSync(vendorPath).isFile()) {
|
|
996
|
+
sendText(res, 404, "Not found");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
sendFile(res, vendorPath);
|
|
1000
|
+
});
|
|
1001
|
+
server.on("clientError", (_err, socket) => {
|
|
1002
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1003
|
+
});
|
|
1004
|
+
await new Promise((resolve5, reject) => {
|
|
1005
|
+
server.listen(port, host, () => resolve5());
|
|
1006
|
+
server.once("error", (err) => {
|
|
1007
|
+
if (err.code === "EADDRINUSE") {
|
|
1008
|
+
reject(new Error(`Port ${port} is already in use on ${host}.`));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
reject(err);
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
const boundAddress = server.address();
|
|
1015
|
+
const boundPort = typeof boundAddress === "object" && boundAddress ? boundAddress.port : port;
|
|
1016
|
+
const serverUrl = `http://${host}:${boundPort}`;
|
|
1017
|
+
const fileUrl = previewFile ? `${serverUrl}?file=${encodeURIComponent(`${serverUrl}${previewFile.routePath}`)}` : serverUrl;
|
|
1018
|
+
if (openBrowser) {
|
|
1019
|
+
const open = await import("open").catch(() => null);
|
|
1020
|
+
if (open) await open.default(fileUrl);
|
|
1021
|
+
}
|
|
1022
|
+
let stopped = false;
|
|
1023
|
+
return {
|
|
1024
|
+
url: serverUrl,
|
|
1025
|
+
fileUrl,
|
|
1026
|
+
host,
|
|
1027
|
+
port: boundPort,
|
|
1028
|
+
stop: () => new Promise((resolve5, reject) => {
|
|
1029
|
+
if (stopped) {
|
|
1030
|
+
resolve5();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
stopped = true;
|
|
1034
|
+
server.close((err) => err ? reject(err) : resolve5());
|
|
1035
|
+
})
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// src/client.ts
|
|
1041
|
+
var SoMark = class {
|
|
1042
|
+
parser;
|
|
1043
|
+
usage;
|
|
1044
|
+
pdf;
|
|
1045
|
+
preview;
|
|
1046
|
+
_http;
|
|
1047
|
+
constructor(opts = {}) {
|
|
1048
|
+
const cfg = resolveConfig(opts);
|
|
1049
|
+
this._http = new HttpClient({
|
|
1050
|
+
apiKey: cfg.apiKey,
|
|
1051
|
+
baseUrl: cfg.baseUrl,
|
|
1052
|
+
timeout: cfg.timeout,
|
|
1053
|
+
maxRetries: cfg.maxRetries
|
|
1054
|
+
});
|
|
1055
|
+
this.parser = new DocumentParser(this._http);
|
|
1056
|
+
this.usage = new UsageClient(this._http);
|
|
1057
|
+
this.pdf = new PDFProcessor();
|
|
1058
|
+
this.preview = new SoMarkDownPreview();
|
|
1059
|
+
}
|
|
1060
|
+
toString() {
|
|
1061
|
+
const key = this._http.getApiKey();
|
|
1062
|
+
const masked = key && key.length > 8 ? `${key.slice(0, 4)}...${key.slice(-4)}` : key ? "set" : "not set";
|
|
1063
|
+
return `SoMark(apiKey=${masked})`;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// src/cli/help.ts
|
|
1068
|
+
function examples(...commands) {
|
|
1069
|
+
return `
|
|
1070
|
+
Examples:
|
|
1071
|
+
${commands.map((command) => ` ${command}`).join("\n")}`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/cli/commands/parse.ts
|
|
1075
|
+
function registerParseCommand(program2) {
|
|
1076
|
+
program2.command("parse [files...]").description("Parse a document").option("--formats <formats>", "Output formats (comma-separated): md,markdown,json,zip", "md").option("--out <path>", "Output file path").option("--file-list <path>", "Text file with one document path per line").option("--image-fmt <mode>", "Image return mode: url|base64|file|none", "url").option("--formula-fmt <fmt>", "Formula format: latex|mathml|ascii").option("--table-fmt <fmt>", "Table format: markdown|html|image").option("--cs-fmt <fmt>", "Chemical structure format: image").option("--title-levels", "Enable title level recognition").option("--cross-page-text", "Enable text cross-page merge").option("--cross-page-table", "Enable table cross-page merge").option("--no-inline-image", "Disable inline images").option("--no-table-image", "Disable table images").option("--no-image-understanding", "Disable image understanding").option("--keep-header-footer", "Keep headers and footers").option("--async", "Submit async task only").option("--task-id <id>", "Query/wait for existing task").option("--wait", "Wait for async task (use with --task-id)").option("--verbose", "Verbose output").addHelpText("after", examples(
|
|
1077
|
+
"somark parse document.pdf --formats md,json --out parsed/",
|
|
1078
|
+
"somark parse a.pdf b.pdf --out parsed/",
|
|
1079
|
+
"somark parse --file-list files.txt --out parsed/",
|
|
1080
|
+
"somark parse document.pdf --async",
|
|
1081
|
+
"somark parse --task-id TASK_ID --wait"
|
|
1082
|
+
)).action(async (filesArg, opts, cmd) => {
|
|
1083
|
+
const parentOpts = cmd.parent?.opts() ?? {};
|
|
1084
|
+
const client = new SoMark({
|
|
1085
|
+
apiKey: parentOpts["apiKey"],
|
|
1086
|
+
baseUrl: parentOpts["baseUrl"],
|
|
1087
|
+
timeout: parentOpts["timeout"] ? Number(parentOpts["timeout"]) : void 0
|
|
1088
|
+
});
|
|
1089
|
+
const formats = normalizeSaveFormats(opts.formats);
|
|
1090
|
+
const extractConfig = {
|
|
1091
|
+
enableTitleLevelRecognition: !!opts.titleLevels,
|
|
1092
|
+
enableTextCrossPage: !!opts.crossPageText,
|
|
1093
|
+
enableTableCrossPage: !!opts.crossPageTable,
|
|
1094
|
+
enableInlineImage: opts.inlineImage !== false,
|
|
1095
|
+
enableTableImage: opts.tableImage !== false,
|
|
1096
|
+
enableImageUnderstanding: opts.imageUnderstanding !== false,
|
|
1097
|
+
keepHeaderFooter: !!opts.keepHeaderFooter
|
|
1098
|
+
};
|
|
1099
|
+
const elementFormats = {
|
|
1100
|
+
image: opts.imageFmt,
|
|
1101
|
+
formula: opts.formulaFmt,
|
|
1102
|
+
table: opts.tableFmt,
|
|
1103
|
+
cs: opts.csFmt
|
|
1104
|
+
};
|
|
1105
|
+
const files = collectInputFiles(filesArg ?? [], opts.fileList);
|
|
1106
|
+
const isBatch = files.length > 1 || opts.fileList !== void 0;
|
|
1107
|
+
try {
|
|
1108
|
+
if (opts.taskId) {
|
|
1109
|
+
const task = client.parser.task(opts.taskId);
|
|
1110
|
+
if (opts.wait) {
|
|
1111
|
+
const stdoutMode = opts.out === void 0 && formats.length === 1;
|
|
1112
|
+
let response2;
|
|
1113
|
+
if (stdoutMode) {
|
|
1114
|
+
response2 = await task.wait();
|
|
1115
|
+
} else {
|
|
1116
|
+
const spinner2 = ora(`Waiting for task ${opts.taskId}...`).start();
|
|
1117
|
+
response2 = await task.wait();
|
|
1118
|
+
spinner2.succeed("Done");
|
|
1119
|
+
}
|
|
1120
|
+
response2.sourceFileName = response2.sourceFileName ?? task.fileName;
|
|
1121
|
+
const outputPlan2 = planOutput(response2.sourceFileName ?? "output", opts.out, formats, { allowStdout: true });
|
|
1122
|
+
if (outputPlan2.stdout) {
|
|
1123
|
+
process.stdout.write(response2.contentForFormat(formats[0]));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
await saveOutputPlan(response2, outputPlan2);
|
|
1127
|
+
for (const output of planPaths(outputPlan2)) console.log(`Output saved: ${output}`);
|
|
1128
|
+
console.log(`Task ID: ${response2.taskId}, Pages: ${response2.pages}`);
|
|
1129
|
+
} else {
|
|
1130
|
+
await task.refresh();
|
|
1131
|
+
console.log(`Task ${opts.taskId}: status=${task.status}`);
|
|
1132
|
+
}
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (opts.async) {
|
|
1136
|
+
if (!files.length) {
|
|
1137
|
+
console.error(chalk.red("File argument required for async mode"));
|
|
1138
|
+
process.exit(3);
|
|
1139
|
+
}
|
|
1140
|
+
if (isBatch) {
|
|
1141
|
+
const spinner2 = ora("Submitting tasks...").start();
|
|
1142
|
+
const tasks = await client.parser.create({ file: files, formats, elementFormats, extractConfig });
|
|
1143
|
+
spinner2.succeed("Done");
|
|
1144
|
+
renderBatchResults(files, tasks, { asyncMode: true, formats, out: opts.out, verbose: !!opts.verbose });
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const file2 = files[0];
|
|
1148
|
+
const task = await client.parser.create({ file: file2, formats, elementFormats, extractConfig });
|
|
1149
|
+
console.log(`Task submitted: ${task.id}`);
|
|
1150
|
+
console.log(`Status: ${task.status}`);
|
|
1151
|
+
console.log(`
|
|
1152
|
+
To retrieve results later:`);
|
|
1153
|
+
console.log(` somark parse --task-id ${task.id} --wait`);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (!files.length) {
|
|
1157
|
+
console.error(chalk.red("File argument required"));
|
|
1158
|
+
process.exit(3);
|
|
1159
|
+
}
|
|
1160
|
+
if (isBatch) {
|
|
1161
|
+
validateBatchOutput(opts.out, formats);
|
|
1162
|
+
const spinner2 = ora("Parsing files...").start();
|
|
1163
|
+
const responses = await client.parser.parse({ file: files, formats, elementFormats, extractConfig });
|
|
1164
|
+
spinner2.succeed("Done");
|
|
1165
|
+
await saveBatchOutputs(files, responses, opts.out, formats);
|
|
1166
|
+
renderBatchResults(files, responses, { asyncMode: false, formats, out: opts.out, verbose: !!opts.verbose });
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const file = files[0];
|
|
1170
|
+
const outputPlan = planOutput(file, opts.out, formats, { allowStdout: true });
|
|
1171
|
+
if (outputPlan.stdout) {
|
|
1172
|
+
const response2 = await client.parser.parse({ file, formats, elementFormats, extractConfig });
|
|
1173
|
+
process.stdout.write(response2.contentForFormat(formats[0]));
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const spinner = ora(`Parsing ${file}...`).start();
|
|
1177
|
+
const response = await client.parser.parse({ file, formats, elementFormats, extractConfig });
|
|
1178
|
+
spinner.succeed(chalk.green(`Done in ${response.pages} pages`));
|
|
1179
|
+
await saveOutputPlan(response, outputPlan);
|
|
1180
|
+
for (const output of planPaths(outputPlan)) console.log(`Output saved: ${output}`);
|
|
1181
|
+
if (opts.verbose) console.log(`Task ID: ${response.taskId}`);
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
const e = err;
|
|
1184
|
+
console.error(chalk.red(`\u2717 ${e.message}`));
|
|
1185
|
+
process.exit(e.constructor.name.includes("Auth") || e.constructor.name.includes("Connection") ? 2 : 1);
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function collectInputFiles(files, fileList) {
|
|
1190
|
+
try {
|
|
1191
|
+
return resolveCliFileInputs(files, fileList).files;
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
console.error(chalk.red(`\u2717 ${err.message}`));
|
|
1194
|
+
process.exit(3);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function validateBatchOutput(out, formats = ["md"]) {
|
|
1198
|
+
planOutput("output", out, formats, { batch: true });
|
|
1199
|
+
}
|
|
1200
|
+
function planPaths(plan) {
|
|
1201
|
+
return plan.targets.flatMap((target) => target.path ? [target.path] : []);
|
|
1202
|
+
}
|
|
1203
|
+
async function saveOutputPlan(response, plan) {
|
|
1204
|
+
for (const target of plan.targets) {
|
|
1205
|
+
if (!target.stdout && target.path) await response.save(target.path, target.format);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function saveBatchOutputs(files, responses, out, formats) {
|
|
1209
|
+
await Promise.all(responses.map((response, index) => saveOutputPlan(response, planOutput(files[index], out, formats, { batch: true }))));
|
|
1210
|
+
}
|
|
1211
|
+
function renderBatchResults(files, items, opts) {
|
|
1212
|
+
console.log(`Files: ${files.length}`);
|
|
1213
|
+
console.table(items.map((item, index) => {
|
|
1214
|
+
const file = files[index];
|
|
1215
|
+
const outputOrTask = opts.asyncMode ? item.id : outputLabel(file, item, opts);
|
|
1216
|
+
return {
|
|
1217
|
+
"#": index + 1,
|
|
1218
|
+
file,
|
|
1219
|
+
exists: "yes",
|
|
1220
|
+
status: opts.asyncMode ? "submitted" : "success",
|
|
1221
|
+
outputOrTask,
|
|
1222
|
+
error: ""
|
|
1223
|
+
};
|
|
1224
|
+
}));
|
|
1225
|
+
console.log(`Summary: total=${items.length} success=${items.length} failed=0`);
|
|
1226
|
+
}
|
|
1227
|
+
function outputLabel(file, response, opts) {
|
|
1228
|
+
const output = planPaths(planOutput(file, opts.out, opts.formats, { batch: true })).join(", ");
|
|
1229
|
+
return opts.verbose && response.taskId ? `${output} task=${response.taskId}` : output;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/cli/commands/usage.ts
|
|
1233
|
+
import chalk2 from "chalk";
|
|
1234
|
+
function registerUsageCommand(program2) {
|
|
1235
|
+
program2.command("usage").description("Query API usage").option("--format <fmt>", "Output format: text|json|table", "table").addHelpText("after", examples("somark usage --format table")).action(async (opts, cmd) => {
|
|
1236
|
+
const parentOpts = cmd.parent?.opts() ?? {};
|
|
1237
|
+
const client = new SoMark({
|
|
1238
|
+
apiKey: parentOpts["apiKey"],
|
|
1239
|
+
baseUrl: parentOpts["baseUrl"],
|
|
1240
|
+
timeout: parentOpts["timeout"] ? Number(parentOpts["timeout"]) : void 0
|
|
1241
|
+
});
|
|
1242
|
+
try {
|
|
1243
|
+
const usage = await client.usage.get();
|
|
1244
|
+
if (opts.format === "json") {
|
|
1245
|
+
console.log(JSON.stringify(usage.toJSON(), null, 2));
|
|
1246
|
+
} else if (opts.format === "text") {
|
|
1247
|
+
console.log(`Paid Pages Remaining: ${usage.remainingPaidPages.toLocaleString()}`);
|
|
1248
|
+
console.log(`Free Pages Today: ${usage.remainingFreePagesToday.toLocaleString()}`);
|
|
1249
|
+
console.log(`Free Pages This Month: ${usage.remainingFreePagesThisMonth.toLocaleString()}`);
|
|
1250
|
+
console.log(`Dashboard: ${usage.dashboardUrl}`);
|
|
1251
|
+
} else {
|
|
1252
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1253
|
+
console.log("\u2502 SoMark Usage \u2502 \u2502");
|
|
1254
|
+
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
1255
|
+
console.log(`\u2502 Paid Pages Remaining \u2502 ${String(usage.remainingPaidPages.toLocaleString()).padEnd(20)}\u2502`);
|
|
1256
|
+
console.log(`\u2502 Free Pages Today \u2502 ${String(usage.remainingFreePagesToday.toLocaleString()).padEnd(20)}\u2502`);
|
|
1257
|
+
console.log(`\u2502 Free Pages This Month \u2502 ${String(usage.remainingFreePagesThisMonth.toLocaleString()).padEnd(20)}\u2502`);
|
|
1258
|
+
console.log(`\u2502 Dashboard \u2502 ${String(usage.dashboardUrl).substring(0, 20).padEnd(20)}\u2502`);
|
|
1259
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1260
|
+
}
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
console.error(chalk2.red(`\u2717 ${err.message}`));
|
|
1263
|
+
process.exit(2);
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/cli/commands/pdf.ts
|
|
1269
|
+
import chalk3 from "chalk";
|
|
1270
|
+
function registerPdfCommand(program2) {
|
|
1271
|
+
const pdf = program2.command("pdf").description("PDF operations").addHelpText("after", examples("somark pdf toimg document.pdf --out ./pages"));
|
|
1272
|
+
pdf.command("toimg <file>").description("Convert PDF pages to images (local, no API key needed)").option("--out <dir>", "Output directory", "./").option("--format <template>", "Filename template: {name}.page-{n}.png").option("--dpi <n>", "Resolution in DPI", "150").addHelpText("after", examples("somark pdf toimg document.pdf --out ./pages")).action(async (file, opts) => {
|
|
1273
|
+
const resource = new PDFProcessor();
|
|
1274
|
+
const result = await resource.toImages({
|
|
1275
|
+
file,
|
|
1276
|
+
out: opts.out,
|
|
1277
|
+
filenameFormat: opts.format,
|
|
1278
|
+
dpi: Number(opts.dpi)
|
|
1279
|
+
});
|
|
1280
|
+
if (result.ok) {
|
|
1281
|
+
console.log(chalk3.green(`\u2713 Converted ${result.files.length} page(s) to ${opts.out}`));
|
|
1282
|
+
result.files.forEach((f) => console.log(` ${f}`));
|
|
1283
|
+
} else {
|
|
1284
|
+
console.error(chalk3.red(`\u2717 ${result.error}`));
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/cli/commands/preview.ts
|
|
1291
|
+
import chalk4 from "chalk";
|
|
1292
|
+
function registerPreviewCommand(program2) {
|
|
1293
|
+
program2.command("preview [file]").description("Start a local SoMarkDown preview server").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <n>", "Port to listen on", "7878").option("--no-open", "Don't open browser").addHelpText("after", examples("somark preview output.md --port 7878")).action(async (file, opts) => {
|
|
1294
|
+
const resource = new SoMarkDownPreview();
|
|
1295
|
+
try {
|
|
1296
|
+
const server = await resource.start({
|
|
1297
|
+
file,
|
|
1298
|
+
host: opts.host,
|
|
1299
|
+
port: Number(opts.port),
|
|
1300
|
+
openBrowser: opts.open
|
|
1301
|
+
});
|
|
1302
|
+
console.log(chalk4.green(`Preview server started: ${server.url}`));
|
|
1303
|
+
if (file) console.log(`File URL: ${server.fileUrl}`);
|
|
1304
|
+
console.log("Press Ctrl+C to stop.");
|
|
1305
|
+
process.once("SIGINT", async () => {
|
|
1306
|
+
await server.stop();
|
|
1307
|
+
console.log("\nServer stopped.");
|
|
1308
|
+
process.exit(0);
|
|
1309
|
+
});
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
console.error(chalk4.red(`\u2717 ${err.message}`));
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/cli/commands/doctor.ts
|
|
1318
|
+
import chalk5 from "chalk";
|
|
1319
|
+
import * as path6 from "path";
|
|
1320
|
+
import * as fs6 from "fs";
|
|
1321
|
+
|
|
1322
|
+
// src/cli/output.ts
|
|
1323
|
+
function printWarning(message) {
|
|
1324
|
+
emitWarnings([message]);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/cli/commands/doctor.ts
|
|
1328
|
+
var VIEWER_REPO_URL = "https://github.com/SoMarkAI/SoMarkDownViewer.git";
|
|
1329
|
+
function placeholderEntries(targetDir) {
|
|
1330
|
+
if (!fs6.existsSync(targetDir) || !fs6.statSync(targetDir).isDirectory()) return [];
|
|
1331
|
+
return fs6.readdirSync(targetDir).filter((name) => name !== ".DS_Store");
|
|
1332
|
+
}
|
|
1333
|
+
function resolveVendorInstallDir() {
|
|
1334
|
+
return path6.resolve(process.cwd(), "vendor/somarkdown-viewer");
|
|
1335
|
+
}
|
|
1336
|
+
async function repairViewerInstall() {
|
|
1337
|
+
const { execFileSync } = await import("child_process");
|
|
1338
|
+
const targetDir = resolveVendorInstallDir();
|
|
1339
|
+
const parentDir = path6.dirname(targetDir);
|
|
1340
|
+
const gitDir = path6.join(targetDir, ".git");
|
|
1341
|
+
const scriptPath = path6.join(targetDir, "get_latest_smd.sh");
|
|
1342
|
+
fs6.mkdirSync(parentDir, { recursive: true });
|
|
1343
|
+
if (fs6.existsSync(gitDir)) {
|
|
1344
|
+
execFileSync("git", ["-C", targetDir, "fetch", "--depth", "1", "origin", "main"], { stdio: "inherit" });
|
|
1345
|
+
execFileSync("git", ["-C", targetDir, "checkout", "main"], { stdio: "inherit" });
|
|
1346
|
+
execFileSync("git", ["-C", targetDir, "pull", "--ff-only", "origin", "main"], { stdio: "inherit" });
|
|
1347
|
+
} else if (!fs6.existsSync(targetDir)) {
|
|
1348
|
+
execFileSync("git", ["clone", "--depth", "1", VIEWER_REPO_URL, targetDir], { stdio: "inherit" });
|
|
1349
|
+
} else {
|
|
1350
|
+
const entries = placeholderEntries(targetDir);
|
|
1351
|
+
const isPlaceholder = entries.length === 0 || entries.length === 1 && entries[0] === ".gitkeep";
|
|
1352
|
+
const hasViewerFiles = fs6.existsSync(path6.join(targetDir, "index.html")) || fs6.existsSync(scriptPath);
|
|
1353
|
+
if (isPlaceholder) {
|
|
1354
|
+
fs6.rmSync(targetDir, { recursive: true, force: true });
|
|
1355
|
+
execFileSync("git", ["clone", "--depth", "1", VIEWER_REPO_URL, targetDir], { stdio: "inherit" });
|
|
1356
|
+
} else if (!hasViewerFiles) {
|
|
1357
|
+
throw new Error(`Cannot repair preview assets automatically because ${targetDir} exists but is not a SoMarkDownViewer checkout.`);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (fs6.existsSync(scriptPath)) {
|
|
1361
|
+
execFileSync("bash", [scriptPath], { stdio: "inherit" });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
throw new Error(`get_latest_smd.sh not found after repair in ${targetDir}.`);
|
|
1365
|
+
}
|
|
1366
|
+
function registerDoctorCommand(program2) {
|
|
1367
|
+
const doctor = program2.command("doctor").description("Diagnose installation").addHelpText("after", examples("somark doctor", "somark doctor fix", "somark doctor ping"));
|
|
1368
|
+
doctor.action(runChecks);
|
|
1369
|
+
doctor.command("fix").description("Attempt to fix installation issues").addHelpText("after", examples("somark doctor fix")).action(async () => {
|
|
1370
|
+
try {
|
|
1371
|
+
await repairViewerInstall();
|
|
1372
|
+
console.log(chalk5.green("\u2713 Preview assets repaired."));
|
|
1373
|
+
} catch (e) {
|
|
1374
|
+
console.error(chalk5.red(`\u2717 Failed: ${e.message}`));
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
doctor.command("ping").description("Check network connectivity").addHelpText("after", examples("somark doctor ping")).action(async () => {
|
|
1378
|
+
const targets = ["https://1.1.1.1", "https://somark.tech"];
|
|
1379
|
+
for (const target of targets) {
|
|
1380
|
+
try {
|
|
1381
|
+
const res = await fetch(target, { signal: AbortSignal.timeout(5e3) });
|
|
1382
|
+
console.log(chalk5.green(`\u2713 ${target} (${res.status})`));
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
console.error(chalk5.red(`\u2717 ${target}: ${e.message}`));
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
async function runChecks() {
|
|
1390
|
+
console.log(chalk5.bold("SoMark Doctor\n"));
|
|
1391
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
1392
|
+
if (major >= 18) {
|
|
1393
|
+
console.log(chalk5.green(`\u2713 Node.js ${process.versions.node}`));
|
|
1394
|
+
} else {
|
|
1395
|
+
console.error(chalk5.red(`\u2717 Node.js ${process.versions.node} (>=18 required)`));
|
|
1396
|
+
}
|
|
1397
|
+
try {
|
|
1398
|
+
await import("@hyzyla/pdfium");
|
|
1399
|
+
console.log(chalk5.green("\u2713 @hyzyla/pdfium installed"));
|
|
1400
|
+
} catch {
|
|
1401
|
+
printWarning("@hyzyla/pdfium not installed. Run: npm install somark-js");
|
|
1402
|
+
}
|
|
1403
|
+
const { resolveConfig: resolveConfig2 } = await import("./config-GOZJZCRT.mjs");
|
|
1404
|
+
const cfg = resolveConfig2();
|
|
1405
|
+
if (cfg.apiKey) {
|
|
1406
|
+
const k = cfg.apiKey;
|
|
1407
|
+
const masked = k.length > 8 ? `${k.slice(0, 4)}...${k.slice(-4)}` : "***";
|
|
1408
|
+
console.log(chalk5.green(`\u2713 API key configured (${masked})`));
|
|
1409
|
+
} else {
|
|
1410
|
+
printWarning("API key not configured. Run: somark login");
|
|
1411
|
+
}
|
|
1412
|
+
const vendorLocation = findVendorLocation();
|
|
1413
|
+
try {
|
|
1414
|
+
const resolvedVendorLocation = validateVendorLocation(vendorLocation);
|
|
1415
|
+
const source = describeVendorSource(resolvedVendorLocation.source);
|
|
1416
|
+
console.log(chalk5.green(`\u2713 preview assets ready (${source}: ${resolvedVendorLocation.dir})`));
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1419
|
+
const hasVendorDir = vendorLocation && fs6.existsSync(vendorLocation.dir);
|
|
1420
|
+
if (hasVendorDir) {
|
|
1421
|
+
printWarning(message);
|
|
1422
|
+
} else {
|
|
1423
|
+
console.error(chalk5.red(`\u2717 ${message}`));
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
const res = await fetch("https://somark.tech", { signal: AbortSignal.timeout(5e3) });
|
|
1428
|
+
console.log(chalk5.green(`\u2713 somark.tech reachable (${res.status})`));
|
|
1429
|
+
} catch (e) {
|
|
1430
|
+
console.error(chalk5.red(`\u2717 somark.tech unreachable: ${e.message}`));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/cli/commands/config.ts
|
|
1435
|
+
import chalk6 from "chalk";
|
|
1436
|
+
function registerConfigCommand(program2) {
|
|
1437
|
+
const config = program2.command("config").description("Manage configuration").addHelpText("after", examples("somark config set api_key sk-...", "somark config get api_key", "somark config list"));
|
|
1438
|
+
config.command("set <key> <value>").description("Set a config value (keys: api_key, base_url, timeout, max_retries)").option("--show", "Show secret values in output").addHelpText("after", examples("somark config set api_key sk-...")).action((key, value, opts) => {
|
|
1439
|
+
try {
|
|
1440
|
+
saveConfig({ [key]: value });
|
|
1441
|
+
const displayValue = opts.show ? value : maskConfigValue(key, value);
|
|
1442
|
+
console.log(chalk6.green(`\u2713 Set ${key} = ${displayValue}`));
|
|
1443
|
+
} catch (e) {
|
|
1444
|
+
console.error(chalk6.red(`\u2717 ${e.message}`));
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
config.command("get <key>").description("Get a config value").option("--show", "Show secret values in output").addHelpText("after", examples("somark config get api_key")).action((key, opts) => {
|
|
1449
|
+
const val = getConfigValue(key);
|
|
1450
|
+
if (val !== void 0) console.log(`${key} = ${opts.show ? val : maskConfigValue(key, val)}`);
|
|
1451
|
+
else console.log(`${key} is not set`);
|
|
1452
|
+
});
|
|
1453
|
+
config.command("list").description("List all config values (effective: env > file > default)").option("--show", "Show secret values in output").addHelpText("after", examples("somark config list")).action((opts) => {
|
|
1454
|
+
const cfg = listResolvedConfig();
|
|
1455
|
+
const sourceLabel = (s) => s === "env" ? chalk6.cyan("[env]") : s === "file" ? chalk6.yellow("[file]") : chalk6.dim("[default]");
|
|
1456
|
+
Object.entries(cfg).forEach(([k, { value, source }]) => {
|
|
1457
|
+
const displayValue = opts.show ? value : maskConfigValue(k, value);
|
|
1458
|
+
console.log(`${k} = ${displayValue} ${sourceLabel(source)}`);
|
|
1459
|
+
});
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/cli/commands/login.ts
|
|
1464
|
+
import chalk7 from "chalk";
|
|
1465
|
+
import * as readline from "readline";
|
|
1466
|
+
function registerLoginCommand(program2) {
|
|
1467
|
+
program2.command("login").description("Login to SoMark and save API key").option("--no-open", "Don't open browser").addHelpText("after", examples("somark login --no-open")).action(async (opts) => {
|
|
1468
|
+
const apiKeyUrl = "https://somark.tech/workbench/apikey";
|
|
1469
|
+
if (opts.open !== false) {
|
|
1470
|
+
console.log(`Opening ${apiKeyUrl} in your browser...`);
|
|
1471
|
+
const open = await import("open").catch(() => null);
|
|
1472
|
+
if (open) await open.default(apiKeyUrl);
|
|
1473
|
+
} else {
|
|
1474
|
+
console.log(`Visit ${apiKeyUrl} to get your API key.`);
|
|
1475
|
+
}
|
|
1476
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1477
|
+
const apiKey = await new Promise((resolve5) => rl.question("Enter your API key: ", resolve5));
|
|
1478
|
+
rl.close();
|
|
1479
|
+
if (!apiKey.startsWith("sk-")) {
|
|
1480
|
+
console.error(chalk7.red("\u2717 Invalid API key format (should start with 'sk-')"));
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
saveConfig({ api_key: apiKey });
|
|
1484
|
+
console.log(chalk7.green("\u2713 Authenticated. Config saved to ~/.somark/config.toml"));
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/cli/index.ts
|
|
1489
|
+
var { version } = package_default;
|
|
1490
|
+
var program = new Command();
|
|
1491
|
+
program.name("somark").description("SoMark SDK + CLI \u2014 Document parsing, PDF processing, and SoMarkDown preview. [JS]").version(version).helpOption("-h, --help", "display help for command").option("--api-key <key>", "API key", process.env.SOMARK_API_KEY).option("--base-url <url>", "Base URL").option("--timeout <seconds>", "Request timeout in seconds", "60").option("--no-warnings", "Suppress SoMark warnings").addHelpText("after", examples("somark parse document.pdf --formats md,json --out output.md", "somark usage --format table"));
|
|
1492
|
+
registerParseCommand(program);
|
|
1493
|
+
registerUsageCommand(program);
|
|
1494
|
+
registerPdfCommand(program);
|
|
1495
|
+
registerPreviewCommand(program);
|
|
1496
|
+
registerDoctorCommand(program);
|
|
1497
|
+
registerConfigCommand(program);
|
|
1498
|
+
registerLoginCommand(program);
|
|
1499
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
1500
|
+
const optsWithGlobals = actionCommand.optsWithGlobals?.() ?? program.opts();
|
|
1501
|
+
setWarningsSuppressed(Boolean(optsWithGlobals.warnings === false));
|
|
1502
|
+
});
|
|
1503
|
+
program.parse();
|