mcp-scraper 0.3.25 → 0.3.27
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/bin/api-server.cjs +2439 -346
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +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 +88 -33
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +10 -1357
- package/dist/bin/mcp-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs +1 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-FE2WC4JR.js → chunk-6KHGYJIT.js} +1431 -35
- package/dist/chunk-6KHGYJIT.js.map +1 -0
- package/dist/chunk-DYV72F6A.js +7 -0
- package/dist/chunk-DYV72F6A.js.map +1 -0
- package/dist/{chunk-WYCER2HW.js → chunk-H74L743B.js} +12 -11
- package/dist/chunk-H74L743B.js.map +1 -0
- package/dist/{chunk-G7PQ64LM.js → chunk-QSSH4RCX.js} +2 -2
- package/dist/chunk-QSSH4RCX.js.map +1 -0
- package/dist/{chunk-DOBQN3EY.js → chunk-T3VZD2I4.js} +2 -2
- package/dist/{chunk-DOBQN3EY.js.map → chunk-T3VZD2I4.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-RRCKVBHG.js → server-KUNOK47B.js} +695 -29
- package/dist/server-KUNOK47B.js.map +1 -0
- package/dist/{worker-UC6D2756.js → worker-NV4GTIRG.js} +3 -3
- package/docs/spec-kernel-computer-controls.md +166 -0
- package/docs/spec-server-instructions.md +94 -0
- package/package.json +1 -1
- package/dist/chunk-FE2WC4JR.js.map +0 -1
- package/dist/chunk-G7PQ64LM.js.map +0 -1
- package/dist/chunk-WPHAMNT7.js +0 -7
- package/dist/chunk-WPHAMNT7.js.map +0 -1
- package/dist/chunk-WYCER2HW.js.map +0 -1
- package/dist/server-RRCKVBHG.js.map +0 -1
- /package/dist/{worker-UC6D2756.js.map → worker-NV4GTIRG.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -4805,10 +4805,10 @@ async function completeWorkflowRunRecord(id, status, manifest) {
|
|
|
4805
4805
|
args: [status, JSON.stringify(manifest), id]
|
|
4806
4806
|
});
|
|
4807
4807
|
}
|
|
4808
|
-
async function failWorkflowRunRecord(id,
|
|
4808
|
+
async function failWorkflowRunRecord(id, errorMessage5, manifest) {
|
|
4809
4809
|
await getDb().execute({
|
|
4810
4810
|
sql: `UPDATE workflow_runs SET status = 'failed', error_message = ?, manifest_json = ?, completed_at = datetime('now') WHERE id = ?`,
|
|
4811
|
-
args: [
|
|
4811
|
+
args: [errorMessage5, manifest == null ? null : JSON.stringify(manifest), id]
|
|
4812
4812
|
});
|
|
4813
4813
|
}
|
|
4814
4814
|
function workflowArtifactContentType(kind) {
|
|
@@ -10047,22 +10047,23 @@ var init_rates = __esm({
|
|
|
10047
10047
|
"use strict";
|
|
10048
10048
|
MC_COSTS = {
|
|
10049
10049
|
serp: 100,
|
|
10050
|
-
paa:
|
|
10050
|
+
paa: 200,
|
|
10051
|
+
paa_base: 1e3,
|
|
10051
10052
|
page_scrape: 100,
|
|
10052
|
-
url_map:
|
|
10053
|
+
url_map: 500,
|
|
10053
10054
|
yt_channel: 200,
|
|
10054
10055
|
yt_transcription: 200,
|
|
10055
10056
|
fb_ad: 200,
|
|
10056
|
-
maps_search:
|
|
10057
|
-
maps_place:
|
|
10057
|
+
maps_search: 500,
|
|
10058
|
+
maps_place: 3e3,
|
|
10058
10059
|
maps_review: 100,
|
|
10059
|
-
fb_search:
|
|
10060
|
+
fb_search: 600,
|
|
10060
10061
|
fb_transcribe: 200,
|
|
10061
|
-
instagram_profile:
|
|
10062
|
-
instagram_media:
|
|
10062
|
+
instagram_profile: 400,
|
|
10063
|
+
instagram_media: 400,
|
|
10063
10064
|
instagram_transcribe: 50,
|
|
10064
10065
|
browser_minute: 12e3,
|
|
10065
|
-
reddit_thread:
|
|
10066
|
+
reddit_thread: 2e3
|
|
10066
10067
|
};
|
|
10067
10068
|
MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
|
|
10068
10069
|
BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
|
|
@@ -10081,8 +10082,8 @@ var init_rates = __esm({
|
|
|
10081
10082
|
label: "PAA harvest",
|
|
10082
10083
|
aliases: ["harvest_paa", "paa", "people also ask", "questions"],
|
|
10083
10084
|
credits: mcToCredits(MC_COSTS.paa),
|
|
10084
|
-
unit:
|
|
10085
|
-
notes:
|
|
10085
|
+
unit: `per question (+${mcToCredits(MC_COSTS.paa_base)} credit base)`,
|
|
10086
|
+
notes: `Full SERP feature extraction. Billed ${mcToCredits(MC_COSTS.paa_base)} credit base + ${mcToCredits(MC_COSTS.paa)} per question actually returned (unused estimate refunded).`
|
|
10086
10087
|
},
|
|
10087
10088
|
{
|
|
10088
10089
|
key: "page_scrape",
|
|
@@ -10872,16 +10873,6 @@ var init_catalog_seed = __esm({
|
|
|
10872
10873
|
isSpecial: true,
|
|
10873
10874
|
tagline: "Generate a rank-tracker blueprint",
|
|
10874
10875
|
params: [{ key: "domain", label: "Domain", type: "string", required: true }]
|
|
10875
|
-
},
|
|
10876
|
-
{
|
|
10877
|
-
key: "deep_research_workflow",
|
|
10878
|
-
label: "Deep Research",
|
|
10879
|
-
kind: "simple",
|
|
10880
|
-
endpoint: "/workflows/run",
|
|
10881
|
-
mcpName: "workflow_run",
|
|
10882
|
-
isSpecial: true,
|
|
10883
|
-
tagline: "Multi-source, fact-checked research report",
|
|
10884
|
-
params: [{ key: "question", label: "Question", type: "string", required: true }]
|
|
10885
10876
|
}
|
|
10886
10877
|
]
|
|
10887
10878
|
}
|
|
@@ -17315,9 +17306,18 @@ function parseData(raw) {
|
|
|
17315
17306
|
function entityIdsSection(ids) {
|
|
17316
17307
|
if (!ids) return "";
|
|
17317
17308
|
const lines = [];
|
|
17318
|
-
if (ids.
|
|
17319
|
-
|
|
17320
|
-
|
|
17309
|
+
if (ids.entities?.length) {
|
|
17310
|
+
for (const e of ids.entities) {
|
|
17311
|
+
const idParts = [e.kgId && `MID ${e.kgId}`, e.cid && `CID ${e.cid}`, e.gcid && `GCID ${e.gcid}`].filter(Boolean);
|
|
17312
|
+
if (idParts.length) lines.push(`- **${e.name}** \u2014 ${idParts.join(", ")}`);
|
|
17313
|
+
}
|
|
17314
|
+
}
|
|
17315
|
+
const linkedNames = new Set((ids.entities ?? []).map((e) => e.name));
|
|
17316
|
+
if (!linkedNames.size) {
|
|
17317
|
+
if (ids.kgIds?.length) lines.push(`- **Knowledge Graph MID:** ${ids.kgIds.join(", ")}`);
|
|
17318
|
+
if (ids.cids?.length) lines.push(`- **CID:** ${ids.cids.join(", ")}`);
|
|
17319
|
+
if (ids.gcids?.length) lines.push(`- **GCID:** ${ids.gcids.join(", ")}`);
|
|
17320
|
+
}
|
|
17321
17321
|
return lines.length ? `
|
|
17322
17322
|
## Entity IDs
|
|
17323
17323
|
${lines.join("\n")}` : "";
|
|
@@ -17439,7 +17439,7 @@ ${paaTable}${serpTable}${entityIdsSection(entityIds)}${aiSection}${statsLine}${d
|
|
|
17439
17439
|
snippet: r.snippet ?? null
|
|
17440
17440
|
})),
|
|
17441
17441
|
aiOverview: aiOvw ? { detected: aiOvw.detected === true, text: aiOvw.text ?? null } : null,
|
|
17442
|
-
entityIds: entityIds ? { kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null,
|
|
17442
|
+
entityIds: entityIds ? { entities: entityIds.entities ?? [], kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null,
|
|
17443
17443
|
durationMs: durationMs ?? null
|
|
17444
17444
|
}
|
|
17445
17445
|
};
|
|
@@ -17500,7 +17500,7 @@ ${serpTable}${localSection}${entityIdsSection(entityIds)}${aiSection}${debugSect
|
|
|
17500
17500
|
websiteUrl: b.websiteUrl ?? null
|
|
17501
17501
|
})),
|
|
17502
17502
|
aiOverview: aiOvw ? { detected: aiOvw.detected === true, text: aiOvw.text ?? null } : null,
|
|
17503
|
-
entityIds: entityIds ? { kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null
|
|
17503
|
+
entityIds: entityIds ? { entities: entityIds.entities ?? [], kgIds: entityIds.kgIds ?? [], cids: entityIds.cids ?? [], gcids: entityIds.gcids ?? [] } : null
|
|
17504
17504
|
}
|
|
17505
17505
|
};
|
|
17506
17506
|
}
|
|
@@ -22926,7 +22926,7 @@ var init_PAAExtractor = __esm({
|
|
|
22926
22926
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
22927
22927
|
}
|
|
22928
22928
|
return Array.from(document.querySelectorAll(selectors.item)).map((pair) => ({
|
|
22929
|
-
question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || "",
|
|
22929
|
+
question: pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "",
|
|
22930
22930
|
answer: cleanText(pair.querySelector(selectors.answerContainer)) || void 0,
|
|
22931
22931
|
sourceTitle: pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0,
|
|
22932
22932
|
sourceSite: pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0,
|
|
@@ -24879,7 +24879,7 @@ var PACKAGE_VERSION;
|
|
|
24879
24879
|
var init_version = __esm({
|
|
24880
24880
|
"src/version.ts"() {
|
|
24881
24881
|
"use strict";
|
|
24882
|
-
PACKAGE_VERSION = "0.3.
|
|
24882
|
+
PACKAGE_VERSION = "0.3.27";
|
|
24883
24883
|
}
|
|
24884
24884
|
});
|
|
24885
24885
|
|
|
@@ -24889,33 +24889,73 @@ var init_server_instructions = __esm({
|
|
|
24889
24889
|
"src/mcp/server-instructions.ts"() {
|
|
24890
24890
|
"use strict";
|
|
24891
24891
|
SERVER_INSTRUCTIONS = `
|
|
24892
|
-
|
|
24893
|
-
|
|
24894
|
-
|
|
24895
|
-
|
|
24896
|
-
|
|
24897
|
-
|
|
24898
|
-
|
|
24899
|
-
-
|
|
24900
|
-
-
|
|
24901
|
-
|
|
24902
|
-
|
|
24903
|
-
|
|
24904
|
-
|
|
24905
|
-
-
|
|
24906
|
-
|
|
24907
|
-
-
|
|
24908
|
-
|
|
24909
|
-
|
|
24910
|
-
|
|
24911
|
-
|
|
24912
|
-
|
|
24913
|
-
-
|
|
24914
|
-
|
|
24915
|
-
|
|
24916
|
-
|
|
24917
|
-
-
|
|
24918
|
-
|
|
24892
|
+
# MCP Scraper
|
|
24893
|
+
|
|
24894
|
+
Scrapes and analyzes web, search, maps, social, and site data. **Pick a tool by NAME from the map
|
|
24895
|
+
below, then load its schema before calling.** Where a tool needs a value another tool produces, the
|
|
24896
|
+
seam is noted so you can chain them.
|
|
24897
|
+
|
|
24898
|
+
## Search & research
|
|
24899
|
+
- Organic Google results, rankings, local pack -> **search_serp**.
|
|
24900
|
+
- Full SERP plus People-Also-Ask and AI-Overview detail -> **harvest_paa**.
|
|
24901
|
+
- \`search_serp\` returns \`organicResults[].url\` (often including reddit.com threads) \u2014 feed those into
|
|
24902
|
+
\`extract_url\` or \`reddit_thread\`.
|
|
24903
|
+
|
|
24904
|
+
## Pages & sites
|
|
24905
|
+
- One page -> **extract_url** (takes a url).
|
|
24906
|
+
- Whole site, crawl + SEO report -> **extract_site** (takes a url).
|
|
24907
|
+
- Just the URL list/inventory -> **map_site_urls** (takes a url).
|
|
24908
|
+
- \`map_site_urls\` returns urls you can feed straight into \`extract_url\`.
|
|
24909
|
+
|
|
24910
|
+
## Google Maps
|
|
24911
|
+
- Find multiple places/competitors/prospects -> **maps_search** (returns \`results[]\` with name,
|
|
24912
|
+
placeUrl, cid).
|
|
24913
|
+
- One business deep-dive + reviews -> **maps_place_intel** (takes businessName + location, NOT an id \u2014
|
|
24914
|
+
call it directly with a name from a \`maps_search\` result or from the user).
|
|
24915
|
+
|
|
24916
|
+
## YouTube
|
|
24917
|
+
- Find or list videos -> **youtube_harvest** (returns \`videos[].videoId\`).
|
|
24918
|
+
- Transcribe one video -> **youtube_transcribe** (takes a videoId from \`youtube_harvest\`, or a url).
|
|
24919
|
+
|
|
24920
|
+
## Facebook
|
|
24921
|
+
- Find advertisers -> **facebook_ad_search** (returns \`advertisers[]\` with pageId, libraryId).
|
|
24922
|
+
- One advertiser's ads -> **facebook_page_intel** (takes pageId, libraryId, or query; returns
|
|
24923
|
+
\`ads[].videoUrl\`).
|
|
24924
|
+
- Transcribe an ad video -> **facebook_ad_transcribe** (takes a videoUrl from \`facebook_page_intel\`).
|
|
24925
|
+
- Transcribe an organic reel/video/post -> **facebook_video_transcribe** (takes the Facebook url directly).
|
|
24926
|
+
|
|
24927
|
+
## Instagram
|
|
24928
|
+
- Profile inventory -> **instagram_profile_content** (returns post/reel/tv urls).
|
|
24929
|
+
- One post or reel -> **instagram_media_download** (takes a url from \`instagram_profile_content\`, or a
|
|
24930
|
+
url the user gives).
|
|
24931
|
+
|
|
24932
|
+
## Reddit
|
|
24933
|
+
- A reddit.com thread/post URL -> **reddit_thread**. Returns the post plus its comment tree and handles
|
|
24934
|
+
Reddit's bot wall itself (no login needed). Find threads first with \`search_serp\`.
|
|
24935
|
+
|
|
24936
|
+
## Other sites & logins (browser agent)
|
|
24937
|
+
For an arbitrary site or a logged-in dashboard with no dedicated tool, use the browser_* agent. **First
|
|
24938
|
+
decide whether the site needs a login:**
|
|
24939
|
+
- **Needs a login** (ChatGPT, Claude, any account) -> save it first: **browser_profile_connect** returns
|
|
24940
|
+
an mcpscraper.dev sign-in link for the user; one profile holds MANY logins (call it again with the same
|
|
24941
|
+
profile + a new domain to add accounts). Poll **browser_profile_list** until AUTHENTICATED, then
|
|
24942
|
+
**browser_open** with that profile.
|
|
24943
|
+
- **No login** -> **browser_open**, then navigate/read.
|
|
24944
|
+
|
|
24945
|
+
## Workflows
|
|
24946
|
+
Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when the whole job is one of these:
|
|
24947
|
+
- Build a local business directory for a niche across a state/region -> **directory_workflow**.
|
|
24948
|
+
- Stand up a rank-tracking blueprint (database + cron + ingestion plan) -> **rank_tracker_workflow**.
|
|
24949
|
+
- Find which sources an AI answer cites for a query (AEO) -> **query_fanout_workflow** (open a browser
|
|
24950
|
+
session on chatgpt.com or claude.ai FIRST, then run it against that session).
|
|
24951
|
+
|
|
24952
|
+
## Notes
|
|
24953
|
+
- Bulk / full-site crawls: call \`extract_site\` with \`rotateProxies:true\` for blocked or rate-limited
|
|
24954
|
+
sites. It returns a saved folder/artifact plus a summary, not the full content inline.
|
|
24955
|
+
- Browser sessions and media transcription cost credits and take longer \u2014 prefer the cheapest tool that
|
|
24956
|
+
answers the question (plain search/extract before browser agents).
|
|
24957
|
+
- Large results are saved to disk or an artifact and returned as a summary plus a path; read the path for
|
|
24958
|
+
full detail rather than expecting the whole payload inline.
|
|
24919
24959
|
`.trim();
|
|
24920
24960
|
}
|
|
24921
24961
|
});
|
|
@@ -25212,6 +25252,12 @@ var init_mcp_tool_schemas = __esm({
|
|
|
25212
25252
|
text: NullableString
|
|
25213
25253
|
}).nullable();
|
|
25214
25254
|
EntityIdsOutput = import_zod29.z.object({
|
|
25255
|
+
entities: import_zod29.z.array(import_zod29.z.object({
|
|
25256
|
+
name: import_zod29.z.string(),
|
|
25257
|
+
kgId: import_zod29.z.string().nullable(),
|
|
25258
|
+
cid: import_zod29.z.string().nullable(),
|
|
25259
|
+
gcid: import_zod29.z.string().nullable()
|
|
25260
|
+
})).describe("Each entity actually named on the page, with whichever of its kgId/cid/gcid were found \u2014 the linked view. kgIds/cids/gcids below are the same IDs as flat deduplicated lists, without the name link, kept for backward compatibility."),
|
|
25215
25261
|
kgIds: import_zod29.z.array(import_zod29.z.string()),
|
|
25216
25262
|
cids: import_zod29.z.array(import_zod29.z.string()),
|
|
25217
25263
|
gcids: import_zod29.z.array(import_zod29.z.string())
|
|
@@ -25983,7 +26029,7 @@ function buildPrompt(input, modes, tools, tables, jobs, metrics, expression, tar
|
|
|
25983
26029
|
"- Every scheduled batch must insert or update rank_tracker_runs before calling MCP tools.",
|
|
25984
26030
|
"- Use idempotency_key = project_id + keyword_id + location_id + mode + device + scheduled_date.",
|
|
25985
26031
|
"- Mark runs as running, succeeded, failed, or skipped; persist error_message on failures.",
|
|
25986
|
-
"-
|
|
26032
|
+
"- Call credits_info first to read the account's real concurrency_limit (most paid accounts have 3, 10, or 20, not 1), then run up to that many MCP calls in parallel \u2014 never sequential-by-default and never more than the limit, or calls start failing with 429s.",
|
|
25987
26033
|
"- Retry retryable upstream failures with backoff, but do not duplicate rows for the same idempotency key.",
|
|
25988
26034
|
"",
|
|
25989
26035
|
"Mode-specific ingestion rules:",
|
|
@@ -26615,93 +26661,1591 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
26615
26661
|
}
|
|
26616
26662
|
});
|
|
26617
26663
|
|
|
26618
|
-
// src/
|
|
26619
|
-
function
|
|
26620
|
-
return
|
|
26664
|
+
// src/services/fanout/export.ts
|
|
26665
|
+
function outputBaseDir3() {
|
|
26666
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path12.join)((0, import_node_os7.homedir)(), "Downloads", "mcp-scraper");
|
|
26621
26667
|
}
|
|
26622
|
-
function
|
|
26623
|
-
return (
|
|
26668
|
+
function safe(value) {
|
|
26669
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
26624
26670
|
}
|
|
26625
|
-
function
|
|
26626
|
-
|
|
26671
|
+
function writeTable(path6, rows, delimiter) {
|
|
26672
|
+
(0, import_node_fs9.writeFileSync)(path6, import_papaparse3.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
26627
26673
|
}
|
|
26628
|
-
function
|
|
26629
|
-
|
|
26630
|
-
|
|
26631
|
-
|
|
26632
|
-
|
|
26633
|
-
|
|
26674
|
+
function exportFanout(enriched) {
|
|
26675
|
+
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
26676
|
+
const outputDir = outputBaseDir3();
|
|
26677
|
+
const relativeDir = (0, import_node_path12.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
26678
|
+
const dir = (0, import_node_path12.join)(outputDir, relativeDir);
|
|
26679
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
26680
|
+
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
26681
|
+
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
26682
|
+
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
26683
|
+
return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
|
|
26684
|
+
});
|
|
26685
|
+
const sourceRows2 = enriched.browsedUrls.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
|
|
26686
|
+
const browsedOnlyRows = enriched.browsedOnly.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
|
|
26687
|
+
const snippetRows = enriched.snippets.map((s) => ({ domain: s.domain, url: s.url, title: s.title, text: s.text }));
|
|
26688
|
+
const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
|
|
26689
|
+
const relativePaths = {
|
|
26690
|
+
relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
|
|
26691
|
+
dir: relativeDir,
|
|
26692
|
+
json: (0, import_node_path12.join)(relativeDir, "fanout.json"),
|
|
26693
|
+
queriesCsv: (0, import_node_path12.join)(relativeDir, "queries.csv"),
|
|
26694
|
+
queriesTsv: (0, import_node_path12.join)(relativeDir, "queries.tsv"),
|
|
26695
|
+
citationsCsv: (0, import_node_path12.join)(relativeDir, "citations.csv"),
|
|
26696
|
+
sourcesCsv: (0, import_node_path12.join)(relativeDir, "sources.csv"),
|
|
26697
|
+
browsedOnlyCsv: (0, import_node_path12.join)(relativeDir, "browsed-only.csv"),
|
|
26698
|
+
snippetsCsv: (0, import_node_path12.join)(relativeDir, "snippets.csv"),
|
|
26699
|
+
domainsCsv: (0, import_node_path12.join)(relativeDir, "domains.csv"),
|
|
26700
|
+
report: (0, import_node_path12.join)(relativeDir, "report.html")
|
|
26634
26701
|
};
|
|
26702
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
|
|
26703
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
|
|
26704
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
|
|
26705
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
|
|
26706
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.sourcesCsv), sourceRows2, ",");
|
|
26707
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
|
|
26708
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
|
|
26709
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
|
|
26710
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
|
|
26711
|
+
return relativePaths;
|
|
26635
26712
|
}
|
|
26636
|
-
function
|
|
26637
|
-
return
|
|
26638
|
-
}
|
|
26639
|
-
function isBearerJwt(token) {
|
|
26640
|
-
return !token.startsWith("sk_") && token.split(".").length === 3;
|
|
26713
|
+
function esc(s) {
|
|
26714
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
26641
26715
|
}
|
|
26642
|
-
function
|
|
26643
|
-
|
|
26644
|
-
|
|
26645
|
-
|
|
26716
|
+
function renderReportHtml(enriched) {
|
|
26717
|
+
const data = JSON.stringify(enriched).replace(/</g, "\\u003c");
|
|
26718
|
+
return `<!doctype html>
|
|
26719
|
+
<html lang="en">
|
|
26720
|
+
<head>
|
|
26721
|
+
<meta charset="utf-8" />
|
|
26722
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26723
|
+
<title>AI Search Fan-Out \u2014 ${esc(enriched.platform)}</title>
|
|
26724
|
+
<style>
|
|
26725
|
+
:root{--bg:#0b0d12;--panel:#151922;--line:#252b38;--ink:#e8ecf3;--mut:#8b95a7;--acc:#5b9dff;--cite:#39d98a;--browse:#5a6677}
|
|
26726
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}
|
|
26727
|
+
.wrap{max-width:980px;margin:0 auto;padding:28px 20px 80px}
|
|
26728
|
+
h1{font-size:20px;margin:0 0 2px}.sub{color:var(--mut);margin:0 0 20px;font-size:13px}
|
|
26729
|
+
.stat{font-size:30px;font-weight:700}.stat small{font-size:13px;font-weight:400;color:var(--mut)}
|
|
26730
|
+
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:18px 0}
|
|
26731
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
|
26732
|
+
.card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
|
|
26733
|
+
.bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
|
|
26734
|
+
.bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
|
|
26735
|
+
.bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
|
|
26736
|
+
.tabs{display:flex;gap:8px;margin:22px 0 12px}.tab{background:var(--panel);border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;cursor:pointer}
|
|
26737
|
+
.tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
|
|
26738
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
|
26739
|
+
.chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
|
|
26740
|
+
.chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
|
|
26741
|
+
.row{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:6px 0;display:flex;gap:10px;align-items:flex-start}
|
|
26742
|
+
.row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
|
|
26743
|
+
.tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
|
|
26744
|
+
.dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
|
|
26745
|
+
.pill{font-size:11px;padding:2px 7px;border-radius:6px}.pill.cite{background:rgba(57,217,138,.15);color:var(--cite)}.pill.brow{background:rgba(90,102,119,.2);color:#aeb8c8}
|
|
26746
|
+
.copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
|
|
26747
|
+
.hide{display:none}
|
|
26748
|
+
</style>
|
|
26749
|
+
</head>
|
|
26750
|
+
<body><div class="wrap">
|
|
26751
|
+
<h1>AI Search Fan-Out</h1>
|
|
26752
|
+
<p class="sub" id="sub"></p>
|
|
26753
|
+
<div class="stat" id="stat"></div>
|
|
26754
|
+
<div class="cards" id="cards"></div>
|
|
26755
|
+
<div id="charts"></div>
|
|
26756
|
+
<div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
|
|
26757
|
+
<div class="chips" id="chips"></div>
|
|
26758
|
+
<div id="list"></div>
|
|
26759
|
+
</div>
|
|
26760
|
+
<script>
|
|
26761
|
+
const D=${data};
|
|
26762
|
+
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
26763
|
+
document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
|
|
26764
|
+
document.getElementById('stat').innerHTML='Total Fan-Out: '+D.counts.subQueries+' <small>sub-queries \xB7 '+D.counts.browsed+' URLs researched \xB7 '+D.counts.cited+' cited</small>';
|
|
26765
|
+
const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
|
|
26766
|
+
const cc=document.getElementById('cards');cards.forEach(([k,v])=>{const c=el('div','card');c.appendChild(el('b',null,v));c.appendChild(el('span',null,k));cc.appendChild(c)});
|
|
26767
|
+
function barChart(title,obj){const max=Math.max(1,...Object.values(obj));const box=el('div','bars');box.appendChild(el('div',null,'<b>'+title+'</b>'));Object.entries(obj).forEach(([k,v])=>{const r=el('div','bar');r.appendChild(el('div','lab',k));const t=el('div','track');const f=el('div','fill');f.style.width=(v/max*100)+'%';t.appendChild(f);r.appendChild(t);r.appendChild(el('div','n',v));box.appendChild(r)});return box}
|
|
26768
|
+
const charts=document.getElementById('charts');
|
|
26769
|
+
charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
|
|
26770
|
+
let tab='q',filter=null;
|
|
26771
|
+
const chips=document.getElementById('chips'),list=document.getElementById('list');
|
|
26772
|
+
function render(){
|
|
26773
|
+
chips.innerHTML='';list.innerHTML='';
|
|
26774
|
+
if(tab==='q'){
|
|
26775
|
+
D.queries.forEach((q,i)=>{const r=el('div','row');r.appendChild(el('div','idx',i+1));const m=el('div','main');m.appendChild(el('div',null,esc(q)));r.appendChild(m);const b=el('button','copy','copy');b.onclick=()=>navigator.clipboard.writeText(q);r.appendChild(b);list.appendChild(r)});
|
|
26776
|
+
} else {
|
|
26777
|
+
const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
|
|
26778
|
+
Object.entries(counts).forEach(([k,v])=>{const c=el('button','chip'+(filter===k?' on':''),k+' <b>'+v+'</b>');c.onclick=()=>{filter=filter===k?null:k;render()};chips.appendChild(c)});
|
|
26779
|
+
D.browsedUrls.forEach(s=>{if(filter&&s.siteType!==filter)return;const r=el('div','row');const m=el('div','main');m.appendChild(el('div',null,'<span class="dom">'+esc(s.domain)+'</span> <span class="pill '+(s.cited?'cite':'brow')+'">'+(s.cited?'cited '+s.timesCited+'\xD7':'browsed')+'</span> <span class="tag">'+s.siteType+'</span>'));m.appendChild(el('div','url',esc(s.url)));r.appendChild(m);list.appendChild(r)});
|
|
26780
|
+
}
|
|
26646
26781
|
}
|
|
26647
|
-
|
|
26648
|
-
|
|
26649
|
-
|
|
26650
|
-
|
|
26651
|
-
|
|
26652
|
-
return payload;
|
|
26782
|
+
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
26783
|
+
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('on'));t.classList.add('on');tab=t.dataset.tab;filter=null;render()});
|
|
26784
|
+
render();
|
|
26785
|
+
</script>
|
|
26786
|
+
</body></html>`;
|
|
26653
26787
|
}
|
|
26654
|
-
var
|
|
26655
|
-
var
|
|
26656
|
-
"src/
|
|
26788
|
+
var import_node_fs9, import_node_os7, import_node_path12, import_papaparse3;
|
|
26789
|
+
var init_export = __esm({
|
|
26790
|
+
"src/services/fanout/export.ts"() {
|
|
26657
26791
|
"use strict";
|
|
26658
|
-
|
|
26659
|
-
|
|
26792
|
+
import_node_fs9 = require("fs");
|
|
26793
|
+
import_node_os7 = require("os");
|
|
26794
|
+
import_node_path12 = require("path");
|
|
26795
|
+
import_papaparse3 = __toESM(require("papaparse"), 1);
|
|
26660
26796
|
}
|
|
26661
26797
|
});
|
|
26662
26798
|
|
|
26663
|
-
// src/mcp/
|
|
26664
|
-
|
|
26665
|
-
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26672
|
-
|
|
26673
|
-
|
|
26674
|
-
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26678
|
-
|
|
26679
|
-
|
|
26799
|
+
// src/mcp/browser-agent-tool-schemas.ts
|
|
26800
|
+
var import_zod30, NullableString2, BrowserRawObject, BrowserElementOutput, BrowserBaseOutput, BrowserReplayBaseOutput, BrowserOpenInputSchema, BrowserProfileConnectInputSchema, BrowserProfileListInputSchema, BrowserSessionInputSchema, BrowserLocateTargetSchema, BrowserLocateInputSchema, BrowserGotoInputSchema, BrowserClickInputSchema, BrowserTypeInputSchema, BrowserScrollInputSchema, BrowserPressInputSchema, BrowserReplayStopInputSchema, BrowserReplayDownloadInputSchema, BrowserReplayAnnotationSchema, BrowserReplayMarkInputSchema, BrowserReplayAnnotateInputSchema, BrowserListInputSchema, BrowserCaptureFanoutInputSchema, FanoutSourceOutput, BrowserCaptureFanoutOutputSchema, BrowserOpenOutputSchema, BrowserProfileLogin, BrowserProfileConnectOutputSchema, BrowserProfileListOutputSchema, BrowserScreenshotOutputSchema, BrowserReadOutputSchema, BrowserLocateOutputSchema, BrowserActionOutputSchema, BrowserReplayStartOutputSchema, BrowserReplayStopOutputSchema, BrowserListReplaysOutputSchema, BrowserReplayDownloadOutputSchema, BrowserReplayMarkOutputSchema, BrowserReplayAnnotateOutputSchema, BrowserCloseOutputSchema, BrowserListSessionsOutputSchema;
|
|
26801
|
+
var init_browser_agent_tool_schemas = __esm({
|
|
26802
|
+
"src/mcp/browser-agent-tool-schemas.ts"() {
|
|
26803
|
+
"use strict";
|
|
26804
|
+
import_zod30 = require("zod");
|
|
26805
|
+
NullableString2 = import_zod30.z.string().nullable();
|
|
26806
|
+
BrowserRawObject = import_zod30.z.record(import_zod30.z.unknown());
|
|
26807
|
+
BrowserElementOutput = import_zod30.z.record(import_zod30.z.unknown());
|
|
26808
|
+
BrowserBaseOutput = {
|
|
26809
|
+
ok: import_zod30.z.boolean().describe("Whether the browser-agent action succeeded."),
|
|
26810
|
+
tool: import_zod30.z.string().describe("Browser Agent MCP tool that produced this response."),
|
|
26811
|
+
session_id: NullableString2.describe("Browser session id when the response is scoped to a session.")
|
|
26812
|
+
};
|
|
26813
|
+
BrowserReplayBaseOutput = {
|
|
26814
|
+
...BrowserBaseOutput,
|
|
26815
|
+
replay_id: NullableString2.describe("Replay id when the response is scoped to a replay.")
|
|
26816
|
+
};
|
|
26817
|
+
BrowserOpenInputSchema = {
|
|
26818
|
+
label: import_zod30.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
26819
|
+
url: import_zod30.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
26820
|
+
profile: import_zod30.z.string().optional().describe("Optional saved hosted profile name to load a logged-in session for a site."),
|
|
26821
|
+
save_profile_changes: import_zod30.z.boolean().optional().describe("Persist cookies and browser storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
|
|
26822
|
+
timeout_seconds: import_zod30.z.number().int().min(60).max(259200).optional().describe("How long the direct no-proxy browser session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
|
|
26823
|
+
};
|
|
26824
|
+
BrowserProfileConnectInputSchema = {
|
|
26825
|
+
email: import_zod30.z.string().optional().describe("Account email for the login being saved. Used to derive a stable, reusable profile name AND recorded as a note of who is connected. Does NOT import existing cookies \u2014 the user signs in fresh via the returned watch_url."),
|
|
26826
|
+
profile: import_zod30.z.string().optional().describe("Profile to add this login to. Omit to derive one from email. A single profile holds MANY site logins \u2014 pass the same profile name again with a different domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com)."),
|
|
26827
|
+
domain: import_zod30.z.string().optional().describe("Site to log into, e.g. chatgpt.com, claude.ai, reddit.com. Defaults to chatgpt.com. Each domain is a separate login inside the profile."),
|
|
26828
|
+
login_url: import_zod30.z.string().url().optional().describe("Login page for the domain. Defaults to https://<domain>/ (https://chatgpt.com/ when domain is omitted)."),
|
|
26829
|
+
url: import_zod30.z.string().url().optional().describe("Deprecated alias for login_url. Use login_url."),
|
|
26830
|
+
note: import_zod30.z.string().optional().describe('Free-text note describing this login, e.g. "ChatGPT Plus, work account". Surfaced by browser_profile_list so you can see what the profile holds.'),
|
|
26831
|
+
label: import_zod30.z.string().optional().describe("Optional human label for this sign-in setup session."),
|
|
26832
|
+
timeout_seconds: import_zod30.z.number().int().min(60).max(259200).optional().describe("How long the sign-in setup session may live before auto-termination. Defaults to 600.")
|
|
26833
|
+
};
|
|
26834
|
+
BrowserProfileListInputSchema = {
|
|
26835
|
+
profile: import_zod30.z.string().optional().describe("Profile whose saved logins to list. Omit to derive from email."),
|
|
26836
|
+
email: import_zod30.z.string().optional().describe("Account email used to derive the profile name when profile is not given."),
|
|
26837
|
+
domain: import_zod30.z.string().optional().describe("Restrict to one site login, e.g. chatgpt.com. Use this to poll a single login until its status reads AUTHENTICATED."),
|
|
26838
|
+
connection_id: import_zod30.z.string().optional().describe("A specific login connection id returned by browser_profile_connect, to poll just that one.")
|
|
26839
|
+
};
|
|
26840
|
+
BrowserSessionInputSchema = {
|
|
26841
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
|
|
26842
|
+
};
|
|
26843
|
+
BrowserLocateTargetSchema = import_zod30.z.object({
|
|
26844
|
+
name: import_zod30.z.string().optional().describe("Optional label for this target, echoed in the result."),
|
|
26845
|
+
selector: import_zod30.z.string().optional().describe('CSS selector for the exact DOM element to locate, for example h1, input[name="q"], or [data-testid="result"].'),
|
|
26846
|
+
text: import_zod30.z.string().optional().describe("Visible text to locate when a selector is not known. The tool returns the text range bounds when possible."),
|
|
26847
|
+
match: import_zod30.z.enum(["contains", "exact"]).default("contains").describe("How to match text targets. Defaults to contains."),
|
|
26848
|
+
index: import_zod30.z.number().int().min(0).default(0).describe("Zero-based match index when multiple elements match.")
|
|
26849
|
+
}).refine((value) => Boolean(value.selector || value.text), {
|
|
26850
|
+
message: "target requires selector or text"
|
|
26851
|
+
});
|
|
26852
|
+
BrowserLocateInputSchema = {
|
|
26853
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26854
|
+
targets: import_zod30.z.array(BrowserLocateTargetSchema).min(1).max(20).describe("DOM targets to locate in the current viewport. Use selectors for exact elements, or text for visible text ranges.")
|
|
26855
|
+
};
|
|
26856
|
+
BrowserGotoInputSchema = {
|
|
26857
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26858
|
+
url: import_zod30.z.string().url().describe("URL to navigate the browser to.")
|
|
26859
|
+
};
|
|
26860
|
+
BrowserClickInputSchema = {
|
|
26861
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26862
|
+
x: import_zod30.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
26863
|
+
y: import_zod30.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
|
|
26864
|
+
button: import_zod30.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
|
|
26865
|
+
num_clicks: import_zod30.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
|
|
26866
|
+
};
|
|
26867
|
+
BrowserTypeInputSchema = {
|
|
26868
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26869
|
+
text: import_zod30.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
|
|
26870
|
+
delay: import_zod30.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
|
|
26871
|
+
};
|
|
26872
|
+
BrowserScrollInputSchema = {
|
|
26873
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26874
|
+
delta_y: import_zod30.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
|
|
26875
|
+
delta_x: import_zod30.z.number().default(0).describe("Horizontal scroll in wheel units."),
|
|
26876
|
+
x: import_zod30.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
|
|
26877
|
+
y: import_zod30.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
|
|
26878
|
+
};
|
|
26879
|
+
BrowserPressInputSchema = {
|
|
26880
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26881
|
+
keys: import_zod30.z.array(import_zod30.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
|
|
26882
|
+
};
|
|
26883
|
+
BrowserReplayStopInputSchema = {
|
|
26884
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26885
|
+
replay_id: import_zod30.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
|
|
26886
|
+
};
|
|
26887
|
+
BrowserReplayDownloadInputSchema = {
|
|
26888
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26889
|
+
replay_id: import_zod30.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
26890
|
+
filename: import_zod30.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
|
|
26891
|
+
};
|
|
26892
|
+
BrowserReplayAnnotationSchema = import_zod30.z.object({
|
|
26893
|
+
type: import_zod30.z.enum(["box", "circle", "underline", "arrow", "label"]).default("box").describe("Annotation style to draw over the replay."),
|
|
26894
|
+
start_seconds: import_zod30.z.number().min(0).default(0).describe("When the annotation should appear in the replay."),
|
|
26895
|
+
end_seconds: import_zod30.z.number().min(0).optional().describe("When the annotation should disappear. Defaults to two seconds after start_seconds."),
|
|
26896
|
+
left: import_zod30.z.number().optional().describe("Target left edge in browser screenshot pixels. Use element.left from browser_screenshot/browser_read."),
|
|
26897
|
+
top: import_zod30.z.number().optional().describe("Target top edge in browser screenshot pixels. Use element.top from browser_screenshot/browser_read."),
|
|
26898
|
+
width: import_zod30.z.number().positive().optional().describe("Target width in browser screenshot pixels. Use element.width from browser_screenshot/browser_read."),
|
|
26899
|
+
height: import_zod30.z.number().positive().optional().describe("Target height in browser screenshot pixels. Use element.height from browser_screenshot/browser_read."),
|
|
26900
|
+
x: import_zod30.z.number().optional().describe("Point target x coordinate in browser screenshot pixels when no box is available."),
|
|
26901
|
+
y: import_zod30.z.number().optional().describe("Point target y coordinate in browser screenshot pixels when no box is available."),
|
|
26902
|
+
from_x: import_zod30.z.number().optional().describe("Arrow start x coordinate in browser screenshot pixels. Defaults near the target."),
|
|
26903
|
+
from_y: import_zod30.z.number().optional().describe("Arrow start y coordinate in browser screenshot pixels. Defaults near the target."),
|
|
26904
|
+
to_x: import_zod30.z.number().optional().describe("Arrow target x coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
26905
|
+
to_y: import_zod30.z.number().optional().describe("Arrow target y coordinate in browser screenshot pixels. Defaults to the target box center."),
|
|
26906
|
+
label: import_zod30.z.string().max(120).optional().describe("Optional text callout to render near the annotation."),
|
|
26907
|
+
color: import_zod30.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
26908
|
+
thickness: import_zod30.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
|
|
26909
|
+
});
|
|
26910
|
+
BrowserReplayMarkInputSchema = {
|
|
26911
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself. A replay must already be recording."),
|
|
26912
|
+
target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
|
|
26913
|
+
type: import_zod30.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
|
|
26914
|
+
label: import_zod30.z.string().max(120).optional().describe("Optional callout text to render near the target."),
|
|
26915
|
+
color: import_zod30.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
|
|
26916
|
+
thickness: import_zod30.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5."),
|
|
26917
|
+
padding: import_zod30.z.number().min(0).max(80).default(8).describe("Pixels to expand the DOM bounds so the highlight does not touch the text edge."),
|
|
26918
|
+
start_offset_seconds: import_zod30.z.number().min(-5).max(10).default(-0.25).describe("Offset from the current replay time. Negative values make the callout appear just before the mark action is captured."),
|
|
26919
|
+
duration_seconds: import_zod30.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
|
|
26920
|
+
};
|
|
26921
|
+
BrowserReplayAnnotateInputSchema = {
|
|
26922
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
|
|
26923
|
+
replay_id: import_zod30.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
|
|
26924
|
+
annotations: import_zod30.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations to render on the replay. Prefer annotations returned by browser_replay_mark; otherwise use exact DOM bounds from browser_locate/browser_screenshot/browser_read."),
|
|
26925
|
+
filename: import_zod30.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
|
|
26926
|
+
source_width: import_zod30.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
|
|
26927
|
+
source_height: import_zod30.z.number().positive().optional().describe("Height of the coordinate space used for annotations. When this is a page viewport height smaller than the replay video height, the annotator infers the browser chrome top offset."),
|
|
26928
|
+
source_left_offset: import_zod30.z.number().min(0).optional().describe("Optional explicit X offset from annotation coordinates to replay video coordinates. Usually omitted."),
|
|
26929
|
+
source_top_offset: import_zod30.z.number().min(0).optional().describe("Optional explicit Y offset from annotation coordinates to replay video coordinates. Usually omitted because browser chrome offset is inferred when source_height is a page viewport height.")
|
|
26930
|
+
};
|
|
26931
|
+
BrowserListInputSchema = {
|
|
26932
|
+
include_closed: import_zod30.z.boolean().default(false).describe("Include closed sessions in the list.")
|
|
26933
|
+
};
|
|
26934
|
+
BrowserCaptureFanoutInputSchema = {
|
|
26935
|
+
session_id: import_zod30.z.string().describe("The session id returned by browser_open or browser_list_sessions. The session must be on chatgpt.com or claude.ai and logged in through a saved hosted profile. Use only a returned session_id; do not construct one yourself."),
|
|
26936
|
+
prompt: import_zod30.z.string().optional().describe("Optional prompt to type into the chat composer and submit before capturing. Omit to passively capture the fan-out of a prompt the user just ran in the visible browser. The prompt must trigger web search for there to be a fan-out."),
|
|
26937
|
+
wait_ms: import_zod30.z.number().int().min(0).max(18e4).optional().describe("How long to wait for the answer stream to finish before parsing. Defaults to 90000 when a prompt is sent, 8000 for passive capture. Capture ends as soon as the answer stream finishes."),
|
|
26938
|
+
first_party_domain: import_zod30.z.string().optional().describe("The brand/site being researched, for example example.com. Sources on this domain are tagged First-party/vendor. Set this when the prompt is about a specific business."),
|
|
26939
|
+
reset: import_zod30.z.boolean().default(false).describe("Clear any previously buffered stream for this page before capturing. Use when re-running to avoid mixing turns."),
|
|
26940
|
+
export: import_zod30.z.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, all sources, browsed-only sources, snippets, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout, returning paths relative to MCP_SCRAPER_OUTPUT_DIR.")
|
|
26941
|
+
};
|
|
26942
|
+
FanoutSourceOutput = import_zod30.z.object({
|
|
26943
|
+
url: import_zod30.z.string(),
|
|
26944
|
+
domain: import_zod30.z.string(),
|
|
26945
|
+
title: import_zod30.z.string(),
|
|
26946
|
+
cited: import_zod30.z.boolean(),
|
|
26947
|
+
timesCited: import_zod30.z.number().int().min(0),
|
|
26948
|
+
snippet: import_zod30.z.string(),
|
|
26949
|
+
round: import_zod30.z.number().int().nullable(),
|
|
26950
|
+
siteType: import_zod30.z.string().describe("URL category: First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, or Blog.")
|
|
26951
|
+
});
|
|
26952
|
+
BrowserCaptureFanoutOutputSchema = {
|
|
26953
|
+
...BrowserBaseOutput,
|
|
26954
|
+
tool: import_zod30.z.literal("query_fanout_workflow"),
|
|
26955
|
+
platform: import_zod30.z.enum(["ChatGPT", "Claude"]).describe("Which AI-search surface the fan-out was captured from."),
|
|
26956
|
+
captured_at: import_zod30.z.string(),
|
|
26957
|
+
prompt: import_zod30.z.string().describe("The user prompt that triggered the captured fan-out, when recoverable."),
|
|
26958
|
+
meta: import_zod30.z.object({
|
|
26959
|
+
model: import_zod30.z.string(),
|
|
26960
|
+
finishType: import_zod30.z.string(),
|
|
26961
|
+
title: import_zod30.z.string(),
|
|
26962
|
+
rounds: import_zod30.z.number().int().min(0)
|
|
26963
|
+
}),
|
|
26964
|
+
queries: import_zod30.z.array(import_zod30.z.string()).describe("Every web-search sub-query the model issued, in capture order. Classify these by funnel stage and type yourself from the text."),
|
|
26965
|
+
browsed_urls: import_zod30.z.array(FanoutSourceOutput).describe("Every researched URL (cited and browsed-only), cited first."),
|
|
26966
|
+
cited_urls: import_zod30.z.array(FanoutSourceOutput).describe("The subset of researched URLs actually cited in the final answer."),
|
|
26967
|
+
browsed_only: import_zod30.z.array(FanoutSourceOutput).describe("Researched URLs the model pulled but did not cite."),
|
|
26968
|
+
snippets: import_zod30.z.array(import_zod30.z.object({ url: import_zod30.z.string(), domain: import_zod30.z.string(), title: import_zod30.z.string(), text: import_zod30.z.string() })),
|
|
26969
|
+
counts: import_zod30.z.object({
|
|
26970
|
+
subQueries: import_zod30.z.number().int().min(0),
|
|
26971
|
+
browsed: import_zod30.z.number().int().min(0),
|
|
26972
|
+
cited: import_zod30.z.number().int().min(0),
|
|
26973
|
+
browsedOnly: import_zod30.z.number().int().min(0)
|
|
26974
|
+
}),
|
|
26975
|
+
aggregates: import_zod30.z.object({
|
|
26976
|
+
topSites: import_zod30.z.array(import_zod30.z.object({ domain: import_zod30.z.string(), count: import_zod30.z.number().int(), cited: import_zod30.z.boolean(), timesCited: import_zod30.z.number().int(), siteType: import_zod30.z.string() })),
|
|
26977
|
+
citationOrder: import_zod30.z.array(import_zod30.z.object({ rank: import_zod30.z.number().int(), domain: import_zod30.z.string(), url: import_zod30.z.string(), timesCited: import_zod30.z.number().int() })),
|
|
26978
|
+
byCategory: import_zod30.z.record(import_zod30.z.number().int())
|
|
26979
|
+
}).describe("Objective aggregates: top sourced sites by frequency, citation order, and URL-category counts."),
|
|
26980
|
+
first_party_domain: import_zod30.z.string().nullable(),
|
|
26981
|
+
exports: import_zod30.z.object({
|
|
26982
|
+
relativeTo: import_zod30.z.string(),
|
|
26983
|
+
dir: import_zod30.z.string(),
|
|
26984
|
+
json: import_zod30.z.string(),
|
|
26985
|
+
queriesCsv: import_zod30.z.string(),
|
|
26986
|
+
queriesTsv: import_zod30.z.string(),
|
|
26987
|
+
citationsCsv: import_zod30.z.string(),
|
|
26988
|
+
sourcesCsv: import_zod30.z.string(),
|
|
26989
|
+
browsedOnlyCsv: import_zod30.z.string(),
|
|
26990
|
+
snippetsCsv: import_zod30.z.string(),
|
|
26991
|
+
domainsCsv: import_zod30.z.string(),
|
|
26992
|
+
report: import_zod30.z.string()
|
|
26993
|
+
}).nullable().describe("Relative export paths when export=true, otherwise null. Paths are relative to MCP_SCRAPER_OUTPUT_DIR, or ~/Downloads/mcp-scraper when that env var is not set."),
|
|
26994
|
+
debug: import_zod30.z.object({
|
|
26995
|
+
interceptorReady: import_zod30.z.boolean(),
|
|
26996
|
+
rawBytes: import_zod30.z.number().int().min(0),
|
|
26997
|
+
unmappedKeys: import_zod30.z.array(import_zod30.z.string()),
|
|
26998
|
+
note: import_zod30.z.string()
|
|
26999
|
+
}).optional()
|
|
27000
|
+
};
|
|
27001
|
+
BrowserOpenOutputSchema = {
|
|
27002
|
+
ok: import_zod30.z.boolean(),
|
|
27003
|
+
tool: import_zod30.z.literal("browser_open"),
|
|
27004
|
+
session_id: import_zod30.z.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
|
|
27005
|
+
watch_url: import_zod30.z.string().describe("Human watch/takeover URL for this browser session on mcpscraper.dev."),
|
|
27006
|
+
live_view_url: NullableString2.describe("Deprecated; always null. Open watch_url to view the live session."),
|
|
27007
|
+
url: NullableString2.describe("Initial URL requested by the caller, when provided."),
|
|
27008
|
+
hint: import_zod30.z.string(),
|
|
27009
|
+
raw: BrowserRawObject.optional()
|
|
27010
|
+
};
|
|
27011
|
+
BrowserProfileLogin = import_zod30.z.object({
|
|
27012
|
+
connection_id: NullableString2.describe("Auth connection id for this login."),
|
|
27013
|
+
domain: import_zod30.z.string().describe("Site this login is for, e.g. chatgpt.com."),
|
|
27014
|
+
status: import_zod30.z.string().describe("Auth status, e.g. NEEDS_AUTH or AUTHENTICATED."),
|
|
27015
|
+
account_email: NullableString2.describe("Account email recorded for this login, when known."),
|
|
27016
|
+
note: NullableString2.describe("Free-text note describing this login."),
|
|
27017
|
+
watch_url: NullableString2.describe("mcpscraper.dev sign-in link when this login still needs the user to authenticate."),
|
|
27018
|
+
last_connected_at: NullableString2.describe("When this login was last saved or refreshed.")
|
|
27019
|
+
});
|
|
27020
|
+
BrowserProfileConnectOutputSchema = {
|
|
27021
|
+
ok: import_zod30.z.boolean(),
|
|
27022
|
+
tool: import_zod30.z.literal("browser_profile_connect"),
|
|
27023
|
+
session_id: NullableString2,
|
|
27024
|
+
auth_connection_id: import_zod30.z.string(),
|
|
27025
|
+
watch_url: import_zod30.z.string().describe("mcpscraper.dev sign-in link to give the user so they can complete this login."),
|
|
27026
|
+
live_view_url: NullableString2.describe("Deprecated; always null. Open watch_url to view the sign-in."),
|
|
27027
|
+
profile: import_zod30.z.string().describe("Profile this login was added to. Reuse it to stack more logins or to open a session."),
|
|
27028
|
+
domain: import_zod30.z.string(),
|
|
27029
|
+
setup_url: import_zod30.z.string(),
|
|
27030
|
+
account_email: NullableString2,
|
|
27031
|
+
note: NullableString2,
|
|
27032
|
+
status: import_zod30.z.string(),
|
|
27033
|
+
flow_status: NullableString2,
|
|
27034
|
+
flow_step: NullableString2,
|
|
27035
|
+
flow_expires_at: NullableString2,
|
|
27036
|
+
post_login_url: NullableString2,
|
|
27037
|
+
connected_logins: import_zod30.z.array(BrowserProfileLogin).describe("Every login currently saved in this profile, so you can see what else is connected."),
|
|
27038
|
+
next_steps: import_zod30.z.array(import_zod30.z.string()),
|
|
27039
|
+
raw: BrowserRawObject.optional()
|
|
27040
|
+
};
|
|
27041
|
+
BrowserProfileListOutputSchema = {
|
|
27042
|
+
ok: import_zod30.z.boolean(),
|
|
27043
|
+
tool: import_zod30.z.literal("browser_profile_list"),
|
|
27044
|
+
session_id: import_zod30.z.null(),
|
|
27045
|
+
profile: import_zod30.z.string().describe("Profile these logins belong to."),
|
|
27046
|
+
connections: import_zod30.z.array(BrowserProfileLogin).describe("All site logins saved in this profile, each with its current auth status and note."),
|
|
27047
|
+
count: import_zod30.z.number().int().min(0)
|
|
27048
|
+
};
|
|
27049
|
+
BrowserScreenshotOutputSchema = {
|
|
27050
|
+
...BrowserBaseOutput,
|
|
27051
|
+
tool: import_zod30.z.literal("browser_screenshot"),
|
|
27052
|
+
url: NullableString2,
|
|
27053
|
+
title: NullableString2,
|
|
27054
|
+
text: import_zod30.z.string(),
|
|
27055
|
+
elements: import_zod30.z.array(BrowserElementOutput),
|
|
27056
|
+
screenshot: import_zod30.z.object({
|
|
27057
|
+
mime_type: import_zod30.z.string(),
|
|
27058
|
+
inline: import_zod30.z.boolean()
|
|
27059
|
+
}).nullable()
|
|
27060
|
+
};
|
|
27061
|
+
BrowserReadOutputSchema = {
|
|
27062
|
+
...BrowserBaseOutput,
|
|
27063
|
+
tool: import_zod30.z.literal("browser_read"),
|
|
27064
|
+
url: NullableString2,
|
|
27065
|
+
title: NullableString2,
|
|
27066
|
+
text: import_zod30.z.string(),
|
|
27067
|
+
elements: import_zod30.z.array(BrowserElementOutput),
|
|
27068
|
+
raw: BrowserRawObject.optional()
|
|
27069
|
+
};
|
|
27070
|
+
BrowserLocateOutputSchema = {
|
|
27071
|
+
...BrowserBaseOutput,
|
|
27072
|
+
tool: import_zod30.z.literal("browser_locate"),
|
|
27073
|
+
url: NullableString2,
|
|
27074
|
+
title: NullableString2,
|
|
27075
|
+
viewport: BrowserRawObject.nullable(),
|
|
27076
|
+
replay: BrowserRawObject.nullable(),
|
|
27077
|
+
targets: import_zod30.z.array(BrowserRawObject),
|
|
27078
|
+
raw: BrowserRawObject.optional()
|
|
27079
|
+
};
|
|
27080
|
+
BrowserActionOutputSchema = {
|
|
27081
|
+
...BrowserBaseOutput,
|
|
27082
|
+
result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
|
|
27083
|
+
nextRecommendedTool: import_zod30.z.string().nullable()
|
|
27084
|
+
};
|
|
27085
|
+
BrowserReplayStartOutputSchema = {
|
|
27086
|
+
...BrowserReplayBaseOutput,
|
|
27087
|
+
tool: import_zod30.z.literal("browser_replay_start"),
|
|
27088
|
+
view_url: NullableString2,
|
|
27089
|
+
download_url: NullableString2,
|
|
27090
|
+
raw: BrowserRawObject.optional()
|
|
27091
|
+
};
|
|
27092
|
+
BrowserReplayStopOutputSchema = {
|
|
27093
|
+
...BrowserReplayBaseOutput,
|
|
27094
|
+
tool: import_zod30.z.literal("browser_replay_stop"),
|
|
27095
|
+
view_url: NullableString2,
|
|
27096
|
+
download_url: NullableString2,
|
|
27097
|
+
raw: BrowserRawObject.optional()
|
|
27098
|
+
};
|
|
27099
|
+
BrowserListReplaysOutputSchema = {
|
|
27100
|
+
...BrowserBaseOutput,
|
|
27101
|
+
tool: import_zod30.z.literal("browser_list_replays"),
|
|
27102
|
+
replays: import_zod30.z.array(BrowserRawObject),
|
|
27103
|
+
count: import_zod30.z.number().int().min(0)
|
|
27104
|
+
};
|
|
27105
|
+
BrowserReplayDownloadOutputSchema = {
|
|
27106
|
+
...BrowserReplayBaseOutput,
|
|
27107
|
+
tool: import_zod30.z.literal("browser_replay_download"),
|
|
27108
|
+
file_path: NullableString2,
|
|
27109
|
+
bytes: import_zod30.z.number().int().min(0).nullable(),
|
|
27110
|
+
mime_type: NullableString2,
|
|
27111
|
+
download_url: NullableString2
|
|
27112
|
+
};
|
|
27113
|
+
BrowserReplayMarkOutputSchema = {
|
|
27114
|
+
...BrowserReplayBaseOutput,
|
|
27115
|
+
tool: import_zod30.z.literal("browser_replay_mark"),
|
|
27116
|
+
annotation: BrowserRawObject,
|
|
27117
|
+
source_width: import_zod30.z.number().nullable(),
|
|
27118
|
+
source_height: import_zod30.z.number().nullable(),
|
|
27119
|
+
target: BrowserRawObject.nullable(),
|
|
27120
|
+
hint: import_zod30.z.string()
|
|
27121
|
+
};
|
|
27122
|
+
BrowserReplayAnnotateOutputSchema = {
|
|
27123
|
+
...BrowserReplayBaseOutput,
|
|
27124
|
+
tool: import_zod30.z.literal("browser_replay_annotate"),
|
|
27125
|
+
source_file_path: NullableString2,
|
|
27126
|
+
annotated_file_path: NullableString2,
|
|
27127
|
+
bytes: import_zod30.z.number().int().min(0).nullable(),
|
|
27128
|
+
width: import_zod30.z.number().int().min(0).nullable(),
|
|
27129
|
+
height: import_zod30.z.number().int().min(0).nullable(),
|
|
27130
|
+
annotation_count: import_zod30.z.number().int().min(0).nullable(),
|
|
27131
|
+
mime_type: NullableString2
|
|
27132
|
+
};
|
|
27133
|
+
BrowserCloseOutputSchema = {
|
|
27134
|
+
...BrowserBaseOutput,
|
|
27135
|
+
tool: import_zod30.z.literal("browser_close"),
|
|
27136
|
+
closed: import_zod30.z.boolean(),
|
|
27137
|
+
raw: BrowserRawObject.optional()
|
|
27138
|
+
};
|
|
27139
|
+
BrowserListSessionsOutputSchema = {
|
|
27140
|
+
ok: import_zod30.z.boolean(),
|
|
27141
|
+
tool: import_zod30.z.literal("browser_list_sessions"),
|
|
27142
|
+
session_id: import_zod30.z.null(),
|
|
27143
|
+
sessions: import_zod30.z.array(BrowserRawObject),
|
|
27144
|
+
count: import_zod30.z.number().int().min(0)
|
|
27145
|
+
};
|
|
27146
|
+
}
|
|
27147
|
+
});
|
|
27148
|
+
|
|
27149
|
+
// src/mcp/replay-annotator.ts
|
|
27150
|
+
function finiteNumber(value) {
|
|
27151
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
27152
|
+
}
|
|
27153
|
+
function clamp(value, min, max) {
|
|
27154
|
+
return Math.min(max, Math.max(min, value));
|
|
27155
|
+
}
|
|
27156
|
+
function formatAssTime(seconds) {
|
|
27157
|
+
const safe2 = Math.max(0, seconds);
|
|
27158
|
+
const hours = Math.floor(safe2 / 3600);
|
|
27159
|
+
const minutes = Math.floor(safe2 % 3600 / 60);
|
|
27160
|
+
const wholeSeconds = Math.floor(safe2 % 60);
|
|
27161
|
+
const centiseconds = Math.floor((safe2 - Math.floor(safe2)) * 100);
|
|
27162
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(centiseconds).padStart(2, "0")}`;
|
|
27163
|
+
}
|
|
27164
|
+
function cssHexToAssColor(color) {
|
|
27165
|
+
const normalized = color?.trim().match(/^#?([0-9a-fA-F]{6})$/)?.[1] ?? "ff3b30";
|
|
27166
|
+
const red = normalized.slice(0, 2);
|
|
27167
|
+
const green = normalized.slice(2, 4);
|
|
27168
|
+
const blue = normalized.slice(4, 6);
|
|
27169
|
+
return `&H${blue}${green}${red}&`;
|
|
27170
|
+
}
|
|
27171
|
+
function escapeAssText(value) {
|
|
27172
|
+
return value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{").replace(/\}/g, "\\}").replace(/\r?\n/g, "\\N");
|
|
27173
|
+
}
|
|
27174
|
+
function resolveCoordinateTransform(video, source, options) {
|
|
27175
|
+
const explicitOffsetX = finiteNumber(options.sourceLeftOffset) ? options.sourceLeftOffset : null;
|
|
27176
|
+
const explicitOffsetY = finiteNumber(options.sourceTopOffset) ? options.sourceTopOffset : null;
|
|
27177
|
+
const inferredTopOffset = explicitOffsetY == null && Math.round(source.width) === Math.round(video.width) && source.height < video.height && video.height - source.height <= 220 ? video.height - source.height : 0;
|
|
27178
|
+
const offsetX = explicitOffsetX ?? 0;
|
|
27179
|
+
const offsetY = explicitOffsetY ?? inferredTopOffset;
|
|
27180
|
+
return {
|
|
27181
|
+
scaleX: (video.width - offsetX) / source.width,
|
|
27182
|
+
scaleY: (video.height - offsetY) / source.height,
|
|
27183
|
+
offsetX,
|
|
27184
|
+
offsetY
|
|
27185
|
+
};
|
|
26680
27186
|
}
|
|
26681
|
-
|
|
26682
|
-
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
|
|
26686
|
-
|
|
26687
|
-
|
|
26688
|
-
|
|
26689
|
-
|
|
26690
|
-
|
|
26691
|
-
|
|
26692
|
-
|
|
26693
|
-
|
|
26694
|
-
|
|
26695
|
-
|
|
26696
|
-
return user2.api_key;
|
|
27187
|
+
function transformPoint(x, y, transform) {
|
|
27188
|
+
return {
|
|
27189
|
+
x: x * transform.scaleX + transform.offsetX,
|
|
27190
|
+
y: y * transform.scaleY + transform.offsetY
|
|
27191
|
+
};
|
|
27192
|
+
}
|
|
27193
|
+
function scaleRect(annotation, transform) {
|
|
27194
|
+
if (finiteNumber(annotation.left) && finiteNumber(annotation.top) && finiteNumber(annotation.width) && finiteNumber(annotation.height)) {
|
|
27195
|
+
const origin = transformPoint(annotation.left, annotation.top, transform);
|
|
27196
|
+
return {
|
|
27197
|
+
left: origin.x,
|
|
27198
|
+
top: origin.y,
|
|
27199
|
+
width: annotation.width * transform.scaleX,
|
|
27200
|
+
height: annotation.height * transform.scaleY
|
|
27201
|
+
};
|
|
26697
27202
|
}
|
|
26698
|
-
|
|
26699
|
-
|
|
26700
|
-
|
|
26701
|
-
|
|
26702
|
-
|
|
27203
|
+
if (finiteNumber(annotation.x) && finiteNumber(annotation.y)) {
|
|
27204
|
+
const radius = finiteNumber(annotation.width) ? annotation.width / 2 : 28;
|
|
27205
|
+
const point = transformPoint(annotation.x, annotation.y, transform);
|
|
27206
|
+
return {
|
|
27207
|
+
left: point.x - radius * transform.scaleX,
|
|
27208
|
+
top: point.y - radius * transform.scaleY,
|
|
27209
|
+
width: radius * 2 * transform.scaleX,
|
|
27210
|
+
height: radius * 2 * transform.scaleY
|
|
27211
|
+
};
|
|
27212
|
+
}
|
|
27213
|
+
throw new Error("annotation needs either left/top/width/height or x/y");
|
|
27214
|
+
}
|
|
27215
|
+
function rectPath(rect) {
|
|
27216
|
+
const x1 = Math.round(rect.left);
|
|
27217
|
+
const y1 = Math.round(rect.top);
|
|
27218
|
+
const x2 = Math.round(rect.left + rect.width);
|
|
27219
|
+
const y2 = Math.round(rect.top + rect.height);
|
|
27220
|
+
return `m ${x1} ${y1} l ${x2} ${y1} l ${x2} ${y2} l ${x1} ${y2} l ${x1} ${y1}`;
|
|
27221
|
+
}
|
|
27222
|
+
function ellipsePath(rect) {
|
|
27223
|
+
const cx = rect.left + rect.width / 2;
|
|
27224
|
+
const cy = rect.top + rect.height / 2;
|
|
27225
|
+
const rx = Math.max(4, rect.width / 2);
|
|
27226
|
+
const ry = Math.max(4, rect.height / 2);
|
|
27227
|
+
const points = [];
|
|
27228
|
+
for (let i = 0; i <= 32; i += 1) {
|
|
27229
|
+
const t = Math.PI * 2 * i / 32;
|
|
27230
|
+
const x = Math.round(cx + Math.cos(t) * rx);
|
|
27231
|
+
const y = Math.round(cy + Math.sin(t) * ry);
|
|
27232
|
+
points.push(`${i === 0 ? "m" : "l"} ${x} ${y}`);
|
|
27233
|
+
}
|
|
27234
|
+
return points.join(" ");
|
|
27235
|
+
}
|
|
27236
|
+
function filledRectPath(left, top, width, height) {
|
|
27237
|
+
return rectPath({ left, top, width, height });
|
|
27238
|
+
}
|
|
27239
|
+
function arrowPath(fromX, fromY, toX, toY, thickness) {
|
|
27240
|
+
const dx = toX - fromX;
|
|
27241
|
+
const dy = toY - fromY;
|
|
27242
|
+
const length = Math.max(1, Math.hypot(dx, dy));
|
|
27243
|
+
const ux = dx / length;
|
|
27244
|
+
const uy = dy / length;
|
|
27245
|
+
const px = -uy;
|
|
27246
|
+
const py = ux;
|
|
27247
|
+
const half = thickness / 2;
|
|
27248
|
+
const head = Math.max(14, thickness * 4);
|
|
27249
|
+
const baseX = toX - ux * head;
|
|
27250
|
+
const baseY = toY - uy * head;
|
|
27251
|
+
const p1x = Math.round(fromX + px * half);
|
|
27252
|
+
const p1y = Math.round(fromY + py * half);
|
|
27253
|
+
const p2x = Math.round(baseX + px * half);
|
|
27254
|
+
const p2y = Math.round(baseY + py * half);
|
|
27255
|
+
const p3x = Math.round(baseX + px * head * 0.55);
|
|
27256
|
+
const p3y = Math.round(baseY + py * head * 0.55);
|
|
27257
|
+
const p4x = Math.round(toX);
|
|
27258
|
+
const p4y = Math.round(toY);
|
|
27259
|
+
const p5x = Math.round(baseX - px * head * 0.55);
|
|
27260
|
+
const p5y = Math.round(baseY - py * head * 0.55);
|
|
27261
|
+
const p6x = Math.round(baseX - px * half);
|
|
27262
|
+
const p6y = Math.round(baseY - py * half);
|
|
27263
|
+
const p7x = Math.round(fromX - px * half);
|
|
27264
|
+
const p7y = Math.round(fromY - py * half);
|
|
27265
|
+
return `m ${p1x} ${p1y} l ${p2x} ${p2y} l ${p3x} ${p3y} l ${p4x} ${p4y} l ${p5x} ${p5y} l ${p6x} ${p6y} l ${p7x} ${p7y} l ${p1x} ${p1y}`;
|
|
27266
|
+
}
|
|
27267
|
+
function eventLine(start, end, body) {
|
|
27268
|
+
return `Dialogue: 0,${formatAssTime(start)},${formatAssTime(end)},Default,,0,0,0,,${body}`;
|
|
27269
|
+
}
|
|
27270
|
+
function labelLine(start, end, x, y, label) {
|
|
27271
|
+
const safeX = Math.round(x);
|
|
27272
|
+
const safeY = Math.round(y);
|
|
27273
|
+
return eventLine(
|
|
27274
|
+
start,
|
|
27275
|
+
end,
|
|
27276
|
+
`{\\an7\\pos(${safeX},${safeY})\\fs30\\bord4\\shad0\\c&HFFFFFF&\\3c&H000000&}${escapeAssText(label)}`
|
|
27277
|
+
);
|
|
26703
27278
|
}
|
|
26704
|
-
function
|
|
27279
|
+
function shapeLine(start, end, path6, color, thickness, filled = false) {
|
|
27280
|
+
const fillAlpha = filled ? "&H00&" : "&HFF&";
|
|
27281
|
+
const border = filled ? 0 : thickness;
|
|
27282
|
+
return eventLine(
|
|
27283
|
+
start,
|
|
27284
|
+
end,
|
|
27285
|
+
`{\\an7\\pos(0,0)\\p1\\bord${border}\\shad0\\1a${fillAlpha}\\1c${color}\\3c${color}}${path6}`
|
|
27286
|
+
);
|
|
27287
|
+
}
|
|
27288
|
+
function buildAssSubtitle(options, video) {
|
|
27289
|
+
const source = {
|
|
27290
|
+
width: options.sourceWidth && options.sourceWidth > 0 ? options.sourceWidth : video.width,
|
|
27291
|
+
height: options.sourceHeight && options.sourceHeight > 0 ? options.sourceHeight : video.height
|
|
27292
|
+
};
|
|
27293
|
+
const transform = resolveCoordinateTransform(video, source, options);
|
|
27294
|
+
const lines = [];
|
|
27295
|
+
for (const annotation of options.annotations) {
|
|
27296
|
+
const start = Math.max(0, annotation.start_seconds ?? 0);
|
|
27297
|
+
const end = annotation.end_seconds && annotation.end_seconds > start ? annotation.end_seconds : start + 2;
|
|
27298
|
+
const type = annotation.type ?? "box";
|
|
27299
|
+
const color = cssHexToAssColor(annotation.color);
|
|
27300
|
+
const thickness = Math.round(clamp(annotation.thickness ?? 5, 2, 24));
|
|
27301
|
+
if (type === "label") {
|
|
27302
|
+
if (!annotation.label) throw new Error("label annotation needs label");
|
|
27303
|
+
const rect2 = scaleRect(annotation, transform);
|
|
27304
|
+
lines.push(labelLine(start, end, rect2.left, Math.max(8, rect2.top), annotation.label));
|
|
27305
|
+
continue;
|
|
27306
|
+
}
|
|
27307
|
+
if (type === "arrow") {
|
|
27308
|
+
const rect2 = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? null : scaleRect(annotation, transform);
|
|
27309
|
+
const toPoint = finiteNumber(annotation.to_x) && finiteNumber(annotation.to_y) ? transformPoint(annotation.to_x, annotation.to_y, transform) : null;
|
|
27310
|
+
const toX = toPoint ? toPoint.x : rect2.left + rect2.width / 2;
|
|
27311
|
+
const toY = toPoint ? toPoint.y : rect2.top + rect2.height / 2;
|
|
27312
|
+
const fromPoint = finiteNumber(annotation.from_x) && finiteNumber(annotation.from_y) ? transformPoint(annotation.from_x, annotation.from_y, transform) : null;
|
|
27313
|
+
const fromX = fromPoint ? fromPoint.x : clamp(toX - 120, 12, video.width - 12);
|
|
27314
|
+
const fromY = fromPoint ? fromPoint.y : clamp(toY - 90, 12, video.height - 12);
|
|
27315
|
+
lines.push(shapeLine(start, end, arrowPath(fromX, fromY, toX, toY, thickness), color, thickness, true));
|
|
27316
|
+
if (annotation.label) lines.push(labelLine(start, end, fromX + 8, Math.max(96, fromY - 36), annotation.label));
|
|
27317
|
+
continue;
|
|
27318
|
+
}
|
|
27319
|
+
const rect = scaleRect(annotation, transform);
|
|
27320
|
+
if (type === "underline") {
|
|
27321
|
+
const underlineTop = rect.top + rect.height + thickness;
|
|
27322
|
+
lines.push(shapeLine(start, end, filledRectPath(rect.left, underlineTop, rect.width, thickness), color, 0, true));
|
|
27323
|
+
} else if (type === "circle") {
|
|
27324
|
+
lines.push(shapeLine(start, end, ellipsePath(rect), color, thickness));
|
|
27325
|
+
} else {
|
|
27326
|
+
lines.push(shapeLine(start, end, rectPath(rect), color, thickness));
|
|
27327
|
+
}
|
|
27328
|
+
if (annotation.label) lines.push(labelLine(start, end, rect.left, Math.max(8, rect.top - 38), annotation.label));
|
|
27329
|
+
}
|
|
27330
|
+
return [
|
|
27331
|
+
"[Script Info]",
|
|
27332
|
+
"ScriptType: v4.00+",
|
|
27333
|
+
`PlayResX: ${Math.round(video.width)}`,
|
|
27334
|
+
`PlayResY: ${Math.round(video.height)}`,
|
|
27335
|
+
"ScaledBorderAndShadow: yes",
|
|
27336
|
+
"",
|
|
27337
|
+
"[V4+ Styles]",
|
|
27338
|
+
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding",
|
|
27339
|
+
"Style: Default,Arial,30,&H00FFFFFF,&H000000FF,&H00000000,&H90000000,0,0,0,0,100,100,0,0,1,2,0,7,0,0,0,1",
|
|
27340
|
+
"",
|
|
27341
|
+
"[Events]",
|
|
27342
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
27343
|
+
...lines,
|
|
27344
|
+
""
|
|
27345
|
+
].join("\n");
|
|
27346
|
+
}
|
|
27347
|
+
async function videoSize(inputFilePath) {
|
|
27348
|
+
const { stdout } = await execFileAsync2("ffprobe", [
|
|
27349
|
+
"-v",
|
|
27350
|
+
"error",
|
|
27351
|
+
"-select_streams",
|
|
27352
|
+
"v:0",
|
|
27353
|
+
"-show_entries",
|
|
27354
|
+
"stream=width,height",
|
|
27355
|
+
"-of",
|
|
27356
|
+
"json",
|
|
27357
|
+
inputFilePath
|
|
27358
|
+
], { maxBuffer: 1024 * 1024 });
|
|
27359
|
+
const parsed = JSON.parse(stdout);
|
|
27360
|
+
const stream = parsed.streams?.[0];
|
|
27361
|
+
if (!stream?.width || !stream.height) throw new Error("could not read replay video dimensions");
|
|
27362
|
+
return { width: stream.width, height: stream.height };
|
|
27363
|
+
}
|
|
27364
|
+
function ffmpegFilterPath(path6) {
|
|
27365
|
+
return path6.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
27366
|
+
}
|
|
27367
|
+
async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
|
|
27368
|
+
if (!options.annotations.length) throw new Error("annotations must include at least one item");
|
|
27369
|
+
const size = await videoSize(inputFilePath);
|
|
27370
|
+
const tmp = await (0, import_promises10.mkdtemp)((0, import_node_path13.join)((0, import_node_os8.tmpdir)(), "mcp-scraper-ass-"));
|
|
27371
|
+
const assPath = (0, import_node_path13.join)(tmp, "annotations.ass");
|
|
27372
|
+
try {
|
|
27373
|
+
await (0, import_promises10.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
|
|
27374
|
+
await execFileAsync2("ffmpeg", [
|
|
27375
|
+
"-y",
|
|
27376
|
+
"-i",
|
|
27377
|
+
inputFilePath,
|
|
27378
|
+
"-vf",
|
|
27379
|
+
`ass=${ffmpegFilterPath(assPath)}`,
|
|
27380
|
+
"-c:v",
|
|
27381
|
+
"libx264",
|
|
27382
|
+
"-pix_fmt",
|
|
27383
|
+
"yuv420p",
|
|
27384
|
+
"-movflags",
|
|
27385
|
+
"+faststart",
|
|
27386
|
+
"-c:a",
|
|
27387
|
+
"copy",
|
|
27388
|
+
outputFilePath
|
|
27389
|
+
], { maxBuffer: 1024 * 1024 * 20 });
|
|
27390
|
+
const out = await (0, import_promises10.stat)(outputFilePath);
|
|
27391
|
+
return {
|
|
27392
|
+
filePath: outputFilePath,
|
|
27393
|
+
bytes: out.size,
|
|
27394
|
+
width: size.width,
|
|
27395
|
+
height: size.height,
|
|
27396
|
+
annotationCount: options.annotations.length
|
|
27397
|
+
};
|
|
27398
|
+
} finally {
|
|
27399
|
+
await (0, import_promises10.rm)(tmp, { recursive: true, force: true });
|
|
27400
|
+
}
|
|
27401
|
+
}
|
|
27402
|
+
var import_node_child_process4, import_promises10, import_node_os8, import_node_path13, import_node_util2, execFileAsync2;
|
|
27403
|
+
var init_replay_annotator = __esm({
|
|
27404
|
+
"src/mcp/replay-annotator.ts"() {
|
|
27405
|
+
"use strict";
|
|
27406
|
+
import_node_child_process4 = require("child_process");
|
|
27407
|
+
import_promises10 = require("fs/promises");
|
|
27408
|
+
import_node_os8 = require("os");
|
|
27409
|
+
import_node_path13 = require("path");
|
|
27410
|
+
import_node_util2 = require("util");
|
|
27411
|
+
execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
|
|
27412
|
+
}
|
|
27413
|
+
});
|
|
27414
|
+
|
|
27415
|
+
// src/mcp/browser-agent-mcp-server.ts
|
|
27416
|
+
function structuredResult(value, isError = false) {
|
|
27417
|
+
const safe2 = sanitizeOutboundDiagnostics(value);
|
|
27418
|
+
return {
|
|
27419
|
+
content: [{ type: "text", text: JSON.stringify(safe2) }],
|
|
27420
|
+
structuredContent: safe2,
|
|
27421
|
+
isError
|
|
27422
|
+
};
|
|
27423
|
+
}
|
|
27424
|
+
function errorMessage4(value) {
|
|
27425
|
+
if (value && typeof value === "object") {
|
|
27426
|
+
const data = value;
|
|
27427
|
+
if (typeof data.error === "string") return data.error;
|
|
27428
|
+
if (typeof data.message === "string") return data.message;
|
|
27429
|
+
}
|
|
27430
|
+
return typeof value === "string" ? value : "Browser Agent request failed";
|
|
27431
|
+
}
|
|
27432
|
+
function errorResult(tool, value, sessionId = null, replayId = null) {
|
|
27433
|
+
return structuredResult({
|
|
27434
|
+
ok: false,
|
|
27435
|
+
tool,
|
|
27436
|
+
session_id: sessionId,
|
|
27437
|
+
...replayId !== null ? { replay_id: replayId } : {},
|
|
27438
|
+
error: errorMessage4(value),
|
|
27439
|
+
raw: value && typeof value === "object" ? value : { value }
|
|
27440
|
+
}, true);
|
|
27441
|
+
}
|
|
27442
|
+
function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
|
|
27443
|
+
if (!ok) return errorResult(tool, data, sessionId);
|
|
27444
|
+
return structuredResult({
|
|
27445
|
+
ok: true,
|
|
27446
|
+
tool,
|
|
27447
|
+
session_id: sessionId,
|
|
27448
|
+
result: data && typeof data === "object" ? data : { value: data },
|
|
27449
|
+
nextRecommendedTool
|
|
27450
|
+
});
|
|
27451
|
+
}
|
|
27452
|
+
function outputBaseDir4() {
|
|
27453
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path14.join)((0, import_node_os9.homedir)(), "Downloads", "mcp-scraper");
|
|
27454
|
+
}
|
|
27455
|
+
function safeFilePart2(value) {
|
|
27456
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
|
|
27457
|
+
}
|
|
27458
|
+
function replayFilePath(sessionId, replayId, filename) {
|
|
27459
|
+
const requested = filename?.trim();
|
|
27460
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
27461
|
+
const name = requested ? safeFilePart2(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart2(sessionId)}-${safeFilePart2(replayId)}`;
|
|
27462
|
+
return (0, import_node_path14.join)(outputBaseDir4(), "browser-replays", `${name}.mp4`);
|
|
27463
|
+
}
|
|
27464
|
+
function slugPart(value) {
|
|
27465
|
+
const trimmed = value?.trim().toLowerCase();
|
|
27466
|
+
if (!trimmed) return null;
|
|
27467
|
+
return trimmed.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").replace(/[^a-z0-9._-]+/g, "-").replace(/[._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
27468
|
+
}
|
|
27469
|
+
function savedProfileNameFromEmail(email) {
|
|
27470
|
+
const value = slugPart(email);
|
|
27471
|
+
if (!value) return null;
|
|
27472
|
+
return value.slice(0, 80) || null;
|
|
27473
|
+
}
|
|
27474
|
+
function isEnrichedFanout(value) {
|
|
27475
|
+
if (!value || typeof value !== "object") return false;
|
|
27476
|
+
const candidate = value;
|
|
27477
|
+
return typeof candidate.platform === "string" && typeof candidate.capturedAt === "string" && typeof candidate.prompt === "string" && Array.isArray(candidate.queries) && Array.isArray(candidate.browsedUrls) && Array.isArray(candidate.citedUrls) && Array.isArray(candidate.browsedOnly) && Array.isArray(candidate.snippets) && !!candidate.counts && !!candidate.aggregates;
|
|
27478
|
+
}
|
|
27479
|
+
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
27480
|
+
const requested = filename?.trim();
|
|
27481
|
+
if (requested) return replayFilePath(sessionId, replayId, requested);
|
|
27482
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
27483
|
+
return replayFilePath(sessionId, replayId, `${stamp}-${safeFilePart2(sessionId)}-${safeFilePart2(replayId)}-annotated`);
|
|
27484
|
+
}
|
|
27485
|
+
function finiteNumber2(value) {
|
|
27486
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
27487
|
+
}
|
|
27488
|
+
function expandElementBounds(element, viewport, padding) {
|
|
27489
|
+
const sourceWidth = finiteNumber2(viewport?.width) && viewport.width > 0 ? viewport.width : 1920;
|
|
27490
|
+
const sourceHeight = finiteNumber2(viewport?.height) && viewport.height > 0 ? viewport.height : 1080;
|
|
27491
|
+
const left = Math.max(0, element.left - padding);
|
|
27492
|
+
const top = Math.max(0, element.top - padding);
|
|
27493
|
+
const right = Math.min(sourceWidth, element.left + element.width + padding);
|
|
27494
|
+
const bottom = Math.min(sourceHeight, element.top + element.height + padding);
|
|
27495
|
+
return {
|
|
27496
|
+
left,
|
|
27497
|
+
top,
|
|
27498
|
+
width: Math.max(1, right - left),
|
|
27499
|
+
height: Math.max(1, bottom - top),
|
|
27500
|
+
sourceWidth,
|
|
27501
|
+
sourceHeight
|
|
27502
|
+
};
|
|
27503
|
+
}
|
|
27504
|
+
function registerBrowserAgentMcpTools(server, opts) {
|
|
27505
|
+
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
27506
|
+
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
27507
|
+
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
27508
|
+
async function req(method, path6, body, requestTimeoutMs = timeoutMs) {
|
|
27509
|
+
try {
|
|
27510
|
+
const res = await fetch(`${baseUrl}${path6}`, {
|
|
27511
|
+
method,
|
|
27512
|
+
headers: { "Content-Type": "application/json", "x-api-key": opts.apiKey },
|
|
27513
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
27514
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
27515
|
+
});
|
|
27516
|
+
const data = await res.json().catch(() => ({}));
|
|
27517
|
+
return { ok: res.ok, data };
|
|
27518
|
+
} catch (err) {
|
|
27519
|
+
return { ok: false, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
27520
|
+
}
|
|
27521
|
+
}
|
|
27522
|
+
async function downloadReplay(sessionId, replayId, filename) {
|
|
27523
|
+
const path6 = `/agent/sessions/${encodeURIComponent(sessionId)}/replays/${encodeURIComponent(replayId)}/download`;
|
|
27524
|
+
try {
|
|
27525
|
+
const res = await fetch(`${baseUrl}${path6}`, {
|
|
27526
|
+
method: "GET",
|
|
27527
|
+
headers: { "x-api-key": opts.apiKey },
|
|
27528
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
27529
|
+
});
|
|
27530
|
+
if (!res.ok) {
|
|
27531
|
+
const data = await res.json().catch(async () => ({ error: await res.text().catch(() => `HTTP ${res.status}`) }));
|
|
27532
|
+
return { ok: false, data };
|
|
27533
|
+
}
|
|
27534
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
27535
|
+
const filePath = replayFilePath(sessionId, replayId, filename);
|
|
27536
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path14.join)(outputBaseDir4(), "browser-replays"), { recursive: true });
|
|
27537
|
+
(0, import_node_fs10.writeFileSync)(filePath, bytes);
|
|
27538
|
+
return {
|
|
27539
|
+
ok: true,
|
|
27540
|
+
data: {
|
|
27541
|
+
replay_id: replayId,
|
|
27542
|
+
file_path: filePath,
|
|
27543
|
+
bytes: bytes.length,
|
|
27544
|
+
mime_type: res.headers.get("content-type") ?? "video/mp4",
|
|
27545
|
+
download_url: `${baseUrl}${path6}`
|
|
27546
|
+
}
|
|
27547
|
+
};
|
|
27548
|
+
} catch (err) {
|
|
27549
|
+
return { ok: false, data: { error: err instanceof Error ? err.message : String(err) } };
|
|
27550
|
+
}
|
|
27551
|
+
}
|
|
27552
|
+
const annotations = (title, readOnly = false) => ({
|
|
27553
|
+
title,
|
|
27554
|
+
readOnlyHint: readOnly,
|
|
27555
|
+
destructiveHint: false,
|
|
27556
|
+
idempotentHint: false,
|
|
27557
|
+
openWorldHint: true
|
|
27558
|
+
});
|
|
27559
|
+
function normalizeLogin(conn) {
|
|
27560
|
+
return {
|
|
27561
|
+
connection_id: conn.connection_id ?? null,
|
|
27562
|
+
domain: conn.domain,
|
|
27563
|
+
status: conn.status,
|
|
27564
|
+
account_email: conn.account_email ?? null,
|
|
27565
|
+
note: conn.note ?? null,
|
|
27566
|
+
watch_url: conn.connection_id && conn.status !== "AUTHENTICATED" ? `${consoleBase}/console/auth/${conn.connection_id}` : null,
|
|
27567
|
+
last_connected_at: conn.last_connected_at ?? null
|
|
27568
|
+
};
|
|
27569
|
+
}
|
|
27570
|
+
async function fetchConnectedLogins(profile) {
|
|
27571
|
+
const list = await req("POST", "/agent/profiles/list", { profile });
|
|
27572
|
+
return list.ok && Array.isArray(list.data?.connections) ? list.data.connections.map(normalizeLogin) : [];
|
|
27573
|
+
}
|
|
27574
|
+
server.registerTool(
|
|
27575
|
+
"browser_profile_connect",
|
|
27576
|
+
{
|
|
27577
|
+
title: "Save a Site Login to a Profile",
|
|
27578
|
+
description: `Save a logged-in browser session so this server can reuse a site the user is signed into (ChatGPT, Claude, Reddit, or any account-gated site). Returns an mcpscraper.dev watch_url \u2014 give it to the user, they sign in once, and the cookies are saved to a named profile for later browser_open calls. ONE profile holds MANY logins: call this again with the SAME profile and a DIFFERENT domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com). Pass note to record what each login is. Side effects: opens a short-lived sign-in session and persists cookies to the profile; it does NOT import the user's existing browser cookies (they log in fresh). NOT for one-off scraping of public pages (use extract_url) and NOT a way to drive the browser (use browser_open). Sample asks: "save my ChatGPT login", "connect my Reddit account", "add my Claude login to the same profile". After the user signs in, poll browser_profile_list until the login reads AUTHENTICATED, then browser_open with the profile.`,
|
|
27579
|
+
inputSchema: BrowserProfileConnectInputSchema,
|
|
27580
|
+
outputSchema: BrowserProfileConnectOutputSchema,
|
|
27581
|
+
annotations: annotations("Save a Site Login to a Profile")
|
|
27582
|
+
},
|
|
27583
|
+
async (input) => {
|
|
27584
|
+
const domain = slugPart(input.domain) || "chatgpt.com";
|
|
27585
|
+
const setupUrl = input.login_url ?? input.url ?? (domain === "chatgpt.com" ? "https://chatgpt.com/" : `https://${domain}/`);
|
|
27586
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
27587
|
+
if (!profile) {
|
|
27588
|
+
return errorResult("browser_profile_connect", {
|
|
27589
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
27590
|
+
});
|
|
27591
|
+
}
|
|
27592
|
+
const accountEmail = input.email?.trim() || null;
|
|
27593
|
+
const note = input.note?.trim() || null;
|
|
27594
|
+
const open = await req("POST", "/agent/profiles/onboard", {
|
|
27595
|
+
label: input.label ?? `Login setup: ${domain} \u2192 ${profile}`,
|
|
27596
|
+
profile,
|
|
27597
|
+
domain,
|
|
27598
|
+
login_url: setupUrl,
|
|
27599
|
+
...accountEmail ? { account_email: accountEmail } : {},
|
|
27600
|
+
...note ? { note } : {}
|
|
27601
|
+
});
|
|
27602
|
+
if (!open.ok) return errorResult("browser_profile_connect", open.data);
|
|
27603
|
+
const connectedLogins = await fetchConnectedLogins(profile).catch(() => []);
|
|
27604
|
+
return structuredResult({
|
|
27605
|
+
ok: true,
|
|
27606
|
+
tool: "browser_profile_connect",
|
|
27607
|
+
session_id: null,
|
|
27608
|
+
auth_connection_id: open.data.connection_id,
|
|
27609
|
+
watch_url: `${consoleBase}/console/auth/${open.data.connection_id}`,
|
|
27610
|
+
live_view_url: null,
|
|
27611
|
+
profile,
|
|
27612
|
+
domain,
|
|
27613
|
+
setup_url: setupUrl,
|
|
27614
|
+
account_email: accountEmail,
|
|
27615
|
+
note,
|
|
27616
|
+
status: open.data.status ?? null,
|
|
27617
|
+
flow_status: open.data.flow_status ?? null,
|
|
27618
|
+
flow_step: open.data.flow_step ?? null,
|
|
27619
|
+
flow_expires_at: open.data.flow_expires_at ?? null,
|
|
27620
|
+
post_login_url: open.data.post_login_url ?? null,
|
|
27621
|
+
connected_logins: connectedLogins,
|
|
27622
|
+
next_steps: [
|
|
27623
|
+
`Give the user watch_url so they can sign in to ${domain}.`,
|
|
27624
|
+
"Poll browser_profile_list (with this profile) until the login reads AUTHENTICATED.",
|
|
27625
|
+
"To add another account to this same profile, call browser_profile_connect again with the same profile and a different domain.",
|
|
27626
|
+
"When authenticated, call browser_open with this profile to drive the logged-in session."
|
|
27627
|
+
],
|
|
27628
|
+
raw: open.data
|
|
27629
|
+
});
|
|
27630
|
+
}
|
|
27631
|
+
);
|
|
27632
|
+
server.registerTool(
|
|
27633
|
+
"browser_profile_list",
|
|
27634
|
+
{
|
|
27635
|
+
title: "List Saved Logins in a Profile",
|
|
27636
|
+
description: `List every site login saved in a profile, each with its current auth status (NEEDS_AUTH / AUTHENTICATED), account email, and note. Use it to (1) see what a profile is connected to before opening a session, and (2) poll a just-saved login until it flips to AUTHENTICATED. Read-only, no cost. Pass profile (or email to derive it); narrow with domain or connection_id to poll a single login. Sample asks: "what's connected in my profile", "is my ChatGPT login ready yet", "which accounts are saved". Pairs with browser_profile_connect (which adds logins) and browser_open (which uses them).`,
|
|
27637
|
+
inputSchema: BrowserProfileListInputSchema,
|
|
27638
|
+
outputSchema: BrowserProfileListOutputSchema,
|
|
27639
|
+
annotations: annotations("List Saved Logins in a Profile", true)
|
|
27640
|
+
},
|
|
27641
|
+
async (input) => {
|
|
27642
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
27643
|
+
if (!profile) {
|
|
27644
|
+
return errorResult("browser_profile_list", {
|
|
27645
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
27646
|
+
});
|
|
27647
|
+
}
|
|
27648
|
+
const domain = slugPart(input.domain) || void 0;
|
|
27649
|
+
const res = await req("POST", "/agent/profiles/list", {
|
|
27650
|
+
profile,
|
|
27651
|
+
...domain ? { domain } : {},
|
|
27652
|
+
...input.connection_id ? { connection_id: input.connection_id } : {}
|
|
27653
|
+
});
|
|
27654
|
+
if (!res.ok) return errorResult("browser_profile_list", res.data);
|
|
27655
|
+
const connections = (Array.isArray(res.data?.connections) ? res.data.connections : []).map(normalizeLogin);
|
|
27656
|
+
return structuredResult({
|
|
27657
|
+
ok: true,
|
|
27658
|
+
tool: "browser_profile_list",
|
|
27659
|
+
session_id: null,
|
|
27660
|
+
profile,
|
|
27661
|
+
connections,
|
|
27662
|
+
count: connections.length
|
|
27663
|
+
});
|
|
27664
|
+
}
|
|
27665
|
+
);
|
|
27666
|
+
server.registerTool(
|
|
27667
|
+
"browser_open",
|
|
27668
|
+
{
|
|
27669
|
+
title: "Open Browser Session",
|
|
27670
|
+
description: "Open a direct no-proxy hosted browser session you can drive. Pass a saved profile name to load a session that is already logged into the sites saved in that profile (one profile can hold several logins). To set up or refresh a login first, use browser_profile_connect, then poll browser_profile_list until it reads AUTHENTICATED. Returns a session_id used by all other browser_* tools.",
|
|
27671
|
+
inputSchema: BrowserOpenInputSchema,
|
|
27672
|
+
outputSchema: BrowserOpenOutputSchema,
|
|
27673
|
+
annotations: annotations("Open Browser Session")
|
|
27674
|
+
},
|
|
27675
|
+
async (input) => {
|
|
27676
|
+
const profile = input.profile ?? browserServiceProfileName();
|
|
27677
|
+
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
27678
|
+
const open = await req("POST", "/agent/sessions", {
|
|
27679
|
+
label: input.label,
|
|
27680
|
+
...profile ? { profile } : {},
|
|
27681
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
27682
|
+
disable_default_proxy: true,
|
|
27683
|
+
timeout_seconds: input.timeout_seconds,
|
|
27684
|
+
...input.url ? { url: input.url } : {}
|
|
27685
|
+
});
|
|
27686
|
+
if (!open.ok) return errorResult("browser_open", open.data);
|
|
27687
|
+
const session = open.data;
|
|
27688
|
+
return structuredResult({
|
|
27689
|
+
ok: true,
|
|
27690
|
+
tool: "browser_open",
|
|
27691
|
+
session_id: session.session_id,
|
|
27692
|
+
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
27693
|
+
live_view_url: null,
|
|
27694
|
+
url: input.url ?? null,
|
|
27695
|
+
hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
|
|
27696
|
+
raw: session
|
|
27697
|
+
});
|
|
27698
|
+
}
|
|
27699
|
+
);
|
|
27700
|
+
server.registerTool(
|
|
27701
|
+
"browser_screenshot",
|
|
27702
|
+
{
|
|
27703
|
+
title: "See Page (Screenshot + Elements)",
|
|
27704
|
+
description: "Capture what the browser currently shows. Returns a screenshot image PLUS a text snapshot listing interactive elements with their center x,y coordinates, the page url and title, and visible text. This is your primary way to perceive the page. Click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
|
|
27705
|
+
inputSchema: BrowserSessionInputSchema,
|
|
27706
|
+
outputSchema: BrowserScreenshotOutputSchema,
|
|
27707
|
+
annotations: annotations("See Page", true)
|
|
27708
|
+
},
|
|
27709
|
+
async (input) => {
|
|
27710
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
27711
|
+
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
27712
|
+
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
27713
|
+
const content = [];
|
|
27714
|
+
if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
|
|
27715
|
+
const structured = {
|
|
27716
|
+
ok: true,
|
|
27717
|
+
tool: "browser_screenshot",
|
|
27718
|
+
session_id: input.session_id,
|
|
27719
|
+
url: url ?? null,
|
|
27720
|
+
title: title ?? null,
|
|
27721
|
+
text: typeof text === "string" ? text : "",
|
|
27722
|
+
elements: Array.isArray(elements) ? elements : [],
|
|
27723
|
+
screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
|
|
27724
|
+
};
|
|
27725
|
+
content.push({
|
|
27726
|
+
type: "text",
|
|
27727
|
+
text: JSON.stringify(structured)
|
|
27728
|
+
});
|
|
27729
|
+
return { content, structuredContent: structured };
|
|
27730
|
+
}
|
|
27731
|
+
);
|
|
27732
|
+
server.registerTool(
|
|
27733
|
+
"browser_read",
|
|
27734
|
+
{
|
|
27735
|
+
title: "Read Page Text + Elements",
|
|
27736
|
+
description: "Return the page url, title, visible text, and the list of interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a target element to click.",
|
|
27737
|
+
inputSchema: BrowserSessionInputSchema,
|
|
27738
|
+
outputSchema: BrowserReadOutputSchema,
|
|
27739
|
+
annotations: annotations("Read Page", true)
|
|
27740
|
+
},
|
|
27741
|
+
async (input) => {
|
|
27742
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
27743
|
+
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
27744
|
+
return structuredResult({
|
|
27745
|
+
ok: true,
|
|
27746
|
+
tool: "browser_read",
|
|
27747
|
+
session_id: input.session_id,
|
|
27748
|
+
url: res.data?.url ?? null,
|
|
27749
|
+
title: res.data?.title ?? null,
|
|
27750
|
+
text: typeof res.data?.text === "string" ? res.data.text : "",
|
|
27751
|
+
elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
|
|
27752
|
+
raw: res.data
|
|
27753
|
+
});
|
|
27754
|
+
}
|
|
27755
|
+
);
|
|
27756
|
+
server.registerTool(
|
|
27757
|
+
"browser_locate",
|
|
27758
|
+
{
|
|
27759
|
+
title: "Locate DOM Targets",
|
|
27760
|
+
description: "Locate exact visible DOM elements or text ranges in the current browser viewport and return left/top/width/height bounds in screenshot pixels. Use this before drawing annotations or when a callout must literally circle, box, underline, or point to a real element. Prefer CSS selectors for exact UI elements; use text when selector is unknown. When a replay is actively recording, the result includes replay_elapsed_seconds for timing.",
|
|
27761
|
+
inputSchema: BrowserLocateInputSchema,
|
|
27762
|
+
outputSchema: BrowserLocateOutputSchema,
|
|
27763
|
+
annotations: annotations("Locate DOM Targets", true)
|
|
27764
|
+
},
|
|
27765
|
+
async (input) => {
|
|
27766
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
27767
|
+
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
27768
|
+
return structuredResult({
|
|
27769
|
+
ok: true,
|
|
27770
|
+
tool: "browser_locate",
|
|
27771
|
+
session_id: input.session_id,
|
|
27772
|
+
url: res.data?.url ?? null,
|
|
27773
|
+
title: res.data?.title ?? null,
|
|
27774
|
+
viewport: res.data?.viewport ?? null,
|
|
27775
|
+
replay: res.data?.replay ?? null,
|
|
27776
|
+
targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
|
|
27777
|
+
raw: res.data
|
|
27778
|
+
});
|
|
27779
|
+
}
|
|
27780
|
+
);
|
|
27781
|
+
server.registerTool(
|
|
27782
|
+
"browser_goto",
|
|
27783
|
+
{
|
|
27784
|
+
title: "Navigate To URL",
|
|
27785
|
+
description: "Navigate an existing browser session to a URL when the user asks you to go to another page or continue browsing in the same session. Use browser_open first if no session exists. Follow with browser_screenshot to see redirects, login walls, CAPTCHA/Cloudflare state, or the loaded page.",
|
|
27786
|
+
inputSchema: BrowserGotoInputSchema,
|
|
27787
|
+
outputSchema: BrowserActionOutputSchema,
|
|
27788
|
+
annotations: annotations("Navigate To URL")
|
|
27789
|
+
},
|
|
27790
|
+
async (input) => {
|
|
27791
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
27792
|
+
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
27793
|
+
}
|
|
27794
|
+
);
|
|
27795
|
+
server.registerTool(
|
|
27796
|
+
"browser_click",
|
|
27797
|
+
{
|
|
27798
|
+
title: "Click",
|
|
27799
|
+
description: "Click a visible page target using screenshot pixel coordinates. Use this when the user asks you to press a button, open a menu, choose a result, or activate a visible UI element. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates. Follow with browser_screenshot when the click may change the page.",
|
|
27800
|
+
inputSchema: BrowserClickInputSchema,
|
|
27801
|
+
outputSchema: BrowserActionOutputSchema,
|
|
27802
|
+
annotations: annotations("Click")
|
|
27803
|
+
},
|
|
27804
|
+
async (input) => {
|
|
27805
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
27806
|
+
x: input.x,
|
|
27807
|
+
y: input.y,
|
|
27808
|
+
button: input.button,
|
|
27809
|
+
num_clicks: input.num_clicks
|
|
27810
|
+
});
|
|
27811
|
+
return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
27812
|
+
}
|
|
27813
|
+
);
|
|
27814
|
+
server.registerTool(
|
|
27815
|
+
"browser_type",
|
|
27816
|
+
{
|
|
27817
|
+
title: "Type Text",
|
|
27818
|
+
description: 'Type text into the currently focused browser field. Use this when the user asks you to fill a search box, form field, login field, or editable text area. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit, and browser_screenshot afterward when page state matters.',
|
|
27819
|
+
inputSchema: BrowserTypeInputSchema,
|
|
27820
|
+
outputSchema: BrowserActionOutputSchema,
|
|
27821
|
+
annotations: annotations("Type Text")
|
|
27822
|
+
},
|
|
27823
|
+
async (input) => {
|
|
27824
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
27825
|
+
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
27826
|
+
}
|
|
27827
|
+
);
|
|
27828
|
+
server.registerTool(
|
|
27829
|
+
"browser_scroll",
|
|
27830
|
+
{
|
|
27831
|
+
title: "Scroll",
|
|
27832
|
+
description: "Scroll the page to reveal more content before reading, clicking, or locating elements. Positive delta_y scrolls down; negative delta_y scrolls up. Follow with browser_screenshot or browser_read to inspect newly revealed content.",
|
|
27833
|
+
inputSchema: BrowserScrollInputSchema,
|
|
27834
|
+
outputSchema: BrowserActionOutputSchema,
|
|
27835
|
+
annotations: annotations("Scroll")
|
|
27836
|
+
},
|
|
27837
|
+
async (input) => {
|
|
27838
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
27839
|
+
delta_y: input.delta_y,
|
|
27840
|
+
delta_x: input.delta_x,
|
|
27841
|
+
x: input.x,
|
|
27842
|
+
y: input.y
|
|
27843
|
+
});
|
|
27844
|
+
return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
27845
|
+
}
|
|
27846
|
+
);
|
|
27847
|
+
server.registerTool(
|
|
27848
|
+
"browser_press",
|
|
27849
|
+
{
|
|
27850
|
+
title: "Press Keys",
|
|
27851
|
+
description: 'Press keyboard keys or combinations in the active browser session. Use this for submit, Escape, Tab navigation, select-all, or keyboard shortcuts. Examples: ["Return"], ["Escape"], ["Tab"], ["Ctrl+a"], ["Ctrl+Shift+Tab"]. Use browser_type for text entry and browser_screenshot after keypresses that may change the page.',
|
|
27852
|
+
inputSchema: BrowserPressInputSchema,
|
|
27853
|
+
outputSchema: BrowserActionOutputSchema,
|
|
27854
|
+
annotations: annotations("Press Keys")
|
|
27855
|
+
},
|
|
27856
|
+
async (input) => {
|
|
27857
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
27858
|
+
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
27859
|
+
}
|
|
27860
|
+
);
|
|
27861
|
+
server.registerTool(
|
|
27862
|
+
"browser_replay_start",
|
|
27863
|
+
{
|
|
27864
|
+
title: "Start Recording",
|
|
27865
|
+
description: "Start recording an MP4 replay of the session. Returns replay_id, view_url when available, and a download_url. Use to capture a task for later review; stop with browser_replay_stop.",
|
|
27866
|
+
inputSchema: BrowserSessionInputSchema,
|
|
27867
|
+
outputSchema: BrowserReplayStartOutputSchema,
|
|
27868
|
+
annotations: annotations("Start Recording")
|
|
27869
|
+
},
|
|
27870
|
+
async (input) => {
|
|
27871
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
27872
|
+
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
27873
|
+
return structuredResult({
|
|
27874
|
+
ok: true,
|
|
27875
|
+
tool: "browser_replay_start",
|
|
27876
|
+
session_id: input.session_id,
|
|
27877
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
|
|
27878
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
27879
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
27880
|
+
raw: res.data
|
|
27881
|
+
});
|
|
27882
|
+
}
|
|
27883
|
+
);
|
|
27884
|
+
server.registerTool(
|
|
27885
|
+
"browser_replay_stop",
|
|
27886
|
+
{
|
|
27887
|
+
title: "Stop Recording",
|
|
27888
|
+
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 to the configured output directory.",
|
|
27889
|
+
inputSchema: BrowserReplayStopInputSchema,
|
|
27890
|
+
outputSchema: BrowserReplayStopOutputSchema,
|
|
27891
|
+
annotations: annotations("Stop Recording")
|
|
27892
|
+
},
|
|
27893
|
+
async (input) => {
|
|
27894
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
27895
|
+
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
27896
|
+
return structuredResult({
|
|
27897
|
+
ok: true,
|
|
27898
|
+
tool: "browser_replay_stop",
|
|
27899
|
+
session_id: input.session_id,
|
|
27900
|
+
replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
|
|
27901
|
+
view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
|
|
27902
|
+
download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
|
|
27903
|
+
raw: res.data
|
|
27904
|
+
});
|
|
27905
|
+
}
|
|
27906
|
+
);
|
|
27907
|
+
server.registerTool(
|
|
27908
|
+
"browser_list_replays",
|
|
27909
|
+
{
|
|
27910
|
+
title: "List Replay Videos",
|
|
27911
|
+
description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
|
|
27912
|
+
inputSchema: BrowserSessionInputSchema,
|
|
27913
|
+
outputSchema: BrowserListReplaysOutputSchema,
|
|
27914
|
+
annotations: annotations("List Replay Videos", true)
|
|
27915
|
+
},
|
|
27916
|
+
async (input) => {
|
|
27917
|
+
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
27918
|
+
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
27919
|
+
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
27920
|
+
return structuredResult({
|
|
27921
|
+
ok: true,
|
|
27922
|
+
tool: "browser_list_replays",
|
|
27923
|
+
session_id: input.session_id,
|
|
27924
|
+
replays,
|
|
27925
|
+
count: replays.length
|
|
27926
|
+
});
|
|
27927
|
+
}
|
|
27928
|
+
);
|
|
27929
|
+
server.registerTool(
|
|
27930
|
+
"browser_replay_download",
|
|
27931
|
+
{
|
|
27932
|
+
title: "Download Replay MP4",
|
|
27933
|
+
description: "Download a replay recording through MCP Scraper and save the MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
|
|
27934
|
+
inputSchema: BrowserReplayDownloadInputSchema,
|
|
27935
|
+
outputSchema: BrowserReplayDownloadOutputSchema,
|
|
27936
|
+
annotations: annotations("Download Replay MP4")
|
|
27937
|
+
},
|
|
27938
|
+
async (input) => {
|
|
27939
|
+
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
27940
|
+
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
27941
|
+
return structuredResult({
|
|
27942
|
+
ok: true,
|
|
27943
|
+
tool: "browser_replay_download",
|
|
27944
|
+
session_id: input.session_id,
|
|
27945
|
+
replay_id: input.replay_id,
|
|
27946
|
+
file_path: res.data?.file_path ?? null,
|
|
27947
|
+
bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
|
|
27948
|
+
mime_type: res.data?.mime_type ?? null,
|
|
27949
|
+
download_url: res.data?.download_url ?? null
|
|
27950
|
+
});
|
|
27951
|
+
}
|
|
27952
|
+
);
|
|
27953
|
+
server.registerTool(
|
|
27954
|
+
"browser_replay_mark",
|
|
27955
|
+
{
|
|
27956
|
+
title: "Mark Replay Annotation",
|
|
27957
|
+
description: "While a replay is actively recording, locate one exact DOM target and return a ready-to-use annotation object with DOM bounds and replay-relative timing. Use this instead of guessing start_seconds or drawing rough rectangles. Workflow: start browser_replay_start, navigate until the target is visible and stable, call browser_replay_mark for each callout, then stop the replay and pass the returned annotations to browser_replay_annotate.",
|
|
27958
|
+
inputSchema: BrowserReplayMarkInputSchema,
|
|
27959
|
+
outputSchema: BrowserReplayMarkOutputSchema,
|
|
27960
|
+
annotations: annotations("Mark Replay Annotation", true)
|
|
27961
|
+
},
|
|
27962
|
+
async (input) => {
|
|
27963
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
27964
|
+
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
27965
|
+
const target = res.data?.targets?.[0];
|
|
27966
|
+
const element = target?.element;
|
|
27967
|
+
const elapsed = res.data?.replay?.replay_elapsed_seconds;
|
|
27968
|
+
if (!target?.found || !element) {
|
|
27969
|
+
return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
|
|
27970
|
+
}
|
|
27971
|
+
if (!finiteNumber2(elapsed)) {
|
|
27972
|
+
return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
|
|
27973
|
+
}
|
|
27974
|
+
const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
|
|
27975
|
+
const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
|
|
27976
|
+
const duration = input.duration_seconds ?? 4;
|
|
27977
|
+
const annotation = {
|
|
27978
|
+
type: input.type ?? "box",
|
|
27979
|
+
start_seconds: Number(start.toFixed(3)),
|
|
27980
|
+
end_seconds: Number((start + duration).toFixed(3)),
|
|
27981
|
+
left: Math.round(padded.left),
|
|
27982
|
+
top: Math.round(padded.top),
|
|
27983
|
+
width: Math.round(padded.width),
|
|
27984
|
+
height: Math.round(padded.height),
|
|
27985
|
+
...input.label ? { label: input.label } : {},
|
|
27986
|
+
...input.color ? { color: input.color } : {},
|
|
27987
|
+
...input.thickness ? { thickness: input.thickness } : {}
|
|
27988
|
+
};
|
|
27989
|
+
return structuredResult({
|
|
27990
|
+
ok: true,
|
|
27991
|
+
tool: "browser_replay_mark",
|
|
27992
|
+
session_id: input.session_id,
|
|
27993
|
+
replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
|
|
27994
|
+
annotation,
|
|
27995
|
+
source_width: padded.sourceWidth,
|
|
27996
|
+
source_height: padded.sourceHeight,
|
|
27997
|
+
replay: res.data.replay,
|
|
27998
|
+
target,
|
|
27999
|
+
hint: "Append annotation to your annotations array. Use source_width/source_height with browser_replay_annotate."
|
|
28000
|
+
});
|
|
28001
|
+
}
|
|
28002
|
+
);
|
|
28003
|
+
server.registerTool(
|
|
28004
|
+
"browser_replay_annotate",
|
|
28005
|
+
{
|
|
28006
|
+
title: "Annotate Replay MP4",
|
|
28007
|
+
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
|
|
28008
|
+
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
28009
|
+
outputSchema: BrowserReplayAnnotateOutputSchema,
|
|
28010
|
+
annotations: annotations("Annotate Replay MP4")
|
|
28011
|
+
},
|
|
28012
|
+
async (input) => {
|
|
28013
|
+
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
28014
|
+
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
28015
|
+
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
28016
|
+
try {
|
|
28017
|
+
const sourcePath = String(downloaded.data.file_path);
|
|
28018
|
+
const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
|
|
28019
|
+
(0, import_node_fs10.mkdirSync)((0, import_node_path14.join)(outputBaseDir4(), "browser-replays"), { recursive: true });
|
|
28020
|
+
const result = await annotateReplayVideo(sourcePath, outputPath, {
|
|
28021
|
+
annotations: input.annotations,
|
|
28022
|
+
sourceWidth: input.source_width,
|
|
28023
|
+
sourceHeight: input.source_height,
|
|
28024
|
+
sourceLeftOffset: input.source_left_offset,
|
|
28025
|
+
sourceTopOffset: input.source_top_offset
|
|
28026
|
+
});
|
|
28027
|
+
return structuredResult({
|
|
28028
|
+
ok: true,
|
|
28029
|
+
tool: "browser_replay_annotate",
|
|
28030
|
+
session_id: input.session_id,
|
|
28031
|
+
replay_id: input.replay_id,
|
|
28032
|
+
source_file_path: sourcePath,
|
|
28033
|
+
annotated_file_path: result.filePath,
|
|
28034
|
+
bytes: result.bytes,
|
|
28035
|
+
width: result.width,
|
|
28036
|
+
height: result.height,
|
|
28037
|
+
annotation_count: result.annotationCount,
|
|
28038
|
+
mime_type: "video/mp4"
|
|
28039
|
+
});
|
|
28040
|
+
} catch (err) {
|
|
28041
|
+
return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
|
|
28042
|
+
}
|
|
28043
|
+
}
|
|
28044
|
+
);
|
|
28045
|
+
server.registerTool(
|
|
28046
|
+
"browser_close",
|
|
28047
|
+
{
|
|
28048
|
+
title: "Close Browser Session",
|
|
28049
|
+
description: "Close and release a browser session when the task is done, when the user asks to stop the browser, or when active browser billing should end. Use browser_list_sessions first if you need to recover the session_id.",
|
|
28050
|
+
inputSchema: BrowserSessionInputSchema,
|
|
28051
|
+
outputSchema: BrowserCloseOutputSchema,
|
|
28052
|
+
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
28053
|
+
},
|
|
28054
|
+
async (input) => {
|
|
28055
|
+
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
28056
|
+
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
28057
|
+
return structuredResult({
|
|
28058
|
+
ok: true,
|
|
28059
|
+
tool: "browser_close",
|
|
28060
|
+
session_id: input.session_id,
|
|
28061
|
+
closed: true,
|
|
28062
|
+
raw: res.data
|
|
28063
|
+
});
|
|
28064
|
+
}
|
|
28065
|
+
);
|
|
28066
|
+
server.registerTool(
|
|
28067
|
+
"browser_list_sessions",
|
|
28068
|
+
{
|
|
28069
|
+
title: "List Browser Sessions",
|
|
28070
|
+
description: "List browser sessions and their status, with a watch_url for each. Use this to recover a session_id, find an active browser, or decide which session to close. Use browser_open to create a new session.",
|
|
28071
|
+
inputSchema: BrowserListInputSchema,
|
|
28072
|
+
outputSchema: BrowserListSessionsOutputSchema,
|
|
28073
|
+
annotations: annotations("List Browser Sessions", true)
|
|
28074
|
+
},
|
|
28075
|
+
async (input) => {
|
|
28076
|
+
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
28077
|
+
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
28078
|
+
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
28079
|
+
return structuredResult({
|
|
28080
|
+
ok: true,
|
|
28081
|
+
tool: "browser_list_sessions",
|
|
28082
|
+
session_id: null,
|
|
28083
|
+
sessions,
|
|
28084
|
+
count: sessions.length
|
|
28085
|
+
});
|
|
28086
|
+
}
|
|
28087
|
+
);
|
|
28088
|
+
server.registerTool(
|
|
28089
|
+
"query_fanout_workflow",
|
|
28090
|
+
{
|
|
28091
|
+
title: "Capture AI Search Fan-Out",
|
|
28092
|
+
description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): the exact sub-queries the model issued, every researched URL split into cited vs browsed-only (with citation frequency and snippet), each source tagged by category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. Set export=true for durable JSON, CSV, TSV, and HTML artifacts; export paths are returned relative to MCP_SCRAPER_OUTPUT_DIR. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already connected or if the profile disconnected: call browser_profile_connect, give the user the watch_url, let them complete login, then poll browser_profile_list until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
|
|
28093
|
+
inputSchema: BrowserCaptureFanoutInputSchema,
|
|
28094
|
+
outputSchema: BrowserCaptureFanoutOutputSchema,
|
|
28095
|
+
annotations: annotations("Capture AI Search Fan-Out")
|
|
28096
|
+
},
|
|
28097
|
+
async (input) => {
|
|
28098
|
+
const emit = (result, exports3) => structuredResult({
|
|
28099
|
+
ok: true,
|
|
28100
|
+
tool: "query_fanout_workflow",
|
|
28101
|
+
session_id: input.session_id,
|
|
28102
|
+
platform: result.platform,
|
|
28103
|
+
captured_at: result.capturedAt,
|
|
28104
|
+
prompt: result.prompt,
|
|
28105
|
+
meta: result.meta,
|
|
28106
|
+
queries: result.queries,
|
|
28107
|
+
browsed_urls: result.browsedUrls,
|
|
28108
|
+
cited_urls: result.citedUrls,
|
|
28109
|
+
browsed_only: result.browsedOnly,
|
|
28110
|
+
snippets: result.snippets,
|
|
28111
|
+
counts: result.counts,
|
|
28112
|
+
aggregates: result.aggregates,
|
|
28113
|
+
first_party_domain: result.firstPartyDomain,
|
|
28114
|
+
exports: exports3 ?? null,
|
|
28115
|
+
...result.debug ? { debug: result.debug } : {}
|
|
28116
|
+
});
|
|
28117
|
+
const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
|
|
28118
|
+
prompt: input.prompt,
|
|
28119
|
+
wait_ms: input.wait_ms,
|
|
28120
|
+
first_party_domain: input.first_party_domain,
|
|
28121
|
+
reset: input.reset,
|
|
28122
|
+
export: input.export
|
|
28123
|
+
}, Math.max(timeoutMs, (input.wait_ms ?? (input.prompt ? 9e4 : 8e3)) + 3e4));
|
|
28124
|
+
if (!res.ok) return errorResult("query_fanout_workflow", res.data, input.session_id);
|
|
28125
|
+
const hosted = res.data?.result ?? res.data;
|
|
28126
|
+
let exports2 = res.data?.exports ?? null;
|
|
28127
|
+
if (input.export && !exports2 && isEnrichedFanout(hosted)) {
|
|
28128
|
+
try {
|
|
28129
|
+
exports2 = exportFanout(hosted);
|
|
28130
|
+
} catch (err) {
|
|
28131
|
+
return errorResult(
|
|
28132
|
+
"query_fanout_workflow",
|
|
28133
|
+
{
|
|
28134
|
+
error: `Fan-out captured but local export failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
28135
|
+
result: hosted
|
|
28136
|
+
},
|
|
28137
|
+
input.session_id
|
|
28138
|
+
);
|
|
28139
|
+
}
|
|
28140
|
+
}
|
|
28141
|
+
return emit(hosted, exports2);
|
|
28142
|
+
}
|
|
28143
|
+
);
|
|
28144
|
+
}
|
|
28145
|
+
var import_mcp2, import_node_fs10, import_node_os9, import_node_path14;
|
|
28146
|
+
var init_browser_agent_mcp_server = __esm({
|
|
28147
|
+
"src/mcp/browser-agent-mcp-server.ts"() {
|
|
28148
|
+
"use strict";
|
|
28149
|
+
import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
28150
|
+
import_node_fs10 = require("fs");
|
|
28151
|
+
import_node_os9 = require("os");
|
|
28152
|
+
import_node_path14 = require("path");
|
|
28153
|
+
init_browser_service_env();
|
|
28154
|
+
init_export();
|
|
28155
|
+
init_version();
|
|
28156
|
+
init_browser_agent_tool_schemas();
|
|
28157
|
+
init_replay_annotator();
|
|
28158
|
+
init_outbound_sanitize();
|
|
28159
|
+
}
|
|
28160
|
+
});
|
|
28161
|
+
|
|
28162
|
+
// src/mcp/mcp-oauth.ts
|
|
28163
|
+
function oauthIssuer() {
|
|
28164
|
+
return (process.env.OAUTH_ISSUER ?? "https://mcpscraper.dev").replace(/\/$/, "");
|
|
28165
|
+
}
|
|
28166
|
+
function oauthResourceUrl() {
|
|
28167
|
+
return (process.env.MCP_SCRAPER_RESOURCE_URL ?? "https://mcpscraper.dev/mcp").replace(/\/$/, "");
|
|
28168
|
+
}
|
|
28169
|
+
function oauthResourceBaseUrl() {
|
|
28170
|
+
return (process.env.OAUTH_RS_BASE_URL ?? "https://mcpscraper.dev").replace(/\/$/, "");
|
|
28171
|
+
}
|
|
28172
|
+
function protectedResourceMetadata() {
|
|
28173
|
+
return {
|
|
28174
|
+
resource: oauthResourceUrl(),
|
|
28175
|
+
authorization_servers: [oauthIssuer()],
|
|
28176
|
+
scopes_supported: ["scraper.use"],
|
|
28177
|
+
bearer_methods_supported: ["header"]
|
|
28178
|
+
};
|
|
28179
|
+
}
|
|
28180
|
+
function wwwAuthenticateValue() {
|
|
28181
|
+
return `Bearer resource_metadata="${oauthResourceBaseUrl()}/.well-known/oauth-protected-resource"`;
|
|
28182
|
+
}
|
|
28183
|
+
function isBearerJwt(token) {
|
|
28184
|
+
return !token.startsWith("sk_") && token.split(".").length === 3;
|
|
28185
|
+
}
|
|
28186
|
+
function getJwks() {
|
|
28187
|
+
if (jwksSingleton) return jwksSingleton;
|
|
28188
|
+
jwksSingleton = (0, import_jose.createRemoteJWKSet)(new URL(`${oauthIssuer()}/.well-known/jwks.json`));
|
|
28189
|
+
return jwksSingleton;
|
|
28190
|
+
}
|
|
28191
|
+
async function verifyScraperOAuthJwt(token) {
|
|
28192
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, getJwks(), {
|
|
28193
|
+
issuer: oauthIssuer(),
|
|
28194
|
+
audience: oauthResourceUrl()
|
|
28195
|
+
});
|
|
28196
|
+
return payload;
|
|
28197
|
+
}
|
|
28198
|
+
var import_jose, jwksSingleton;
|
|
28199
|
+
var init_mcp_oauth = __esm({
|
|
28200
|
+
"src/mcp/mcp-oauth.ts"() {
|
|
28201
|
+
"use strict";
|
|
28202
|
+
import_jose = require("jose");
|
|
28203
|
+
jwksSingleton = null;
|
|
28204
|
+
}
|
|
28205
|
+
});
|
|
28206
|
+
|
|
28207
|
+
// src/mcp/mcp-routes.ts
|
|
28208
|
+
function mcpAuthError(message = "authentication required") {
|
|
28209
|
+
const body = JSON.stringify({
|
|
28210
|
+
jsonrpc: "2.0",
|
|
28211
|
+
id: null,
|
|
28212
|
+
error: {
|
|
28213
|
+
code: -32001,
|
|
28214
|
+
message
|
|
28215
|
+
}
|
|
28216
|
+
});
|
|
28217
|
+
return new Response(body, {
|
|
28218
|
+
status: 401,
|
|
28219
|
+
headers: {
|
|
28220
|
+
"Content-Type": "application/json",
|
|
28221
|
+
"WWW-Authenticate": wwwAuthenticateValue()
|
|
28222
|
+
}
|
|
28223
|
+
});
|
|
28224
|
+
}
|
|
28225
|
+
async function requireMcpCallerKey(c) {
|
|
28226
|
+
const xApiKey = c.req.header("x-api-key")?.trim();
|
|
28227
|
+
const authHeader = c.req.header("Authorization")?.trim();
|
|
28228
|
+
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7).trim() : void 0;
|
|
28229
|
+
if (!xApiKey && bearerToken && isBearerJwt(bearerToken)) {
|
|
28230
|
+
let identity;
|
|
28231
|
+
try {
|
|
28232
|
+
const payload = await verifyScraperOAuthJwt(bearerToken);
|
|
28233
|
+
identity = typeof payload.sub === "string" ? payload.sub : "";
|
|
28234
|
+
} catch {
|
|
28235
|
+
return mcpAuthError("Bearer token is invalid or expired");
|
|
28236
|
+
}
|
|
28237
|
+
if (!identity) return mcpAuthError("Bearer token has no subject");
|
|
28238
|
+
const user2 = await getUserByEmail(identity);
|
|
28239
|
+
if (!user2 || !user2.api_key || user2.key_active !== 1) return mcpAuthError("No active MCP Scraper account for this identity");
|
|
28240
|
+
return user2.api_key;
|
|
28241
|
+
}
|
|
28242
|
+
const callerKey = xApiKey ?? bearerToken;
|
|
28243
|
+
if (!callerKey) return mcpAuthError();
|
|
28244
|
+
const user = await getUserByApiKey(callerKey);
|
|
28245
|
+
if (!user) return mcpAuthError();
|
|
28246
|
+
return callerKey;
|
|
28247
|
+
}
|
|
28248
|
+
function registerSerpIntelligenceCaptureTools(server, executor) {
|
|
26705
28249
|
server.registerTool("capture_serp_snapshot", {
|
|
26706
28250
|
title: "SERP Intelligence Snapshot",
|
|
26707
28251
|
description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location and infer gl/hl. Routing is automatic \u2014 leave proxyMode unset. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
|
|
@@ -26725,6 +28269,7 @@ var init_mcp_routes = __esm({
|
|
|
26725
28269
|
import_webStandardStreamableHttp = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
26726
28270
|
init_paa_mcp_server();
|
|
26727
28271
|
init_http_mcp_tool_executor();
|
|
28272
|
+
init_browser_agent_mcp_server();
|
|
26728
28273
|
init_mcp_response_formatter();
|
|
26729
28274
|
init_db();
|
|
26730
28275
|
init_mcp_oauth();
|
|
@@ -26742,8 +28287,10 @@ var init_mcp_routes = __esm({
|
|
|
26742
28287
|
sessionIdGenerator: void 0,
|
|
26743
28288
|
enableJsonResponse: true
|
|
26744
28289
|
});
|
|
28290
|
+
const consoleBaseUrl = process.env.BROWSER_AGENT_CONSOLE_URL?.trim() || baseUrl;
|
|
26745
28291
|
const server = buildPaaExtractorMcpServer(executor, { savesReportsLocally: false });
|
|
26746
28292
|
registerSerpIntelligenceCaptureTools(server, executor);
|
|
28293
|
+
registerBrowserAgentMcpTools(server, { baseUrl, apiKey: callerKey, consoleBaseUrl });
|
|
26747
28294
|
await server.connect(transport);
|
|
26748
28295
|
return transport.handleRequest(c.req.raw);
|
|
26749
28296
|
} catch {
|
|
@@ -27595,200 +29142,65 @@ var init_build = __esm({
|
|
|
27595
29142
|
|
|
27596
29143
|
// src/services/fanout/classify.ts
|
|
27597
29144
|
function classifyDomain(domain, firstPartyDomain) {
|
|
27598
|
-
const d = domain.toLowerCase().replace(/^www\./, "");
|
|
27599
|
-
if (firstPartyDomain) {
|
|
27600
|
-
const fp = firstPartyDomain.toLowerCase().replace(/^www\./, "");
|
|
27601
|
-
if (fp && (d === fp || d.endsWith("." + fp))) return "First-party/vendor";
|
|
27602
|
-
}
|
|
27603
|
-
if (/(^|\.)reddit\.com$/i.test(d)) return "Reddit";
|
|
27604
|
-
if (SOCIAL.test(d)) return "Social/video";
|
|
27605
|
-
if (ENCYCLOPEDIA.test(d)) return "Encyclopedia";
|
|
27606
|
-
if (REVIEW.test(d)) return "Review site";
|
|
27607
|
-
if (NEWS.test(d) || /(^|\.)news\./i.test(d)) return "News/media";
|
|
27608
|
-
if (/^docs?\.|^developer\.|^developers\.|readthedocs|\.dev$/i.test(d)) return "Docs";
|
|
27609
|
-
return "Blog";
|
|
27610
|
-
}
|
|
27611
|
-
function emptyCategoryCounts() {
|
|
27612
|
-
return { "First-party/vendor": 0, "News/media": 0, Reddit: 0, "Social/video": 0, Encyclopedia: 0, "Review site": 0, Docs: 0, Blog: 0 };
|
|
27613
|
-
}
|
|
27614
|
-
function enrichFanout(result, opts = {}) {
|
|
27615
|
-
const addSiteType = (s) => ({ ...s, siteType: classifyDomain(s.domain, opts.firstPartyDomain) });
|
|
27616
|
-
const browsedUrls = result.browsedUrls.map(addSiteType);
|
|
27617
|
-
const citedUrls = result.citedUrls.map(addSiteType);
|
|
27618
|
-
const browsedOnly = result.browsedOnly.map(addSiteType);
|
|
27619
|
-
const byCategory = emptyCategoryCounts();
|
|
27620
|
-
const domainAgg = /* @__PURE__ */ new Map();
|
|
27621
|
-
for (const s of browsedUrls) {
|
|
27622
|
-
byCategory[s.siteType]++;
|
|
27623
|
-
const cur = domainAgg.get(s.domain);
|
|
27624
|
-
if (!cur) domainAgg.set(s.domain, { domain: s.domain, count: 1, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType });
|
|
27625
|
-
else {
|
|
27626
|
-
cur.count++;
|
|
27627
|
-
cur.cited = cur.cited || s.cited;
|
|
27628
|
-
cur.timesCited += s.timesCited;
|
|
27629
|
-
}
|
|
27630
|
-
}
|
|
27631
|
-
const topSites = [...domainAgg.values()].sort((a, b) => b.count - a.count || b.timesCited - a.timesCited);
|
|
27632
|
-
const citationOrder = citedUrls.slice().sort((a, b) => b.timesCited - a.timesCited).map((s, i) => ({ rank: i + 1, domain: s.domain, url: s.url, timesCited: s.timesCited }));
|
|
27633
|
-
return {
|
|
27634
|
-
platform: result.platform,
|
|
27635
|
-
capturedAt: result.capturedAt,
|
|
27636
|
-
prompt: result.prompt,
|
|
27637
|
-
meta: result.meta,
|
|
27638
|
-
queries: result.subQueries,
|
|
27639
|
-
browsedUrls,
|
|
27640
|
-
citedUrls,
|
|
27641
|
-
browsedOnly,
|
|
27642
|
-
snippets: result.snippets,
|
|
27643
|
-
counts: result.counts,
|
|
27644
|
-
aggregates: { topSites, citationOrder, byCategory },
|
|
27645
|
-
firstPartyDomain: opts.firstPartyDomain ? opts.firstPartyDomain.toLowerCase().replace(/^www\./, "") : null,
|
|
27646
|
-
...result.debug ? { debug: result.debug } : {}
|
|
27647
|
-
};
|
|
27648
|
-
}
|
|
27649
|
-
var SOCIAL, ENCYCLOPEDIA, REVIEW, NEWS;
|
|
27650
|
-
var init_classify = __esm({
|
|
27651
|
-
"src/services/fanout/classify.ts"() {
|
|
27652
|
-
"use strict";
|
|
27653
|
-
SOCIAL = /(^|\.)(youtube\.com|youtu\.be|instagram\.com|tiktok\.com|facebook\.com|fb\.com|x\.com|twitter\.com|linkedin\.com|pinterest\.com|threads\.net|vimeo\.com)$/i;
|
|
27654
|
-
ENCYCLOPEDIA = /(^|\.)(wikipedia\.org|wikimedia\.org|britannica\.com|merriam-webster\.com|dictionary\.com|fandom\.com|investopedia\.com)$/i;
|
|
27655
|
-
REVIEW = /(^|\.)(yelp\.com|trustpilot\.com|g2\.com|capterra\.com|angi\.com|angieslist\.com|bbb\.org|consumeraffairs\.com|bestcompany\.com|sitejabber\.com|clutch\.co|bestpickreports\.com|homeguide\.com|porch\.com|ontoplist\.com|thumbtack\.com)$/i;
|
|
27656
|
-
NEWS = /(^|\.)(reuters\.com|apnews\.com|nytimes\.com|wsj\.com|washingtonpost\.com|bloomberg\.com|cnbc\.com|forbes\.com|bbc\.com|theguardian\.com|cnn\.com|npr\.org|usatoday\.com|expressnews\.com|mysanantonio\.com|beaumontenterprise\.com)$/i;
|
|
27657
|
-
}
|
|
27658
|
-
});
|
|
27659
|
-
|
|
27660
|
-
// src/services/fanout/export.ts
|
|
27661
|
-
function outputBaseDir3() {
|
|
27662
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path12.join)((0, import_node_os7.homedir)(), "Downloads", "mcp-scraper");
|
|
27663
|
-
}
|
|
27664
|
-
function safe(value) {
|
|
27665
|
-
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
27666
|
-
}
|
|
27667
|
-
function writeTable(path6, rows, delimiter) {
|
|
27668
|
-
(0, import_node_fs9.writeFileSync)(path6, import_papaparse3.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
27669
|
-
}
|
|
27670
|
-
function exportFanout(enriched) {
|
|
27671
|
-
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
27672
|
-
const outputDir = outputBaseDir3();
|
|
27673
|
-
const relativeDir = (0, import_node_path12.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
27674
|
-
const dir = (0, import_node_path12.join)(outputDir, relativeDir);
|
|
27675
|
-
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
27676
|
-
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
27677
|
-
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
27678
|
-
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
27679
|
-
return { rank: c.rank, domain: c.domain, url: c.url, timesCited: c.timesCited, siteType: src?.siteType ?? "", title: src?.title ?? "" };
|
|
27680
|
-
});
|
|
27681
|
-
const sourceRows2 = enriched.browsedUrls.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
|
|
27682
|
-
const browsedOnlyRows = enriched.browsedOnly.map((s) => ({ domain: s.domain, url: s.url, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType, title: s.title, round: s.round ?? "" }));
|
|
27683
|
-
const snippetRows = enriched.snippets.map((s) => ({ domain: s.domain, url: s.url, title: s.title, text: s.text }));
|
|
27684
|
-
const domainRows = enriched.aggregates.topSites.map((d) => ({ domain: d.domain, count: d.count, cited: d.cited, timesCited: d.timesCited, siteType: d.siteType }));
|
|
27685
|
-
const relativePaths = {
|
|
27686
|
-
relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
|
|
27687
|
-
dir: relativeDir,
|
|
27688
|
-
json: (0, import_node_path12.join)(relativeDir, "fanout.json"),
|
|
27689
|
-
queriesCsv: (0, import_node_path12.join)(relativeDir, "queries.csv"),
|
|
27690
|
-
queriesTsv: (0, import_node_path12.join)(relativeDir, "queries.tsv"),
|
|
27691
|
-
citationsCsv: (0, import_node_path12.join)(relativeDir, "citations.csv"),
|
|
27692
|
-
sourcesCsv: (0, import_node_path12.join)(relativeDir, "sources.csv"),
|
|
27693
|
-
browsedOnlyCsv: (0, import_node_path12.join)(relativeDir, "browsed-only.csv"),
|
|
27694
|
-
snippetsCsv: (0, import_node_path12.join)(relativeDir, "snippets.csv"),
|
|
27695
|
-
domainsCsv: (0, import_node_path12.join)(relativeDir, "domains.csv"),
|
|
27696
|
-
report: (0, import_node_path12.join)(relativeDir, "report.html")
|
|
27697
|
-
};
|
|
27698
|
-
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
|
|
27699
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
|
|
27700
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
|
|
27701
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
|
|
27702
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.sourcesCsv), sourceRows2, ",");
|
|
27703
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
|
|
27704
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
|
|
27705
|
-
writeTable((0, import_node_path12.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
|
|
27706
|
-
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
|
|
27707
|
-
return relativePaths;
|
|
29145
|
+
const d = domain.toLowerCase().replace(/^www\./, "");
|
|
29146
|
+
if (firstPartyDomain) {
|
|
29147
|
+
const fp = firstPartyDomain.toLowerCase().replace(/^www\./, "");
|
|
29148
|
+
if (fp && (d === fp || d.endsWith("." + fp))) return "First-party/vendor";
|
|
29149
|
+
}
|
|
29150
|
+
if (/(^|\.)reddit\.com$/i.test(d)) return "Reddit";
|
|
29151
|
+
if (SOCIAL.test(d)) return "Social/video";
|
|
29152
|
+
if (ENCYCLOPEDIA.test(d)) return "Encyclopedia";
|
|
29153
|
+
if (REVIEW.test(d)) return "Review site";
|
|
29154
|
+
if (NEWS.test(d) || /(^|\.)news\./i.test(d)) return "News/media";
|
|
29155
|
+
if (/^docs?\.|^developer\.|^developers\.|readthedocs|\.dev$/i.test(d)) return "Docs";
|
|
29156
|
+
return "Blog";
|
|
27708
29157
|
}
|
|
27709
|
-
function
|
|
27710
|
-
return
|
|
29158
|
+
function emptyCategoryCounts() {
|
|
29159
|
+
return { "First-party/vendor": 0, "News/media": 0, Reddit: 0, "Social/video": 0, Encyclopedia: 0, "Review site": 0, Docs: 0, Blog: 0 };
|
|
27711
29160
|
}
|
|
27712
|
-
function
|
|
27713
|
-
const
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
|
|
27720
|
-
|
|
27721
|
-
|
|
27722
|
-
|
|
27723
|
-
|
|
27724
|
-
|
|
27725
|
-
.
|
|
27726
|
-
.
|
|
27727
|
-
|
|
27728
|
-
.card b{font-size:22px;display:block}.card span{color:var(--mut);font-size:12px}
|
|
27729
|
-
.bars{margin:8px 0 22px}.bar{display:flex;align-items:center;gap:8px;margin:4px 0}
|
|
27730
|
-
.bar .lab{width:150px;color:var(--mut);font-size:12px;text-align:right}.bar .track{flex:1;background:#10141c;border-radius:5px;overflow:hidden}
|
|
27731
|
-
.bar .fill{background:var(--acc);height:16px}.bar .n{width:34px;font-size:12px;color:var(--mut)}
|
|
27732
|
-
.tabs{display:flex;gap:8px;margin:22px 0 12px}.tab{background:var(--panel);border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:8px;cursor:pointer}
|
|
27733
|
-
.tab.on{background:var(--acc);border-color:var(--acc);color:#04101f;font-weight:600}
|
|
27734
|
-
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
|
|
27735
|
-
.chip{background:var(--panel);border:1px solid var(--line);color:var(--mut);padding:4px 10px;border-radius:20px;cursor:pointer;font-size:12px}
|
|
27736
|
-
.chip.on{border-color:var(--acc);color:var(--ink)}.chip b{color:var(--ink)}
|
|
27737
|
-
.row{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:6px 0;display:flex;gap:10px;align-items:flex-start}
|
|
27738
|
-
.row .idx{color:var(--mut);min-width:22px}.row .main{flex:1}
|
|
27739
|
-
.tag{font-size:11px;padding:2px 7px;border-radius:6px;border:1px solid var(--line);color:var(--mut);margin-right:5px}
|
|
27740
|
-
.dom{font-weight:600}.url{color:var(--mut);font-size:12px;word-break:break-all}
|
|
27741
|
-
.pill{font-size:11px;padding:2px 7px;border-radius:6px}.pill.cite{background:rgba(57,217,138,.15);color:var(--cite)}.pill.brow{background:rgba(90,102,119,.2);color:#aeb8c8}
|
|
27742
|
-
.copy{background:none;border:1px solid var(--line);color:var(--mut);border-radius:6px;cursor:pointer;font-size:11px;padding:2px 8px}
|
|
27743
|
-
.hide{display:none}
|
|
27744
|
-
</style>
|
|
27745
|
-
</head>
|
|
27746
|
-
<body><div class="wrap">
|
|
27747
|
-
<h1>AI Search Fan-Out</h1>
|
|
27748
|
-
<p class="sub" id="sub"></p>
|
|
27749
|
-
<div class="stat" id="stat"></div>
|
|
27750
|
-
<div class="cards" id="cards"></div>
|
|
27751
|
-
<div id="charts"></div>
|
|
27752
|
-
<div class="tabs"><button class="tab on" data-tab="q">Queries</button><button class="tab" data-tab="u">URLs</button></div>
|
|
27753
|
-
<div class="chips" id="chips"></div>
|
|
27754
|
-
<div id="list"></div>
|
|
27755
|
-
</div>
|
|
27756
|
-
<script>
|
|
27757
|
-
const D=${data};
|
|
27758
|
-
const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
27759
|
-
document.getElementById('sub').textContent=D.platform+' \xB7 '+(D.meta.model||'model n/a')+' \xB7 '+D.meta.rounds+' search round(s) \xB7 prompt: '+(D.prompt||'');
|
|
27760
|
-
document.getElementById('stat').innerHTML='Total Fan-Out: '+D.counts.subQueries+' <small>sub-queries \xB7 '+D.counts.browsed+' URLs researched \xB7 '+D.counts.cited+' cited</small>';
|
|
27761
|
-
const cards=[['Sub-queries',D.counts.subQueries],['Researched',D.counts.browsed],['Cited',D.counts.cited],['Browsed-only',D.counts.browsedOnly]];
|
|
27762
|
-
const cc=document.getElementById('cards');cards.forEach(([k,v])=>{const c=el('div','card');c.appendChild(el('b',null,v));c.appendChild(el('span',null,k));cc.appendChild(c)});
|
|
27763
|
-
function barChart(title,obj){const max=Math.max(1,...Object.values(obj));const box=el('div','bars');box.appendChild(el('div',null,'<b>'+title+'</b>'));Object.entries(obj).forEach(([k,v])=>{const r=el('div','bar');r.appendChild(el('div','lab',k));const t=el('div','track');const f=el('div','fill');f.style.width=(v/max*100)+'%';t.appendChild(f);r.appendChild(t);r.appendChild(el('div','n',v));box.appendChild(r)});return box}
|
|
27764
|
-
const charts=document.getElementById('charts');
|
|
27765
|
-
charts.appendChild(barChart('URL Categories',D.aggregates.byCategory));
|
|
27766
|
-
let tab='q',filter=null;
|
|
27767
|
-
const chips=document.getElementById('chips'),list=document.getElementById('list');
|
|
27768
|
-
function render(){
|
|
27769
|
-
chips.innerHTML='';list.innerHTML='';
|
|
27770
|
-
if(tab==='q'){
|
|
27771
|
-
D.queries.forEach((q,i)=>{const r=el('div','row');r.appendChild(el('div','idx',i+1));const m=el('div','main');m.appendChild(el('div',null,esc(q)));r.appendChild(m);const b=el('button','copy','copy');b.onclick=()=>navigator.clipboard.writeText(q);r.appendChild(b);list.appendChild(r)});
|
|
27772
|
-
} else {
|
|
27773
|
-
const counts={};D.browsedUrls.forEach(s=>{counts[s.siteType]=(counts[s.siteType]||0)+1});
|
|
27774
|
-
Object.entries(counts).forEach(([k,v])=>{const c=el('button','chip'+(filter===k?' on':''),k+' <b>'+v+'</b>');c.onclick=()=>{filter=filter===k?null:k;render()};chips.appendChild(c)});
|
|
27775
|
-
D.browsedUrls.forEach(s=>{if(filter&&s.siteType!==filter)return;const r=el('div','row');const m=el('div','main');m.appendChild(el('div',null,'<span class="dom">'+esc(s.domain)+'</span> <span class="pill '+(s.cited?'cite':'brow')+'">'+(s.cited?'cited '+s.timesCited+'\xD7':'browsed')+'</span> <span class="tag">'+s.siteType+'</span>'));m.appendChild(el('div','url',esc(s.url)));r.appendChild(m);list.appendChild(r)});
|
|
29161
|
+
function enrichFanout(result, opts = {}) {
|
|
29162
|
+
const addSiteType = (s) => ({ ...s, siteType: classifyDomain(s.domain, opts.firstPartyDomain) });
|
|
29163
|
+
const browsedUrls = result.browsedUrls.map(addSiteType);
|
|
29164
|
+
const citedUrls = result.citedUrls.map(addSiteType);
|
|
29165
|
+
const browsedOnly = result.browsedOnly.map(addSiteType);
|
|
29166
|
+
const byCategory = emptyCategoryCounts();
|
|
29167
|
+
const domainAgg = /* @__PURE__ */ new Map();
|
|
29168
|
+
for (const s of browsedUrls) {
|
|
29169
|
+
byCategory[s.siteType]++;
|
|
29170
|
+
const cur = domainAgg.get(s.domain);
|
|
29171
|
+
if (!cur) domainAgg.set(s.domain, { domain: s.domain, count: 1, cited: s.cited, timesCited: s.timesCited, siteType: s.siteType });
|
|
29172
|
+
else {
|
|
29173
|
+
cur.count++;
|
|
29174
|
+
cur.cited = cur.cited || s.cited;
|
|
29175
|
+
cur.timesCited += s.timesCited;
|
|
29176
|
+
}
|
|
27776
29177
|
}
|
|
29178
|
+
const topSites = [...domainAgg.values()].sort((a, b) => b.count - a.count || b.timesCited - a.timesCited);
|
|
29179
|
+
const citationOrder = citedUrls.slice().sort((a, b) => b.timesCited - a.timesCited).map((s, i) => ({ rank: i + 1, domain: s.domain, url: s.url, timesCited: s.timesCited }));
|
|
29180
|
+
return {
|
|
29181
|
+
platform: result.platform,
|
|
29182
|
+
capturedAt: result.capturedAt,
|
|
29183
|
+
prompt: result.prompt,
|
|
29184
|
+
meta: result.meta,
|
|
29185
|
+
queries: result.subQueries,
|
|
29186
|
+
browsedUrls,
|
|
29187
|
+
citedUrls,
|
|
29188
|
+
browsedOnly,
|
|
29189
|
+
snippets: result.snippets,
|
|
29190
|
+
counts: result.counts,
|
|
29191
|
+
aggregates: { topSites, citationOrder, byCategory },
|
|
29192
|
+
firstPartyDomain: opts.firstPartyDomain ? opts.firstPartyDomain.toLowerCase().replace(/^www\./, "") : null,
|
|
29193
|
+
...result.debug ? { debug: result.debug } : {}
|
|
29194
|
+
};
|
|
27777
29195
|
}
|
|
27778
|
-
|
|
27779
|
-
|
|
27780
|
-
|
|
27781
|
-
</script>
|
|
27782
|
-
</body></html>`;
|
|
27783
|
-
}
|
|
27784
|
-
var import_node_fs9, import_node_os7, import_node_path12, import_papaparse3;
|
|
27785
|
-
var init_export = __esm({
|
|
27786
|
-
"src/services/fanout/export.ts"() {
|
|
29196
|
+
var SOCIAL, ENCYCLOPEDIA, REVIEW, NEWS;
|
|
29197
|
+
var init_classify = __esm({
|
|
29198
|
+
"src/services/fanout/classify.ts"() {
|
|
27787
29199
|
"use strict";
|
|
27788
|
-
|
|
27789
|
-
|
|
27790
|
-
|
|
27791
|
-
|
|
29200
|
+
SOCIAL = /(^|\.)(youtube\.com|youtu\.be|instagram\.com|tiktok\.com|facebook\.com|fb\.com|x\.com|twitter\.com|linkedin\.com|pinterest\.com|threads\.net|vimeo\.com)$/i;
|
|
29201
|
+
ENCYCLOPEDIA = /(^|\.)(wikipedia\.org|wikimedia\.org|britannica\.com|merriam-webster\.com|dictionary\.com|fandom\.com|investopedia\.com)$/i;
|
|
29202
|
+
REVIEW = /(^|\.)(yelp\.com|trustpilot\.com|g2\.com|capterra\.com|angi\.com|angieslist\.com|bbb\.org|consumeraffairs\.com|bestcompany\.com|sitejabber\.com|clutch\.co|bestpickreports\.com|homeguide\.com|porch\.com|ontoplist\.com|thumbtack\.com)$/i;
|
|
29203
|
+
NEWS = /(^|\.)(reuters\.com|apnews\.com|nytimes\.com|wsj\.com|washingtonpost\.com|bloomberg\.com|cnbc\.com|forbes\.com|bbc\.com|theguardian\.com|cnn\.com|npr\.org|usatoday\.com|expressnews\.com|mysanantonio\.com|beaumontenterprise\.com)$/i;
|
|
27792
29204
|
}
|
|
27793
29205
|
});
|
|
27794
29206
|
|
|
@@ -29181,7 +30593,7 @@ function decryptMemoryKey(stored) {
|
|
|
29181
30593
|
}
|
|
29182
30594
|
async function memoryCall(toolName, args, userMemoryKey) {
|
|
29183
30595
|
try {
|
|
29184
|
-
const res = await fetch(`${MEMORY_BASE_URL()}/api/mcp/
|
|
30596
|
+
const res = await fetch(`${MEMORY_BASE_URL()}/api/mcp/memoryServer/tools/${toolName}/execute`, {
|
|
29185
30597
|
method: "POST",
|
|
29186
30598
|
headers: { "content-type": "application/json" },
|
|
29187
30599
|
body: JSON.stringify({ data: { ...args, apiKey: userMemoryKey } })
|
|
@@ -29572,7 +30984,7 @@ var init_oauth_routes = __esm({
|
|
|
29572
30984
|
init_memory();
|
|
29573
30985
|
init_mcp_oauth();
|
|
29574
30986
|
ISSUER = "https://mcpscraper.dev";
|
|
29575
|
-
RESOURCE = () => process.env.MCP_MEMORY_RESOURCE_URL ?? "https://
|
|
30987
|
+
RESOURCE = () => process.env.MCP_MEMORY_RESOURCE_URL ?? "https://memory.mcpscraper.dev/mcp";
|
|
29576
30988
|
SCRAPER_RESOURCE = () => (process.env.MCP_SCRAPER_RESOURCE_URL ?? "https://mcpscraper.dev/mcp").replace(/\/$/, "");
|
|
29577
30989
|
MEMORY_SCOPES = ["memory.read", "memory.write", "memory.admin"];
|
|
29578
30990
|
SCRAPER_SCOPES = ["scraper.use"];
|
|
@@ -29782,6 +31194,684 @@ var init_oauth_routes = __esm({
|
|
|
29782
31194
|
}
|
|
29783
31195
|
});
|
|
29784
31196
|
|
|
31197
|
+
// src/api/chat-routes.ts
|
|
31198
|
+
function tokenOk(token) {
|
|
31199
|
+
return !!token && token.startsWith("mk_") && token.length > 10;
|
|
31200
|
+
}
|
|
31201
|
+
function jsonError(message, status = 400) {
|
|
31202
|
+
return new Response(JSON.stringify({ ok: false, error: message }), { status, headers: { "content-type": "application/json" } });
|
|
31203
|
+
}
|
|
31204
|
+
function parseAttachNote(v) {
|
|
31205
|
+
if (!v || typeof v !== "object") return void 0;
|
|
31206
|
+
const a = v;
|
|
31207
|
+
return a.vault && a.path ? { vault: a.vault, path: a.path } : void 0;
|
|
31208
|
+
}
|
|
31209
|
+
function renderChatPage(token) {
|
|
31210
|
+
return `<!doctype html>
|
|
31211
|
+
<html>
|
|
31212
|
+
<head>
|
|
31213
|
+
<meta charset="utf-8">
|
|
31214
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
31215
|
+
<title>Chat Everywhere</title>
|
|
31216
|
+
<style>
|
|
31217
|
+
:root { color-scheme: light }
|
|
31218
|
+
* { box-sizing: border-box }
|
|
31219
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; color: #1B1523; background: #EDEAF2; }
|
|
31220
|
+
[id="app"] { display: flex; height: 100vh; }
|
|
31221
|
+
[id="sidebar"] { width: 264px; flex-shrink: 0; display: flex; flex-direction: column; background: #2B0E36; color: #fff; }
|
|
31222
|
+
[id="sidebar"] .brand { display:flex; align-items:center; gap:8px; padding:18px 16px 14px; border-bottom:1px solid rgba(255,255,255,.08); font-weight:700; }
|
|
31223
|
+
[id="sidebar"] .section-label { font:700 11px system-ui; color:#B79FC4; letter-spacing:.05em; text-transform:uppercase; padding:14px 14px 4px; }
|
|
31224
|
+
[id="channel-list"], [id="dm-list"] { padding: 0 8px; }
|
|
31225
|
+
.chan-row { width:100%; display:flex; align-items:center; gap:7px; padding:7px 8px; border-radius:6px; border:none; background:transparent; color:#D9C7E0; cursor:pointer; text-align:left; font:500 13.5px system-ui; }
|
|
31226
|
+
.chan-row:hover, .chan-row.active { background: rgba(255,255,255,.08); color:#fff; }
|
|
31227
|
+
.badge { background:#FF6F91; color:#fff; font:700 10.5px system-ui; padding:1px 6px; border-radius:999px; margin-left:auto; }
|
|
31228
|
+
[id="mentions-btn"] { margin: 8px 14px; display:flex; align-items:center; gap:8px; background:rgba(255,255,255,.07); border:none; border-radius:7px; padding:8px 10px; color:#EFE7F4; cursor:pointer; font:600 13px system-ui; }
|
|
31229
|
+
[id="create-btn"] { margin: 4px 14px; background: transparent; border: 1px dashed rgba(255,255,255,.25); border-radius:7px; padding:6px 10px; color:#D9C7E0; cursor:pointer; font:600 12px system-ui; }
|
|
31230
|
+
[id="main"] { flex: 1; display:flex; flex-direction:column; background:#fff; min-width:0; }
|
|
31231
|
+
[id="chan-header"] { display:flex; align-items:center; justify-content:space-between; padding:14px 20px; border-bottom:1px solid #E6DEEA; }
|
|
31232
|
+
[id="chan-title"] { font:700 16px system-ui; }
|
|
31233
|
+
[id="manage-btn"] { background:transparent; border:1px solid #E6DEEA; border-radius:999px; padding:5px 12px; font:600 12px system-ui; color:#6B6275; cursor:pointer; }
|
|
31234
|
+
[id="messages"] { flex:1; overflow:auto; padding:10px 8px; }
|
|
31235
|
+
.msg { display:flex; gap:10px; padding:10px 16px; border-bottom:1px solid #F0ECF3; }
|
|
31236
|
+
.avatar { width:36px; height:36px; border-radius:8px; display:flex; align-items:center; justify-content:center; color:#fff; font:600 12px system-ui; flex-shrink:0; }
|
|
31237
|
+
.msg-body { flex:1; min-width:0; position:relative; }
|
|
31238
|
+
.msg { position:relative; }
|
|
31239
|
+
.msg-head { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
|
31240
|
+
.msg-author { font:600 14px system-ui; }
|
|
31241
|
+
.agent-pill { font:700 9px system-ui; letter-spacing:.05em; color:#fff; background:#0E8074; padding:2px 6px; border-radius:4px; }
|
|
31242
|
+
.msg-ts { font:400 12px system-ui; color:#A89BB5; }
|
|
31243
|
+
.msg-content { font-size:15px; line-height:1.5; color:#2B2433; white-space:pre-wrap; margin-top:2px; }
|
|
31244
|
+
.reactions { display:flex; gap:6px; margin-top:7px; flex-wrap:wrap; align-items:center; }
|
|
31245
|
+
.reaction { display:flex; align-items:center; gap:4px; padding:3px 8px; border-radius:999px; border:1px solid #E6DEEA; background:#F7F4FA; cursor:pointer; font:500 12px system-ui; color:#5A5165; }
|
|
31246
|
+
.reply-summary, .thread-link { margin-top:7px; display:inline-flex; align-items:center; gap:5px; border:none; background:transparent; cursor:pointer; font:600 12.5px system-ui; color:#6B3FA0; padding:0; }
|
|
31247
|
+
.seen { display:flex; align-items:center; gap:5px; margin-top:6px; font-size:11px; color:#A89BB5; }
|
|
31248
|
+
.msg-actions { position:absolute; top:-6px; right:8px; display:flex; gap:2px; background:#fff; border:1px solid #E6DEEA; border-radius:8px; padding:2px; box-shadow:0 2px 6px rgba(0,0,0,.05); opacity:0; pointer-events:none; transition:opacity .1s; }
|
|
31249
|
+
.msg:hover .msg-actions, .msg-actions.pinned-open { opacity:1; pointer-events:auto; }
|
|
31250
|
+
.icon-btn { display:flex; align-items:center; justify-content:center; width:26px; height:26px; border-radius:6px; border:none; background:transparent; color:#948C9C; cursor:pointer; font-size:14px; flex-shrink:0; }
|
|
31251
|
+
.icon-btn:hover { background:#F2EEF5; color:#6B3FA0; }
|
|
31252
|
+
.emoji-popover { position:absolute; top:32px; right:10px; background:#fff; border:1px solid #E6DEEA; border-radius:10px; box-shadow:0 8px 24px rgba(0,0,0,.12); padding:6px; display:none; gap:2px; z-index:40; }
|
|
31253
|
+
.emoji-popover.open { display:flex; }
|
|
31254
|
+
.emoji-opt { width:30px; height:30px; display:flex; align-items:center; justify-content:center; border:none; background:transparent; cursor:pointer; font-size:17px; border-radius:6px; }
|
|
31255
|
+
.emoji-opt:hover { background:#F2EEF5; }
|
|
31256
|
+
[id="composer-row"] { padding:12px 20px 16px; border-top:1px solid #E6DEEA; }
|
|
31257
|
+
[id="composer"] { width:100%; box-sizing:border-box; resize:none; height:46px; border:1px solid #E6DEEA; border-radius:9px; padding:11px 14px; font:14px system-ui; outline:none; }
|
|
31258
|
+
[id="composer"]:focus { border-color:#6B3FA0; }
|
|
31259
|
+
[id="send-btn"] { background:#6B3FA0; color:#fff; border:none; border-radius:7px; padding:8px 18px; font:600 13.5px system-ui; cursor:pointer; margin-top:8px; float:right; }
|
|
31260
|
+
[id="thread-panel"] { width:340px; flex-shrink:0; border-left:1px solid #E6DEEA; display:none; flex-direction:column; background:#FCFBFD; }
|
|
31261
|
+
[id="thread-panel"].open { display:flex; }
|
|
31262
|
+
[id="thread-header"] { display:flex; align-items:center; justify-content:space-between; padding:14px 16px; border-bottom:1px solid #E6DEEA; font:700 14.5px system-ui; }
|
|
31263
|
+
[id="thread-close"] { background:transparent; border:none; cursor:pointer; font:600 16px system-ui; color:#948C9C; }
|
|
31264
|
+
[id="thread-messages"] { flex:1; overflow:auto; }
|
|
31265
|
+
[id="thread-composer-row"] { padding:12px 16px 16px; border-top:1px solid #E6DEEA; }
|
|
31266
|
+
[id="thread-composer"] { width:100%; box-sizing:border-box; resize:none; height:40px; border:1px solid #E6DEEA; border-radius:9px; padding:9px 12px; font:13.5px system-ui; outline:none; }
|
|
31267
|
+
.overlay { position:fixed; inset:0; background:rgba(27,21,35,.42); display:none; align-items:center; justify-content:center; z-index:50; }
|
|
31268
|
+
.overlay.open { display:flex; }
|
|
31269
|
+
.modal { width:420px; max-height:640px; background:#fff; border-radius:12px; box-shadow:0 30px 70px rgba(0,0,0,.25); display:flex; flex-direction:column; overflow:hidden; }
|
|
31270
|
+
.modal-head { display:flex; align-items:center; justify-content:space-between; padding:16px 18px; border-bottom:1px solid #E6DEEA; font:700 15px system-ui; }
|
|
31271
|
+
.modal-body { padding:16px 18px; overflow:auto; flex:1; }
|
|
31272
|
+
.modal-foot { padding:14px 18px; border-top:1px solid #E6DEEA; display:flex; justify-content:flex-end; gap:8px; }
|
|
31273
|
+
.modal label { font:600 12px system-ui; color:#6B6275; display:block; margin-bottom:6px; margin-top: 12px; }
|
|
31274
|
+
.modal input, .modal select { width:100%; box-sizing:border-box; border:1px solid #E6DEEA; border-radius:8px; padding:9px 12px; font:14px system-ui; outline:none; }
|
|
31275
|
+
.btn { background:transparent; border:1px solid #E6DEEA; border-radius:7px; padding:8px 16px; font:600 13px system-ui; cursor:pointer; color:#6B6275; }
|
|
31276
|
+
.btn-primary { background:#6B3FA0; color:#fff; border:none; border-radius:7px; padding:8px 18px; font:600 13px system-ui; cursor:pointer; }
|
|
31277
|
+
.member-row { display:flex; align-items:center; gap:9px; padding:6px 4px; font:500 13.5px system-ui; }
|
|
31278
|
+
.member-row .x { margin-left:auto; background:transparent; border:none; cursor:pointer; color:#C2BACB; font:600 14px system-ui; }
|
|
31279
|
+
.empty { padding: 40px; text-align:center; color:#948C9C; font:14px system-ui; }
|
|
31280
|
+
.note-chip { margin-top:8px; display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:8px; border:1px solid #E6DEEA; background:#FAF7FC; cursor:pointer; font:600 12.5px system-ui; color:#6B3FA0; text-decoration:underline; text-decoration-color:#D9CCEB; }
|
|
31281
|
+
.note-chip:hover { background:#F2EEF5; }
|
|
31282
|
+
[id="composer-row"] { position:relative; }
|
|
31283
|
+
.attach-chip-row { display:none; align-items:center; gap:6px; margin-bottom:6px; font:500 12.5px system-ui; color:#6B3FA0; }
|
|
31284
|
+
.attach-chip-row.show { display:flex; }
|
|
31285
|
+
.attach-chip-row button { background:transparent; border:none; cursor:pointer; color:#B7ADC2; font:600 13px system-ui; }
|
|
31286
|
+
[id="attach-btn"] { background:transparent; border:1px solid #E6DEEA; border-radius:7px; padding:8px 12px; font:600 13px system-ui; cursor:pointer; color:#6B6275; margin-top:8px; float:right; margin-right:8px; }
|
|
31287
|
+
[id="thread-attach-btn"] { background:transparent; border:1px solid #E6DEEA; border-radius:7px; padding:7px 10px; font:600 12.5px system-ui; cursor:pointer; color:#6B6275; margin-top:7px; float:right; margin-right:8px; }
|
|
31288
|
+
[id="note-picker-search"] { margin-bottom:10px; }
|
|
31289
|
+
.note-pick-row { padding:8px 6px; border-radius:7px; cursor:pointer; font:500 13.5px system-ui; color:#3D3548; }
|
|
31290
|
+
.note-pick-row:hover { background:#F2EEF5; }
|
|
31291
|
+
.note-pick-row .vault-tag { color:#A89BB5; font-size:11.5px; margin-left:6px; }
|
|
31292
|
+
[id="note-card-body"] { font-size:14.5px; line-height:1.55; color:#2B2433; white-space:pre-wrap; }
|
|
31293
|
+
</style>
|
|
31294
|
+
</head>
|
|
31295
|
+
<body>
|
|
31296
|
+
<div id="app">
|
|
31297
|
+
<div id="sidebar">
|
|
31298
|
+
<div class="brand">Chat Everywhere</div>
|
|
31299
|
+
<button id="mentions-btn">@ Mentions <span id="mentions-badge" class="badge" style="display:none"></span></button>
|
|
31300
|
+
<div class="section-label">Channels</div>
|
|
31301
|
+
<div id="channel-list"></div>
|
|
31302
|
+
<button id="create-btn">+ New channel</button>
|
|
31303
|
+
</div>
|
|
31304
|
+
<div id="main">
|
|
31305
|
+
<div id="chan-header" style="display:none">
|
|
31306
|
+
<div id="chan-title"></div>
|
|
31307
|
+
<button id="manage-btn">Members</button>
|
|
31308
|
+
</div>
|
|
31309
|
+
<div id="messages"><div class="empty">Pick a channel on the left.</div></div>
|
|
31310
|
+
<div id="composer-row" style="display:none">
|
|
31311
|
+
<div id="attach-chip-row" class="attach-chip-row"><span>\u{1F4CE} <span id="attach-chip-label"></span></span><button id="attach-chip-clear" title="Remove attachment">x</button></div>
|
|
31312
|
+
<textarea id="composer" placeholder="Message... @ to mention someone"></textarea>
|
|
31313
|
+
<button id="send-btn">Send</button>
|
|
31314
|
+
<button id="attach-btn" title="Attach a note">\u{1F4CE} Attach note</button>
|
|
31315
|
+
<div style="clear:both"></div>
|
|
31316
|
+
</div>
|
|
31317
|
+
</div>
|
|
31318
|
+
<div id="thread-panel">
|
|
31319
|
+
<div id="thread-header"><span>Thread</span><button id="thread-close">x</button></div>
|
|
31320
|
+
<div id="thread-messages"></div>
|
|
31321
|
+
<div id="thread-composer-row">
|
|
31322
|
+
<div id="thread-attach-chip-row" class="attach-chip-row"><span>\u{1F4CE} <span id="thread-attach-chip-label"></span></span><button id="thread-attach-chip-clear" title="Remove attachment">x</button></div>
|
|
31323
|
+
<textarea id="thread-composer" placeholder="Reply in thread"></textarea>
|
|
31324
|
+
<button id="thread-send-btn" class="btn-primary" style="margin-top:7px;float:right">Reply</button>
|
|
31325
|
+
<button id="thread-attach-btn" title="Attach a note">\u{1F4CE} Attach</button>
|
|
31326
|
+
<div style="clear:both"></div>
|
|
31327
|
+
</div>
|
|
31328
|
+
</div>
|
|
31329
|
+
</div>
|
|
31330
|
+
|
|
31331
|
+
<div id="note-picker-overlay" class="overlay">
|
|
31332
|
+
<div class="modal">
|
|
31333
|
+
<div class="modal-head">Attach a note <button id="note-picker-close" style="background:none;border:none;font-size:18px;cursor:pointer;color:#948C9C">x</button></div>
|
|
31334
|
+
<div class="modal-body">
|
|
31335
|
+
<input id="note-picker-search" placeholder="Search your notes by title or path..." />
|
|
31336
|
+
<div id="note-picker-list"></div>
|
|
31337
|
+
</div>
|
|
31338
|
+
</div>
|
|
31339
|
+
</div>
|
|
31340
|
+
|
|
31341
|
+
<div id="note-card-overlay" class="overlay">
|
|
31342
|
+
<div class="modal">
|
|
31343
|
+
<div class="modal-head"><span id="note-card-title"></span><button id="note-card-close" style="background:none;border:none;font-size:18px;cursor:pointer;color:#948C9C">x</button></div>
|
|
31344
|
+
<div class="modal-body"><div id="note-card-body"></div></div>
|
|
31345
|
+
</div>
|
|
31346
|
+
</div>
|
|
31347
|
+
|
|
31348
|
+
<div id="mentions-overlay" class="overlay">
|
|
31349
|
+
<div class="modal">
|
|
31350
|
+
<div class="modal-head">Where am I tagged <button id="mentions-close" style="background:none;border:none;font-size:18px;cursor:pointer;color:#948C9C">x</button></div>
|
|
31351
|
+
<div id="mentions-list" class="modal-body"></div>
|
|
31352
|
+
</div>
|
|
31353
|
+
</div>
|
|
31354
|
+
|
|
31355
|
+
<div id="create-overlay" class="overlay">
|
|
31356
|
+
<div class="modal">
|
|
31357
|
+
<div class="modal-head">New channel</div>
|
|
31358
|
+
<div class="modal-body">
|
|
31359
|
+
<label>Channel name</label>
|
|
31360
|
+
<input id="new-channel-name" placeholder="e.g. fundraise-q3" />
|
|
31361
|
+
<label>Invite by email (comma-separated, optional)</label>
|
|
31362
|
+
<input id="new-channel-invites" placeholder="andrew@x.com, seo@y.com" />
|
|
31363
|
+
<div id="create-error" style="color:#C2410C;font:13px system-ui;margin-top:8px;display:none"></div>
|
|
31364
|
+
</div>
|
|
31365
|
+
<div class="modal-foot">
|
|
31366
|
+
<button class="btn" id="create-cancel">Cancel</button>
|
|
31367
|
+
<button class="btn-primary" id="create-submit">Create</button>
|
|
31368
|
+
</div>
|
|
31369
|
+
</div>
|
|
31370
|
+
</div>
|
|
31371
|
+
|
|
31372
|
+
<div id="manage-overlay" class="overlay">
|
|
31373
|
+
<div class="modal">
|
|
31374
|
+
<div class="modal-head">Members</div>
|
|
31375
|
+
<div class="modal-body">
|
|
31376
|
+
<div id="member-list"></div>
|
|
31377
|
+
<label>Invite someone</label>
|
|
31378
|
+
<input id="invite-email" placeholder="email" />
|
|
31379
|
+
<select id="invite-scope" style="margin-top:8px">
|
|
31380
|
+
<option value="write">Write - can post</option>
|
|
31381
|
+
<option value="read">Read - view only</option>
|
|
31382
|
+
<option value="admin">Admin - manage members</option>
|
|
31383
|
+
</select>
|
|
31384
|
+
<div id="invite-error" style="color:#C2410C;font:13px system-ui;margin-top:8px;display:none"></div>
|
|
31385
|
+
</div>
|
|
31386
|
+
<div class="modal-foot">
|
|
31387
|
+
<button class="btn-primary" id="invite-submit">Invite</button>
|
|
31388
|
+
<button class="btn" id="manage-close">Done</button>
|
|
31389
|
+
</div>
|
|
31390
|
+
</div>
|
|
31391
|
+
</div>
|
|
31392
|
+
|
|
31393
|
+
<script>
|
|
31394
|
+
const TOKEN = ${JSON.stringify(token)};
|
|
31395
|
+
const state = { channels: [], activeVault: null, activeName: null, members: [], threadParent: null, pollTimer: null, pendingAttach: null, threadPendingAttach: null, ownNotes: null, pickerTarget: 'main' };
|
|
31396
|
+
|
|
31397
|
+
function api(path, opts) {
|
|
31398
|
+
const url = '/chat/api/' + path;
|
|
31399
|
+
return fetch(url, opts).then(r => r.json());
|
|
31400
|
+
}
|
|
31401
|
+
function get(path, params) {
|
|
31402
|
+
const qs = new URLSearchParams({ token: TOKEN, ...params }).toString();
|
|
31403
|
+
return api(path + '?' + qs);
|
|
31404
|
+
}
|
|
31405
|
+
function post(path, body) {
|
|
31406
|
+
return api(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: TOKEN, ...body }) });
|
|
31407
|
+
}
|
|
31408
|
+
|
|
31409
|
+
function colorFor(email) {
|
|
31410
|
+
let h = 0;
|
|
31411
|
+
for (let i = 0; i < email.length; i++) h = (h * 31 + email.charCodeAt(i)) % 360;
|
|
31412
|
+
return 'hsl(' + h + ', 55%, 45%)';
|
|
31413
|
+
}
|
|
31414
|
+
function initialsFor(email) {
|
|
31415
|
+
const local = (email || '?').split('@')[0];
|
|
31416
|
+
return (local.slice(0, 2) || '??').toUpperCase();
|
|
31417
|
+
}
|
|
31418
|
+
function esc(s) {
|
|
31419
|
+
return (s || '').replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
31420
|
+
}
|
|
31421
|
+
function fmtTime(iso) {
|
|
31422
|
+
try { return new Date(iso).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); } catch (e) { return ''; }
|
|
31423
|
+
}
|
|
31424
|
+
|
|
31425
|
+
async function loadChannels() {
|
|
31426
|
+
const r = await get('channels', {});
|
|
31427
|
+
if (!r.ok) { document.getElementById('channel-list').innerHTML = '<div class="empty">' + esc(r.error || 'failed to load') + '</div>'; return; }
|
|
31428
|
+
state.channels = r.channels || [];
|
|
31429
|
+
const list = document.getElementById('channel-list');
|
|
31430
|
+
list.innerHTML = '';
|
|
31431
|
+
state.channels.forEach(ch => {
|
|
31432
|
+
const btn = document.createElement('button');
|
|
31433
|
+
btn.className = 'chan-row' + (ch.handle === state.activeVault ? ' active' : '');
|
|
31434
|
+
btn.textContent = '# ' + ch.vault;
|
|
31435
|
+
btn.onclick = () => openChannel(ch.handle, ch.vault);
|
|
31436
|
+
list.appendChild(btn);
|
|
31437
|
+
});
|
|
31438
|
+
if (!state.channels.length) list.innerHTML = '<div class="empty" style="color:#B79FC4">No channels yet</div>';
|
|
31439
|
+
}
|
|
31440
|
+
|
|
31441
|
+
async function openChannel(handle, name) {
|
|
31442
|
+
state.activeVault = handle;
|
|
31443
|
+
state.activeName = name;
|
|
31444
|
+
closeThread();
|
|
31445
|
+
document.querySelectorAll('.chan-row').forEach(b => b.classList.toggle('active', b.textContent === '# ' + name));
|
|
31446
|
+
document.getElementById('chan-header').style.display = 'flex';
|
|
31447
|
+
document.getElementById('chan-title').textContent = '#' + name;
|
|
31448
|
+
document.getElementById('composer-row').style.display = 'block';
|
|
31449
|
+
await loadMembers();
|
|
31450
|
+
await loadMessages();
|
|
31451
|
+
restartPolling();
|
|
31452
|
+
}
|
|
31453
|
+
|
|
31454
|
+
const QUICK_EMOJI = ['\u{1F44D}', '\u2764\uFE0F', '\u{1F602}', '\u{1F389}', '\u{1F440}', '\u2705'];
|
|
31455
|
+
|
|
31456
|
+
function renderMessage(m, container, compact) {
|
|
31457
|
+
const div = document.createElement('div');
|
|
31458
|
+
div.className = 'msg';
|
|
31459
|
+
const author = m.authorIdentity || 'unknown';
|
|
31460
|
+
div.innerHTML =
|
|
31461
|
+
'<div class="avatar" style="background:' + colorFor(author) + (compact ? ';width:28px;height:28px' : '') + '">' + initialsFor(author) + '</div>' +
|
|
31462
|
+
'<div class="msg-body">' +
|
|
31463
|
+
'<div class="msg-head"><span class="msg-author">' + esc(author) + '</span>' +
|
|
31464
|
+
(m.authorIsAgent ? '<span class="agent-pill">AGENT</span>' : '') +
|
|
31465
|
+
'<span class="msg-ts">' + fmtTime(m.postedAt) + '</span></div>' +
|
|
31466
|
+
(!compact ? '<span class="msg-actions">' +
|
|
31467
|
+
'<button class="icon-btn reply-icon" title="Reply">\u{1F4AC}</button>' +
|
|
31468
|
+
'<button class="icon-btn react-icon" title="Add reaction">\u{1F642}</button>' +
|
|
31469
|
+
'</span>' : '') +
|
|
31470
|
+
'<div class="msg-content"></div>' +
|
|
31471
|
+
(m.attachedNote ? '<button class="note-chip">\u{1F4CE} ' + esc(m.attachedNote.title) + '</button>' : '') +
|
|
31472
|
+
(m.reactions && m.reactions.length ? '<div class="reactions"></div>' : '') +
|
|
31473
|
+
(!compact ? '<button class="reply-summary" style="' + (m.replyCount ? '' : 'display:none') + '">' + (m.replyCount || 0) + ' replies' + (m.lastReplyAt ? ' - last ' + fmtTime(m.lastReplyAt) : '') + '</button>' : '') +
|
|
31474
|
+
(!compact ? '<div class="emoji-popover"></div>' : '') +
|
|
31475
|
+
(m.readBy && m.readBy.length ? '<div class="seen">seen by ' + m.readBy.length + '</div>' : '') +
|
|
31476
|
+
'</div>';
|
|
31477
|
+
div.querySelector('.msg-content').textContent = m.content;
|
|
31478
|
+
const noteChip = div.querySelector('.note-chip');
|
|
31479
|
+
if (noteChip) noteChip.onclick = () => openNoteCard(state.activeVault, m.messageId);
|
|
31480
|
+
if (m.reactions && m.reactions.length) {
|
|
31481
|
+
const counts = {};
|
|
31482
|
+
m.reactions.forEach(r => { counts[r.emoji] = (counts[r.emoji] || 0) + 1; });
|
|
31483
|
+
const rdiv = div.querySelector('.reactions');
|
|
31484
|
+
Object.entries(counts).forEach(([emoji, count]) => {
|
|
31485
|
+
const b = document.createElement('button');
|
|
31486
|
+
b.className = 'reaction';
|
|
31487
|
+
b.textContent = emoji + ' ' + count;
|
|
31488
|
+
b.onclick = () => react(m.messageId, emoji);
|
|
31489
|
+
rdiv.appendChild(b);
|
|
31490
|
+
});
|
|
31491
|
+
}
|
|
31492
|
+
if (!compact) {
|
|
31493
|
+
const replyBtn = div.querySelector('.reply-summary');
|
|
31494
|
+
if (replyBtn) replyBtn.onclick = () => openThread(m.messageId);
|
|
31495
|
+
const replyIcon = div.querySelector('.reply-icon');
|
|
31496
|
+
if (replyIcon) replyIcon.onclick = () => openThread(m.messageId);
|
|
31497
|
+
const reactIcon = div.querySelector('.react-icon');
|
|
31498
|
+
const popover = div.querySelector('.emoji-popover');
|
|
31499
|
+
if (reactIcon && popover) {
|
|
31500
|
+
QUICK_EMOJI.forEach(emoji => {
|
|
31501
|
+
const b = document.createElement('button');
|
|
31502
|
+
b.className = 'emoji-opt';
|
|
31503
|
+
b.textContent = emoji;
|
|
31504
|
+
b.onclick = () => { react(m.messageId, emoji); popover.classList.remove('open'); };
|
|
31505
|
+
popover.appendChild(b);
|
|
31506
|
+
});
|
|
31507
|
+
const actions = div.querySelector('.msg-actions');
|
|
31508
|
+
reactIcon.onclick = (e) => {
|
|
31509
|
+
e.stopPropagation();
|
|
31510
|
+
document.querySelectorAll('.emoji-popover.open').forEach(p => { if (p !== popover) { p.classList.remove('open'); p.closest('.msg-body').querySelector('.msg-actions').classList.remove('pinned-open'); } });
|
|
31511
|
+
popover.classList.toggle('open');
|
|
31512
|
+
if (actions) actions.classList.toggle('pinned-open', popover.classList.contains('open'));
|
|
31513
|
+
};
|
|
31514
|
+
document.addEventListener('click', () => { popover.classList.remove('open'); if (actions) actions.classList.remove('pinned-open'); });
|
|
31515
|
+
}
|
|
31516
|
+
}
|
|
31517
|
+
container.appendChild(div);
|
|
31518
|
+
}
|
|
31519
|
+
|
|
31520
|
+
async function loadMessages() {
|
|
31521
|
+
const r = await get('messages', { vault: state.activeVault });
|
|
31522
|
+
const box = document.getElementById('messages');
|
|
31523
|
+
box.innerHTML = '';
|
|
31524
|
+
if (!r.ok) { box.innerHTML = '<div class="empty">' + esc(r.error) + '</div>'; return; }
|
|
31525
|
+
(r.messages || []).forEach(m => renderMessage(m, box, false));
|
|
31526
|
+
box.scrollTop = box.scrollHeight;
|
|
31527
|
+
}
|
|
31528
|
+
|
|
31529
|
+
async function react(messageId, emoji) {
|
|
31530
|
+
await post('react', { vault: state.activeVault, messageId, emoji });
|
|
31531
|
+
if (state.threadParent) await loadThread(state.threadParent); else await loadMessages();
|
|
31532
|
+
}
|
|
31533
|
+
|
|
31534
|
+
document.getElementById('send-btn').onclick = async () => {
|
|
31535
|
+
const ta = document.getElementById('composer');
|
|
31536
|
+
const content = ta.value.trim();
|
|
31537
|
+
if (!content && !state.pendingAttach) return;
|
|
31538
|
+
ta.value = '';
|
|
31539
|
+
const attachNote = state.pendingAttach ? { vault: state.pendingAttach.vault, path: state.pendingAttach.path } : undefined;
|
|
31540
|
+
clearAttach('main');
|
|
31541
|
+
await post('post', { vault: state.activeVault, content: content || (attachNote ? 'shared a note' : ''), attachNote });
|
|
31542
|
+
await loadMessages();
|
|
31543
|
+
};
|
|
31544
|
+
document.getElementById('composer').addEventListener('keydown', e => {
|
|
31545
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); document.getElementById('send-btn').click(); }
|
|
31546
|
+
});
|
|
31547
|
+
|
|
31548
|
+
function clearAttach(target) {
|
|
31549
|
+
if (target === 'main') {
|
|
31550
|
+
state.pendingAttach = null;
|
|
31551
|
+
document.getElementById('attach-chip-row').classList.remove('show');
|
|
31552
|
+
} else {
|
|
31553
|
+
state.threadPendingAttach = null;
|
|
31554
|
+
document.getElementById('thread-attach-chip-row').classList.remove('show');
|
|
31555
|
+
}
|
|
31556
|
+
}
|
|
31557
|
+
document.getElementById('attach-chip-clear').onclick = () => clearAttach('main');
|
|
31558
|
+
document.getElementById('thread-attach-chip-clear').onclick = () => clearAttach('thread');
|
|
31559
|
+
|
|
31560
|
+
async function openNotePicker(target) {
|
|
31561
|
+
state.pickerTarget = target;
|
|
31562
|
+
document.getElementById('note-picker-search').value = '';
|
|
31563
|
+
document.getElementById('note-picker-overlay').classList.add('open');
|
|
31564
|
+
if (!state.ownNotes) {
|
|
31565
|
+
const r = await get('notes-own', {});
|
|
31566
|
+
state.ownNotes = r.ok ? (r.notes || []) : [];
|
|
31567
|
+
}
|
|
31568
|
+
renderNotePickerList(state.ownNotes);
|
|
31569
|
+
document.getElementById('note-picker-search').focus();
|
|
31570
|
+
}
|
|
31571
|
+
function renderNotePickerList(notes) {
|
|
31572
|
+
const box = document.getElementById('note-picker-list');
|
|
31573
|
+
box.innerHTML = '';
|
|
31574
|
+
if (!notes.length) { box.innerHTML = '<div class="empty">No notes found.</div>'; return; }
|
|
31575
|
+
notes.forEach(n => {
|
|
31576
|
+
const row = document.createElement('div');
|
|
31577
|
+
row.className = 'note-pick-row';
|
|
31578
|
+
row.innerHTML = esc(n.title) + '<span class="vault-tag">' + esc(n.vault) + '/' + esc(n.path) + '</span>';
|
|
31579
|
+
row.onclick = () => {
|
|
31580
|
+
const chip = { vault: n.vault, path: n.path, title: n.title };
|
|
31581
|
+
if (state.pickerTarget === 'main') {
|
|
31582
|
+
state.pendingAttach = chip;
|
|
31583
|
+
document.getElementById('attach-chip-label').textContent = n.title;
|
|
31584
|
+
document.getElementById('attach-chip-row').classList.add('show');
|
|
31585
|
+
} else {
|
|
31586
|
+
state.threadPendingAttach = chip;
|
|
31587
|
+
document.getElementById('thread-attach-chip-label').textContent = n.title;
|
|
31588
|
+
document.getElementById('thread-attach-chip-row').classList.add('show');
|
|
31589
|
+
}
|
|
31590
|
+
document.getElementById('note-picker-overlay').classList.remove('open');
|
|
31591
|
+
};
|
|
31592
|
+
box.appendChild(row);
|
|
31593
|
+
});
|
|
31594
|
+
}
|
|
31595
|
+
document.getElementById('attach-btn').onclick = () => openNotePicker('main');
|
|
31596
|
+
document.getElementById('thread-attach-btn').onclick = () => openNotePicker('thread');
|
|
31597
|
+
document.getElementById('note-picker-close').onclick = () => document.getElementById('note-picker-overlay').classList.remove('open');
|
|
31598
|
+
document.getElementById('note-picker-search').addEventListener('input', e => {
|
|
31599
|
+
const q = e.target.value.trim().toLowerCase();
|
|
31600
|
+
const filtered = !q ? state.ownNotes : (state.ownNotes || []).filter(n => n.title.toLowerCase().includes(q) || n.path.toLowerCase().includes(q));
|
|
31601
|
+
renderNotePickerList(filtered || []);
|
|
31602
|
+
});
|
|
31603
|
+
|
|
31604
|
+
async function openNoteCard(vault, messageId) {
|
|
31605
|
+
document.getElementById('note-card-title').textContent = 'Loading...';
|
|
31606
|
+
document.getElementById('note-card-body').textContent = '';
|
|
31607
|
+
document.getElementById('note-card-overlay').classList.add('open');
|
|
31608
|
+
const r = await get('message-note', { vault, messageId });
|
|
31609
|
+
if (!r.ok) {
|
|
31610
|
+
document.getElementById('note-card-title').textContent = 'Unavailable';
|
|
31611
|
+
document.getElementById('note-card-body').textContent = r.error || 'Could not load this note.';
|
|
31612
|
+
return;
|
|
31613
|
+
}
|
|
31614
|
+
document.getElementById('note-card-title').textContent = r.note.title;
|
|
31615
|
+
document.getElementById('note-card-body').textContent = r.note.content;
|
|
31616
|
+
}
|
|
31617
|
+
document.getElementById('note-card-close').onclick = () => document.getElementById('note-card-overlay').classList.remove('open');
|
|
31618
|
+
|
|
31619
|
+
async function openThread(parentMessageId) {
|
|
31620
|
+
state.threadParent = parentMessageId;
|
|
31621
|
+
document.getElementById('thread-panel').classList.add('open');
|
|
31622
|
+
await loadThread(parentMessageId);
|
|
31623
|
+
}
|
|
31624
|
+
function closeThread() {
|
|
31625
|
+
state.threadParent = null;
|
|
31626
|
+
document.getElementById('thread-panel').classList.remove('open');
|
|
31627
|
+
}
|
|
31628
|
+
document.getElementById('thread-close').onclick = closeThread;
|
|
31629
|
+
|
|
31630
|
+
async function loadThread(parentMessageId) {
|
|
31631
|
+
const rootBox = document.getElementById('thread-messages');
|
|
31632
|
+
rootBox.innerHTML = '';
|
|
31633
|
+
const all = await get('messages', { vault: state.activeVault });
|
|
31634
|
+
const root = (all.messages || []).find(m => m.messageId === parentMessageId);
|
|
31635
|
+
if (root) renderMessage(root, rootBox, true);
|
|
31636
|
+
const r = await get('messages', { vault: state.activeVault, parentMessageId });
|
|
31637
|
+
const replyHeader = document.createElement('div');
|
|
31638
|
+
replyHeader.style.cssText = 'padding:6px 16px;font:600 11.5px system-ui;color:#948C9C;border-bottom:1px solid #F2EEF5;margin-bottom:4px';
|
|
31639
|
+
replyHeader.textContent = (r.messages || []).length + ' replies';
|
|
31640
|
+
rootBox.appendChild(replyHeader);
|
|
31641
|
+
(r.messages || []).forEach(m => renderMessage(m, rootBox, true));
|
|
31642
|
+
}
|
|
31643
|
+
document.getElementById('thread-send-btn').onclick = async () => {
|
|
31644
|
+
const ta = document.getElementById('thread-composer');
|
|
31645
|
+
const content = ta.value.trim();
|
|
31646
|
+
if ((!content && !state.threadPendingAttach) || !state.threadParent) return;
|
|
31647
|
+
ta.value = '';
|
|
31648
|
+
const attachNote = state.threadPendingAttach ? { vault: state.threadPendingAttach.vault, path: state.threadPendingAttach.path } : undefined;
|
|
31649
|
+
clearAttach('thread');
|
|
31650
|
+
await post('reply', { vault: state.activeVault, parentMessageId: state.threadParent, content: content || (attachNote ? 'shared a note' : ''), attachNote });
|
|
31651
|
+
await loadThread(state.threadParent);
|
|
31652
|
+
await loadChannels();
|
|
31653
|
+
};
|
|
31654
|
+
|
|
31655
|
+
async function loadMembers() {
|
|
31656
|
+
const r = await get('members', { vault: state.activeVault });
|
|
31657
|
+
state.members = r.ok ? (r.members || []) : [];
|
|
31658
|
+
}
|
|
31659
|
+
|
|
31660
|
+
document.getElementById('manage-btn').onclick = async () => {
|
|
31661
|
+
await loadMembers();
|
|
31662
|
+
const list = document.getElementById('member-list');
|
|
31663
|
+
list.innerHTML = '';
|
|
31664
|
+
state.members.forEach(m => {
|
|
31665
|
+
const row = document.createElement('div');
|
|
31666
|
+
row.className = 'member-row';
|
|
31667
|
+
row.innerHTML = '<span class="avatar" style="width:24px;height:24px;background:' + colorFor(m.identity) + '">' + initialsFor(m.identity) + '</span>' +
|
|
31668
|
+
'<span>' + esc(m.identity) + (m.isAgent ? ' <span class="agent-pill">AGENT</span>' : '') + '</span>' +
|
|
31669
|
+
'<span style="font:600 10.5px system-ui;color:#6B6275;background:#F2EEF5;padding:2px 7px;border-radius:999px;margin-left:auto">' + (m.scope.admin ? 'admin' : m.scope.write ? 'write' : 'read') + '</span>';
|
|
31670
|
+
list.appendChild(row);
|
|
31671
|
+
});
|
|
31672
|
+
document.getElementById('manage-overlay').classList.add('open');
|
|
31673
|
+
};
|
|
31674
|
+
document.getElementById('manage-close').onclick = () => document.getElementById('manage-overlay').classList.remove('open');
|
|
31675
|
+
document.getElementById('invite-submit').onclick = async () => {
|
|
31676
|
+
const email = document.getElementById('invite-email').value.trim();
|
|
31677
|
+
const scope = document.getElementById('invite-scope').value;
|
|
31678
|
+
const errEl = document.getElementById('invite-error');
|
|
31679
|
+
errEl.style.display = 'none';
|
|
31680
|
+
if (!email) return;
|
|
31681
|
+
const r = await post('invite', { vault: state.activeVault, email, scope });
|
|
31682
|
+
if (!r.ok) { errEl.textContent = r.error || 'invite failed'; errEl.style.display = 'block'; return; }
|
|
31683
|
+
document.getElementById('invite-email').value = '';
|
|
31684
|
+
document.getElementById('manage-btn').click();
|
|
31685
|
+
};
|
|
31686
|
+
|
|
31687
|
+
document.getElementById('create-btn').onclick = () => document.getElementById('create-overlay').classList.add('open');
|
|
31688
|
+
document.getElementById('create-cancel').onclick = () => document.getElementById('create-overlay').classList.remove('open');
|
|
31689
|
+
document.getElementById('create-submit').onclick = async () => {
|
|
31690
|
+
const name = document.getElementById('new-channel-name').value.trim();
|
|
31691
|
+
const invitesRaw = document.getElementById('new-channel-invites').value.trim();
|
|
31692
|
+
const errEl = document.getElementById('create-error');
|
|
31693
|
+
errEl.style.display = 'none';
|
|
31694
|
+
if (!name) return;
|
|
31695
|
+
const inviteEmails = invitesRaw ? invitesRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
31696
|
+
const r = await post('channel', { name, inviteEmails });
|
|
31697
|
+
if (!r.ok) { errEl.textContent = r.error || 'create failed'; errEl.style.display = 'block'; return; }
|
|
31698
|
+
document.getElementById('create-overlay').classList.remove('open');
|
|
31699
|
+
document.getElementById('new-channel-name').value = '';
|
|
31700
|
+
document.getElementById('new-channel-invites').value = '';
|
|
31701
|
+
await loadChannels();
|
|
31702
|
+
if (r.members && r.members.some(m => !m.ok)) {
|
|
31703
|
+
alert('Channel created, but some invites failed: ' + r.members.filter(m => !m.ok).map(m => m.identity + ' (' + m.error + ')').join(', '));
|
|
31704
|
+
}
|
|
31705
|
+
};
|
|
31706
|
+
|
|
31707
|
+
document.getElementById('mentions-btn').onclick = async () => {
|
|
31708
|
+
const r = await get('mentions', {});
|
|
31709
|
+
const list = document.getElementById('mentions-list');
|
|
31710
|
+
list.innerHTML = '';
|
|
31711
|
+
(r.mentions || []).forEach(m => {
|
|
31712
|
+
const row = document.createElement('button');
|
|
31713
|
+
row.style.cssText = 'display:block;width:100%;text-align:left;background:transparent;border:none;cursor:pointer;padding:10px 0;border-bottom:1px solid #F2EEF5';
|
|
31714
|
+
row.innerHTML = '<div style="font:600 11.5px system-ui;color:#6B3FA0">' + esc(m.channel) + '</div>' +
|
|
31715
|
+
'<div style="font:600 13.5px system-ui">' + esc(m.authorIdentity) + ' <span style="font:400 12px;color:#948C9C">- ' + fmtTime(m.mentionedAt) + '</span></div>' +
|
|
31716
|
+
'<div style="font:400 13.5px;color:#3D3548;margin-top:2px">' + esc(m.content) + '</div>';
|
|
31717
|
+
row.onclick = () => { document.getElementById('mentions-overlay').classList.remove('open'); openChannel(m.channel, (state.channels.find(c => c.handle === m.channel) || {}).vault || m.channel); };
|
|
31718
|
+
list.appendChild(row);
|
|
31719
|
+
});
|
|
31720
|
+
if (!(r.mentions || []).length) list.innerHTML = '<div class="empty">No mentions yet</div>';
|
|
31721
|
+
document.getElementById('mentions-overlay').classList.add('open');
|
|
31722
|
+
};
|
|
31723
|
+
document.getElementById('mentions-close').onclick = () => document.getElementById('mentions-overlay').classList.remove('open');
|
|
31724
|
+
|
|
31725
|
+
function restartPolling() {
|
|
31726
|
+
if (state.pollTimer) clearInterval(state.pollTimer);
|
|
31727
|
+
state.pollTimer = setInterval(async () => {
|
|
31728
|
+
if (document.visibilityState !== 'visible' || !state.activeVault) return;
|
|
31729
|
+
const r = await get('poll', { vault: state.activeVault });
|
|
31730
|
+
if (r.ok && ((r.newMessages || []).length || (r.newReplies || []).length || (r.newReactions || []).length)) {
|
|
31731
|
+
await loadMessages();
|
|
31732
|
+
if (state.threadParent) await loadThread(state.threadParent);
|
|
31733
|
+
}
|
|
31734
|
+
}, 4000);
|
|
31735
|
+
}
|
|
31736
|
+
|
|
31737
|
+
loadChannels();
|
|
31738
|
+
</script>
|
|
31739
|
+
</body>
|
|
31740
|
+
</html>`;
|
|
31741
|
+
}
|
|
31742
|
+
var import_hono15, chatApp;
|
|
31743
|
+
var init_chat_routes = __esm({
|
|
31744
|
+
"src/api/chat-routes.ts"() {
|
|
31745
|
+
"use strict";
|
|
31746
|
+
import_hono15 = require("hono");
|
|
31747
|
+
init_memory();
|
|
31748
|
+
chatApp = new import_hono15.Hono();
|
|
31749
|
+
chatApp.get("/api/channels", async (c) => {
|
|
31750
|
+
const token = c.req.query("token");
|
|
31751
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31752
|
+
const r = await memoryCall("listVaultsTool", {}, token);
|
|
31753
|
+
if (!r.ok) return c.json(r);
|
|
31754
|
+
const vaults = (r.vaults ?? []).filter((v) => v.kind === "channel");
|
|
31755
|
+
return c.json({ ok: true, channels: vaults });
|
|
31756
|
+
});
|
|
31757
|
+
chatApp.get("/api/messages", async (c) => {
|
|
31758
|
+
const token = c.req.query("token");
|
|
31759
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31760
|
+
const vault = c.req.query("vault");
|
|
31761
|
+
if (!vault) return jsonError("vault is required");
|
|
31762
|
+
const parentMessageId = c.req.query("parentMessageId") || void 0;
|
|
31763
|
+
const r = await memoryCall("listChannelMessagesTool", { vault, parentMessageId }, token);
|
|
31764
|
+
return c.json(r);
|
|
31765
|
+
});
|
|
31766
|
+
chatApp.get("/api/poll", async (c) => {
|
|
31767
|
+
const token = c.req.query("token");
|
|
31768
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31769
|
+
const vault = c.req.query("vault");
|
|
31770
|
+
if (!vault) return jsonError("vault is required");
|
|
31771
|
+
const r = await memoryCall("pollChannelTool", { vault }, token);
|
|
31772
|
+
return c.json(r);
|
|
31773
|
+
});
|
|
31774
|
+
chatApp.post("/api/post", async (c) => {
|
|
31775
|
+
const body = await c.req.json().catch(() => ({}));
|
|
31776
|
+
if (!tokenOk(body.token)) return jsonError("missing or malformed token", 401);
|
|
31777
|
+
if (!body.vault || !body.content) return jsonError("vault and content are required");
|
|
31778
|
+
const attachNote = parseAttachNote(body.attachNote);
|
|
31779
|
+
const r = await memoryCall("postMessageTool", { vault: body.vault, content: body.content, ...attachNote ? { attachNote } : {} }, body.token);
|
|
31780
|
+
return c.json(r);
|
|
31781
|
+
});
|
|
31782
|
+
chatApp.post("/api/reply", async (c) => {
|
|
31783
|
+
const body = await c.req.json().catch(() => ({}));
|
|
31784
|
+
if (!tokenOk(body.token)) return jsonError("missing or malformed token", 401);
|
|
31785
|
+
if (!body.vault || !body.parentMessageId || !body.content) return jsonError("vault, parentMessageId and content are required");
|
|
31786
|
+
const attachNote = parseAttachNote(body.attachNote);
|
|
31787
|
+
const r = await memoryCall("replyMessageTool", { vault: body.vault, parentMessageId: body.parentMessageId, content: body.content, ...attachNote ? { attachNote } : {} }, body.token);
|
|
31788
|
+
return c.json(r);
|
|
31789
|
+
});
|
|
31790
|
+
chatApp.get("/api/notes-own", async (c) => {
|
|
31791
|
+
const token = c.req.query("token");
|
|
31792
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31793
|
+
const vaultsRes = await memoryCall("listVaultsTool", {}, token);
|
|
31794
|
+
if (!vaultsRes.ok) return c.json(vaultsRes);
|
|
31795
|
+
const ownVaults = (vaultsRes.vaults ?? []).filter((v) => v.role === "owner" && v.kind === "notes");
|
|
31796
|
+
const lists = await Promise.all(ownVaults.map((v) => memoryCall("listTool", { vault: v.handle }, token)));
|
|
31797
|
+
const notes = ownVaults.flatMap((v, i) => {
|
|
31798
|
+
const r = lists[i];
|
|
31799
|
+
return r.ok ? (r.notes ?? []).map((n) => ({ vault: v.handle, path: n.path, title: n.title })) : [];
|
|
31800
|
+
});
|
|
31801
|
+
return c.json({ ok: true, notes });
|
|
31802
|
+
});
|
|
31803
|
+
chatApp.get("/api/message-note", async (c) => {
|
|
31804
|
+
const token = c.req.query("token");
|
|
31805
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31806
|
+
const vault = c.req.query("vault");
|
|
31807
|
+
const messageId = c.req.query("messageId");
|
|
31808
|
+
if (!vault || !messageId) return jsonError("vault and messageId are required");
|
|
31809
|
+
const r = await memoryCall("getMessageNoteTool", { vault, messageId }, token);
|
|
31810
|
+
return c.json(r);
|
|
31811
|
+
});
|
|
31812
|
+
chatApp.post("/api/react", async (c) => {
|
|
31813
|
+
const body = await c.req.json().catch(() => ({}));
|
|
31814
|
+
if (!tokenOk(body.token)) return jsonError("missing or malformed token", 401);
|
|
31815
|
+
if (!body.vault || !body.messageId || !body.emoji) return jsonError("vault, messageId and emoji are required");
|
|
31816
|
+
const r = await memoryCall("reactMessageTool", { vault: body.vault, messageId: body.messageId, emoji: body.emoji, remove: !!body.remove }, body.token);
|
|
31817
|
+
return c.json(r);
|
|
31818
|
+
});
|
|
31819
|
+
chatApp.get("/api/mentions", async (c) => {
|
|
31820
|
+
const token = c.req.query("token");
|
|
31821
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31822
|
+
const r = await memoryCall("myMentionsTool", {}, token);
|
|
31823
|
+
return c.json(r);
|
|
31824
|
+
});
|
|
31825
|
+
chatApp.get("/api/members", async (c) => {
|
|
31826
|
+
const token = c.req.query("token");
|
|
31827
|
+
if (!tokenOk(token)) return jsonError("missing or malformed token", 401);
|
|
31828
|
+
const vault = c.req.query("vault");
|
|
31829
|
+
if (!vault) return jsonError("vault is required");
|
|
31830
|
+
const r = await memoryCall("listChannelMembersTool", { vault }, token);
|
|
31831
|
+
return c.json(r);
|
|
31832
|
+
});
|
|
31833
|
+
chatApp.post("/api/channel", async (c) => {
|
|
31834
|
+
const body = await c.req.json().catch(() => ({}));
|
|
31835
|
+
if (!tokenOk(body.token)) return jsonError("missing or malformed token", 401);
|
|
31836
|
+
if (!body.name) return jsonError("name is required");
|
|
31837
|
+
const inviteEmails = (body.inviteEmails ?? []).filter(Boolean).slice(0, 99);
|
|
31838
|
+
const r = await memoryCall(
|
|
31839
|
+
"createChannelTool",
|
|
31840
|
+
{ name: body.name, inviteMembers: inviteEmails.map((identity) => ({ identity })) },
|
|
31841
|
+
body.token
|
|
31842
|
+
);
|
|
31843
|
+
return c.json(r);
|
|
31844
|
+
});
|
|
31845
|
+
chatApp.post("/api/invite", async (c) => {
|
|
31846
|
+
const body = await c.req.json().catch(() => ({}));
|
|
31847
|
+
if (!tokenOk(body.token)) return jsonError("missing or malformed token", 401);
|
|
31848
|
+
if (!body.vault || !body.email) return jsonError("vault and email are required");
|
|
31849
|
+
const scopeMap = {
|
|
31850
|
+
read: { read: true },
|
|
31851
|
+
write: { read: true, write: true },
|
|
31852
|
+
admin: { read: true, write: true, admin: true }
|
|
31853
|
+
};
|
|
31854
|
+
const scope = scopeMap[body.scope ?? "write"] ?? scopeMap.write;
|
|
31855
|
+
const r = await memoryCall("shareVaultTool", { vault: body.vault, granteeIdentity: body.email, scope }, body.token);
|
|
31856
|
+
return c.json(r);
|
|
31857
|
+
});
|
|
31858
|
+
chatApp.post("/api/member-remove", async (c) => {
|
|
31859
|
+
const body = await c.req.json().catch(() => ({}));
|
|
31860
|
+
if (!tokenOk(body.token)) return jsonError("missing or malformed token", 401);
|
|
31861
|
+
if (!body.vault || !body.identity) return jsonError("vault and identity are required");
|
|
31862
|
+
const r = await memoryCall("removeChannelMemberTool", { vault: body.vault, identity: body.identity }, body.token);
|
|
31863
|
+
return c.json(r);
|
|
31864
|
+
});
|
|
31865
|
+
chatApp.get("/:token", (c) => {
|
|
31866
|
+
const token = c.req.param("token");
|
|
31867
|
+
if (!tokenOk(token)) {
|
|
31868
|
+
return c.html(`<!doctype html><html><body style="font-family:system-ui;padding:40px;color:#3D3548"><h2>This chat link isn't valid.</h2><p>Ask the AI to run get-chat-link again, or revoke-chat-link then get-chat-link if you think it was compromised.</p></body></html>`, 401);
|
|
31869
|
+
}
|
|
31870
|
+
return c.html(renderChatPage(token));
|
|
31871
|
+
});
|
|
31872
|
+
}
|
|
31873
|
+
});
|
|
31874
|
+
|
|
29785
31875
|
// src/api/site-audit-worker.ts
|
|
29786
31876
|
async function drainSiteAuditQueue(budget) {
|
|
29787
31877
|
const processedIds = [];
|
|
@@ -29898,21 +31988,21 @@ async function cleanupVercel(token, cutoff) {
|
|
|
29898
31988
|
return { deleted, store: "vercel-blob" };
|
|
29899
31989
|
}
|
|
29900
31990
|
async function cleanupLocal(cutoff) {
|
|
29901
|
-
const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
29902
|
-
const dir = (0,
|
|
31991
|
+
const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path15.join)((0, import_node_os10.homedir)(), "Downloads", "mcp-scraper");
|
|
31992
|
+
const dir = (0, import_node_path15.join)(baseDir, "blobs", SCRAPE_FALLBACK_PREFIX.replace(/\/$/, ""));
|
|
29903
31993
|
let deleted = 0;
|
|
29904
31994
|
let entries;
|
|
29905
31995
|
try {
|
|
29906
|
-
entries = await (0,
|
|
31996
|
+
entries = await (0, import_promises11.readdir)(dir);
|
|
29907
31997
|
} catch {
|
|
29908
31998
|
return { deleted: 0, store: "local" };
|
|
29909
31999
|
}
|
|
29910
32000
|
for (const name of entries) {
|
|
29911
|
-
const path6 = (0,
|
|
32001
|
+
const path6 = (0, import_node_path15.join)(dir, name);
|
|
29912
32002
|
try {
|
|
29913
|
-
const s = await (0,
|
|
32003
|
+
const s = await (0, import_promises11.stat)(path6);
|
|
29914
32004
|
if (s.isFile() && s.mtimeMs < cutoff) {
|
|
29915
|
-
await (0,
|
|
32005
|
+
await (0, import_promises11.unlink)(path6);
|
|
29916
32006
|
deleted++;
|
|
29917
32007
|
}
|
|
29918
32008
|
} catch {
|
|
@@ -29929,13 +32019,13 @@ async function cleanupExpiredScrapeBlobs(maxAgeMs = SCRAPE_BLOB_TTL_MS) {
|
|
|
29929
32019
|
return { deleted: 0, store: "none" };
|
|
29930
32020
|
}
|
|
29931
32021
|
}
|
|
29932
|
-
var
|
|
32022
|
+
var import_promises11, import_node_os10, import_node_path15;
|
|
29933
32023
|
var init_scrape_blob_cleanup = __esm({
|
|
29934
32024
|
"src/api/scrape-blob-cleanup.ts"() {
|
|
29935
32025
|
"use strict";
|
|
29936
|
-
|
|
29937
|
-
|
|
29938
|
-
|
|
32026
|
+
import_promises11 = require("fs/promises");
|
|
32027
|
+
import_node_os10 = require("os");
|
|
32028
|
+
import_node_path15 = require("path");
|
|
29939
32029
|
init_scrape_vault_sink();
|
|
29940
32030
|
}
|
|
29941
32031
|
});
|
|
@@ -30390,6 +32480,7 @@ function opForCostPath(p) {
|
|
|
30390
32480
|
return "fb_ad";
|
|
30391
32481
|
}
|
|
30392
32482
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
32483
|
+
if (p === "/reddit/thread") return "reddit_thread";
|
|
30393
32484
|
if (p.startsWith("/api/internal/site-architecture-auditor")) return "audit_site";
|
|
30394
32485
|
if (p.startsWith("/agent/")) return "browser_session";
|
|
30395
32486
|
return null;
|
|
@@ -30426,7 +32517,7 @@ function countPaaQuestions2(result) {
|
|
|
30426
32517
|
return Array.isArray(value.flat) ? value.flat.length : 0;
|
|
30427
32518
|
}
|
|
30428
32519
|
function paaCostForQuestionCount2(questionCount) {
|
|
30429
|
-
return Math.max(1, questionCount) * MC_COSTS.paa;
|
|
32520
|
+
return MC_COSTS.paa_base + Math.max(1, questionCount) * MC_COSTS.paa;
|
|
30430
32521
|
}
|
|
30431
32522
|
async function checkHarvestLimits(user, reuseLockId) {
|
|
30432
32523
|
if (reuseLockId && await reuseExistingConcurrencyGate(user.id, reuseLockId, 30 * 60)) return null;
|
|
@@ -30444,7 +32535,7 @@ async function checkHarvestLimits(user, reuseLockId) {
|
|
|
30444
32535
|
}
|
|
30445
32536
|
return null;
|
|
30446
32537
|
}
|
|
30447
|
-
var import_resend,
|
|
32538
|
+
var import_resend, import_hono16, import_hono17, import_factory6, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth2, adminAuth, sessionAuth, app, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
|
|
30448
32539
|
var init_server = __esm({
|
|
30449
32540
|
"src/api/server.ts"() {
|
|
30450
32541
|
"use strict";
|
|
@@ -30462,8 +32553,8 @@ var init_server = __esm({
|
|
|
30462
32553
|
init_media_extractor();
|
|
30463
32554
|
init_site_mapper();
|
|
30464
32555
|
init_site_extractor();
|
|
30465
|
-
|
|
30466
|
-
|
|
32556
|
+
import_hono16 = require("hono");
|
|
32557
|
+
import_hono17 = require("inngest/hono");
|
|
30467
32558
|
init_client();
|
|
30468
32559
|
init_site_audit();
|
|
30469
32560
|
init_site_extract();
|
|
@@ -30487,6 +32578,7 @@ var init_server = __esm({
|
|
|
30487
32578
|
init_cost_telemetry();
|
|
30488
32579
|
init_stripe_routes();
|
|
30489
32580
|
init_oauth_routes();
|
|
32581
|
+
init_chat_routes();
|
|
30490
32582
|
init_site_audit_worker();
|
|
30491
32583
|
import_factory6 = require("hono/factory");
|
|
30492
32584
|
import_cookie2 = require("hono/cookie");
|
|
@@ -30549,7 +32641,7 @@ var init_server = __esm({
|
|
|
30549
32641
|
c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
|
|
30550
32642
|
return next();
|
|
30551
32643
|
});
|
|
30552
|
-
app = new
|
|
32644
|
+
app = new import_hono16.Hono();
|
|
30553
32645
|
STRIPE_API_VERSION = "2026-02-25.clover";
|
|
30554
32646
|
app.use("*", async (c, next) => {
|
|
30555
32647
|
await next();
|
|
@@ -30766,17 +32858,17 @@ var init_server = __esm({
|
|
|
30766
32858
|
app.get("/memory/connect", auth2, async (c) => {
|
|
30767
32859
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
30768
32860
|
if (!key) return c.json({ error: error ?? "memory unavailable" }, 502);
|
|
30769
|
-
const
|
|
32861
|
+
const url = "https://memory.mcpscraper.dev/mcp";
|
|
30770
32862
|
const config = {
|
|
30771
32863
|
mcpServers: {
|
|
30772
|
-
|
|
30773
|
-
|
|
30774
|
-
|
|
30775
|
-
env: { MCP_MEMORY_KEY: key }
|
|
32864
|
+
Memory: {
|
|
32865
|
+
url,
|
|
32866
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
30776
32867
|
}
|
|
30777
32868
|
}
|
|
30778
32869
|
};
|
|
30779
|
-
|
|
32870
|
+
const codeCommand = `claude mcp add --transport http memory ${url} --header "Authorization: Bearer ${key}"`;
|
|
32871
|
+
return c.json({ ok: true, url, key, config, codeCommand });
|
|
30780
32872
|
});
|
|
30781
32873
|
app.post("/memory/vault", auth2, async (c) => {
|
|
30782
32874
|
const { key, error } = await getOrCreateUserMemoryKey(c.get("user"));
|
|
@@ -30968,7 +33060,7 @@ var init_server = __esm({
|
|
|
30968
33060
|
serpOnly: body.serpOnly ?? false,
|
|
30969
33061
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
30970
33062
|
};
|
|
30971
|
-
const harvestCost = options.serpOnly ? MC_COSTS.serp : options.maxQuestions
|
|
33063
|
+
const harvestCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount2(options.maxQuestions);
|
|
30972
33064
|
const { ok: harvestOk, balance_mc: harvestBal } = await debitMc(user.id, harvestCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
30973
33065
|
if (!harvestOk) return c.json(insufficientBalanceResponse(harvestBal, harvestCost), 402);
|
|
30974
33066
|
const jobId = await createJob(user.id, options.query, { ...options, billingHoldMc: harvestCost }, body.callback_url);
|
|
@@ -31004,7 +33096,7 @@ var init_server = __esm({
|
|
|
31004
33096
|
serpOnly: body.serpOnly ?? false,
|
|
31005
33097
|
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
31006
33098
|
};
|
|
31007
|
-
const syncCost = options.serpOnly ? MC_COSTS.serp : options.maxQuestions
|
|
33099
|
+
const syncCost = options.serpOnly ? MC_COSTS.serp : paaCostForQuestionCount2(options.maxQuestions);
|
|
31008
33100
|
const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
31009
33101
|
if (!syncOk) return c.json(insufficientBalanceResponse(syncBal, syncCost), 402);
|
|
31010
33102
|
const jobId = await createRunningJob(user.id, options.query, options);
|
|
@@ -31612,7 +33704,7 @@ var init_server = __esm({
|
|
|
31612
33704
|
]);
|
|
31613
33705
|
return c.json({ drained: results.length, results, sweepResult, reaped: reapResult, expired: expiredResult, blobCleanup, workflowDispatch: workflowDispatchResult });
|
|
31614
33706
|
});
|
|
31615
|
-
app.on(["GET", "POST", "PUT"], "/api/inngest", (0,
|
|
33707
|
+
app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono17.serve)({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
|
|
31616
33708
|
app.route("/api/internal/site-architecture-auditor", siteAuditApp);
|
|
31617
33709
|
app.route("/youtube", youtubeApp);
|
|
31618
33710
|
app.route("/screenshot", screenshotApp);
|
|
@@ -31625,6 +33717,7 @@ var init_server = __esm({
|
|
|
31625
33717
|
app.route("/serp-intelligence", serpIntelligenceApp);
|
|
31626
33718
|
app.route("/mcp", mcpApp);
|
|
31627
33719
|
app.route("/agent", buildBrowserAgentRoutes());
|
|
33720
|
+
app.route("/chat", chatApp);
|
|
31628
33721
|
app.get("/console", (c) => c.html(renderConsoleHtml()));
|
|
31629
33722
|
app.get("/console/auth/:id", async (c) => {
|
|
31630
33723
|
const id = c.req.param("id");
|
|
@@ -31749,10 +33842,10 @@ ${ATTRIBUTION_PIXEL_SCRIPT}
|
|
|
31749
33842
|
});
|
|
31750
33843
|
|
|
31751
33844
|
// bin/api-server.ts
|
|
31752
|
-
var
|
|
33845
|
+
var import_node_fs11 = require("fs");
|
|
31753
33846
|
function loadDotEnv() {
|
|
31754
33847
|
try {
|
|
31755
|
-
for (const line of (0,
|
|
33848
|
+
for (const line of (0, import_node_fs11.readFileSync)(".env", "utf8").split("\n")) {
|
|
31756
33849
|
const eq = line.indexOf("=");
|
|
31757
33850
|
if (eq < 1 || line.trimStart().startsWith("#")) continue;
|
|
31758
33851
|
const k = line.slice(0, eq).trim();
|