@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/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.0";
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
- let credentials = null;
498
- let origin = null;
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
- if (credentials && origin && reqOrigin === origin && !route.request().headers().authorization) {
541
- const basic = Buffer.from(
542
- `${credentials.username}:${credentials.password}`
543
- ).toString("base64");
544
- const headers = {
545
- ...route.request().headers(),
546
- Authorization: `Basic ${basic}`
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 shortly after scraping finished");
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
- await job.completionPromise;
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
- job.error = error instanceof CancellationError ? error : new CancellationError("Job cancelled by signal");
2965
- logger.info(`🚫 Job execution cancelled: ${jobId}: ${job.error.message}`);
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(job.error);
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__ */ jsx("h2", { class: "text-xl font-semibold mb-2 text-gray-900 dark:text-white", children: "Job Queue" }),
4680
- /* @__PURE__ */ jsx("div", { id: "jobQueue", "hx-get": "/api/jobs", "hx-trigger": "load, every 1s", children: /* @__PURE__ */ jsxs("div", { class: "animate-pulse", children: [
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: "indexedDocs",
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__ */ jsx(
4732
- "span",
4733
- {
4734
- class: `px-1.5 py-0.5 text-xs font-medium me-2 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"}`,
4735
- children: job.status
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 me-2 px-1.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300", children: "Error" })
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 || "unversioned";
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": "{ confirming: false, isDeleting: false, timeoutId: null }",
5401
- "x-bind:class": `confirming ? "${confirmingStateClasses}" : "${defaultStateClasses}"`,
5402
- "x-bind:disabled": "isDeleting",
5403
- "x-on:click": "\n if (confirming) {\n clearTimeout(timeoutId);\n timeoutId = null;\n isDeleting = true; // Set deleting state directly\n // Dispatch a standard browser event instead of calling htmx directly\n $el.dispatchEvent(new CustomEvent('confirmed-delete', { bubbles: true }));\n } else {\n confirming = true;\n timeoutId = setTimeout(() => { confirming = false; timeoutId = null; }, 3000);\n }\n ",
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("span", { "x-show": "!confirming && !isDeleting", children: [
5410
- /* @__PURE__ */ jsx(
5411
- "svg",
5412
- {
5413
- class: "w-4 h-4",
5414
- "aria-hidden": "true",
5415
- xmlns: "http://www.w3.org/2000/svg",
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
- stroke: "currentColor",
5422
- "stroke-linecap": "round",
5423
- "stroke-linejoin": "round",
5424
- "stroke-width": "2",
5425
- 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"
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
- /* @__PURE__ */ jsx("span", { class: "sr-only", children: "Remove version" })
5431
- ] }),
5432
- /* @__PURE__ */ jsx("span", { "x-show": "confirming && !isDeleting", children: "Confirm?" }),
5433
- /* @__PURE__ */ jsxs("span", { "x-show": "isDeleting", children: [
5434
- /* @__PURE__ */ jsx(LoadingSpinner, {}),
5435
- /* @__PURE__ */ jsx("span", { class: "sr-only", children: "Loading..." })
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.sort((a, b) => {
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
- sortedVersions.map((version2) => /* @__PURE__ */ jsx("option", { value: version2.version || "unversioned", safe: true, children: version2.version || "Unversioned" }))
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.sort((a, b) => {
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
  });