@arabold/docs-mcp-server 1.16.1 → 1.17.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/README.md +2 -0
- package/dist/assets/main.css +1 -1
- package/dist/assets/main.js +2166 -2135
- package/dist/assets/main.js.map +1 -1
- package/dist/index.js +522 -159
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/main.css +1 -1
- package/public/assets/main.js +2166 -2135
- package/public/assets/main.js.map +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import Fastify from "fastify";
|
|
|
42
42
|
import { jsxs, jsx, Fragment } from "@kitajs/html/jsx-runtime";
|
|
43
43
|
import DOMPurify from "dompurify";
|
|
44
44
|
const name = "@arabold/docs-mcp-server";
|
|
45
|
-
const version = "1.16.
|
|
45
|
+
const version = "1.16.1";
|
|
46
46
|
const description = "MCP server for fetching and searching documentation";
|
|
47
47
|
const type = "module";
|
|
48
48
|
const bin = { "docs-mcp-server": "dist/index.js" };
|
|
@@ -179,6 +179,41 @@ class CancelJobTool {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
+
class ClearCompletedJobsTool {
|
|
183
|
+
manager;
|
|
184
|
+
/**
|
|
185
|
+
* Creates an instance of ClearCompletedJobsTool.
|
|
186
|
+
* @param manager The PipelineManager instance.
|
|
187
|
+
*/
|
|
188
|
+
constructor(manager) {
|
|
189
|
+
this.manager = manager;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Executes the tool to clear all completed jobs from the pipeline.
|
|
193
|
+
* @param input - The input parameters (currently unused).
|
|
194
|
+
* @returns A promise that resolves with the outcome of the clear operation.
|
|
195
|
+
*/
|
|
196
|
+
async execute(input) {
|
|
197
|
+
try {
|
|
198
|
+
const clearedCount = await this.manager.clearCompletedJobs();
|
|
199
|
+
const message = clearedCount > 0 ? `Successfully cleared ${clearedCount} completed job${clearedCount === 1 ? "" : "s"} from the queue.` : "No completed jobs to clear.";
|
|
200
|
+
logger.debug(`[ClearCompletedJobsTool] ${message}`);
|
|
201
|
+
return {
|
|
202
|
+
message,
|
|
203
|
+
success: true,
|
|
204
|
+
clearedCount
|
|
205
|
+
};
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const errorMessage = `Failed to clear completed jobs: ${error instanceof Error ? error.message : String(error)}`;
|
|
208
|
+
logger.error(`❌ [ClearCompletedJobsTool] ${errorMessage}`);
|
|
209
|
+
return {
|
|
210
|
+
message: errorMessage,
|
|
211
|
+
success: false,
|
|
212
|
+
clearedCount: 0
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
182
217
|
class ToolError extends Error {
|
|
183
218
|
constructor(message, toolName) {
|
|
184
219
|
super(message);
|
|
@@ -473,6 +508,7 @@ class HtmlPlaywrightMiddleware {
|
|
|
473
508
|
* - Parses credentials from the URL (if present).
|
|
474
509
|
* - Uses browser.newContext({ httpCredentials }) for HTTP Basic Auth on the main page and subresources.
|
|
475
510
|
* - Injects Authorization header for all same-origin requests if credentials are present and not already set.
|
|
511
|
+
* - Forwards all custom headers from context.options?.headers to Playwright requests.
|
|
476
512
|
* - Waits for common loading indicators to disappear before extracting HTML.
|
|
477
513
|
*
|
|
478
514
|
* @param context The middleware context containing the HTML and source URL.
|
|
@@ -494,20 +530,8 @@ class HtmlPlaywrightMiddleware {
|
|
|
494
530
|
let page = null;
|
|
495
531
|
let browserContext = null;
|
|
496
532
|
let renderedHtml = null;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
try {
|
|
500
|
-
const url = new URL(context.source);
|
|
501
|
-
origin = url.origin;
|
|
502
|
-
if (url.username && url.password) {
|
|
503
|
-
credentials = { username: url.username, password: url.password };
|
|
504
|
-
logger.debug(
|
|
505
|
-
`Playwright: Detected credentials for ${origin} (username: ${url.username})`
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
} catch (e) {
|
|
509
|
-
logger.warn(`⚠️ Could not parse URL for credential extraction: ${context.source}`);
|
|
510
|
-
}
|
|
533
|
+
const { credentials, origin } = extractCredentialsAndOrigin(context.source);
|
|
534
|
+
const customHeaders = context.options?.headers ?? {};
|
|
511
535
|
try {
|
|
512
536
|
const browser = await this.ensureBrowser();
|
|
513
537
|
if (credentials) {
|
|
@@ -537,17 +561,14 @@ class HtmlPlaywrightMiddleware {
|
|
|
537
561
|
if (["image", "stylesheet", "font", "media"].includes(resourceType)) {
|
|
538
562
|
return route.abort();
|
|
539
563
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return route.continue({ headers });
|
|
549
|
-
}
|
|
550
|
-
return route.continue();
|
|
564
|
+
const headers = mergePlaywrightHeaders(
|
|
565
|
+
route.request().headers(),
|
|
566
|
+
customHeaders,
|
|
567
|
+
credentials ?? void 0,
|
|
568
|
+
origin ?? void 0,
|
|
569
|
+
reqOrigin ?? void 0
|
|
570
|
+
);
|
|
571
|
+
return route.continue({ headers });
|
|
551
572
|
});
|
|
552
573
|
await page.goto(context.source, { waitUntil: "load" });
|
|
553
574
|
await page.waitForSelector("body");
|
|
@@ -581,6 +602,38 @@ class HtmlPlaywrightMiddleware {
|
|
|
581
602
|
await next();
|
|
582
603
|
}
|
|
583
604
|
}
|
|
605
|
+
function extractCredentialsAndOrigin(urlString) {
|
|
606
|
+
try {
|
|
607
|
+
const url = new URL(urlString);
|
|
608
|
+
const origin = url.origin;
|
|
609
|
+
if (url.username && url.password) {
|
|
610
|
+
return {
|
|
611
|
+
credentials: { username: url.username, password: url.password },
|
|
612
|
+
origin
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return { credentials: null, origin };
|
|
616
|
+
} catch {
|
|
617
|
+
return { credentials: null, origin: null };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function mergePlaywrightHeaders(requestHeaders, customHeaders, credentials, origin, reqOrigin) {
|
|
621
|
+
let headers = { ...requestHeaders };
|
|
622
|
+
for (const [key, value] of Object.entries(customHeaders)) {
|
|
623
|
+
if (key.toLowerCase() === "authorization" && headers.authorization) continue;
|
|
624
|
+
headers[key] = value;
|
|
625
|
+
}
|
|
626
|
+
if (credentials && origin && reqOrigin === origin && !headers.authorization) {
|
|
627
|
+
const basic = Buffer.from(`${credentials.username}:${credentials.password}`).toString(
|
|
628
|
+
"base64"
|
|
629
|
+
);
|
|
630
|
+
headers = {
|
|
631
|
+
...headers,
|
|
632
|
+
Authorization: `Basic ${basic}`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return headers;
|
|
636
|
+
}
|
|
584
637
|
class HtmlSanitizerMiddleware {
|
|
585
638
|
// Default selectors to remove
|
|
586
639
|
defaultSelectorsToRemove = [
|
|
@@ -1001,7 +1054,7 @@ class FetchUrlTool {
|
|
|
1001
1054
|
* @throws {ToolError} If fetching or processing fails
|
|
1002
1055
|
*/
|
|
1003
1056
|
async execute(options) {
|
|
1004
|
-
const { url, scrapeMode = ScrapeMode.Auto } = options;
|
|
1057
|
+
const { url, scrapeMode = ScrapeMode.Auto, headers } = options;
|
|
1005
1058
|
const canFetchResults = this.fetchers.map((f) => f.canFetch(url));
|
|
1006
1059
|
const fetcherIndex = canFetchResults.findIndex((result) => result === true);
|
|
1007
1060
|
if (fetcherIndex === -1) {
|
|
@@ -1018,7 +1071,9 @@ class FetchUrlTool {
|
|
|
1018
1071
|
logger.info(`📡 Fetching ${url}...`);
|
|
1019
1072
|
const rawContent = await fetcher.fetch(url, {
|
|
1020
1073
|
followRedirects: options.followRedirects ?? true,
|
|
1021
|
-
maxRetries: 3
|
|
1074
|
+
maxRetries: 3,
|
|
1075
|
+
headers
|
|
1076
|
+
// propagate custom headers
|
|
1022
1077
|
});
|
|
1023
1078
|
logger.info("🔄 Processing content...");
|
|
1024
1079
|
let processed;
|
|
@@ -1037,7 +1092,9 @@ class FetchUrlTool {
|
|
|
1037
1092
|
followRedirects: options.followRedirects ?? true,
|
|
1038
1093
|
excludeSelectors: void 0,
|
|
1039
1094
|
ignoreErrors: false,
|
|
1040
|
-
scrapeMode
|
|
1095
|
+
scrapeMode,
|
|
1096
|
+
headers
|
|
1097
|
+
// propagate custom headers
|
|
1041
1098
|
},
|
|
1042
1099
|
fetcher
|
|
1043
1100
|
);
|
|
@@ -1298,7 +1355,9 @@ class ScrapeTool {
|
|
|
1298
1355
|
scrapeMode: scraperOptions?.scrapeMode ?? ScrapeMode.Auto,
|
|
1299
1356
|
// Pass scrapeMode enum
|
|
1300
1357
|
includePatterns: scraperOptions?.includePatterns,
|
|
1301
|
-
excludePatterns: scraperOptions?.excludePatterns
|
|
1358
|
+
excludePatterns: scraperOptions?.excludePatterns,
|
|
1359
|
+
headers: scraperOptions?.headers
|
|
1360
|
+
// <-- propagate headers
|
|
1302
1361
|
});
|
|
1303
1362
|
if (waitForCompletion) {
|
|
1304
1363
|
try {
|
|
@@ -1906,6 +1965,24 @@ async function startStdioServer(tools) {
|
|
|
1906
1965
|
logger.info("🤖 MCP server listening on stdio");
|
|
1907
1966
|
return server;
|
|
1908
1967
|
}
|
|
1968
|
+
class PipelineError extends Error {
|
|
1969
|
+
constructor(message, cause) {
|
|
1970
|
+
super(message);
|
|
1971
|
+
this.cause = cause;
|
|
1972
|
+
this.name = this.constructor.name;
|
|
1973
|
+
if (cause?.stack) {
|
|
1974
|
+
this.stack = `${this.stack}
|
|
1975
|
+
Caused by: ${cause.stack}`;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
class PipelineStateError extends PipelineError {
|
|
1980
|
+
}
|
|
1981
|
+
class CancellationError extends PipelineError {
|
|
1982
|
+
constructor(message = "Operation cancelled") {
|
|
1983
|
+
super(message);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1909
1986
|
class FingerprintGenerator {
|
|
1910
1987
|
headerGenerator;
|
|
1911
1988
|
/**
|
|
@@ -1997,6 +2074,9 @@ class HttpFetcher {
|
|
|
1997
2074
|
const axiosError = error;
|
|
1998
2075
|
const status = axiosError.response?.status;
|
|
1999
2076
|
const code = axiosError.code;
|
|
2077
|
+
if (options?.signal?.aborted || code === "ERR_CANCELED") {
|
|
2078
|
+
throw new CancellationError("HTTP fetch cancelled");
|
|
2079
|
+
}
|
|
2000
2080
|
if (!followRedirects && status && status >= 300 && status < 400) {
|
|
2001
2081
|
const location = axiosError.response?.headers?.location;
|
|
2002
2082
|
if (location) {
|
|
@@ -2064,6 +2144,7 @@ async function initializeTools(docService, pipelineManager) {
|
|
|
2064
2144
|
listJobs: new ListJobsTool(pipelineManager),
|
|
2065
2145
|
getJobInfo: new GetJobInfoTool(pipelineManager),
|
|
2066
2146
|
cancelJob: new CancelJobTool(pipelineManager),
|
|
2147
|
+
// clearCompletedJobs: new ClearCompletedJobsTool(pipelineManager),
|
|
2067
2148
|
remove: new RemoveTool(docService, pipelineManager),
|
|
2068
2149
|
fetchUrl: new FetchUrlTool(new HttpFetcher(), new FileFetcher())
|
|
2069
2150
|
};
|
|
@@ -2172,24 +2253,6 @@ function isSubpath(baseUrl, targetUrl) {
|
|
|
2172
2253
|
const basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
2173
2254
|
return targetUrl.pathname.startsWith(basePath);
|
|
2174
2255
|
}
|
|
2175
|
-
class PipelineError extends Error {
|
|
2176
|
-
constructor(message, cause) {
|
|
2177
|
-
super(message);
|
|
2178
|
-
this.cause = cause;
|
|
2179
|
-
this.name = this.constructor.name;
|
|
2180
|
-
if (cause?.stack) {
|
|
2181
|
-
this.stack = `${this.stack}
|
|
2182
|
-
Caused by: ${cause.stack}`;
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
class PipelineStateError extends PipelineError {
|
|
2187
|
-
}
|
|
2188
|
-
class CancellationError extends PipelineError {
|
|
2189
|
-
constructor(message = "Operation cancelled") {
|
|
2190
|
-
super(message);
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
2256
|
function isRegexPattern(pattern) {
|
|
2194
2257
|
return pattern.length > 2 && pattern.startsWith("/") && pattern.endsWith("/");
|
|
2195
2258
|
}
|
|
@@ -2416,12 +2479,22 @@ class WebScraperStrategy extends BaseScraperStrategy {
|
|
|
2416
2479
|
return false;
|
|
2417
2480
|
}
|
|
2418
2481
|
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Processes a single queue item by fetching its content and processing it through pipelines.
|
|
2484
|
+
* @param item - The queue item to process.
|
|
2485
|
+
* @param options - Scraper options including headers for HTTP requests.
|
|
2486
|
+
* @param _progressCallback - Optional progress callback (not used here).
|
|
2487
|
+
* @param signal - Optional abort signal for request cancellation.
|
|
2488
|
+
* @returns An object containing the processed document and extracted links.
|
|
2489
|
+
*/
|
|
2419
2490
|
async processItem(item, options, _progressCallback, signal) {
|
|
2420
2491
|
const { url } = item;
|
|
2421
2492
|
try {
|
|
2422
2493
|
const fetchOptions = {
|
|
2423
2494
|
signal,
|
|
2424
|
-
followRedirects: options.followRedirects
|
|
2495
|
+
followRedirects: options.followRedirects,
|
|
2496
|
+
headers: options.headers
|
|
2497
|
+
// Forward custom headers
|
|
2425
2498
|
};
|
|
2426
2499
|
const rawContent = await this.httpFetcher.fetch(url, fetchOptions);
|
|
2427
2500
|
let processed;
|
|
@@ -2725,7 +2798,7 @@ class PipelineWorker {
|
|
|
2725
2798
|
// Pass signal to scraper service
|
|
2726
2799
|
);
|
|
2727
2800
|
if (signal.aborted) {
|
|
2728
|
-
throw new CancellationError("Job cancelled
|
|
2801
|
+
throw new CancellationError("Job cancelled");
|
|
2729
2802
|
}
|
|
2730
2803
|
logger.debug(`[${jobId}] Worker finished job successfully.`);
|
|
2731
2804
|
} catch (error) {
|
|
@@ -2862,13 +2935,21 @@ class PipelineManager {
|
|
|
2862
2935
|
}
|
|
2863
2936
|
/**
|
|
2864
2937
|
* Returns a promise that resolves when the specified job completes, fails, or is cancelled.
|
|
2938
|
+
* For cancelled jobs, this resolves successfully rather than rejecting.
|
|
2865
2939
|
*/
|
|
2866
2940
|
async waitForJobCompletion(jobId) {
|
|
2867
2941
|
const job = this.jobMap.get(jobId);
|
|
2868
2942
|
if (!job) {
|
|
2869
2943
|
throw new PipelineStateError(`Job not found: ${jobId}`);
|
|
2870
2944
|
}
|
|
2871
|
-
|
|
2945
|
+
try {
|
|
2946
|
+
await job.completionPromise;
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (error instanceof CancellationError || job.status === PipelineJobStatus.CANCELLED) {
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
throw error;
|
|
2952
|
+
}
|
|
2872
2953
|
}
|
|
2873
2954
|
/**
|
|
2874
2955
|
* Attempts to cancel a queued or running job.
|
|
@@ -2907,6 +2988,35 @@ class PipelineManager {
|
|
|
2907
2988
|
break;
|
|
2908
2989
|
}
|
|
2909
2990
|
}
|
|
2991
|
+
/**
|
|
2992
|
+
* Removes all jobs that are in a final state (completed, cancelled, or failed).
|
|
2993
|
+
* Only removes jobs that are not currently in the queue or actively running.
|
|
2994
|
+
* @returns The number of jobs that were cleared.
|
|
2995
|
+
*/
|
|
2996
|
+
async clearCompletedJobs() {
|
|
2997
|
+
const completedStatuses = [
|
|
2998
|
+
PipelineJobStatus.COMPLETED,
|
|
2999
|
+
PipelineJobStatus.CANCELLED,
|
|
3000
|
+
PipelineJobStatus.FAILED
|
|
3001
|
+
];
|
|
3002
|
+
let clearedCount = 0;
|
|
3003
|
+
const jobsToRemove = [];
|
|
3004
|
+
for (const [jobId, job] of this.jobMap.entries()) {
|
|
3005
|
+
if (completedStatuses.includes(job.status)) {
|
|
3006
|
+
jobsToRemove.push(jobId);
|
|
3007
|
+
clearedCount++;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
for (const jobId of jobsToRemove) {
|
|
3011
|
+
this.jobMap.delete(jobId);
|
|
3012
|
+
}
|
|
3013
|
+
if (clearedCount > 0) {
|
|
3014
|
+
logger.info(`🧹 Cleared ${clearedCount} completed job(s) from the queue`);
|
|
3015
|
+
} else {
|
|
3016
|
+
logger.debug("No completed jobs to clear");
|
|
3017
|
+
}
|
|
3018
|
+
return clearedCount;
|
|
3019
|
+
}
|
|
2910
3020
|
// --- Private Methods ---
|
|
2911
3021
|
/**
|
|
2912
3022
|
* Processes the job queue, starting new workers if capacity allows.
|
|
@@ -2961,10 +3071,10 @@ class PipelineManager {
|
|
|
2961
3071
|
if (error instanceof CancellationError || signal.aborted) {
|
|
2962
3072
|
job.status = PipelineJobStatus.CANCELLED;
|
|
2963
3073
|
job.finishedAt = /* @__PURE__ */ new Date();
|
|
2964
|
-
|
|
2965
|
-
logger.info(`🚫 Job execution cancelled: ${jobId}: ${
|
|
3074
|
+
const cancellationError = error instanceof CancellationError ? error : new CancellationError("Job cancelled by signal");
|
|
3075
|
+
logger.info(`🚫 Job execution cancelled: ${jobId}: ${cancellationError.message}`);
|
|
2966
3076
|
await this.callbacks.onJobStatusChange?.(job);
|
|
2967
|
-
job.rejectCompletion(
|
|
3077
|
+
job.rejectCompletion(cancellationError);
|
|
2968
3078
|
} else {
|
|
2969
3079
|
job.status = PipelineJobStatus.FAILED;
|
|
2970
3080
|
job.error = error instanceof Error ? error : new Error(String(error));
|
|
@@ -4676,8 +4786,23 @@ function registerIndexRoute(server) {
|
|
|
4676
4786
|
reply.type("text/html");
|
|
4677
4787
|
return "<!DOCTYPE html>" + /* @__PURE__ */ jsxs(Layout, { title: "MCP Docs", children: [
|
|
4678
4788
|
/* @__PURE__ */ jsxs("section", { class: "mb-4 p-4 bg-white rounded-lg shadow dark:bg-gray-800 border border-gray-300 dark:border-gray-600", children: [
|
|
4679
|
-
/* @__PURE__ */
|
|
4680
|
-
|
|
4789
|
+
/* @__PURE__ */ jsxs("div", { class: "flex items-center justify-between mb-2", children: [
|
|
4790
|
+
/* @__PURE__ */ jsx("h2", { class: "text-xl font-semibold text-gray-900 dark:text-white", children: "Job Queue" }),
|
|
4791
|
+
/* @__PURE__ */ jsx(
|
|
4792
|
+
"button",
|
|
4793
|
+
{
|
|
4794
|
+
type: "button",
|
|
4795
|
+
class: "text-xs px-3 py-1.5 text-gray-700 bg-gray-100 border border-gray-300 rounded-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-600 dark:text-gray-300 dark:border-gray-500 dark:hover:bg-gray-700 dark:focus:ring-gray-700 transition-colors duration-150",
|
|
4796
|
+
title: "Clear all completed, cancelled, and failed jobs",
|
|
4797
|
+
"hx-post": "/api/jobs/clear-completed",
|
|
4798
|
+
"hx-trigger": "click",
|
|
4799
|
+
"hx-on": "htmx:afterRequest: document.dispatchEvent(new Event('job-list-refresh'))",
|
|
4800
|
+
"hx-swap": "none",
|
|
4801
|
+
children: "Clear Completed Jobs"
|
|
4802
|
+
}
|
|
4803
|
+
)
|
|
4804
|
+
] }),
|
|
4805
|
+
/* @__PURE__ */ jsx("div", { id: "job-queue", "hx-get": "/api/jobs", "hx-trigger": "load, every 1s", children: /* @__PURE__ */ jsxs("div", { class: "animate-pulse", children: [
|
|
4681
4806
|
/* @__PURE__ */ jsx("div", { class: "h-[0.8em] bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4" }),
|
|
4682
4807
|
/* @__PURE__ */ jsx("div", { class: "h-[0.8em] bg-gray-200 rounded-full dark:bg-gray-700 w-full mb-2.5" }),
|
|
4683
4808
|
/* @__PURE__ */ jsx("div", { class: "h-[0.8em] bg-gray-200 rounded-full dark:bg-gray-700 w-full mb-2.5" })
|
|
@@ -4693,7 +4818,7 @@ function registerIndexRoute(server) {
|
|
|
4693
4818
|
/* @__PURE__ */ jsx(
|
|
4694
4819
|
"div",
|
|
4695
4820
|
{
|
|
4696
|
-
id: "
|
|
4821
|
+
id: "indexed-docs",
|
|
4697
4822
|
"hx-get": "/api/libraries",
|
|
4698
4823
|
"hx-trigger": "load, every 10s",
|
|
4699
4824
|
children: /* @__PURE__ */ jsxs("div", { class: "animate-pulse", children: [
|
|
@@ -4707,12 +4832,75 @@ function registerIndexRoute(server) {
|
|
|
4707
4832
|
] });
|
|
4708
4833
|
});
|
|
4709
4834
|
}
|
|
4835
|
+
function registerCancelJobRoute(server, cancelJobTool) {
|
|
4836
|
+
server.post(
|
|
4837
|
+
"/api/jobs/:jobId/cancel",
|
|
4838
|
+
async (request, reply) => {
|
|
4839
|
+
const { jobId } = request.params;
|
|
4840
|
+
const result = await cancelJobTool.execute({ jobId });
|
|
4841
|
+
if (result.success) {
|
|
4842
|
+
return { success: true, message: result.message };
|
|
4843
|
+
} else {
|
|
4844
|
+
reply.status(400);
|
|
4845
|
+
return { success: false, message: result.message };
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
);
|
|
4849
|
+
}
|
|
4850
|
+
function registerClearCompletedJobsRoute(server, clearCompletedJobsTool) {
|
|
4851
|
+
server.post("/api/jobs/clear-completed", async (_, reply) => {
|
|
4852
|
+
try {
|
|
4853
|
+
const result = await clearCompletedJobsTool.execute({});
|
|
4854
|
+
reply.type("application/json");
|
|
4855
|
+
return {
|
|
4856
|
+
success: result.success,
|
|
4857
|
+
message: result.message
|
|
4858
|
+
};
|
|
4859
|
+
} catch (error) {
|
|
4860
|
+
reply.code(500);
|
|
4861
|
+
return {
|
|
4862
|
+
success: false,
|
|
4863
|
+
message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`
|
|
4864
|
+
};
|
|
4865
|
+
}
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
4710
4868
|
const VersionBadge = ({ version: version2 }) => {
|
|
4711
4869
|
if (!version2) {
|
|
4712
4870
|
return null;
|
|
4713
4871
|
}
|
|
4714
4872
|
return /* @__PURE__ */ jsx("span", { class: "bg-purple-100 text-purple-800 text-xs font-medium me-2 px-1.5 py-0.5 rounded dark:bg-purple-900 dark:text-purple-300", children: /* @__PURE__ */ jsx("span", { safe: true, children: version2 }) });
|
|
4715
4873
|
};
|
|
4874
|
+
const LoadingSpinner = () => /* @__PURE__ */ jsxs(
|
|
4875
|
+
"svg",
|
|
4876
|
+
{
|
|
4877
|
+
class: "animate-spin h-4 w-4 text-white",
|
|
4878
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
4879
|
+
fill: "none",
|
|
4880
|
+
viewBox: "0 0 24 24",
|
|
4881
|
+
children: [
|
|
4882
|
+
/* @__PURE__ */ jsx(
|
|
4883
|
+
"circle",
|
|
4884
|
+
{
|
|
4885
|
+
class: "opacity-25",
|
|
4886
|
+
cx: "12",
|
|
4887
|
+
cy: "12",
|
|
4888
|
+
r: "10",
|
|
4889
|
+
stroke: "currentColor",
|
|
4890
|
+
"stroke-width": "4"
|
|
4891
|
+
}
|
|
4892
|
+
),
|
|
4893
|
+
/* @__PURE__ */ jsx(
|
|
4894
|
+
"path",
|
|
4895
|
+
{
|
|
4896
|
+
class: "opacity-75",
|
|
4897
|
+
fill: "currentColor",
|
|
4898
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
4899
|
+
}
|
|
4900
|
+
)
|
|
4901
|
+
]
|
|
4902
|
+
}
|
|
4903
|
+
);
|
|
4716
4904
|
const JobItem = ({ job }) => (
|
|
4717
4905
|
// Use Flowbite Card structure with reduced padding and added border
|
|
4718
4906
|
/* @__PURE__ */ jsx("div", { class: "block p-2 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600", children: /* @__PURE__ */ jsxs("div", { class: "flex items-center justify-between", children: [
|
|
@@ -4728,19 +4916,99 @@ const JobItem = ({ job }) => (
|
|
|
4728
4916
|
] })
|
|
4729
4917
|
] }),
|
|
4730
4918
|
/* @__PURE__ */ jsxs("div", { class: "flex flex-col items-end gap-1", children: [
|
|
4731
|
-
/* @__PURE__ */
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4919
|
+
/* @__PURE__ */ jsxs("div", { class: "flex items-center gap-2", children: [
|
|
4920
|
+
/* @__PURE__ */ jsx(
|
|
4921
|
+
"span",
|
|
4922
|
+
{
|
|
4923
|
+
class: `px-1.5 py-0.5 text-xs font-medium rounded ${job.status === PipelineJobStatus.COMPLETED ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" : job.error ? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" : "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"}`,
|
|
4924
|
+
children: job.status
|
|
4925
|
+
}
|
|
4926
|
+
),
|
|
4927
|
+
(job.status === PipelineJobStatus.QUEUED || job.status === PipelineJobStatus.RUNNING) && /* @__PURE__ */ jsxs(
|
|
4928
|
+
"button",
|
|
4929
|
+
{
|
|
4930
|
+
type: "button",
|
|
4931
|
+
class: "font-medium rounded-lg text-xs p-1 text-center inline-flex items-center transition-colors duration-150 ease-in-out border border-gray-300 bg-white text-red-600 hover:bg-red-50 focus:ring-4 focus:outline-none focus:ring-red-100 dark:border-gray-600 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:focus:ring-red-900",
|
|
4932
|
+
title: "Stop this job",
|
|
4933
|
+
"x-data": "{}",
|
|
4934
|
+
"x-on:click": `
|
|
4935
|
+
if ($store.confirmingAction.type === 'job-cancel' && $store.confirmingAction.id === '${job.id}') {
|
|
4936
|
+
$store.confirmingAction.isStopping = true;
|
|
4937
|
+
fetch('/api/jobs/' + '${job.id}' + '/cancel', {
|
|
4938
|
+
method: 'POST',
|
|
4939
|
+
headers: { 'Accept': 'application/json' },
|
|
4940
|
+
})
|
|
4941
|
+
.then(r => r.json())
|
|
4942
|
+
.then(() => {
|
|
4943
|
+
$store.confirmingAction.type = null;
|
|
4944
|
+
$store.confirmingAction.id = null;
|
|
4945
|
+
$store.confirmingAction.isStopping = false;
|
|
4946
|
+
if ($store.confirmingAction.timeoutId) { clearTimeout($store.confirmingAction.timeoutId); $store.confirmingAction.timeoutId = null; }
|
|
4947
|
+
document.dispatchEvent(new CustomEvent('job-list-refresh'));
|
|
4948
|
+
})
|
|
4949
|
+
.catch(() => { $store.confirmingAction.isStopping = false; });
|
|
4950
|
+
} else {
|
|
4951
|
+
if ($store.confirmingAction.timeoutId) { clearTimeout($store.confirmingAction.timeoutId); $store.confirmingAction.timeoutId = null; }
|
|
4952
|
+
$store.confirmingAction.type = 'job-cancel';
|
|
4953
|
+
$store.confirmingAction.id = '${job.id}';
|
|
4954
|
+
$store.confirmingAction.isStopping = false;
|
|
4955
|
+
$store.confirmingAction.timeoutId = setTimeout(() => {
|
|
4956
|
+
$store.confirmingAction.type = null;
|
|
4957
|
+
$store.confirmingAction.id = null;
|
|
4958
|
+
$store.confirmingAction.isStopping = false;
|
|
4959
|
+
$store.confirmingAction.timeoutId = null;
|
|
4960
|
+
}, 3000);
|
|
4961
|
+
}
|
|
4962
|
+
`,
|
|
4963
|
+
"x-bind:disabled": `$store.confirmingAction.type === 'job-cancel' && $store.confirmingAction.id === '${job.id}' && $store.confirmingAction.isStopping`,
|
|
4964
|
+
children: [
|
|
4965
|
+
/* @__PURE__ */ jsxs(
|
|
4966
|
+
"span",
|
|
4967
|
+
{
|
|
4968
|
+
"x-show": `$store.confirmingAction.type !== 'job-cancel' || $store.confirmingAction.id !== '${job.id}' || $store.confirmingAction.isStopping`,
|
|
4969
|
+
children: [
|
|
4970
|
+
/* @__PURE__ */ jsx(
|
|
4971
|
+
"svg",
|
|
4972
|
+
{
|
|
4973
|
+
class: "w-4 h-4",
|
|
4974
|
+
"aria-hidden": "true",
|
|
4975
|
+
fill: "currentColor",
|
|
4976
|
+
viewBox: "0 0 20 20",
|
|
4977
|
+
children: /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "10", height: "10", rx: "2" })
|
|
4978
|
+
}
|
|
4979
|
+
),
|
|
4980
|
+
/* @__PURE__ */ jsx("span", { class: "sr-only", children: "Stop job" })
|
|
4981
|
+
]
|
|
4982
|
+
}
|
|
4983
|
+
),
|
|
4984
|
+
/* @__PURE__ */ jsx(
|
|
4985
|
+
"span",
|
|
4986
|
+
{
|
|
4987
|
+
"x-show": `$store.confirmingAction.type === 'job-cancel' && $store.confirmingAction.id === '${job.id}' && !$store.confirmingAction.isStopping`,
|
|
4988
|
+
class: "px-2",
|
|
4989
|
+
children: "Cancel?"
|
|
4990
|
+
}
|
|
4991
|
+
),
|
|
4992
|
+
/* @__PURE__ */ jsxs(
|
|
4993
|
+
"span",
|
|
4994
|
+
{
|
|
4995
|
+
"x-show": `$store.confirmingAction.type === 'job-cancel' && $store.confirmingAction.id === '${job.id}' && $store.confirmingAction.isStopping`,
|
|
4996
|
+
children: [
|
|
4997
|
+
/* @__PURE__ */ jsx(LoadingSpinner, {}),
|
|
4998
|
+
/* @__PURE__ */ jsx("span", { class: "sr-only", children: "Stopping..." })
|
|
4999
|
+
]
|
|
5000
|
+
}
|
|
5001
|
+
)
|
|
5002
|
+
]
|
|
5003
|
+
}
|
|
5004
|
+
)
|
|
5005
|
+
] }),
|
|
4738
5006
|
job.error && // Keep the error badge for clarity if an error occurred
|
|
4739
|
-
/* @__PURE__ */ jsx("span", { class: "bg-red-100 text-red-800 text-xs font-medium
|
|
5007
|
+
/* @__PURE__ */ jsx("span", { class: "bg-red-100 text-red-800 text-xs font-medium px-1.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300", children: "Error" })
|
|
4740
5008
|
] })
|
|
4741
5009
|
] }) })
|
|
4742
5010
|
);
|
|
4743
|
-
const JobList = ({ jobs }) => /* @__PURE__ */ jsx("div", { class: "space-y-2", children: jobs.length === 0 ? /* @__PURE__ */ jsx("p", { class: "text-center text-gray-500 dark:text-gray-400", children: "No pending jobs." }) : jobs.map((job) => /* @__PURE__ */ jsx(JobItem, { job })) });
|
|
5011
|
+
const JobList = ({ jobs }) => /* @__PURE__ */ jsx("div", { id: "job-list", class: "space-y-2", children: jobs.length === 0 ? /* @__PURE__ */ jsx("p", { class: "text-center text-gray-500 dark:text-gray-400", children: "No pending jobs." }) : jobs.map((job) => /* @__PURE__ */ jsx(JobItem, { job })) });
|
|
4744
5012
|
function registerJobListRoutes(server, listJobsTool) {
|
|
4745
5013
|
server.get("/api/jobs", async () => {
|
|
4746
5014
|
const result = await listJobsTool.execute({});
|
|
@@ -4899,7 +5167,7 @@ const ScrapeFormContent = () => /* @__PURE__ */ jsxs("div", { class: "mt-4 p-4 b
|
|
|
4899
5167
|
"hx-target": "#job-response",
|
|
4900
5168
|
"hx-swap": "innerHTML",
|
|
4901
5169
|
class: "space-y-2",
|
|
4902
|
-
"x-data": "{\n url: '',\n hasPath: false,\n checkUrlPath() {\n try {\n const url = new URL(this.url);\n this.hasPath = url.pathname !== '/' && url.pathname !== '';\n } catch (e) {\n this.hasPath = false;\n }\n }\n }",
|
|
5170
|
+
"x-data": "{\n url: '',\n hasPath: false,\n headers: [],\n checkUrlPath() {\n try {\n const url = new URL(this.url);\n this.hasPath = url.pathname !== '/' && url.pathname !== '';\n } catch (e) {\n this.hasPath = false;\n }\n }\n }",
|
|
4903
5171
|
children: [
|
|
4904
5172
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
4905
5173
|
/* @__PURE__ */ jsxs("div", { class: "flex items-center", children: [
|
|
@@ -5010,7 +5278,7 @@ const ScrapeFormContent = () => /* @__PURE__ */ jsxs("div", { class: "mt-4 p-4 b
|
|
|
5010
5278
|
] }),
|
|
5011
5279
|
/* @__PURE__ */ jsxs("details", { class: "bg-gray-50 dark:bg-gray-900 p-2 rounded-md", children: [
|
|
5012
5280
|
/* @__PURE__ */ jsx("summary", { class: "cursor-pointer text-sm font-medium text-gray-600 dark:text-gray-400", children: "Advanced Options" }),
|
|
5013
|
-
/* @__PURE__ */ jsxs("div", { class: "mt-2 space-y-2", children: [
|
|
5281
|
+
/* @__PURE__ */ jsxs("div", { class: "mt-2 space-y-2", "x-data": "{ headers: [] }", children: [
|
|
5014
5282
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
5015
5283
|
/* @__PURE__ */ jsxs("div", { class: "flex items-center", children: [
|
|
5016
5284
|
/* @__PURE__ */ jsx(
|
|
@@ -5178,6 +5446,63 @@ const ScrapeFormContent = () => /* @__PURE__ */ jsxs("div", { class: "mt-4 p-4 b
|
|
|
5178
5446
|
}
|
|
5179
5447
|
)
|
|
5180
5448
|
] }),
|
|
5449
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
5450
|
+
/* @__PURE__ */ jsxs("div", { class: "flex items-center mb-1", children: [
|
|
5451
|
+
/* @__PURE__ */ jsx("label", { class: "block text-sm font-medium text-gray-700 dark:text-gray-300", children: "Custom HTTP Headers" }),
|
|
5452
|
+
/* @__PURE__ */ jsx(Tooltip, { text: "Add custom HTTP headers (e.g., for authentication). These will be sent with every HTTP request." })
|
|
5453
|
+
] }),
|
|
5454
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
5455
|
+
/* @__PURE__ */ jsx("template", { "x-for": "(header, idx) in headers", children: /* @__PURE__ */ jsxs("div", { class: "flex space-x-2 mb-1", children: [
|
|
5456
|
+
/* @__PURE__ */ jsx(
|
|
5457
|
+
"input",
|
|
5458
|
+
{
|
|
5459
|
+
type: "text",
|
|
5460
|
+
class: "w-1/3 px-2 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-xs",
|
|
5461
|
+
placeholder: "Header Name",
|
|
5462
|
+
"x-model": "header.name",
|
|
5463
|
+
required: true
|
|
5464
|
+
}
|
|
5465
|
+
),
|
|
5466
|
+
/* @__PURE__ */ jsx("span", { class: "text-gray-500", children: ":" }),
|
|
5467
|
+
/* @__PURE__ */ jsx(
|
|
5468
|
+
"input",
|
|
5469
|
+
{
|
|
5470
|
+
type: "text",
|
|
5471
|
+
class: "w-1/2 px-2 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-xs",
|
|
5472
|
+
placeholder: "Header Value",
|
|
5473
|
+
"x-model": "header.value",
|
|
5474
|
+
required: true
|
|
5475
|
+
}
|
|
5476
|
+
),
|
|
5477
|
+
/* @__PURE__ */ jsx(
|
|
5478
|
+
"button",
|
|
5479
|
+
{
|
|
5480
|
+
type: "button",
|
|
5481
|
+
class: "text-red-500 hover:text-red-700 text-xs",
|
|
5482
|
+
"x-on:click": "headers.splice(idx, 1)",
|
|
5483
|
+
children: "Remove"
|
|
5484
|
+
}
|
|
5485
|
+
),
|
|
5486
|
+
/* @__PURE__ */ jsx(
|
|
5487
|
+
"input",
|
|
5488
|
+
{
|
|
5489
|
+
type: "hidden",
|
|
5490
|
+
name: "header[]",
|
|
5491
|
+
"x-bind:value": "header.name && header.value ? header.name + ':' + header.value : ''"
|
|
5492
|
+
}
|
|
5493
|
+
)
|
|
5494
|
+
] }) }),
|
|
5495
|
+
/* @__PURE__ */ jsx(
|
|
5496
|
+
"button",
|
|
5497
|
+
{
|
|
5498
|
+
type: "button",
|
|
5499
|
+
class: "mt-1 px-2 py-0.5 bg-indigo-100 dark:bg-indigo-900 text-indigo-700 dark:text-indigo-200 rounded text-xs",
|
|
5500
|
+
"x-on:click": "headers.push({ name: '', value: '' })",
|
|
5501
|
+
children: "+ Add Header"
|
|
5502
|
+
}
|
|
5503
|
+
)
|
|
5504
|
+
] })
|
|
5505
|
+
] }),
|
|
5181
5506
|
/* @__PURE__ */ jsxs("div", { class: "flex items-center", children: [
|
|
5182
5507
|
/* @__PURE__ */ jsx(
|
|
5183
5508
|
"input",
|
|
@@ -5247,6 +5572,19 @@ function registerNewJobRoutes(server, scrapeTool) {
|
|
|
5247
5572
|
let parsePatterns = function(input) {
|
|
5248
5573
|
if (!input) return void 0;
|
|
5249
5574
|
return input.split(/\n|,/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5575
|
+
}, parseHeaders = function(input) {
|
|
5576
|
+
if (!input) return void 0;
|
|
5577
|
+
const arr = Array.isArray(input) ? input : [input];
|
|
5578
|
+
const headers = {};
|
|
5579
|
+
for (const entry of arr) {
|
|
5580
|
+
const idx = entry.indexOf(":");
|
|
5581
|
+
if (idx > 0) {
|
|
5582
|
+
const name2 = entry.slice(0, idx).trim();
|
|
5583
|
+
const value = entry.slice(idx + 1).trim();
|
|
5584
|
+
if (name2) headers[name2] = value;
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
5250
5588
|
};
|
|
5251
5589
|
if (!body.url || !body.library) {
|
|
5252
5590
|
reply.status(400);
|
|
@@ -5275,7 +5613,9 @@ function registerNewJobRoutes(server, scrapeTool) {
|
|
|
5275
5613
|
followRedirects: body.followRedirects === "on",
|
|
5276
5614
|
ignoreErrors: body.ignoreErrors === "on",
|
|
5277
5615
|
includePatterns: parsePatterns(body.includePatterns),
|
|
5278
|
-
excludePatterns: parsePatterns(body.excludePatterns)
|
|
5616
|
+
excludePatterns: parsePatterns(body.excludePatterns),
|
|
5617
|
+
headers: parseHeaders(body["header[]"])
|
|
5618
|
+
// <-- propagate custom headers from web UI
|
|
5279
5619
|
}
|
|
5280
5620
|
};
|
|
5281
5621
|
const result = await scrapeTool.execute(scrapeOptions);
|
|
@@ -5314,36 +5654,6 @@ function registerNewJobRoutes(server, scrapeTool) {
|
|
|
5314
5654
|
}
|
|
5315
5655
|
);
|
|
5316
5656
|
}
|
|
5317
|
-
const LoadingSpinner = () => /* @__PURE__ */ jsxs(
|
|
5318
|
-
"svg",
|
|
5319
|
-
{
|
|
5320
|
-
class: "animate-spin h-4 w-4 text-white",
|
|
5321
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
5322
|
-
fill: "none",
|
|
5323
|
-
viewBox: "0 0 24 24",
|
|
5324
|
-
children: [
|
|
5325
|
-
/* @__PURE__ */ jsx(
|
|
5326
|
-
"circle",
|
|
5327
|
-
{
|
|
5328
|
-
class: "opacity-25",
|
|
5329
|
-
cx: "12",
|
|
5330
|
-
cy: "12",
|
|
5331
|
-
r: "10",
|
|
5332
|
-
stroke: "currentColor",
|
|
5333
|
-
"stroke-width": "4"
|
|
5334
|
-
}
|
|
5335
|
-
),
|
|
5336
|
-
/* @__PURE__ */ jsx(
|
|
5337
|
-
"path",
|
|
5338
|
-
{
|
|
5339
|
-
class: "opacity-75",
|
|
5340
|
-
fill: "currentColor",
|
|
5341
|
-
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
5342
|
-
}
|
|
5343
|
-
)
|
|
5344
|
-
]
|
|
5345
|
-
}
|
|
5346
|
-
);
|
|
5347
5657
|
const VersionDetailsRow = ({
|
|
5348
5658
|
version: version2,
|
|
5349
5659
|
libraryName,
|
|
@@ -5352,7 +5662,7 @@ const VersionDetailsRow = ({
|
|
|
5352
5662
|
}) => {
|
|
5353
5663
|
const indexedDate = version2.indexedAt ? new Date(version2.indexedAt).toLocaleDateString() : "N/A";
|
|
5354
5664
|
const versionLabel = version2.version || "Unversioned";
|
|
5355
|
-
const versionParam = version2.version || "
|
|
5665
|
+
const versionParam = version2.version || "";
|
|
5356
5666
|
const sanitizedLibraryName = libraryName.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
5357
5667
|
const sanitizedVersionParam = versionParam.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
5358
5668
|
const rowId = `row-${sanitizedLibraryName}-${sanitizedVersionParam}`;
|
|
@@ -5397,43 +5707,80 @@ const VersionDetailsRow = ({
|
|
|
5397
5707
|
type: "button",
|
|
5398
5708
|
class: "ml-2 font-medium rounded-lg text-sm p-1 text-center inline-flex items-center transition-colors duration-150 ease-in-out",
|
|
5399
5709
|
title: "Remove this version",
|
|
5400
|
-
"x-data": "{
|
|
5401
|
-
"x-bind:class":
|
|
5402
|
-
"x-bind:disabled":
|
|
5403
|
-
"x-on:click":
|
|
5710
|
+
"x-data": "{}",
|
|
5711
|
+
"x-bind:class": `$store.confirmingAction.type === 'version-delete' && $store.confirmingAction.id === '${libraryName}:${versionParam}' ? '${confirmingStateClasses}' : '${defaultStateClasses}'`,
|
|
5712
|
+
"x-bind:disabled": `$store.confirmingAction.type === 'version-delete' && $store.confirmingAction.id === '${libraryName}:${versionParam}' && $store.confirmingAction.isDeleting`,
|
|
5713
|
+
"x-on:click": `
|
|
5714
|
+
if ($store.confirmingAction.type === 'version-delete' && $store.confirmingAction.id === '${libraryName}:${versionParam}') {
|
|
5715
|
+
$store.confirmingAction.isDeleting = true;
|
|
5716
|
+
$el.dispatchEvent(new CustomEvent('confirmed-delete', { bubbles: true }));
|
|
5717
|
+
} else {
|
|
5718
|
+
if ($store.confirmingAction.timeoutId) { clearTimeout($store.confirmingAction.timeoutId); $store.confirmingAction.timeoutId = null; }
|
|
5719
|
+
$store.confirmingAction.type = 'version-delete';
|
|
5720
|
+
$store.confirmingAction.id = '${libraryName}:${versionParam}';
|
|
5721
|
+
$store.confirmingAction.isDeleting = false;
|
|
5722
|
+
$store.confirmingAction.timeoutId = setTimeout(() => {
|
|
5723
|
+
$store.confirmingAction.type = null;
|
|
5724
|
+
$store.confirmingAction.id = null;
|
|
5725
|
+
$store.confirmingAction.isDeleting = false;
|
|
5726
|
+
$store.confirmingAction.timeoutId = null;
|
|
5727
|
+
}, 3000);
|
|
5728
|
+
}
|
|
5729
|
+
`,
|
|
5404
5730
|
"hx-delete": `/api/libraries/${encodeURIComponent(libraryName)}/versions/${encodeURIComponent(versionParam)}`,
|
|
5405
5731
|
"hx-target": `#${rowId}`,
|
|
5406
5732
|
"hx-swap": "outerHTML",
|
|
5407
5733
|
"hx-trigger": "confirmed-delete",
|
|
5408
5734
|
children: [
|
|
5409
|
-
/* @__PURE__ */ jsxs(
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
{
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
fill: "none",
|
|
5417
|
-
viewBox: "0 0 18 20",
|
|
5418
|
-
children: /* @__PURE__ */ jsx(
|
|
5419
|
-
"path",
|
|
5735
|
+
/* @__PURE__ */ jsxs(
|
|
5736
|
+
"span",
|
|
5737
|
+
{
|
|
5738
|
+
"x-show": `!($store.confirmingAction.type === 'version-delete' && $store.confirmingAction.id === '${libraryName}:${versionParam}' && $store.confirmingAction.isDeleting)`,
|
|
5739
|
+
children: [
|
|
5740
|
+
/* @__PURE__ */ jsx(
|
|
5741
|
+
"svg",
|
|
5420
5742
|
{
|
|
5421
|
-
|
|
5422
|
-
"
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5743
|
+
class: "w-4 h-4",
|
|
5744
|
+
"aria-hidden": "true",
|
|
5745
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
5746
|
+
fill: "none",
|
|
5747
|
+
viewBox: "0 0 18 20",
|
|
5748
|
+
children: /* @__PURE__ */ jsx(
|
|
5749
|
+
"path",
|
|
5750
|
+
{
|
|
5751
|
+
stroke: "currentColor",
|
|
5752
|
+
"stroke-linecap": "round",
|
|
5753
|
+
"stroke-linejoin": "round",
|
|
5754
|
+
"stroke-width": "2",
|
|
5755
|
+
d: "M1 5h16M7 8v8m4-8v8M7 1h4a1 1 0 0 1 1 1v3H6V2a1 1 0 0 1-1-1ZM3 5h12v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5Z"
|
|
5756
|
+
}
|
|
5757
|
+
)
|
|
5426
5758
|
}
|
|
5427
|
-
)
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
/* @__PURE__ */
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5759
|
+
),
|
|
5760
|
+
/* @__PURE__ */ jsx("span", { class: "sr-only", children: "Remove version" })
|
|
5761
|
+
]
|
|
5762
|
+
}
|
|
5763
|
+
),
|
|
5764
|
+
/* @__PURE__ */ jsxs(
|
|
5765
|
+
"span",
|
|
5766
|
+
{
|
|
5767
|
+
"x-show": `$store.confirmingAction.type === 'version-delete' && $store.confirmingAction.id === '${libraryName}:${versionParam}' && !$store.confirmingAction.isDeleting`,
|
|
5768
|
+
children: [
|
|
5769
|
+
"Confirm?",
|
|
5770
|
+
/* @__PURE__ */ jsx("span", { class: "sr-only", children: "Confirm delete" })
|
|
5771
|
+
]
|
|
5772
|
+
}
|
|
5773
|
+
),
|
|
5774
|
+
/* @__PURE__ */ jsxs(
|
|
5775
|
+
"span",
|
|
5776
|
+
{
|
|
5777
|
+
"x-show": `$store.confirmingAction.type === 'version-delete' && $store.confirmingAction.id === '${libraryName}:${versionParam}' && $store.confirmingAction.isDeleting`,
|
|
5778
|
+
children: [
|
|
5779
|
+
/* @__PURE__ */ jsx(LoadingSpinner, {}),
|
|
5780
|
+
/* @__PURE__ */ jsx("span", { class: "sr-only", children: "Loading..." })
|
|
5781
|
+
]
|
|
5782
|
+
}
|
|
5783
|
+
)
|
|
5437
5784
|
]
|
|
5438
5785
|
}
|
|
5439
5786
|
)
|
|
@@ -5446,14 +5793,7 @@ const LibraryDetailCard = ({ library }) => (
|
|
|
5446
5793
|
// Use Flowbite Card structure with updated padding and border, and white background
|
|
5447
5794
|
/* @__PURE__ */ jsxs("div", { class: "block p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-300 dark:border-gray-600 mb-4", children: [
|
|
5448
5795
|
/* @__PURE__ */ jsx("h3", { class: "text-lg font-medium text-gray-900 dark:text-white mb-1", children: /* @__PURE__ */ jsx("span", { safe: true, children: library.name }) }),
|
|
5449
|
-
/* @__PURE__ */ jsx("div", { class: "mt-1", children: library.versions.length > 0 ? library.versions.
|
|
5450
|
-
if (!a.version) return -1;
|
|
5451
|
-
if (!b.version) return 1;
|
|
5452
|
-
return semver__default.compare(
|
|
5453
|
-
semver__default.coerce(b.version)?.version ?? "0.0.0",
|
|
5454
|
-
semver__default.coerce(a.version)?.version ?? "0.0.0"
|
|
5455
|
-
);
|
|
5456
|
-
}).map((version2) => /* @__PURE__ */ jsx(
|
|
5796
|
+
/* @__PURE__ */ jsx("div", { class: "mt-1", children: library.versions.length > 0 ? library.versions.map((version2) => /* @__PURE__ */ jsx(
|
|
5457
5797
|
VersionDetailsRow,
|
|
5458
5798
|
{
|
|
5459
5799
|
libraryName: library.name,
|
|
@@ -5467,14 +5807,6 @@ const LibraryDetailCard = ({ library }) => (
|
|
|
5467
5807
|
] })
|
|
5468
5808
|
);
|
|
5469
5809
|
const LibrarySearchCard = ({ library }) => {
|
|
5470
|
-
const sortedVersions = library.versions.sort((a, b) => {
|
|
5471
|
-
if (!a.version) return -1;
|
|
5472
|
-
if (!b.version) return 1;
|
|
5473
|
-
return semver__default.compare(
|
|
5474
|
-
semver__default.coerce(b.version)?.version ?? "0.0.0",
|
|
5475
|
-
semver__default.coerce(a.version)?.version ?? "0.0.0"
|
|
5476
|
-
);
|
|
5477
|
-
});
|
|
5478
5810
|
return /* @__PURE__ */ jsxs("div", { class: "block p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-300 dark:border-gray-600 mb-4", children: [
|
|
5479
5811
|
/* @__PURE__ */ jsxs("h2", { class: "text-xl font-semibold mb-2 text-gray-900 dark:text-white", safe: true, children: [
|
|
5480
5812
|
"Search ",
|
|
@@ -5498,7 +5830,7 @@ const LibrarySearchCard = ({ library }) => {
|
|
|
5498
5830
|
children: [
|
|
5499
5831
|
/* @__PURE__ */ jsx("option", { value: "", children: "Latest" }),
|
|
5500
5832
|
" ",
|
|
5501
|
-
|
|
5833
|
+
library.versions.map((version2) => /* @__PURE__ */ jsx("option", { value: version2.version || "unversioned", safe: true, children: version2.version || "Unversioned" }))
|
|
5502
5834
|
]
|
|
5503
5835
|
}
|
|
5504
5836
|
),
|
|
@@ -5636,14 +5968,7 @@ const LibraryItem = ({ library }) => (
|
|
|
5636
5968
|
children: /* @__PURE__ */ jsx("span", { safe: true, children: library.name })
|
|
5637
5969
|
}
|
|
5638
5970
|
) }),
|
|
5639
|
-
/* @__PURE__ */ jsx("div", { class: "mt-1", children: library.versions.length > 0 ? library.versions.
|
|
5640
|
-
if (!a.version) return -1;
|
|
5641
|
-
if (!b.version) return 1;
|
|
5642
|
-
return semver__default.compare(
|
|
5643
|
-
semver__default.coerce(b.version)?.version ?? "0.0.0",
|
|
5644
|
-
semver__default.coerce(a.version)?.version ?? "0.0.0"
|
|
5645
|
-
);
|
|
5646
|
-
}).map((version2) => /* @__PURE__ */ jsx(VersionDetailsRow, { libraryName: library.name, version: version2 })) : (
|
|
5971
|
+
/* @__PURE__ */ jsx("div", { class: "mt-1", children: library.versions.length > 0 ? library.versions.map((version2) => /* @__PURE__ */ jsx(VersionDetailsRow, { libraryName: library.name, version: version2 })) : (
|
|
5647
5972
|
// Display message if no versions are indexed
|
|
5648
5973
|
/* @__PURE__ */ jsx("p", { class: "text-sm text-gray-500 dark:text-gray-400 italic", children: "No versions indexed." })
|
|
5649
5974
|
) })
|
|
@@ -5690,8 +6015,10 @@ async function startWebServer(port, docService, pipelineManager) {
|
|
|
5690
6015
|
const listLibrariesTool = new ListLibrariesTool(docService);
|
|
5691
6016
|
const listJobsTool = new ListJobsTool(pipelineManager);
|
|
5692
6017
|
const scrapeTool = new ScrapeTool(docService, pipelineManager);
|
|
5693
|
-
const removeTool = new RemoveTool(docService);
|
|
6018
|
+
const removeTool = new RemoveTool(docService, pipelineManager);
|
|
5694
6019
|
const searchTool = new SearchTool(docService);
|
|
6020
|
+
const cancelJobTool = new CancelJobTool(pipelineManager);
|
|
6021
|
+
const clearCompletedJobsTool = new ClearCompletedJobsTool(pipelineManager);
|
|
5695
6022
|
await server.register(fastifyStatic, {
|
|
5696
6023
|
// Use project root to construct absolute path to public directory
|
|
5697
6024
|
root: path.join(getProjectRoot(), "public"),
|
|
@@ -5702,6 +6029,8 @@ async function startWebServer(port, docService, pipelineManager) {
|
|
|
5702
6029
|
registerIndexRoute(server);
|
|
5703
6030
|
registerJobListRoutes(server, listJobsTool);
|
|
5704
6031
|
registerNewJobRoutes(server, scrapeTool);
|
|
6032
|
+
registerCancelJobRoute(server, cancelJobTool);
|
|
6033
|
+
registerClearCompletedJobsRoute(server, clearCompletedJobsTool);
|
|
5705
6034
|
registerLibrariesRoutes(server, listLibrariesTool, removeTool);
|
|
5706
6035
|
registerLibraryDetailRoutes(server, listLibrariesTool, searchTool);
|
|
5707
6036
|
try {
|
|
@@ -5825,7 +6154,7 @@ async function main() {
|
|
|
5825
6154
|
}
|
|
5826
6155
|
try {
|
|
5827
6156
|
const program = new Command();
|
|
5828
|
-
program.name("docs-mcp-server").description("Unified CLI, MCP Server, and Web Interface for Docs MCP Server.").version(packageJson.version).option("--verbose", "Enable verbose (debug) logging", false).option("--silent", "Disable all logging except errors", false).enablePositionalOptions().option(
|
|
6157
|
+
program.name("docs-mcp-server").description("Unified CLI, MCP Server, and Web Interface for Docs MCP Server.").version(packageJson.version).option("--verbose", "Enable verbose (debug) logging", false).option("--silent", "Disable all logging except errors", false).enablePositionalOptions().showHelpAfterError(true).option(
|
|
5829
6158
|
"--protocol <type>",
|
|
5830
6159
|
"Protocol for MCP server (stdio or http)",
|
|
5831
6160
|
DEFAULT_PROTOCOL
|
|
@@ -5910,6 +6239,11 @@ async function main() {
|
|
|
5910
6239
|
"Glob or regex pattern for URLs to exclude (can be specified multiple times, takes precedence over include). Regex patterns must be wrapped in slashes, e.g. /pattern/.",
|
|
5911
6240
|
(val, prev = []) => prev.concat([val]),
|
|
5912
6241
|
[]
|
|
6242
|
+
).option(
|
|
6243
|
+
"--header <name:value>",
|
|
6244
|
+
"Custom HTTP header to send with each request (can be specified multiple times)",
|
|
6245
|
+
(val, prev = []) => prev.concat([val]),
|
|
6246
|
+
[]
|
|
5913
6247
|
).action(async (library, url, options) => {
|
|
5914
6248
|
commandExecuted = true;
|
|
5915
6249
|
const docService = new DocumentManagementService();
|
|
@@ -5919,6 +6253,17 @@ async function main() {
|
|
|
5919
6253
|
pipelineManager = new PipelineManager(docService);
|
|
5920
6254
|
await pipelineManager.start();
|
|
5921
6255
|
const scrapeTool = new ScrapeTool(docService, pipelineManager);
|
|
6256
|
+
const headers = {};
|
|
6257
|
+
if (Array.isArray(options.header)) {
|
|
6258
|
+
for (const entry of options.header) {
|
|
6259
|
+
const idx = entry.indexOf(":");
|
|
6260
|
+
if (idx > 0) {
|
|
6261
|
+
const name2 = entry.slice(0, idx).trim();
|
|
6262
|
+
const value = entry.slice(idx + 1).trim();
|
|
6263
|
+
if (name2) headers[name2] = value;
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
5922
6267
|
const result = await scrapeTool.execute({
|
|
5923
6268
|
url,
|
|
5924
6269
|
library,
|
|
@@ -5932,7 +6277,8 @@ async function main() {
|
|
|
5932
6277
|
followRedirects: options.followRedirects,
|
|
5933
6278
|
scrapeMode: options.scrapeMode,
|
|
5934
6279
|
includePatterns: Array.isArray(options.includePattern) && options.includePattern.length > 0 ? options.includePattern : void 0,
|
|
5935
|
-
excludePatterns: Array.isArray(options.excludePattern) && options.excludePattern.length > 0 ? options.excludePattern : void 0
|
|
6280
|
+
excludePatterns: Array.isArray(options.excludePattern) && options.excludePattern.length > 0 ? options.excludePattern : void 0,
|
|
6281
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0
|
|
5936
6282
|
}
|
|
5937
6283
|
});
|
|
5938
6284
|
if ("pagesScraped" in result)
|
|
@@ -6034,13 +6380,30 @@ async function main() {
|
|
|
6034
6380
|
return value;
|
|
6035
6381
|
},
|
|
6036
6382
|
ScrapeMode.Auto
|
|
6383
|
+
).option(
|
|
6384
|
+
"--header <name:value>",
|
|
6385
|
+
"Custom HTTP header to send with the request (can be specified multiple times)",
|
|
6386
|
+
(val, prev = []) => prev.concat([val]),
|
|
6387
|
+
[]
|
|
6037
6388
|
).action(async (url, options) => {
|
|
6038
6389
|
commandExecuted = true;
|
|
6390
|
+
const headers = {};
|
|
6391
|
+
if (Array.isArray(options.header)) {
|
|
6392
|
+
for (const entry of options.header) {
|
|
6393
|
+
const idx = entry.indexOf(":");
|
|
6394
|
+
if (idx > 0) {
|
|
6395
|
+
const name2 = entry.slice(0, idx).trim();
|
|
6396
|
+
const value = entry.slice(idx + 1).trim();
|
|
6397
|
+
if (name2) headers[name2] = value;
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6039
6401
|
const fetchUrlTool = new FetchUrlTool(new HttpFetcher(), new FileFetcher());
|
|
6040
6402
|
const content = await fetchUrlTool.execute({
|
|
6041
6403
|
url,
|
|
6042
6404
|
followRedirects: options.followRedirects,
|
|
6043
|
-
scrapeMode: options.scrapeMode
|
|
6405
|
+
scrapeMode: options.scrapeMode,
|
|
6406
|
+
headers: Object.keys(headers).length > 0 ? headers : void 0
|
|
6044
6407
|
});
|
|
6045
6408
|
console.log(content);
|
|
6046
6409
|
});
|