mcp-scraper 0.33.6 → 0.34.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 -2
- package/dist/bin/api-server.cjs +13558 -12516
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +13 -2
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +14 -4
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +117 -7
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +7 -7
- package/dist/bin/paa-harvest.cjs +128 -13
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +4 -4
- package/dist/{chunk-DUS4ENOY.js → chunk-2EDFOQD7.js} +2 -2
- package/dist/{chunk-DUS4ENOY.js.map → chunk-2EDFOQD7.js.map} +1 -1
- package/dist/{chunk-C2OD4ELT.js → chunk-3HBPKR5G.js} +2 -2
- package/dist/{chunk-A54ELKJK.js → chunk-4KQVIYIH.js} +139 -17
- package/dist/chunk-4KQVIYIH.js.map +1 -0
- package/dist/{chunk-AIK7WQD5.js → chunk-62DQAWPF.js} +141 -3
- package/dist/chunk-62DQAWPF.js.map +1 -0
- package/dist/{chunk-V73MPRU6.js → chunk-CB5C3BPB.js} +17 -1
- package/dist/chunk-CB5C3BPB.js.map +1 -0
- package/dist/{chunk-6ZUK4H66.js → chunk-J32XSMJI.js} +117 -8
- package/dist/chunk-J32XSMJI.js.map +1 -0
- package/dist/{chunk-LKIP7VDN.js → chunk-JBSGBSGT.js} +46 -3
- package/dist/chunk-JBSGBSGT.js.map +1 -0
- package/dist/chunk-NFMIHEYE.js +7 -0
- package/dist/chunk-NFMIHEYE.js.map +1 -0
- package/dist/{chunk-XL6YIWJ3.js → chunk-SUOHUXQS.js} +3 -3
- package/dist/{chunk-WJJTGRK4.js → chunk-V36LS5YV.js} +2 -2
- package/dist/{chunk-AT2PFWHW.js → chunk-ZID3WQID.js} +2 -2
- package/dist/{db-5SZQUI7A.js → db-YAI5AQOI.js} +16 -2
- package/dist/{extract-bundle-YNXTFCC3.js → extract-bundle-GPZRLBVY.js} +2 -2
- package/dist/index.cjs +128 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -4
- package/dist/{server-SHOYO52X.js → server-LKVPEQFE.js} +1386 -778
- package/dist/server-LKVPEQFE.js.map +1 -0
- package/dist/{site-extract-repository-ZDTO7ODZ.js → site-extract-repository-JHMVHENZ.js} +4 -4
- package/dist/{worker-BJ64GCDE.js → worker-ZZHSYI3Y.js} +7 -7
- package/docs/mcp-tool-manifest.generated.json +213 -3
- package/package.json +1 -1
- package/dist/chunk-6ZUK4H66.js.map +0 -1
- package/dist/chunk-A54ELKJK.js.map +0 -1
- package/dist/chunk-AIK7WQD5.js.map +0 -1
- package/dist/chunk-F65IIUHI.js +0 -7
- package/dist/chunk-F65IIUHI.js.map +0 -1
- package/dist/chunk-LKIP7VDN.js.map +0 -1
- package/dist/chunk-V73MPRU6.js.map +0 -1
- package/dist/server-SHOYO52X.js.map +0 -1
- /package/dist/{chunk-C2OD4ELT.js.map → chunk-3HBPKR5G.js.map} +0 -0
- /package/dist/{chunk-XL6YIWJ3.js.map → chunk-SUOHUXQS.js.map} +0 -0
- /package/dist/{chunk-WJJTGRK4.js.map → chunk-V36LS5YV.js.map} +0 -0
- /package/dist/{chunk-AT2PFWHW.js.map → chunk-ZID3WQID.js.map} +0 -0
- /package/dist/{db-5SZQUI7A.js.map → db-YAI5AQOI.js.map} +0 -0
- /package/dist/{extract-bundle-YNXTFCC3.js.map → extract-bundle-GPZRLBVY.js.map} +0 -0
- /package/dist/{site-extract-repository-ZDTO7ODZ.js.map → site-extract-repository-JHMVHENZ.js.map} +0 -0
- /package/dist/{worker-BJ64GCDE.js.map → worker-ZZHSYI3Y.js.map} +0 -0
|
@@ -278,6 +278,9 @@ var HttpMcpToolExecutor = class {
|
|
|
278
278
|
redditThread(input) {
|
|
279
279
|
return this.call("/reddit/thread", input, this.httpTimeoutOverrideMs ?? 24e4);
|
|
280
280
|
}
|
|
281
|
+
redditTrending(input) {
|
|
282
|
+
return this.call("/reddit/trending", input, this.httpTimeoutOverrideMs ?? 3e5);
|
|
283
|
+
}
|
|
281
284
|
videoFrameAnalysis(input) {
|
|
282
285
|
return this.call("/video/analyze", input);
|
|
283
286
|
}
|
|
@@ -564,7 +567,7 @@ render();
|
|
|
564
567
|
}
|
|
565
568
|
|
|
566
569
|
// src/version.ts
|
|
567
|
-
var PACKAGE_VERSION = "0.
|
|
570
|
+
var PACKAGE_VERSION = "0.34.0";
|
|
568
571
|
|
|
569
572
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
570
573
|
var import_zod = require("zod");
|
|
@@ -3107,8 +3110,8 @@ var SUBSCRIPTION_TIERS = {
|
|
|
3107
3110
|
"price_1TmiHRS8aAcsk3TGwmSNfNIa": { tier: "starter", label: "Starter", price_id: "price_1TmiHRS8aAcsk3TGwmSNfNIa", monthly_usd: 12, credits_mc: 8e6, concurrency: 3, intro_coupon: "mcp-starter-1dollar-intro-12" },
|
|
3108
3111
|
"price_1TrgihS8aAcsk3TG5cglrq4D": { tier: "growth", label: "Growth", price_id: "price_1TrgihS8aAcsk3TG5cglrq4D", monthly_usd: 40, credits_mc: 26666700, concurrency: 10, intro_coupon: null },
|
|
3109
3112
|
"price_1TrgihS8aAcsk3TG4HnG4gbY": { tier: "scale", label: "Scale", price_id: "price_1TrgihS8aAcsk3TG4HnG4gbY", monthly_usd: 100, credits_mc: 8e7, concurrency: 20, intro_coupon: null },
|
|
3110
|
-
"
|
|
3111
|
-
"
|
|
3113
|
+
"price_1TwqIVS8aAcsk3TG0FE3ddDM": { tier: "sonic", label: "Sonic Annual", price_id: "price_1TwqIVS8aAcsk3TG0FE3ddDM", monthly_usd: 25, billed_usd: 300, credits_mc: 25e7, concurrency: 3, intro_coupon: null, billing_interval: "year", credits_never_expire: true, private_offer: true },
|
|
3114
|
+
"price_1TwqIWS8aAcsk3TGbGbtlLak": { tier: "sonic-memory", label: "Sonic Annual + Memory Pro", price_id: "price_1TwqIWS8aAcsk3TGbGbtlLak", monthly_usd: 34.5, billed_usd: 414, credits_mc: 25e7, concurrency: 3, intro_coupon: null, billing_interval: "year", credits_never_expire: true, includes_memory: true, private_offer: true }
|
|
3112
3115
|
};
|
|
3113
3116
|
var SUBSCRIPTION_TIER_BY_KEY = Object.fromEntries(
|
|
3114
3117
|
Object.values(SUBSCRIPTION_TIERS).map((t) => [t.tier, t])
|
|
@@ -4300,6 +4303,65 @@ ${commentMd || "_No comments captured._"}`
|
|
|
4300
4303
|
}
|
|
4301
4304
|
};
|
|
4302
4305
|
}
|
|
4306
|
+
function formatRedditTrending(raw, input) {
|
|
4307
|
+
const parsed = parseData(raw);
|
|
4308
|
+
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
4309
|
+
const d = parsed.data;
|
|
4310
|
+
const threads = d.rankedThreads ?? [];
|
|
4311
|
+
const questions = (d.questions ?? []).filter((q) => q.question);
|
|
4312
|
+
const totals = {
|
|
4313
|
+
threads: d.totals?.threads ?? threads.length,
|
|
4314
|
+
upvotes: d.totals?.upvotes ?? 0,
|
|
4315
|
+
comments: d.totals?.comments ?? 0
|
|
4316
|
+
};
|
|
4317
|
+
const subreddit = d.subreddit ?? input.subreddit ?? null;
|
|
4318
|
+
const threadBlocks = threads.map((t, i) => [
|
|
4319
|
+
`### ${i + 1}. ${t.title || "Untitled"}`,
|
|
4320
|
+
`**${t.subreddit || "r/?"}** \xB7 ${t.score ?? 0} upvotes \xB7 ${t.commentCount ?? 0} comments \xB7 engagement ${t.engagementScore ?? 0}${t.ageText ? ` \xB7 ${t.ageText}` : ""}`,
|
|
4321
|
+
t.url ? `${t.url}` : "",
|
|
4322
|
+
t.topQuestions?.length ? `**Top questions:**
|
|
4323
|
+
${t.topQuestions.map((q) => `- ${q}`).join("\n")}` : ""
|
|
4324
|
+
].filter(Boolean).join("\n")).join("\n\n");
|
|
4325
|
+
const questionList = questions.map((q) => `- ${q.question}${q.threadUrl ? `
|
|
4326
|
+
${q.threadUrl}` : ""}`).join("\n");
|
|
4327
|
+
const full = [
|
|
4328
|
+
`# Reddit Trending: "${d.topic || input.topic}"${subreddit ? ` in r/${subreddit}` : ""}`,
|
|
4329
|
+
`**${totals.threads} threads \xB7 ${totals.upvotes} upvotes \xB7 ${totals.comments} comments** \xB7 last ${d.window === "7d" ? "week" : "month"} \xB7 ${d.threadsScraped ?? 0} of ${d.candidatesFound ?? threads.length} discovered scraped${d.partial ? " (partial \u2014 hit the time limit)" : ""}`,
|
|
4330
|
+
`
|
|
4331
|
+
## Ranked threads
|
|
4332
|
+
${threadBlocks || "_No threads found._"}`,
|
|
4333
|
+
`
|
|
4334
|
+
## Questions people asked
|
|
4335
|
+
${questionList || "_No questions extracted._"}`,
|
|
4336
|
+
`
|
|
4337
|
+
---
|
|
4338
|
+
\u{1F4A1} Deep-dive a winner's full comment tree: pass its url to \`reddit_thread\``
|
|
4339
|
+
].join("\n");
|
|
4340
|
+
return {
|
|
4341
|
+
...oneBlock(full),
|
|
4342
|
+
structuredContent: {
|
|
4343
|
+
topic: d.topic ?? input.topic,
|
|
4344
|
+
subreddit,
|
|
4345
|
+
window: d.window ?? "30d",
|
|
4346
|
+
totals,
|
|
4347
|
+
rankedThreads: threads.map((t) => ({
|
|
4348
|
+
title: t.title ?? "",
|
|
4349
|
+
url: t.url ?? "",
|
|
4350
|
+
subreddit: t.subreddit ?? "",
|
|
4351
|
+
score: Number(t.score ?? 0),
|
|
4352
|
+
commentCount: Number(t.commentCount ?? 0),
|
|
4353
|
+
engagementScore: Number(t.engagementScore ?? 0),
|
|
4354
|
+
ageText: t.ageText ?? "",
|
|
4355
|
+
topQuestions: t.topQuestions ?? []
|
|
4356
|
+
})),
|
|
4357
|
+
questions: questions.map((q) => ({ question: q.question ?? "", threadUrl: q.threadUrl ?? "" })),
|
|
4358
|
+
threadsScraped: Number(d.threadsScraped ?? 0),
|
|
4359
|
+
candidatesFound: Number(d.candidatesFound ?? threads.length),
|
|
4360
|
+
partial: Boolean(d.partial),
|
|
4361
|
+
searchQuery: d.searchQuery ?? ""
|
|
4362
|
+
}
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4303
4365
|
function formatFacebookAdSearch(raw, input) {
|
|
4304
4366
|
const parsed = parseData(raw);
|
|
4305
4367
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
@@ -5668,8 +5730,11 @@ seam is noted so you can chain them.
|
|
|
5668
5730
|
url the user gives).
|
|
5669
5731
|
|
|
5670
5732
|
## Reddit
|
|
5671
|
-
-
|
|
5672
|
-
Reddit's bot wall itself (no login needed). Find threads first with \`search_serp\`.
|
|
5733
|
+
- Read ONE known reddit.com thread/post URL -> **reddit_thread**. Returns the post plus its comment tree and
|
|
5734
|
+
handles Reddit's bot wall itself (no login needed). Find threads first with \`search_serp\` or \`reddit_trending\`.
|
|
5735
|
+
- DISCOVER what a niche is talking about (topic, no known thread) -> **reddit_trending** (takes a topic, optional
|
|
5736
|
+
subreddit; returns the last 30 days' top threads ranked by engagement plus the questions people asked \u2014
|
|
5737
|
+
feed winning \`rankedThreads[].url\` values into \`reddit_thread\` for the full comment tree).
|
|
5673
5738
|
|
|
5674
5739
|
## Other sites & logins (browser agent)
|
|
5675
5740
|
For an arbitrary site or a logged-in dashboard with no dedicated tool, use the browser_* agent. **First
|
|
@@ -6061,6 +6126,7 @@ var HarvestOptionsSchema = import_zod3.z.object({
|
|
|
6061
6126
|
format: import_zod3.z.enum(["json", "csv", "both"]).default("both"),
|
|
6062
6127
|
serpOnly: import_zod3.z.boolean().default(false),
|
|
6063
6128
|
pages: import_zod3.z.number().int().min(1).max(2).default(1),
|
|
6129
|
+
recency: import_zod3.z.enum(["day", "week", "month", "year"]).optional(),
|
|
6064
6130
|
softDeadlineMs: import_zod3.z.number().optional()
|
|
6065
6131
|
});
|
|
6066
6132
|
var MapsPlaceOptionsSchema = import_zod3.z.object({
|
|
@@ -6217,6 +6283,14 @@ var RedditThreadInputSchema = {
|
|
|
6217
6283
|
url: import_zod4.z.string().min(1).describe("A reddit.com thread/post URL (www, old, new Reddit, or redd.it)."),
|
|
6218
6284
|
maxComments: import_zod4.z.number().int().min(1).max(2e3).optional().describe("Optional cap on comments returned. Omit to return all captured comments.")
|
|
6219
6285
|
};
|
|
6286
|
+
var RedditTrendingInputSchema = {
|
|
6287
|
+
topic: import_zod4.z.string().min(1).describe('Topic to scan, in plain words (e.g. "crm for small business"). Not a URL \u2014 pass a known thread URL to reddit_thread instead.'),
|
|
6288
|
+
subreddit: import_zod4.z.string().min(1).optional().describe('Bare subreddit name to scope the scan to one community, e.g. "SEO" (no r/ prefix, no URL). Omit to scan all of Reddit.'),
|
|
6289
|
+
window: import_zod4.z.enum(["week", "month"]).default("month").describe('How recent the threads must be: "week" or "month" (default). Applied via a Google time filter over reddit.com, so it reflects genuine recency.'),
|
|
6290
|
+
maxThreads: import_zod4.z.number().int().min(1).max(40).default(20).describe("How many discovered threads to scrape and rank. Default 20 (scrape-all). Each scraped thread is billed like reddit_thread + its comments, so lower this to cap cost; raise toward 40 for a wider sweep. Scraping runs in parallel and stops early if it nears the request time limit (partial:true in the response)."),
|
|
6291
|
+
includeComments: import_zod4.z.boolean().default(true).describe("Scrape each discovered thread for real upvotes, comments, and the questions people asked, then rank by engagement. Set false for a fast, cheap discovery-only sweep \u2014 returns the discovered threads (title + url) in relevance order with NO engagement stats and NO per-thread billing, so you can then call reddit_thread on the ones you want."),
|
|
6292
|
+
maxCommentsPerThread: import_zod4.z.number().int().min(1).max(200).default(50).describe("Comments captured per scraped thread when includeComments is true. Default 50. Billed per captured comment.")
|
|
6293
|
+
};
|
|
6220
6294
|
var VideoFrameAnalysisInputSchema = {
|
|
6221
6295
|
sourceUrl: import_zod4.z.string().min(1).describe("A YouTube, Facebook, Instagram, TikTok, or Vimeo URL (downloaded automatically), or a direct video file URL (.mp4/.webm/.mov). Videos up to 30 minutes are supported."),
|
|
6222
6296
|
intervalS: import_zod4.z.number().min(1).max(30).optional().describe("Preferred seconds between sampled frames (1-30, default 2). Automatically widened for long videos so the whole duration is covered within the frame budget."),
|
|
@@ -6804,6 +6878,34 @@ var RedditThreadOutputSchema = {
|
|
|
6804
6878
|
body: import_zod4.z.string()
|
|
6805
6879
|
}))
|
|
6806
6880
|
};
|
|
6881
|
+
var RedditTrendingOutputSchema = {
|
|
6882
|
+
topic: import_zod4.z.string(),
|
|
6883
|
+
subreddit: NullableString2,
|
|
6884
|
+
window: import_zod4.z.string(),
|
|
6885
|
+
totals: import_zod4.z.object({
|
|
6886
|
+
threads: import_zod4.z.number().int().min(0),
|
|
6887
|
+
upvotes: import_zod4.z.number().int().min(0),
|
|
6888
|
+
comments: import_zod4.z.number().int().min(0)
|
|
6889
|
+
}),
|
|
6890
|
+
rankedThreads: import_zod4.z.array(import_zod4.z.object({
|
|
6891
|
+
title: import_zod4.z.string(),
|
|
6892
|
+
url: import_zod4.z.string(),
|
|
6893
|
+
subreddit: import_zod4.z.string(),
|
|
6894
|
+
score: import_zod4.z.number().int().min(0),
|
|
6895
|
+
commentCount: import_zod4.z.number().int().min(0),
|
|
6896
|
+
engagementScore: import_zod4.z.number().int().min(0),
|
|
6897
|
+
ageText: import_zod4.z.string(),
|
|
6898
|
+
topQuestions: import_zod4.z.array(import_zod4.z.string())
|
|
6899
|
+
})),
|
|
6900
|
+
questions: import_zod4.z.array(import_zod4.z.object({
|
|
6901
|
+
question: import_zod4.z.string(),
|
|
6902
|
+
threadUrl: import_zod4.z.string()
|
|
6903
|
+
})),
|
|
6904
|
+
threadsScraped: import_zod4.z.number().int().min(0),
|
|
6905
|
+
candidatesFound: import_zod4.z.number().int().min(0),
|
|
6906
|
+
partial: import_zod4.z.boolean(),
|
|
6907
|
+
searchQuery: import_zod4.z.string()
|
|
6908
|
+
};
|
|
6807
6909
|
var FacebookPageIntelOutputSchema = {
|
|
6808
6910
|
advertiserName: NullableString2,
|
|
6809
6911
|
inputMode: import_zod4.z.enum(["pageId", "libraryId", "query"]),
|
|
@@ -7174,7 +7276,8 @@ var SearchSerpInputSchema = {
|
|
|
7174
7276
|
proxyMode: import_zod4.z.enum(["configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Leave unset for the default route. Country/region localization comes from gl/hl plus the city or region in the query."),
|
|
7175
7277
|
proxyZip: import_zod4.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override."),
|
|
7176
7278
|
debug: import_zod4.z.boolean().default(false).describe("Include sanitized diagnostics for debugging."),
|
|
7177
|
-
pages: import_zod4.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132).")
|
|
7279
|
+
pages: import_zod4.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)."),
|
|
7280
|
+
recency: import_zod4.z.enum(["day", "week", "month", "year"]).optional().describe('Restrict results to a recent time window (Google "past day/week/month/year" filter). Omit for all-time. Useful for "what is being said this week" style queries; pairs well with a site: operator in the query.')
|
|
7178
7281
|
};
|
|
7179
7282
|
var CaptureSerpSnapshotInputSchema = {
|
|
7180
7283
|
query: import_zod4.z.string().min(1).describe('Search query to capture. KEEP the place in the query text for localized captures (e.g. "botox clinic austin tx") and also set location.'),
|
|
@@ -8094,6 +8197,13 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
|
|
|
8094
8197
|
outputSchema: recordOutputSchema("reddit_thread", RedditThreadOutputSchema),
|
|
8095
8198
|
annotations: liveWebToolAnnotations("Reddit Thread + Comments")
|
|
8096
8199
|
}, async (input) => formatRedditThread(await executor.redditThread(input), input));
|
|
8200
|
+
server2.registerTool("reddit_trending", {
|
|
8201
|
+
title: "Reddit Trending",
|
|
8202
|
+
description: "Discover the top Reddit conversations about a topic from the last week or month: finds relevant recent threads via a Google site:reddit.com search (optionally scoped to one subreddit), scrapes them for real upvotes, comments, and the questions people asked, and ranks by engagement (upvotes + 2x comments). Scraping runs in parallel across the discovered threads; set includeComments:false for a fast, cheap discovery-only sweep (relevant thread list, no engagement stats, no per-thread billing) and then read the ones you want with reddit_thread. Not for reading one known thread URL \u2014 use reddit_thread for that.",
|
|
8203
|
+
inputSchema: RedditTrendingInputSchema,
|
|
8204
|
+
outputSchema: recordOutputSchema("reddit_trending", RedditTrendingOutputSchema),
|
|
8205
|
+
annotations: liveWebToolAnnotations("Reddit Trending")
|
|
8206
|
+
}, async (input) => formatRedditTrending(await executor.redditTrending(input), input));
|
|
8097
8207
|
server2.registerTool("video_frame_analysis", {
|
|
8098
8208
|
title: "Video Breakdown (frame-by-frame + transcript)",
|
|
8099
8209
|
description: "Produce a deep frame-by-frame + transcript breakdown of a video \u2014 pacing, hook, visual style, and how to replicate it. Accepts a YouTube, Facebook, Instagram, TikTok, or Vimeo URL directly (downloaded for you), or a direct video file URL (.mp4/.webm/.mov). Costs $1 per 120 frames requested (max 480 = $4; refunded down if the video can't use them; refunded fully on failure): returns a runId immediately; poll video_frame_analysis_status until done. Videos up to 30 minutes.",
|
|
@@ -10944,7 +11054,7 @@ function renderInstallTerminal(options) {
|
|
|
10944
11054
|
"1/1 install surfaces ready",
|
|
10945
11055
|
colorize("Newest: any approved connection read can become an indexed Memory snapshot in one call. OAuth stays tenant-isolated and provider content is redacted and marked untrusted.", "lime", color),
|
|
10946
11056
|
"",
|
|
10947
|
-
`${colorize("Tools", "cyan", color)} ${colorize("(
|
|
11057
|
+
`${colorize("Tools", "cyan", color)} ${colorize("(166 MCP tools)", "muted", color)}`,
|
|
10948
11058
|
toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
|
|
10949
11059
|
toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "audit_site", "directory_workflow"], color),
|
|
10950
11060
|
toolRow("build", ["rank_tracker_workflow", "cron plan", "database prompt"], color),
|