crawlforge-mcp-server 5.0.0 → 5.0.2
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/CLAUDE.md +1 -1
- package/package.json +1 -1
- package/server.js +1 -1
- package/src/cli/commands/init.js +11 -5
- package/src/cli/commands/stealth.js +3 -2
- package/src/core/LLMsTxtAnalyzer.js +3 -1
- package/src/core/MonitorScheduler.js +13 -4
- package/src/core/SnapshotManager.js +8 -3
- package/src/core/analysis/ContentAnalyzer.js +3 -1
- package/src/core/crawlers/BFSCrawler.js +19 -12
- package/src/schemas/toolOutputSchemas.js +1 -1
- package/src/tools/extract/extractStructured.js +2 -1
- package/src/tools/llmstxt/generateLLMsTxt.js +12 -7
- package/src/tools/templates/TemplateRegistry.js +6 -2
- package/src/tools/tracking/trackChanges/index.js +36 -3
package/CLAUDE.md
CHANGED
|
@@ -62,7 +62,7 @@ These guidelines are working if: fewer unnecessary changes in diffs, fewer rewri
|
|
|
62
62
|
|
|
63
63
|
CrawlForge MCP Server - A professional MCP (Model Context Protocol) server providing 27 web scraping, crawling, and content processing tools (5 inline + 22 advanced).
|
|
64
64
|
|
|
65
|
-
**Current Version:** 5.0.
|
|
65
|
+
**Current Version:** 5.0.2
|
|
66
66
|
|
|
67
67
|
## Development Commands
|
|
68
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
|
|
5
5
|
"description": "CrawlForge MCP Server - Professional Model Context Protocol server with 27 web scraping, crawling, deep-research, and autonomous-extraction tools. Returns clean Markdown and structured JSON for Claude, Cursor, and any MCP client. Defaults to local Ollama for LLM extraction (no API key needed); OpenAI/Anthropic available as opt-in. Includes a unified multi-format scrape tool, an autonomous agent, pre-built site templates, and Camoufox stealth browsing.",
|
|
6
6
|
"main": "server.js",
|
package/server.js
CHANGED
|
@@ -99,7 +99,7 @@ const taskStore = createTaskStore({ logger });
|
|
|
99
99
|
// Create the server
|
|
100
100
|
const server = new McpServer({
|
|
101
101
|
name: "crawlforge",
|
|
102
|
-
version: "5.0.
|
|
102
|
+
version: "5.0.2",
|
|
103
103
|
description: "Production-ready MCP server with 27 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, real Google SERP rank tracking, change tracking, local-LLM extraction via Ollama, unified multi-format scrape, and autonomous agent tool.",
|
|
104
104
|
homepage: "https://www.crawlforge.dev",
|
|
105
105
|
icon: "https://www.crawlforge.dev/icon.png",
|
package/src/cli/commands/init.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { install, installHook } from '../../skills/installer.js';
|
|
7
|
+
import { isCreatorModeVerified } from '../../core/creatorMode.js';
|
|
7
8
|
|
|
8
9
|
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
9
10
|
|
|
@@ -16,8 +17,8 @@ function loadStoredApiKey() {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function mcpStanza(apiKey) {
|
|
20
|
-
const stanza = { command: 'npx', args: ['-y', 'crawlforge@latest', 'mcp'] };
|
|
20
|
+
export function mcpStanza(apiKey) {
|
|
21
|
+
const stanza = { command: 'npx', args: ['-y', 'crawlforge-mcp-server@latest', 'mcp'] };
|
|
21
22
|
if (apiKey) stanza.env = { CRAWLFORGE_API_KEY: apiKey };
|
|
22
23
|
return stanza;
|
|
23
24
|
}
|
|
@@ -66,15 +67,20 @@ export function register(program) {
|
|
|
66
67
|
.action(async (opts) => {
|
|
67
68
|
const out = (msg) => process.stderr.write(msg + '\n');
|
|
68
69
|
|
|
69
|
-
// 1. API key check
|
|
70
|
+
// 1. API key check (creator mode proceeds keyless — the registered
|
|
71
|
+
// server re-derives creator mode from its own environment, and the
|
|
72
|
+
// secret must never be written into client configs)
|
|
70
73
|
const apiKey = loadStoredApiKey() || process.env.CRAWLFORGE_API_KEY;
|
|
71
|
-
if (
|
|
74
|
+
if (apiKey) {
|
|
75
|
+
out('API key: found (' + apiKey.slice(0, 8) + '...)');
|
|
76
|
+
} else if (isCreatorModeVerified()) {
|
|
77
|
+
out('API key: none (creator mode active — proceeding without a key)');
|
|
78
|
+
} else {
|
|
72
79
|
out('No CrawlForge API key found.');
|
|
73
80
|
out('Run: npx crawlforge-setup');
|
|
74
81
|
out('Then re-run: crawlforge init');
|
|
75
82
|
process.exit(1);
|
|
76
83
|
}
|
|
77
|
-
out('API key: found (' + apiKey.slice(0, 8) + '...)');
|
|
78
84
|
|
|
79
85
|
// 2. Install skills
|
|
80
86
|
const skillTarget = opts.all ? 'all' : 'claude-code';
|
|
@@ -9,7 +9,7 @@ export function register(program) {
|
|
|
9
9
|
program
|
|
10
10
|
.command('stealth <url>')
|
|
11
11
|
.description('Scrape a URL using stealth/anti-bot browser mode')
|
|
12
|
-
.option('--engine <engine>', 'Browser engine:
|
|
12
|
+
.option('--engine <engine>', 'Browser engine: chromium or camoufox', 'chromium')
|
|
13
13
|
.option('--wait <ms>', 'Wait time after page load in milliseconds', '2000')
|
|
14
14
|
.option('--screenshot', 'Capture a screenshot')
|
|
15
15
|
.action(async (url, opts, cmd) => {
|
|
@@ -21,7 +21,8 @@ export function register(program) {
|
|
|
21
21
|
};
|
|
22
22
|
await runTool(wrapperTool, {
|
|
23
23
|
url,
|
|
24
|
-
|
|
24
|
+
// "playwright" was the pre-v4.0.0 name for the chromium engine
|
|
25
|
+
engine: opts.engine === 'playwright' ? 'chromium' : opts.engine,
|
|
25
26
|
wait_for: parseInt(opts.wait, 10),
|
|
26
27
|
screenshot: !!opts.screenshot
|
|
27
28
|
}, cliFlags);
|
|
@@ -228,7 +228,9 @@ export class LLMsTxtAnalyzer {
|
|
|
228
228
|
$('a[href*="api"], a[href*="developer"], a[href*="docs"]').each((_, element) => {
|
|
229
229
|
const href = $(element).attr('href');
|
|
230
230
|
const text = $(element).text().toLowerCase();
|
|
231
|
-
|
|
231
|
+
// Word-boundary match: substring checks flagged "Sapiens"/"rapid"
|
|
232
|
+
// style words as API links.
|
|
233
|
+
if (href && /\b(api|developer)s?\b/.test(text)) {
|
|
232
234
|
apis.push({
|
|
233
235
|
url: new URL(href, baseUrl).toString(),
|
|
234
236
|
type: 'documentation',
|
|
@@ -111,6 +111,7 @@ export class MonitorScheduler {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
async stopMonitor(id) {
|
|
114
|
+
if (!this.store._loaded) await this.store.load();
|
|
114
115
|
this._clearTimer(id);
|
|
115
116
|
const existed = !!this.store.get(id);
|
|
116
117
|
await this.store.remove(id);
|
|
@@ -118,6 +119,7 @@ export class MonitorScheduler {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
async stopByUrl(url) {
|
|
122
|
+
if (!this.store._loaded) await this.store.load();
|
|
121
123
|
let count = 0;
|
|
122
124
|
for (const def of this.store.list()) {
|
|
123
125
|
if (def.url === url) {
|
|
@@ -162,10 +164,17 @@ export class MonitorScheduler {
|
|
|
162
164
|
const ct = this.tool.changeTracker;
|
|
163
165
|
if (ct?.snapshots?.has(def.url)) return;
|
|
164
166
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
// limit > 1 skips legacy empty junk snapshots; Buffer normalization is
|
|
168
|
+
// required because querySnapshots returns stored content as a Buffer —
|
|
169
|
+
// the old string guard silently disabled rehydration after restart.
|
|
170
|
+
const q = await this.tool.snapshotManager.querySnapshots({ url: def.url, limit: 5, includeContent: true });
|
|
171
|
+
for (const snap of q?.snapshots ?? []) {
|
|
172
|
+
let content = snap?.content;
|
|
173
|
+
if (Buffer.isBuffer(content)) content = content.toString('utf8');
|
|
174
|
+
if (content && typeof content === 'string') {
|
|
175
|
+
await ct.createBaseline(def.url, content, def.trackingOptions);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
169
178
|
}
|
|
170
179
|
} catch {
|
|
171
180
|
/* no usable snapshot — first fire will create the baseline */
|
|
@@ -84,10 +84,15 @@ export class SnapshotManager extends EventEmitter {
|
|
|
84
84
|
// the server with a cwd the process cannot write to (e.g. '/').
|
|
85
85
|
const defaultBaseDir = path.join(os.homedir(), '.crawlforge', 'snapshots');
|
|
86
86
|
|
|
87
|
+
// metadata/temp must live under the EFFECTIVE storage dir: anchoring them
|
|
88
|
+
// to defaultBaseDir when a custom storageDir is passed split content from
|
|
89
|
+
// metadata and shared one metadata dir across all custom-dir instances.
|
|
90
|
+
const baseDir = options.storageDir || defaultBaseDir;
|
|
91
|
+
|
|
87
92
|
this.options = {
|
|
88
|
-
storageDir:
|
|
89
|
-
metadataDir: options.metadataDir || path.join(
|
|
90
|
-
tempDir: options.tempDir || path.join(
|
|
93
|
+
storageDir: baseDir,
|
|
94
|
+
metadataDir: options.metadataDir || path.join(baseDir, 'metadata'),
|
|
95
|
+
tempDir: options.tempDir || path.join(baseDir, 'temp'),
|
|
91
96
|
enableCompression: options.enableCompression !== false,
|
|
92
97
|
enableDeltaStorage: options.enableDeltaStorage !== false,
|
|
93
98
|
enableEncryption: options.enableEncryption || false,
|
|
@@ -546,7 +546,9 @@ export class ContentAnalyzer {
|
|
|
546
546
|
const people = doc.people().out('array');
|
|
547
547
|
const places = doc.places().out('array');
|
|
548
548
|
const organizations = doc.organizations().out('array');
|
|
549
|
-
|
|
549
|
+
// .dates() needs the compromise-dates plugin (not installed) and threw,
|
|
550
|
+
// aborting ALL entity extraction; #Date+ tag matching is core compromise.
|
|
551
|
+
const dates = doc.match('#Date+').out('array');
|
|
550
552
|
const money = doc.money().out('array');
|
|
551
553
|
let other = doc.topics().out('array').slice(0, 10);
|
|
552
554
|
|
|
@@ -168,7 +168,13 @@ export class BFSCrawler {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
// Mark as visited
|
|
171
|
+
// Mark as visited. Re-check the cap and dedupe first: the checks at the
|
|
172
|
+
// top of this task ran before the awaited robots lookup, so concurrent
|
|
173
|
+
// queue tasks may have filled the budget (or claimed this URL) since.
|
|
174
|
+
// This block is synchronous, so the cap is exact.
|
|
175
|
+
if (this.visited.size >= this.maxPages || this.visited.has(normalizedUrl)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
172
178
|
this.visited.add(normalizedUrl);
|
|
173
179
|
|
|
174
180
|
try {
|
|
@@ -204,11 +210,15 @@ export class BFSCrawler {
|
|
|
204
210
|
|
|
205
211
|
// Process links for analysis
|
|
206
212
|
if (this.enableLinkAnalysis && this.linkAnalyzer && pageData.links) {
|
|
213
|
+
// Parse the page once and reuse for every link; re-parsing per link is
|
|
214
|
+
// O(links × page size) and starves the event loop on link-dense pages.
|
|
215
|
+
const $page = pageData.originalHtml ? load(pageData.originalHtml) : null;
|
|
216
|
+
const pageBodyText = $page ? $page('body').text() : '';
|
|
207
217
|
for (const link of pageData.links) {
|
|
208
218
|
const absoluteUrl = this.resolveUrl(link, normalizedUrl);
|
|
209
219
|
if (absoluteUrl) {
|
|
210
220
|
// Extract anchor text and context from link
|
|
211
|
-
const linkMetadata = this.extractLinkMetadata(link,
|
|
221
|
+
const linkMetadata = this.extractLinkMetadata(link, $page, pageBodyText);
|
|
212
222
|
this.linkAnalyzer.addLink(normalizedUrl, absoluteUrl, linkMetadata);
|
|
213
223
|
}
|
|
214
224
|
}
|
|
@@ -548,19 +558,18 @@ export class BFSCrawler {
|
|
|
548
558
|
}
|
|
549
559
|
|
|
550
560
|
/**
|
|
551
|
-
* Extract link metadata from
|
|
561
|
+
* Extract link metadata from a pre-parsed page
|
|
552
562
|
* @param {string} href - The href attribute value
|
|
553
|
-
* @param {
|
|
554
|
-
* @param {string}
|
|
563
|
+
* @param {Object} $ - Cheerio document for the page (parsed once per page)
|
|
564
|
+
* @param {string} bodyText - Pre-computed body text of the page
|
|
555
565
|
* @returns {Object} Link metadata
|
|
556
566
|
*/
|
|
557
|
-
extractLinkMetadata(href,
|
|
558
|
-
if (
|
|
567
|
+
extractLinkMetadata(href, $, bodyText = '') {
|
|
568
|
+
if (!$) return {};
|
|
559
569
|
|
|
560
570
|
try {
|
|
561
|
-
const $ = load(html);
|
|
562
571
|
const linkElement = $(`a[href="${href}"]`).first();
|
|
563
|
-
|
|
572
|
+
|
|
564
573
|
if (linkElement.length === 0) {
|
|
565
574
|
return { href };
|
|
566
575
|
}
|
|
@@ -569,10 +578,8 @@ export class BFSCrawler {
|
|
|
569
578
|
const title = linkElement.attr('title');
|
|
570
579
|
const rel = linkElement.attr('rel');
|
|
571
580
|
const className = linkElement.attr('class');
|
|
572
|
-
|
|
581
|
+
|
|
573
582
|
// Get surrounding context (up to 100 characters before and after)
|
|
574
|
-
const linkHtml = linkElement.prop('outerHTML');
|
|
575
|
-
const bodyText = $('body').text();
|
|
576
583
|
const linkTextIndex = bodyText.indexOf(anchorText);
|
|
577
584
|
let context = '';
|
|
578
585
|
|
|
@@ -122,7 +122,7 @@ const serpRankResultShape = z.object({
|
|
|
122
122
|
domain: z.string().optional(),
|
|
123
123
|
url: z.string().nullable().optional(),
|
|
124
124
|
title: z.string().nullable().optional(),
|
|
125
|
-
snippet: z.string().optional()
|
|
125
|
+
snippet: z.string().nullable().optional()
|
|
126
126
|
}).passthrough();
|
|
127
127
|
|
|
128
128
|
const serpRankShape = {
|
|
@@ -28,7 +28,8 @@ const SEMANTIC_FIELD_SELECTORS = {
|
|
|
28
28
|
summary: ['article p', 'main p', 'p'],
|
|
29
29
|
author: ['[rel="author"]', '.author', '.byline'],
|
|
30
30
|
date: ['time', '.date'],
|
|
31
|
-
published: ['time', '.published', '.date']
|
|
31
|
+
published: ['time', '.published', '.date'],
|
|
32
|
+
price: ['[itemprop="price"]', '[class*="price"]']
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const ExtractStructuredSchema = z.object({
|
|
@@ -70,9 +70,12 @@ export class GenerateLLMsTxtTool {
|
|
|
70
70
|
// per-analysis state on `this.analysis`, so a shared instance would let
|
|
71
71
|
// concurrent (or successive) calls cross-contaminate results.
|
|
72
72
|
logger.info(`Analyzing website: ${baseUrl}`);
|
|
73
|
+
// analysisOptions must reach the constructor: the analyzer reads crawl
|
|
74
|
+
// caps (maxPages/maxDepth/…) from this.options, not the per-call arg.
|
|
73
75
|
const analyzer = new LLMsTxtAnalyzer({
|
|
74
76
|
timeout: this.options.timeout,
|
|
75
|
-
userAgent: this.options.userAgent
|
|
77
|
+
userAgent: this.options.userAgent,
|
|
78
|
+
...analysisOptions
|
|
76
79
|
});
|
|
77
80
|
const analysis = await analyzer.analyzeWebsite(url, analysisOptions);
|
|
78
81
|
|
|
@@ -222,6 +225,14 @@ export class GenerateLLMsTxtTool {
|
|
|
222
225
|
emitSection('Tools', flatten('tools'));
|
|
223
226
|
emitSection('Navigation', flatten('navigation'));
|
|
224
227
|
|
|
228
|
+
// Fallback: if no categorized section produced output, list the raw
|
|
229
|
+
// sitemap so llms.txt always carries a URL inventory. Must run BEFORE the
|
|
230
|
+
// APIs section — an APIs entry alone used to set hasBody and suppress it.
|
|
231
|
+
const hasBody = lines.some((l) => l.startsWith('## '));
|
|
232
|
+
if (!hasBody) {
|
|
233
|
+
emitSection('Pages', analysis.structure?.sitemap || []);
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
// APIs as their own section.
|
|
226
237
|
if (Array.isArray(analysis.apis) && analysis.apis.length > 0) {
|
|
227
238
|
lines.push('## APIs');
|
|
@@ -233,12 +244,6 @@ export class GenerateLLMsTxtTool {
|
|
|
233
244
|
lines.push('');
|
|
234
245
|
}
|
|
235
246
|
|
|
236
|
-
// Fallback: if no categorized sections produced output, list the raw sitemap.
|
|
237
|
-
const hasBody = lines.some((l) => l.startsWith('## '));
|
|
238
|
-
if (!hasBody) {
|
|
239
|
-
emitSection('Pages', analysis.structure?.sitemap || []);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
247
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
243
248
|
}
|
|
244
249
|
|
|
@@ -87,9 +87,13 @@ const TEMPLATES = [
|
|
|
87
87
|
description: attr($, 'meta[property="og:description"]', 'content') || text($, 'p.f4.my-3'),
|
|
88
88
|
stars: text($, '#repo-stars-counter-star') || text($, '[aria-label*="stargazers"]'),
|
|
89
89
|
forks: text($, '#repo-network-counter') || text($, '[aria-label*="forks"]'),
|
|
90
|
-
|
|
90
|
+
// React (logged-out) layout has no watchers aria-label; the count is
|
|
91
|
+
// the <strong> right after the single octicon-eye. Language is a
|
|
92
|
+
// client-side skeleton on that layout — unrecoverable from static
|
|
93
|
+
// HTML, so it stays null there (itemprop still works on classic).
|
|
94
|
+
watchers: text($, '.octicon-eye + strong') || text($, '[aria-label*="watchers"]'),
|
|
91
95
|
language: text($, 'span[itemprop="programmingLanguage"]') || text($, '.d-inline-flex[class*="language"]'),
|
|
92
|
-
topics: list($, 'a.topic-tag'),
|
|
96
|
+
topics: list($, 'a.topic-tag, a[href^="/topics/"]'),
|
|
93
97
|
license: text($, 'a[href*="blob/"][href*="LICENSE"]') || text($, '.octicon-law ~ span'),
|
|
94
98
|
last_updated: attr($, 'relative-time', 'datetime'),
|
|
95
99
|
homepage: attr($, 'a[href][rel="noopener noreferrer"]', 'href'),
|
|
@@ -130,11 +130,16 @@ export class TrackChangesTool extends EventEmitter {
|
|
|
130
130
|
|
|
131
131
|
_setupEventHandlers() {
|
|
132
132
|
this.changeTracker.on('changeDetected', async (changeRecord) => {
|
|
133
|
-
|
|
133
|
+
// changeRecord.details is diff analysis and carries no page content —
|
|
134
|
+
// storing `details.current || ''` wrote an empty junk snapshot on every
|
|
135
|
+
// significant change (compareWithBaseline already snapshots the real
|
|
136
|
+
// current content). Only store if a future record ever carries content.
|
|
137
|
+
const current = changeRecord.details?.current;
|
|
138
|
+
if (changeRecord.significance !== 'none' && current) {
|
|
134
139
|
try {
|
|
135
140
|
await this.snapshotManager.storeSnapshot(
|
|
136
141
|
changeRecord.url,
|
|
137
|
-
|
|
142
|
+
current,
|
|
138
143
|
{ changes: changeRecord.details, significance: changeRecord.significance, changeType: changeRecord.changeType }
|
|
139
144
|
);
|
|
140
145
|
} catch (error) {
|
|
@@ -209,9 +214,34 @@ export class TrackChangesTool extends EventEmitter {
|
|
|
209
214
|
};
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
// Rebuild the in-memory baseline from the newest persisted snapshot, so
|
|
218
|
+
// compare works across processes (fresh CLI runs, server restarts). Same
|
|
219
|
+
// fail-soft mechanism as MonitorScheduler._ensureBaseline: no usable
|
|
220
|
+
// snapshot means compare still reports "No baseline" for a genuine first run.
|
|
221
|
+
async rehydrateBaseline(url, trackingOptions = {}) {
|
|
222
|
+
if (this.changeTracker?.snapshots?.has(url)) return;
|
|
223
|
+
try {
|
|
224
|
+
// limit > 1: existing stores contain empty junk snapshots (from the old
|
|
225
|
+
// changeDetected handler) that can tie on timestamp with the real one —
|
|
226
|
+
// take the newest snapshot that actually has content.
|
|
227
|
+
const q = await this.snapshotManager.querySnapshots({ url, limit: 5, includeContent: true });
|
|
228
|
+
for (const snap of q?.snapshots ?? []) {
|
|
229
|
+
let content = snap?.content;
|
|
230
|
+
if (Buffer.isBuffer(content)) content = content.toString('utf8');
|
|
231
|
+
if (content && typeof content === 'string') {
|
|
232
|
+
await this.changeTracker.createBaseline(url, content, trackingOptions);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
/* no usable snapshot — caller's compare will surface "No baseline" */
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
212
241
|
async compareWithBaseline(params) {
|
|
213
242
|
const { url, content, html, trackingOptions, storageOptions = {}, notificationOptions } = params;
|
|
214
243
|
const enableSnapshots = storageOptions.enableSnapshots !== false;
|
|
244
|
+
await this.rehydrateBaseline(url, trackingOptions);
|
|
215
245
|
|
|
216
246
|
let currentContent = content || html;
|
|
217
247
|
let fetchMeta = {};
|
|
@@ -362,7 +392,10 @@ export class TrackChangesTool extends EventEmitter {
|
|
|
362
392
|
const monitorId = scheduledMonitorOptions?.monitorId;
|
|
363
393
|
if (monitorId) {
|
|
364
394
|
const result = await this.scheduler.stopMonitor(monitorId);
|
|
365
|
-
|
|
395
|
+
if (!result.stopped) {
|
|
396
|
+
return { success: false, operation: 'stop_scheduled_monitor', monitorId, stopped: false, error: `No scheduled monitor found with id ${monitorId}`, timestamp: Date.now() };
|
|
397
|
+
}
|
|
398
|
+
return { success: true, operation: 'stop_scheduled_monitor', monitorId, stopped: true, timestamp: Date.now() };
|
|
366
399
|
}
|
|
367
400
|
if (!url) throw new Error('stop_scheduled_monitor requires a url or scheduledMonitorOptions.monitorId');
|
|
368
401
|
const result = await this.scheduler.stopByUrl(url);
|