@wonderwhy-er/desktop-commander 0.2.41 → 0.2.43
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/README.md +0 -2
- package/dist/handlers/filesystem-handlers.js +24 -4
- package/dist/remote-device/remote-channel.d.ts +14 -0
- package/dist/remote-device/remote-channel.js +107 -28
- package/dist/server.d.ts +6 -1
- package/dist/server.js +112 -25
- package/dist/setup-claude-server.js +56 -50
- package/dist/terminal-manager.d.ts +18 -0
- package/dist/terminal-manager.js +130 -18
- package/dist/tools/edit.js +11 -4
- package/dist/tools/filesystem.d.ts +5 -0
- package/dist/tools/filesystem.js +43 -0
- package/dist/tools/fuzzySearch.d.ts +10 -20
- package/dist/tools/fuzzySearch.js +66 -105
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.js +8 -1
- package/dist/tools/pdf/markdown.d.ts +13 -0
- package/dist/tools/pdf/markdown.js +93 -29
- package/dist/tools/schemas.d.ts +20 -0
- package/dist/tools/schemas.js +45 -2
- package/dist/track-installation.js +57 -38
- package/dist/types.d.ts +6 -1
- package/dist/ui/contracts.d.ts +1 -1
- package/dist/ui/contracts.js +4 -1
- package/dist/ui/file-preview/preview-runtime.js +111 -113
- package/dist/ui/file-preview/src/app.js +19 -17
- package/dist/ui/file-preview/src/directory-controller.js +3 -3
- package/dist/ui/file-preview/src/file-type-handlers.js +2 -2
- package/dist/ui/file-preview/src/host/external-actions.d.ts +0 -11
- package/dist/ui/file-preview/src/host/external-actions.js +0 -39
- package/dist/ui/file-preview/src/markdown/controller.js +3 -1
- package/dist/ui/file-preview/src/panel-actions.js +2 -2
- package/dist/uninstall-claude-server.js +54 -47
- package/dist/utils/ab-test.d.ts +4 -0
- package/dist/utils/ab-test.js +6 -0
- package/dist/utils/capture.d.ts +10 -2
- package/dist/utils/capture.js +92 -60
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
|
@@ -1,113 +1,74 @@
|
|
|
1
|
-
import { distance } from 'fastest-levenshtein';
|
|
2
1
|
import { capture } from '../utils/capture.js';
|
|
2
|
+
import { Worker } from 'worker_threads';
|
|
3
|
+
// Re-export so existing callers keep importing from this module.
|
|
4
|
+
export { recursiveFuzzyIndexOf, getSimilarityRatio } from './fuzzySearchCore.js';
|
|
5
|
+
/** Abort fuzzy search in the worker after this many ms to avoid unbounded CPU burn. */
|
|
6
|
+
export const FUZZY_SEARCH_TIMEOUT_MS = 30000;
|
|
3
7
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @param parentDistance Best distance found so far (default: Infinity)
|
|
10
|
-
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
8
|
+
* Inline worker entry: imports the dependency-free core module (passed as
|
|
9
|
+
* moduleUrl) and runs the search off the main thread. The core module is
|
|
10
|
+
* deliberately a leaf — importing this module (or anything app-level) from the
|
|
11
|
+
* worker would boot the whole server per search. Kept as an eval'd snippet so
|
|
12
|
+
* the worker needs no separate file to ship alongside the compiled output.
|
|
11
13
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
result_distance: result.distance
|
|
24
|
-
});
|
|
25
|
-
return result;
|
|
26
|
-
}
|
|
27
|
-
if (end === null)
|
|
28
|
-
end = text.length;
|
|
29
|
-
// For small text segments, use iterative approach
|
|
30
|
-
if (end - start <= 2 * query.length) {
|
|
31
|
-
return iterativeReduction(text, query, start, end, parentDistance);
|
|
32
|
-
}
|
|
33
|
-
let midPoint = start + Math.floor((end - start) / 2);
|
|
34
|
-
let leftEnd = Math.min(end, midPoint + query.length); // Include query length to cover overlaps
|
|
35
|
-
let rightStart = Math.max(start, midPoint - query.length); // Include query length to cover overlaps
|
|
36
|
-
// Calculate distance for current segments
|
|
37
|
-
let leftDistance = distance(text.substring(start, leftEnd), query);
|
|
38
|
-
let rightDistance = distance(text.substring(rightStart, end), query);
|
|
39
|
-
let bestDistance = Math.min(leftDistance, parentDistance, rightDistance);
|
|
40
|
-
// If parent distance is already the best, use iterative approach
|
|
41
|
-
if (parentDistance === bestDistance) {
|
|
42
|
-
return iterativeReduction(text, query, start, end, parentDistance);
|
|
43
|
-
}
|
|
44
|
-
// Recursively search the better half
|
|
45
|
-
if (leftDistance < rightDistance) {
|
|
46
|
-
return recursiveFuzzyIndexOf(text, query, start, leftEnd, bestDistance, depth + 1);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
return recursiveFuzzyIndexOf(text, query, rightStart, end, bestDistance, depth + 1);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
14
|
+
const WORKER_CODE = `
|
|
15
|
+
const { workerData, parentPort } = require('worker_threads');
|
|
16
|
+
import(workerData.moduleUrl)
|
|
17
|
+
.then((m) => {
|
|
18
|
+
parentPort.postMessage({ ok: true, ...m.runFuzzySearch(workerData.text, workerData.query) });
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => {
|
|
21
|
+
parentPort.postMessage({ ok: false, error: String(err && err.stack || err) });
|
|
22
|
+
});
|
|
23
|
+
`;
|
|
24
|
+
const CORE_MODULE_URL = new URL('./fuzzySearchCore.js', import.meta.url).href;
|
|
52
25
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* @param parentDistance Best distance found so far
|
|
59
|
-
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
26
|
+
* Runs the fuzzy search in a Worker thread so the main MCP event loop stays
|
|
27
|
+
* responsive to pings and other tool calls during heavy scans. Rejects if the
|
|
28
|
+
* scan exceeds timeoutMs, terminating the worker so it doesn't linger in the
|
|
29
|
+
* background. Search metrics come back with the result and are captured here,
|
|
30
|
+
* on the main thread, where the client identity is initialized.
|
|
60
31
|
*/
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
32
|
+
export function runFuzzySearchInWorker(text, query, timeoutMs = FUZZY_SEARCH_TIMEOUT_MS) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const worker = new Worker(WORKER_CODE, { eval: true, workerData: { moduleUrl: CORE_MODULE_URL, text, query } });
|
|
35
|
+
// Never let a scan keep the server process alive during shutdown.
|
|
36
|
+
worker.unref();
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
worker.terminate();
|
|
39
|
+
reject(new Error(`Fuzzy search timed out after ${timeoutMs}ms`));
|
|
40
|
+
}, timeoutMs);
|
|
41
|
+
timer.unref();
|
|
42
|
+
worker.on('message', (msg) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
if (msg.ok) {
|
|
45
|
+
captureFuzzySearchMetrics(msg.metrics);
|
|
46
|
+
resolve(msg.result);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
reject(new Error(`Fuzzy search worker failed: ${msg.error}`));
|
|
50
|
+
}
|
|
51
|
+
// Don't let the worker wind down on its own; the answer is already
|
|
52
|
+
// here, and a lingering worker holds its copy of the file text.
|
|
53
|
+
// The promise is settled, so the exit-code rejection below is a no-op.
|
|
54
|
+
worker.terminate();
|
|
55
|
+
});
|
|
56
|
+
worker.on('error', (err) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
reject(err);
|
|
59
|
+
});
|
|
60
|
+
worker.on('exit', (code) => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
if (code !== 0) {
|
|
63
|
+
reject(new Error(`Fuzzy search worker exited with code ${code}`));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
93
66
|
});
|
|
94
|
-
return {
|
|
95
|
-
start: bestStart,
|
|
96
|
-
end: bestEnd,
|
|
97
|
-
value: text.substring(bestStart, bestEnd),
|
|
98
|
-
distance: bestDistance
|
|
99
|
-
};
|
|
100
67
|
}
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
export function getSimilarityRatio(a, b) {
|
|
108
|
-
const maxLength = Math.max(a.length, b.length);
|
|
109
|
-
if (maxLength === 0)
|
|
110
|
-
return 1; // Both strings are empty
|
|
111
|
-
const levenshteinDistance = distance(a, b);
|
|
112
|
-
return 1 - (levenshteinDistance / maxLength);
|
|
68
|
+
/** Same telemetry events the search used to emit inline, now sent from the main thread. */
|
|
69
|
+
function captureFuzzySearchMetrics(metrics) {
|
|
70
|
+
capture('fuzzy_search_recursive_metrics', metrics.recursive);
|
|
71
|
+
if (metrics.iterative) {
|
|
72
|
+
capture('fuzzy_search_iterative_metrics', metrics.iterative);
|
|
73
|
+
}
|
|
113
74
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure fuzzy-search core, kept free of app imports on purpose: it runs inside
|
|
3
|
+
* the worker thread spawned by runFuzzySearchInWorker (fuzzySearch.ts), and
|
|
4
|
+
* anything imported here is loaded per worker. Telemetry is returned as data
|
|
5
|
+
* and captured on the main thread, which has the real client identity.
|
|
6
|
+
*/
|
|
7
|
+
export interface FuzzyMatch {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
value: string;
|
|
11
|
+
distance: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FuzzySearchMetrics {
|
|
14
|
+
recursive: {
|
|
15
|
+
execution_time_ms: number;
|
|
16
|
+
text_length: number;
|
|
17
|
+
query_length: number;
|
|
18
|
+
result_distance: number;
|
|
19
|
+
};
|
|
20
|
+
iterative: {
|
|
21
|
+
execution_time_ms: number;
|
|
22
|
+
iterations: number;
|
|
23
|
+
segment_length: number;
|
|
24
|
+
query_length: number;
|
|
25
|
+
final_distance: number;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Runs a full fuzzy search and returns the match together with the timing
|
|
30
|
+
* metrics that used to be captured inline.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runFuzzySearch(text: string, query: string): {
|
|
33
|
+
result: FuzzyMatch;
|
|
34
|
+
metrics: FuzzySearchMetrics;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Recursively finds the closest match to a query string within text using fuzzy matching
|
|
38
|
+
* @param text The text to search within
|
|
39
|
+
* @param query The query string to find
|
|
40
|
+
* @param start Start index in the text (default: 0)
|
|
41
|
+
* @param end End index in the text (default: text.length)
|
|
42
|
+
* @param parentDistance Best distance found so far (default: Infinity)
|
|
43
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
44
|
+
*/
|
|
45
|
+
export declare function recursiveFuzzyIndexOf(text: string, query: string, start?: number, end?: number | null, parentDistance?: number): FuzzyMatch;
|
|
46
|
+
/**
|
|
47
|
+
* Calculates the similarity ratio between two strings
|
|
48
|
+
* @param a First string
|
|
49
|
+
* @param b Second string
|
|
50
|
+
* @returns Similarity ratio (0-1)
|
|
51
|
+
*/
|
|
52
|
+
export declare function getSimilarityRatio(a: string, b: string): number;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { distance } from 'fastest-levenshtein';
|
|
2
|
+
// Set by iterativeReduction during a search (exactly one terminal call per
|
|
3
|
+
// search) and collected by runFuzzySearch. Single-threaded per context, so a
|
|
4
|
+
// module-level slot is safe.
|
|
5
|
+
let lastIterativeMetrics = null;
|
|
6
|
+
/**
|
|
7
|
+
* Runs a full fuzzy search and returns the match together with the timing
|
|
8
|
+
* metrics that used to be captured inline.
|
|
9
|
+
*/
|
|
10
|
+
export function runFuzzySearch(text, query) {
|
|
11
|
+
const startTime = performance.now();
|
|
12
|
+
lastIterativeMetrics = null;
|
|
13
|
+
const result = recursiveFuzzyIndexOf(text, query);
|
|
14
|
+
return {
|
|
15
|
+
result,
|
|
16
|
+
metrics: {
|
|
17
|
+
recursive: {
|
|
18
|
+
execution_time_ms: performance.now() - startTime,
|
|
19
|
+
text_length: text.length,
|
|
20
|
+
query_length: query.length,
|
|
21
|
+
result_distance: result.distance
|
|
22
|
+
},
|
|
23
|
+
iterative: lastIterativeMetrics
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Recursively finds the closest match to a query string within text using fuzzy matching
|
|
29
|
+
* @param text The text to search within
|
|
30
|
+
* @param query The query string to find
|
|
31
|
+
* @param start Start index in the text (default: 0)
|
|
32
|
+
* @param end End index in the text (default: text.length)
|
|
33
|
+
* @param parentDistance Best distance found so far (default: Infinity)
|
|
34
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
35
|
+
*/
|
|
36
|
+
export function recursiveFuzzyIndexOf(text, query, start = 0, end = null, parentDistance = Infinity) {
|
|
37
|
+
if (end === null)
|
|
38
|
+
end = text.length;
|
|
39
|
+
// For small text segments, use iterative approach
|
|
40
|
+
if (end - start <= 2 * query.length) {
|
|
41
|
+
return iterativeReduction(text, query, start, end, parentDistance);
|
|
42
|
+
}
|
|
43
|
+
let midPoint = start + Math.floor((end - start) / 2);
|
|
44
|
+
let leftEnd = Math.min(end, midPoint + query.length); // Include query length to cover overlaps
|
|
45
|
+
let rightStart = Math.max(start, midPoint - query.length); // Include query length to cover overlaps
|
|
46
|
+
// Calculate distance for current segments
|
|
47
|
+
let leftDistance = distance(text.substring(start, leftEnd), query);
|
|
48
|
+
let rightDistance = distance(text.substring(rightStart, end), query);
|
|
49
|
+
let bestDistance = Math.min(leftDistance, parentDistance, rightDistance);
|
|
50
|
+
// If parent distance is already the best, use iterative approach
|
|
51
|
+
if (parentDistance === bestDistance) {
|
|
52
|
+
return iterativeReduction(text, query, start, end, parentDistance);
|
|
53
|
+
}
|
|
54
|
+
// Recursively search the better half
|
|
55
|
+
if (leftDistance < rightDistance) {
|
|
56
|
+
return recursiveFuzzyIndexOf(text, query, start, leftEnd, bestDistance);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return recursiveFuzzyIndexOf(text, query, rightStart, end, bestDistance);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Iteratively refines the best match by reducing the search area
|
|
64
|
+
* @param text The text to search within
|
|
65
|
+
* @param query The query string to find
|
|
66
|
+
* @param start Start index in the text
|
|
67
|
+
* @param end End index in the text
|
|
68
|
+
* @param parentDistance Best distance found so far
|
|
69
|
+
* @returns Object with start and end indices, matched value, and Levenshtein distance
|
|
70
|
+
*/
|
|
71
|
+
function iterativeReduction(text, query, start, end, parentDistance) {
|
|
72
|
+
const startTime = performance.now();
|
|
73
|
+
let iterations = 0;
|
|
74
|
+
// Seed with the measured distance of this slice. For recursive callers
|
|
75
|
+
// this equals parentDistance (the parent measured exactly this slice), but
|
|
76
|
+
// a top-level call on text <= 2x query length arrives with Infinity, which
|
|
77
|
+
// made the first shrink unconditional and a position-0 match unreachable.
|
|
78
|
+
let bestDistance = distance(text.substring(start, end), query);
|
|
79
|
+
let bestStart = start;
|
|
80
|
+
let bestEnd = end;
|
|
81
|
+
// Improve start position
|
|
82
|
+
let nextDistance = distance(text.substring(bestStart + 1, bestEnd), query);
|
|
83
|
+
while (nextDistance < bestDistance) {
|
|
84
|
+
bestDistance = nextDistance;
|
|
85
|
+
bestStart++;
|
|
86
|
+
const smallerString = text.substring(bestStart + 1, bestEnd);
|
|
87
|
+
nextDistance = distance(smallerString, query);
|
|
88
|
+
iterations++;
|
|
89
|
+
}
|
|
90
|
+
// Improve end position
|
|
91
|
+
nextDistance = distance(text.substring(bestStart, bestEnd - 1), query);
|
|
92
|
+
while (nextDistance < bestDistance) {
|
|
93
|
+
bestDistance = nextDistance;
|
|
94
|
+
bestEnd--;
|
|
95
|
+
const smallerString = text.substring(bestStart, bestEnd - 1);
|
|
96
|
+
nextDistance = distance(smallerString, query);
|
|
97
|
+
iterations++;
|
|
98
|
+
}
|
|
99
|
+
lastIterativeMetrics = {
|
|
100
|
+
execution_time_ms: performance.now() - startTime,
|
|
101
|
+
iterations: iterations,
|
|
102
|
+
segment_length: end - start,
|
|
103
|
+
query_length: query.length,
|
|
104
|
+
final_distance: bestDistance
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
start: bestStart,
|
|
108
|
+
end: bestEnd,
|
|
109
|
+
value: text.substring(bestStart, bestEnd),
|
|
110
|
+
distance: bestDistance
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculates the similarity ratio between two strings
|
|
115
|
+
* @param a First string
|
|
116
|
+
* @param b Second string
|
|
117
|
+
* @returns Similarity ratio (0-1)
|
|
118
|
+
*/
|
|
119
|
+
export function getSimilarityRatio(a, b) {
|
|
120
|
+
const maxLength = Math.max(a.length, b.length);
|
|
121
|
+
if (maxLength === 0)
|
|
122
|
+
return 1; // Both strings are empty
|
|
123
|
+
const levenshteinDistance = distance(a, b);
|
|
124
|
+
return 1 - (levenshteinDistance / maxLength);
|
|
125
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { terminalManager } from '../terminal-manager.js';
|
|
1
|
+
import { terminalManager, MAX_BUFFERED_OUTPUT_CHARS } from '../terminal-manager.js';
|
|
2
2
|
import { commandManager } from '../command-manager.js';
|
|
3
3
|
import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema } from './schemas.js';
|
|
4
4
|
import { capture } from "../utils/capture.js";
|
|
@@ -293,6 +293,13 @@ export async function readProcessOutput(args) {
|
|
|
293
293
|
// Absolute position read
|
|
294
294
|
statusMessage = `[Reading ${result.readCount} lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
|
|
295
295
|
}
|
|
296
|
+
// Surface buffer-cap eviction so the model knows the retained output is not
|
|
297
|
+
// the full output and that line numbers shifted (matches the truncation
|
|
298
|
+
// markers used by other tools).
|
|
299
|
+
if (result.evictedLines && result.evictedLines > 0) {
|
|
300
|
+
const capMB = Math.round(MAX_BUFFERED_OUTPUT_CHARS / 1024 / 1024);
|
|
301
|
+
statusMessage += `\n[WARNING: output exceeded the ${capMB}MB buffer cap; the ${result.evictedLines} earliest lines were evicted and cannot be read. Line numbers and totals refer to the retained buffer only]`;
|
|
302
|
+
}
|
|
296
303
|
// Add process state info
|
|
297
304
|
let processStateMessage = '';
|
|
298
305
|
if (result.isComplete) {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { PageRange } from './lib/pdf2md.js';
|
|
2
2
|
import { PdfParseResult } from './lib/pdf2md.js';
|
|
3
|
+
interface CachedPuppeteerChrome {
|
|
4
|
+
executablePath: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find Chrome in puppeteer's cache directory
|
|
8
|
+
* Returns the executable path if found, undefined otherwise
|
|
9
|
+
*/
|
|
10
|
+
export declare function findPuppeteerChrome(cacheDir?: string): CachedPuppeteerChrome | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Remove stale Puppeteer Chrome builds while preserving the active build.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pruneOldPuppeteerChromeBuilds(activeExecutablePath: string, cacheDir?: string): Promise<void>;
|
|
3
15
|
/**
|
|
4
16
|
* Preemptively ensure Chrome is available for PDF generation.
|
|
5
17
|
* Call this at server startup to trigger download in background if needed.
|
|
@@ -11,3 +23,4 @@ export declare function ensureChromeAvailable(): void;
|
|
|
11
23
|
*/
|
|
12
24
|
export declare function parsePdfToMarkdown(source: string, pageNumbers?: number[] | PageRange): Promise<PdfParseResult>;
|
|
13
25
|
export declare function parseMarkdownToPdf(markdown: string, options?: any): Promise<Buffer>;
|
|
26
|
+
export {};
|
|
@@ -1,48 +1,76 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from 'path';
|
|
2
|
+
import { existsSync, readdirSync } from 'fs';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
5
4
|
import { mdToPdf } from 'md-to-pdf';
|
|
6
5
|
import { pdf2md } from './lib/pdf2md.js';
|
|
6
|
+
import { CONFIG_FILE } from '../../config.js';
|
|
7
7
|
const isUrl = (source) => source.startsWith('http://') || source.startsWith('https://');
|
|
8
8
|
// Cached Chrome path to avoid repeated lookups
|
|
9
9
|
let cachedChromePath = null; // null = not checked yet
|
|
10
10
|
let chromeCheckPromise = null;
|
|
11
11
|
/**
|
|
12
|
-
* Get
|
|
12
|
+
* Get Desktop Commander's private Puppeteer cache directory.
|
|
13
13
|
*/
|
|
14
14
|
function getPuppeteerCacheDir() {
|
|
15
|
-
return join(
|
|
15
|
+
return join(dirname(CONFIG_FILE), 'puppeteer-cache');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the cache path where Puppeteer stores Chrome for Testing builds.
|
|
19
|
+
*/
|
|
20
|
+
function getPuppeteerChromeDir(cacheDir = getPuppeteerCacheDir()) {
|
|
21
|
+
return join(cacheDir, 'chrome');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the platform-specific executable within a cached Chrome build directory.
|
|
25
|
+
*/
|
|
26
|
+
function getChromeExecutablePath(chromeDir, version) {
|
|
27
|
+
const chromePath = process.platform === 'win32'
|
|
28
|
+
? join(chromeDir, version, 'chrome-win64', 'chrome.exe')
|
|
29
|
+
: process.platform === 'darwin'
|
|
30
|
+
? join(chromeDir, version, 'chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing')
|
|
31
|
+
: join(chromeDir, version, 'chrome-linux64', 'chrome');
|
|
32
|
+
if (existsSync(chromePath)) {
|
|
33
|
+
return chromePath;
|
|
34
|
+
}
|
|
35
|
+
// Also check for arm64 mac
|
|
36
|
+
if (process.platform === 'darwin') {
|
|
37
|
+
const armPath = join(chromeDir, version, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
|
|
38
|
+
if (existsSync(armPath)) {
|
|
39
|
+
return armPath;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the cached Chrome build directory that owns an executable path.
|
|
46
|
+
*/
|
|
47
|
+
function getCachedChromeBuildDir(chromeDir, executablePath) {
|
|
48
|
+
const relativePath = relative(chromeDir, executablePath);
|
|
49
|
+
if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const [buildDir] = relativePath.split(sep);
|
|
53
|
+
return buildDir ? join(chromeDir, buildDir) : undefined;
|
|
16
54
|
}
|
|
17
55
|
/**
|
|
18
56
|
* Find Chrome in puppeteer's cache directory
|
|
19
57
|
* Returns the executable path if found, undefined otherwise
|
|
20
58
|
*/
|
|
21
|
-
function findPuppeteerChrome() {
|
|
22
|
-
const
|
|
23
|
-
const chromeDir = join(cacheDir, 'chrome');
|
|
59
|
+
export function findPuppeteerChrome(cacheDir = getPuppeteerCacheDir()) {
|
|
60
|
+
const chromeDir = getPuppeteerChromeDir(cacheDir);
|
|
24
61
|
if (!existsSync(chromeDir)) {
|
|
25
62
|
return undefined;
|
|
26
63
|
}
|
|
27
64
|
try {
|
|
28
65
|
// Look for chrome directories (e.g., win64-143.0.7499.169)
|
|
29
|
-
const
|
|
30
|
-
|
|
66
|
+
const versions = readdirSync(chromeDir, { withFileTypes: true })
|
|
67
|
+
.filter(entry => entry.isDirectory())
|
|
68
|
+
.map(entry => entry.name)
|
|
69
|
+
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
31
70
|
for (const version of versions) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
? join(chromeDir, version, 'chrome-mac-x64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing')
|
|
36
|
-
: join(chromeDir, version, 'chrome-linux64', 'chrome');
|
|
37
|
-
if (existsSync(chromePath)) {
|
|
38
|
-
return chromePath;
|
|
39
|
-
}
|
|
40
|
-
// Also check for arm64 mac
|
|
41
|
-
if (process.platform === 'darwin') {
|
|
42
|
-
const armPath = join(chromeDir, version, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing');
|
|
43
|
-
if (existsSync(armPath)) {
|
|
44
|
-
return armPath;
|
|
45
|
-
}
|
|
71
|
+
const executablePath = getChromeExecutablePath(chromeDir, version);
|
|
72
|
+
if (executablePath) {
|
|
73
|
+
return { executablePath };
|
|
46
74
|
}
|
|
47
75
|
}
|
|
48
76
|
}
|
|
@@ -51,6 +79,37 @@ function findPuppeteerChrome() {
|
|
|
51
79
|
}
|
|
52
80
|
return undefined;
|
|
53
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Remove stale Puppeteer Chrome builds while preserving the active build.
|
|
84
|
+
*/
|
|
85
|
+
export async function pruneOldPuppeteerChromeBuilds(activeExecutablePath, cacheDir = getPuppeteerCacheDir()) {
|
|
86
|
+
const chromeDir = getPuppeteerChromeDir(cacheDir);
|
|
87
|
+
const activeBuildDir = getCachedChromeBuildDir(chromeDir, activeExecutablePath);
|
|
88
|
+
if (!activeBuildDir) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = await fs.readdir(chromeDir, { withFileTypes: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await Promise.all(entries
|
|
99
|
+
.filter(entry => entry.isDirectory())
|
|
100
|
+
.map(async (entry) => {
|
|
101
|
+
const buildDir = join(chromeDir, entry.name);
|
|
102
|
+
if (resolve(buildDir) === resolve(activeBuildDir)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error(`Failed to remove old Chrome cache build at ${buildDir}:`, error);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
54
113
|
/**
|
|
55
114
|
* Find system-installed Chrome/Chromium browser
|
|
56
115
|
* Returns the executable path if found, undefined otherwise
|
|
@@ -91,6 +150,7 @@ async function installChrome() {
|
|
|
91
150
|
const platform = detectBrowserPlatform();
|
|
92
151
|
const buildId = await resolveBuildId(Browser.CHROME, platform, 'stable');
|
|
93
152
|
console.error('Downloading Chrome for PDF generation (this may take a few minutes)...');
|
|
153
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
94
154
|
const installedBrowser = await install({
|
|
95
155
|
browser: Browser.CHROME,
|
|
96
156
|
buildId,
|
|
@@ -101,7 +161,9 @@ async function installChrome() {
|
|
|
101
161
|
},
|
|
102
162
|
});
|
|
103
163
|
console.error('\nChrome download complete.');
|
|
104
|
-
return
|
|
164
|
+
return {
|
|
165
|
+
executablePath: installedBrowser.executablePath,
|
|
166
|
+
};
|
|
105
167
|
}
|
|
106
168
|
/**
|
|
107
169
|
* Find or install Chrome for PDF generation
|
|
@@ -122,8 +184,9 @@ async function getChromePath() {
|
|
|
122
184
|
// 1. Check puppeteer cache first (exact compatible version)
|
|
123
185
|
const cachedChrome = findPuppeteerChrome();
|
|
124
186
|
if (cachedChrome) {
|
|
125
|
-
|
|
126
|
-
|
|
187
|
+
await pruneOldPuppeteerChromeBuilds(cachedChrome.executablePath);
|
|
188
|
+
cachedChromePath = cachedChrome.executablePath;
|
|
189
|
+
return cachedChrome.executablePath;
|
|
127
190
|
}
|
|
128
191
|
// 2. Check system Chrome
|
|
129
192
|
const systemChrome = findSystemChrome();
|
|
@@ -134,8 +197,9 @@ async function getChromePath() {
|
|
|
134
197
|
// 3. Install Chrome as last resort
|
|
135
198
|
try {
|
|
136
199
|
const installedChrome = await installChrome();
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
await pruneOldPuppeteerChromeBuilds(installedChrome.executablePath);
|
|
201
|
+
cachedChromePath = installedChrome.executablePath;
|
|
202
|
+
return installedChrome.executablePath;
|
|
139
203
|
}
|
|
140
204
|
catch (error) {
|
|
141
205
|
console.error('Failed to install Chrome:', error);
|