simple-dynamsoft-mcp 6.2.0 → 6.4.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.
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+
5
+ const projectRoot = process.cwd();
6
+ const metadataPath = join(projectRoot, "data", "metadata", "dynamsoft_sdks.json");
7
+ const checkOnly = process.argv.includes("--check");
8
+ const strictMode = process.argv.includes("--strict") || process.env.VERSION_SYNC_STRICT === "true";
9
+
10
+ function logVersionSync(message) {
11
+ console.log(`[version-sync] ${message}`);
12
+ }
13
+
14
+ function walkFiles(rootDir, fileFilter) {
15
+ if (!existsSync(rootDir)) return [];
16
+ const files = [];
17
+
18
+ function walk(dir) {
19
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
20
+ if (entry.name.startsWith(".git")) continue;
21
+ const fullPath = join(dir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ walk(fullPath);
24
+ continue;
25
+ }
26
+ if (entry.isFile() && fileFilter(fullPath, entry.name)) {
27
+ files.push(fullPath);
28
+ }
29
+ }
30
+ }
31
+
32
+ walk(rootDir);
33
+ return files.sort((a, b) => a.localeCompare(b));
34
+ }
35
+
36
+ function normalizeVersion(version) {
37
+ const parts = String(version)
38
+ .trim()
39
+ .split(".")
40
+ .map((part) => Number.parseInt(part, 10))
41
+ .filter((part) => Number.isInteger(part) && part >= 0);
42
+ if (parts.length < 2) return "";
43
+ return parts.join(".");
44
+ }
45
+
46
+ function compareVersion(a, b) {
47
+ const aParts = String(a).split(".").map((part) => Number.parseInt(part, 10));
48
+ const bParts = String(b).split(".").map((part) => Number.parseInt(part, 10));
49
+ const len = Math.max(aParts.length, bParts.length);
50
+ for (let i = 0; i < len; i += 1) {
51
+ const av = Number.isInteger(aParts[i]) ? aParts[i] : 0;
52
+ const bv = Number.isInteger(bParts[i]) ? bParts[i] : 0;
53
+ if (av > bv) return 1;
54
+ if (av < bv) return -1;
55
+ }
56
+ return 0;
57
+ }
58
+
59
+ function getHighestVersion(versions) {
60
+ const normalized = versions
61
+ .map((item) => normalizeVersion(item))
62
+ .filter(Boolean);
63
+ if (normalized.length === 0) return "";
64
+ normalized.sort((a, b) => compareVersion(a, b)).reverse();
65
+ return normalized[0];
66
+ }
67
+
68
+ function extractVersionCandidates(text) {
69
+ const values = [];
70
+ const regex = /(?:^|[^0-9])v?(\d{1,2}(?:\.\d+){1,3})(?![\d.])/gi;
71
+ const source = String(text || "");
72
+ let match = regex.exec(source);
73
+ while (match) {
74
+ const normalized = normalizeVersion(match[1]);
75
+ if (normalized) values.push(normalized);
76
+ match = regex.exec(source);
77
+ }
78
+ return values;
79
+ }
80
+
81
+ function detectFromReleaseNoteIndexes(docRoot) {
82
+ const markdownFiles = walkFiles(docRoot, (fullPath, name) => {
83
+ if (!name.toLowerCase().endsWith(".md")) return false;
84
+ const normalized = fullPath.replace(/\\/g, "/").toLowerCase();
85
+ return normalized.endsWith("/release-notes/index.md") || normalized.endsWith("/releasenotes/index.md");
86
+ });
87
+
88
+ if (markdownFiles.length === 0) {
89
+ return { version: "", detail: "release-note index files=0" };
90
+ }
91
+
92
+ const versions = [];
93
+ for (const filePath of markdownFiles) {
94
+ const content = readFileSync(filePath, "utf8");
95
+ versions.push(...extractVersionCandidates(content));
96
+ }
97
+
98
+ const version = getHighestVersion(versions);
99
+ return {
100
+ version,
101
+ detail: `release-note index files=${markdownFiles.length} candidates=${versions.length}`
102
+ };
103
+ }
104
+
105
+ function detectFromProductVersionYml(docRoot) {
106
+ const filePath = join(docRoot, "_data", "product_version.yml");
107
+ if (!existsSync(filePath)) return { version: "", detail: "_data/product_version.yml missing" };
108
+ const content = readFileSync(filePath, "utf8");
109
+
110
+ const latestLine = content
111
+ .split(/\r?\n/)
112
+ .find((line) => /latest version/i.test(line));
113
+ if (!latestLine) return { version: "", detail: "latest version line missing in product_version.yml" };
114
+
115
+ const candidates = extractVersionCandidates(latestLine);
116
+ return {
117
+ version: getHighestVersion(candidates),
118
+ detail: `product_version.yml candidates=${candidates.length}`
119
+ };
120
+ }
121
+
122
+ function detectFromLatestVersionJs(docRoot, relativeFile) {
123
+ const filePath = join(docRoot, ...relativeFile.split("/"));
124
+ if (!existsSync(filePath)) return { version: "", detail: `${relativeFile} missing` };
125
+ const content = readFileSync(filePath, "utf8");
126
+ const match = content.match(/versionNoteLatestVersion\s*=\s*["']([0-9.]+)["']/i);
127
+ if (!match) return { version: "", detail: `${relativeFile} has no versionNoteLatestVersion` };
128
+ return { version: normalizeVersion(match[1]), detail: `parsed ${relativeFile}` };
129
+ }
130
+
131
+ function detectFromStrategies(strategies, source, resolvedVersions, metadata) {
132
+ const docsRoot = source.docsPath ? join(projectRoot, source.docsPath) : "";
133
+ const attempts = [];
134
+
135
+ for (const strategy of strategies) {
136
+ if (strategy === "release-note-indexes") {
137
+ if (!docsRoot || !existsSync(docsRoot)) {
138
+ attempts.push("release-note-indexes: docs root missing");
139
+ continue;
140
+ }
141
+ const result = detectFromReleaseNoteIndexes(docsRoot);
142
+ attempts.push(`release-note-indexes: ${result.detail}`);
143
+ if (result.version) {
144
+ return { version: result.version, strategy: "release-note-indexes", attempts };
145
+ }
146
+ continue;
147
+ }
148
+
149
+ if (strategy === "product-version-yml") {
150
+ if (!docsRoot || !existsSync(docsRoot)) {
151
+ attempts.push("product-version-yml: docs root missing");
152
+ continue;
153
+ }
154
+ const result = detectFromProductVersionYml(docsRoot);
155
+ attempts.push(`product-version-yml: ${result.detail}`);
156
+ if (result.version) {
157
+ return { version: result.version, strategy: "product-version-yml", attempts };
158
+ }
159
+ continue;
160
+ }
161
+
162
+ if (typeof strategy === "object" && strategy.type === "latest-version-js") {
163
+ if (!docsRoot || !existsSync(docsRoot)) {
164
+ attempts.push(`latest-version-js(${strategy.file}): docs root missing`);
165
+ continue;
166
+ }
167
+ const result = detectFromLatestVersionJs(docsRoot, strategy.file);
168
+ attempts.push(`latest-version-js(${strategy.file}): ${result.detail}`);
169
+ if (result.version) {
170
+ return { version: result.version, strategy: `latest-version-js(${strategy.file})`, attempts };
171
+ }
172
+ continue;
173
+ }
174
+
175
+ if (typeof strategy === "object" && strategy.type === "max-of-sdks") {
176
+ const candidates = [];
177
+ for (const sdkId of strategy.sdkIds || []) {
178
+ const resolved = resolvedVersions[sdkId] || String(metadata?.sdks?.[sdkId]?.version || "");
179
+ const normalized = normalizeVersion(resolved);
180
+ if (normalized) candidates.push(normalized);
181
+ }
182
+ const detected = getHighestVersion(candidates);
183
+ attempts.push(`max-of-sdks(${(strategy.sdkIds || []).join(",")}): candidates=${candidates.length}`);
184
+ if (detected) {
185
+ return { version: detected, strategy: `max-of-sdks(${(strategy.sdkIds || []).join(",")})`, attempts };
186
+ }
187
+ continue;
188
+ }
189
+ }
190
+
191
+ return { version: "", strategy: "", attempts };
192
+ }
193
+
194
+ const sdkVersionSources = [
195
+ {
196
+ sdkId: "dbr-web",
197
+ docsPath: "data/documentation/barcode-reader-docs-js",
198
+ strategies: ["release-note-indexes"]
199
+ },
200
+ {
201
+ sdkId: "dbr-mobile",
202
+ docsPath: "data/documentation/barcode-reader-docs-mobile",
203
+ strategies: ["release-note-indexes"]
204
+ },
205
+ {
206
+ sdkId: "dbr-server",
207
+ docsPath: "data/documentation/barcode-reader-docs-server",
208
+ strategies: ["release-note-indexes"]
209
+ },
210
+ {
211
+ sdkId: "dwt",
212
+ docsPath: "data/documentation/web-twain-docs",
213
+ strategies: [{ type: "latest-version-js", file: "assets/js/setLatestVersion.js" }]
214
+ },
215
+ {
216
+ sdkId: "ddv",
217
+ docsPath: "data/documentation/document-viewer-docs",
218
+ strategies: ["product-version-yml", "release-note-indexes"]
219
+ },
220
+ {
221
+ sdkId: "dcv-web",
222
+ docsPath: "data/documentation/capture-vision-docs-js",
223
+ strategies: ["release-note-indexes"]
224
+ },
225
+ {
226
+ sdkId: "dcv-mobile",
227
+ docsPath: "data/documentation/capture-vision-docs-mobile",
228
+ strategies: ["release-note-indexes"]
229
+ },
230
+ {
231
+ sdkId: "dcv-server",
232
+ docsPath: "data/documentation/capture-vision-docs-server",
233
+ strategies: ["release-note-indexes"]
234
+ },
235
+ {
236
+ sdkId: "dcv-core",
237
+ docsPath: "data/documentation/capture-vision-docs",
238
+ strategies: ["product-version-yml", { type: "max-of-sdks", sdkIds: ["dcv-server", "dcv-mobile", "dcv-web"] }]
239
+ }
240
+ ];
241
+
242
+ if (!existsSync(metadataPath)) {
243
+ console.error(`[version-sync] metadata file not found: ${metadataPath}`);
244
+ process.exit(1);
245
+ }
246
+
247
+ logVersionSync(
248
+ `start mode=${checkOnly ? "check" : "update"} strict=${strictMode ? "true" : "false"} metadata=${relative(projectRoot, metadataPath)} sources=${sdkVersionSources.length}`
249
+ );
250
+
251
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
252
+ const updates = [];
253
+ const unchanged = [];
254
+ const skipped = [];
255
+ const resolvedVersions = {};
256
+
257
+ for (const source of sdkVersionSources) {
258
+ const sdkEntry = metadata?.sdks?.[source.sdkId];
259
+ if (!sdkEntry) {
260
+ skipped.push(`${source.sdkId} (missing metadata entry)`);
261
+ continue;
262
+ }
263
+
264
+ if (source.docsPath) {
265
+ const docsRoot = join(projectRoot, source.docsPath);
266
+ if (!existsSync(docsRoot)) {
267
+ skipped.push(`${source.sdkId} (${source.docsPath} not found)`);
268
+ continue;
269
+ }
270
+ }
271
+
272
+ const detection = detectFromStrategies(source.strategies, source, resolvedVersions, metadata);
273
+ if (!detection.version) {
274
+ skipped.push(
275
+ `${source.sdkId} (no version found; attempts=${detection.attempts.join(" | ") || "none"})`
276
+ );
277
+ continue;
278
+ }
279
+
280
+ resolvedVersions[source.sdkId] = detection.version;
281
+ const current = String(sdkEntry.version || "");
282
+ if (current !== detection.version) {
283
+ updates.push({
284
+ sdkId: source.sdkId,
285
+ from: current,
286
+ to: detection.version,
287
+ docsPath: source.docsPath || "n/a",
288
+ strategy: detection.strategy
289
+ });
290
+ sdkEntry.version = detection.version;
291
+ } else {
292
+ unchanged.push({
293
+ sdkId: source.sdkId,
294
+ version: current,
295
+ strategy: detection.strategy
296
+ });
297
+ }
298
+ }
299
+
300
+ if (updates.length === 0) {
301
+ logVersionSync("no version updates detected");
302
+ } else {
303
+ for (const update of updates) {
304
+ logVersionSync(
305
+ `update ${update.sdkId}: ${update.from} -> ${update.to} (source=${update.docsPath}, strategy=${update.strategy})`
306
+ );
307
+ }
308
+ }
309
+
310
+ if (unchanged.length > 0) {
311
+ for (const item of unchanged) {
312
+ logVersionSync(`keep ${item.sdkId}: ${item.version} (strategy=${item.strategy})`);
313
+ }
314
+ }
315
+
316
+ if (skipped.length > 0) {
317
+ for (const item of skipped) {
318
+ logVersionSync(`skip ${item}`);
319
+ }
320
+ }
321
+
322
+ logVersionSync(
323
+ `summary updates=${updates.length} unchanged=${unchanged.length} skipped=${skipped.length}`
324
+ );
325
+
326
+ if (strictMode && skipped.length > 0) {
327
+ console.error("[version-sync] strict mode failed: one or more SDK sources could not be resolved.");
328
+ process.exit(1);
329
+ }
330
+
331
+ if (checkOnly) {
332
+ if (updates.length > 0) {
333
+ console.error("[version-sync] version metadata is stale. Run: npm run data:versions");
334
+ process.exit(1);
335
+ }
336
+ process.exit(0);
337
+ }
338
+
339
+ if (updates.length > 0) {
340
+ writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`);
341
+ logVersionSync(`updated ${relative(projectRoot, metadataPath)}`);
342
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resourceIndex, readResourceContent } from "../src/resource-index.js";
4
+
5
+ function parsePositiveInt(value, fallback) {
6
+ const parsed = Number.parseInt(String(value ?? ""), 10);
7
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
8
+ return parsed;
9
+ }
10
+
11
+ async function main() {
12
+ const concurrency = parsePositiveInt(process.env.DOC_VERIFY_CONCURRENCY, 8);
13
+ const docs = resourceIndex.filter((entry) => entry.type === "doc");
14
+ const total = docs.length;
15
+
16
+ console.log(`[doc-verify] start total_docs=${total} concurrency=${concurrency}`);
17
+
18
+ if (total === 0) {
19
+ console.log("[doc-verify] no docs found; skipping");
20
+ return;
21
+ }
22
+
23
+ let index = 0;
24
+ let checked = 0;
25
+ const failures = [];
26
+ const workers = [];
27
+
28
+ const runOne = async () => {
29
+ while (true) {
30
+ const current = index;
31
+ index += 1;
32
+ if (current >= total) return;
33
+
34
+ const entry = docs[current];
35
+ try {
36
+ const content = await readResourceContent(entry.uri);
37
+ if (!content) {
38
+ throw new Error("readResourceContent returned null");
39
+ }
40
+ const hasText = typeof content.text === "string" && content.text.length > 0;
41
+ const hasBlob = typeof content.blob === "string" && content.blob.length > 0;
42
+ if (!hasText && !hasBlob) {
43
+ throw new Error("resource content is empty");
44
+ }
45
+ } catch (error) {
46
+ failures.push({
47
+ uri: entry.uri,
48
+ error: error?.message || String(error)
49
+ });
50
+ } finally {
51
+ checked += 1;
52
+ if (checked % 250 === 0 || checked === total) {
53
+ console.log(`[doc-verify] progress checked=${checked}/${total} failures=${failures.length}`);
54
+ }
55
+ }
56
+ }
57
+ };
58
+
59
+ for (let i = 0; i < Math.min(concurrency, total); i += 1) {
60
+ workers.push(runOne());
61
+ }
62
+ await Promise.all(workers);
63
+
64
+ if (failures.length > 0) {
65
+ console.error(`[doc-verify] failed count=${failures.length}`);
66
+ for (const failure of failures.slice(0, 20)) {
67
+ console.error(`[doc-verify] error uri=${failure.uri} message=${failure.error}`);
68
+ }
69
+ if (failures.length > 20) {
70
+ console.error(`[doc-verify] ... truncated ${failures.length - 20} additional failures`);
71
+ }
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+
76
+ console.log(`[doc-verify] success checked=${checked}`);
77
+ }
78
+
79
+ await main();
@@ -0,0 +1,148 @@
1
+ function toPositiveInt(value, fallback) {
2
+ const parsed = Number.parseInt(String(value ?? ""), 10);
3
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
4
+ return parsed;
5
+ }
6
+
7
+ function toNonNegativeInt(value, fallback) {
8
+ const parsed = Number.parseInt(String(value ?? ""), 10);
9
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
10
+ return parsed;
11
+ }
12
+
13
+ function sleepMs(ms) {
14
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
15
+ }
16
+
17
+ function isRetryableGeminiStatus(status) {
18
+ if (!Number.isFinite(status)) return false;
19
+ if (status === 429 || status === 503) return true;
20
+ return status >= 500 && status < 600;
21
+ }
22
+
23
+ function isRateLimitGeminiStatus(status) {
24
+ if (!Number.isFinite(status)) return false;
25
+ return status === 429 || status === 503;
26
+ }
27
+
28
+ function parseRetryAfterMs(retryAfterHeader, nowMs = Date.now()) {
29
+ const raw = String(retryAfterHeader || "").trim();
30
+ if (!raw) return 0;
31
+
32
+ const seconds = Number.parseInt(raw, 10);
33
+ if (Number.isFinite(seconds) && seconds >= 0) {
34
+ return seconds * 1000;
35
+ }
36
+
37
+ const at = Date.parse(raw);
38
+ if (!Number.isFinite(at)) return 0;
39
+ return Math.max(0, at - nowMs);
40
+ }
41
+
42
+ function normalizeGeminiRetryConfig(config = {}) {
43
+ const maxAttempts = toPositiveInt(config.maxAttempts, 5);
44
+ const baseDelayMs = toPositiveInt(config.baseDelayMs, 500);
45
+ const maxDelayMs = Math.max(baseDelayMs, toPositiveInt(config.maxDelayMs, 10000));
46
+ const requestThrottleMs = toNonNegativeInt(config.requestThrottleMs, 0);
47
+ return {
48
+ maxAttempts,
49
+ baseDelayMs,
50
+ maxDelayMs,
51
+ requestThrottleMs
52
+ };
53
+ }
54
+
55
+ function computeBackoffDelayMs({
56
+ attempt,
57
+ baseDelayMs,
58
+ maxDelayMs,
59
+ retryAfterMs = 0,
60
+ jitterRatio = 0.2,
61
+ random = Math.random
62
+ }) {
63
+ const retryAfter = Math.max(0, Number(retryAfterMs) || 0);
64
+ if (retryAfter > 0) {
65
+ return Math.min(maxDelayMs, retryAfter);
66
+ }
67
+
68
+ const exponent = Math.max(0, Number(attempt) - 1);
69
+ const base = Math.min(maxDelayMs, baseDelayMs * (2 ** exponent));
70
+ const jitterWindow = Math.max(0, base * jitterRatio);
71
+ const jitter = (Number(random()) * 2 - 1) * jitterWindow;
72
+ return Math.max(0, Math.min(maxDelayMs, Math.round(base + jitter)));
73
+ }
74
+
75
+ class GeminiHttpError extends Error {
76
+ constructor(message, { status, retryAfterMs = 0, detail = "" } = {}) {
77
+ super(message);
78
+ this.name = "GeminiHttpError";
79
+ this.status = Number.isFinite(status) ? status : 0;
80
+ this.retryAfterMs = Math.max(0, Number(retryAfterMs) || 0);
81
+ this.detail = detail;
82
+ this.retryable = isRetryableGeminiStatus(this.status);
83
+ this.rateLimited = isRateLimitGeminiStatus(this.status);
84
+ }
85
+ }
86
+
87
+ async function executeWithGeminiRetry({
88
+ operation,
89
+ requestFn,
90
+ retryConfig,
91
+ logger = () => {},
92
+ sleep = sleepMs,
93
+ random = Math.random,
94
+ onRetry = () => {}
95
+ }) {
96
+ const config = normalizeGeminiRetryConfig(retryConfig);
97
+ let lastError = null;
98
+
99
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
100
+ try {
101
+ return await requestFn(attempt);
102
+ } catch (error) {
103
+ lastError = error;
104
+ const status = Number(error?.status);
105
+ const retryable = Boolean(error?.retryable) || isRetryableGeminiStatus(status);
106
+ const rateLimited = Boolean(error?.rateLimited) || isRateLimitGeminiStatus(status);
107
+ if (!retryable || attempt >= config.maxAttempts) {
108
+ throw error;
109
+ }
110
+
111
+ const delayMs = computeBackoffDelayMs({
112
+ attempt,
113
+ baseDelayMs: config.baseDelayMs,
114
+ maxDelayMs: config.maxDelayMs,
115
+ retryAfterMs: Number(error?.retryAfterMs) || 0,
116
+ random
117
+ });
118
+ logger(
119
+ `gemini retry op=${operation} attempt=${attempt}/${config.maxAttempts} status=${status || "n/a"} ` +
120
+ `retryable=${retryable} rate_limited=${rateLimited} delay_ms=${delayMs}`
121
+ );
122
+ onRetry({
123
+ attempt,
124
+ maxAttempts: config.maxAttempts,
125
+ status: Number.isFinite(status) ? status : 0,
126
+ retryable,
127
+ rateLimited,
128
+ delayMs
129
+ });
130
+ if (delayMs > 0) {
131
+ await sleep(delayMs);
132
+ }
133
+ }
134
+ }
135
+
136
+ throw lastError || new Error(`Gemini request failed op=${operation}`);
137
+ }
138
+
139
+ export {
140
+ sleepMs,
141
+ isRetryableGeminiStatus,
142
+ isRateLimitGeminiStatus,
143
+ parseRetryAfterMs,
144
+ normalizeGeminiRetryConfig,
145
+ computeBackoffDelayMs,
146
+ GeminiHttpError,
147
+ executeWithGeminiRetry
148
+ };