mcp-scraper 0.4.5 → 0.4.12
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 +1866 -1376
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- 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 +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +89 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/bin/paa-harvest.cjs +13 -4
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +2 -2
- package/dist/{chunk-NJK5BTUK.js → chunk-2YY46QYT.js} +28 -11
- package/dist/chunk-2YY46QYT.js.map +1 -0
- package/dist/{chunk-WI4A3KFQ.js → chunk-6EXP6DQG.js} +90 -2
- package/dist/chunk-6EXP6DQG.js.map +1 -0
- package/dist/{chunk-5LO6EHEH.js → chunk-EMY7ELRU.js} +35 -7
- package/dist/chunk-EMY7ELRU.js.map +1 -0
- package/dist/{chunk-FUVQR6GK.js → chunk-GL4BW4CP.js} +49 -1
- package/dist/chunk-GL4BW4CP.js.map +1 -0
- package/dist/{chunk-3UH3BEIZ.js → chunk-HE2LQPJ2.js} +2 -2
- package/dist/{chunk-4ZZWFI6L.js → chunk-ONIOF5XW.js} +3 -3
- package/dist/chunk-SHXJQQOH.js +7 -0
- package/dist/chunk-SHXJQQOH.js.map +1 -0
- package/dist/{db-H3S3M6KK.js → db-LIOTIWVN.js} +6 -2
- package/dist/{extract-bundle-UKE273TV.js → extract-bundle-COS56ZDO.js} +2 -2
- package/dist/index.cjs +59 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/{server-XCDFHHLE.js → server-UNID3SJU.js} +570 -276
- package/dist/server-UNID3SJU.js.map +1 -0
- package/dist/{site-extract-repository-THBVEXMP.js → site-extract-repository-HTIY52MW.js} +4 -4
- package/dist/{worker-YHKUJR5P.js → worker-HTYZAYGB.js} +18 -14
- package/dist/worker-HTYZAYGB.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-5LO6EHEH.js.map +0 -1
- package/dist/chunk-FUVQR6GK.js.map +0 -1
- package/dist/chunk-NJK5BTUK.js.map +0 -1
- package/dist/chunk-UGQC2FOX.js +0 -7
- package/dist/chunk-UGQC2FOX.js.map +0 -1
- package/dist/chunk-WI4A3KFQ.js.map +0 -1
- package/dist/server-XCDFHHLE.js.map +0 -1
- package/dist/worker-YHKUJR5P.js.map +0 -1
- /package/dist/{chunk-3UH3BEIZ.js.map → chunk-HE2LQPJ2.js.map} +0 -0
- /package/dist/{chunk-4ZZWFI6L.js.map → chunk-ONIOF5XW.js.map} +0 -0
- /package/dist/{db-H3S3M6KK.js.map → db-LIOTIWVN.js.map} +0 -0
- /package/dist/{extract-bundle-UKE273TV.js.map → extract-bundle-COS56ZDO.js.map} +0 -0
- /package/dist/{site-extract-repository-THBVEXMP.js.map → site-extract-repository-HTIY52MW.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -3617,6 +3617,7 @@ __export(db_exports, {
|
|
|
3617
3617
|
getLedger: () => getLedger,
|
|
3618
3618
|
getMemoryPlan: () => getMemoryPlan,
|
|
3619
3619
|
getMemoryProvisioned: () => getMemoryProvisioned,
|
|
3620
|
+
getPageSnapshot: () => getPageSnapshot,
|
|
3620
3621
|
getRefresh: () => getRefresh,
|
|
3621
3622
|
getUserByApiKey: () => getUserByApiKey,
|
|
3622
3623
|
getUserByEmail: () => getUserByEmail,
|
|
@@ -3672,6 +3673,7 @@ __export(db_exports, {
|
|
|
3672
3673
|
startHarvestAttempt: () => startHarvestAttempt,
|
|
3673
3674
|
stripeEventAlreadyProcessed: () => stripeEventAlreadyProcessed,
|
|
3674
3675
|
updateKpoJobState: () => updateKpoJobState,
|
|
3676
|
+
upsertPageSnapshot: () => upsertPageSnapshot,
|
|
3675
3677
|
verifyPassword: () => verifyPassword
|
|
3676
3678
|
});
|
|
3677
3679
|
function getDb() {
|
|
@@ -4109,6 +4111,20 @@ async function migrate() {
|
|
|
4109
4111
|
} catch {
|
|
4110
4112
|
}
|
|
4111
4113
|
}
|
|
4114
|
+
await db.execute(`
|
|
4115
|
+
CREATE TABLE IF NOT EXISTS page_snapshots (
|
|
4116
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
4117
|
+
url TEXT NOT NULL,
|
|
4118
|
+
content_hash TEXT NOT NULL,
|
|
4119
|
+
content TEXT NOT NULL,
|
|
4120
|
+
title TEXT,
|
|
4121
|
+
content_bytes INTEGER NOT NULL,
|
|
4122
|
+
truncated INTEGER NOT NULL DEFAULT 0,
|
|
4123
|
+
checked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
4124
|
+
PRIMARY KEY (user_id, url)
|
|
4125
|
+
)
|
|
4126
|
+
`);
|
|
4127
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS page_snapshots_user_checked_at ON page_snapshots(user_id, checked_at DESC)`);
|
|
4112
4128
|
}
|
|
4113
4129
|
async function registerClient(clientId, redirectUris, name) {
|
|
4114
4130
|
await getDb().execute({
|
|
@@ -4539,6 +4555,38 @@ async function listJobs(userId) {
|
|
|
4539
4555
|
const res = await getDb().execute({ sql: "SELECT * FROM jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50", args: [userId] });
|
|
4540
4556
|
return res.rows.map((r) => deserialize(rowToRawJob(r)));
|
|
4541
4557
|
}
|
|
4558
|
+
async function getPageSnapshot(userId, url) {
|
|
4559
|
+
const res = await getDb().execute({
|
|
4560
|
+
sql: "SELECT content_hash, content, title, content_bytes, truncated, checked_at FROM page_snapshots WHERE user_id = ? AND url = ?",
|
|
4561
|
+
args: [userId, url]
|
|
4562
|
+
});
|
|
4563
|
+
const row = res.rows[0];
|
|
4564
|
+
if (!row) return null;
|
|
4565
|
+
return {
|
|
4566
|
+
contentHash: String(row.content_hash),
|
|
4567
|
+
content: String(row.content),
|
|
4568
|
+
title: row.title == null ? null : String(row.title),
|
|
4569
|
+
contentBytes: Number(row.content_bytes),
|
|
4570
|
+
truncated: Number(row.truncated) === 1,
|
|
4571
|
+
checkedAt: String(row.checked_at)
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
async function upsertPageSnapshot(userId, url, snapshot) {
|
|
4575
|
+
await getDb().execute({
|
|
4576
|
+
sql: `
|
|
4577
|
+
INSERT INTO page_snapshots (user_id, url, content_hash, content, title, content_bytes, truncated, checked_at)
|
|
4578
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
4579
|
+
ON CONFLICT(user_id, url) DO UPDATE SET
|
|
4580
|
+
content_hash = excluded.content_hash,
|
|
4581
|
+
content = excluded.content,
|
|
4582
|
+
title = excluded.title,
|
|
4583
|
+
content_bytes = excluded.content_bytes,
|
|
4584
|
+
truncated = excluded.truncated,
|
|
4585
|
+
checked_at = excluded.checked_at
|
|
4586
|
+
`,
|
|
4587
|
+
args: [userId, url, snapshot.contentHash, snapshot.content, snapshot.title, snapshot.contentBytes, snapshot.truncated ? 1 : 0]
|
|
4588
|
+
});
|
|
4589
|
+
}
|
|
4542
4590
|
function rowToRequestEvent(row) {
|
|
4543
4591
|
const rawResult = row.result != null ? String(row.result) : null;
|
|
4544
4592
|
return {
|
|
@@ -5293,18 +5341,25 @@ function kernelCostUsd(ms, headful) {
|
|
|
5293
5341
|
return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
|
|
5294
5342
|
}
|
|
5295
5343
|
function vendorCostUsd(vendor, units) {
|
|
5296
|
-
if (vendor === "fal_wizper")
|
|
5344
|
+
if (vendor === "fal_wizper") {
|
|
5345
|
+
const estimatedComputeSec = Math.max(0, units) / FAL_WIZPER_WALLCLOCK_TO_COMPUTE_SEC_FACTOR;
|
|
5346
|
+
return estimatedComputeSec * FAL_WIZPER_USD_PER_COMPUTE_SEC;
|
|
5347
|
+
}
|
|
5297
5348
|
if (vendor === "deepinfra_qwen") return Math.max(0, units) / 1e3 * DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS;
|
|
5349
|
+
if (vendor === "openrouter") return Math.max(0, units);
|
|
5350
|
+
if (vendor === "mcp_memory_video") return Math.max(0, units);
|
|
5351
|
+
if (vendor === "mcp_memory_ai") return Math.max(0, units);
|
|
5298
5352
|
return 0;
|
|
5299
5353
|
}
|
|
5300
|
-
var KERNEL_HEADLESS_USD_PER_SEC, KERNEL_HEADFUL_USD_PER_SEC, HEADLESS_OPS,
|
|
5354
|
+
var KERNEL_HEADLESS_USD_PER_SEC, KERNEL_HEADFUL_USD_PER_SEC, HEADLESS_OPS, FAL_WIZPER_USD_PER_COMPUTE_SEC, FAL_WIZPER_WALLCLOCK_TO_COMPUTE_SEC_FACTOR, DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS;
|
|
5301
5355
|
var init_cost_rates = __esm({
|
|
5302
5356
|
"src/api/cost-rates.ts"() {
|
|
5303
5357
|
"use strict";
|
|
5304
5358
|
KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
|
|
5305
5359
|
KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
|
|
5306
|
-
HEADLESS_OPS = /* @__PURE__ */ new Set(["
|
|
5307
|
-
|
|
5360
|
+
HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "maps_search", "yt_channel", "yt_transcription", "fb_search", "fb_ad", "instagram"]);
|
|
5361
|
+
FAL_WIZPER_USD_PER_COMPUTE_SEC = 111e-5;
|
|
5362
|
+
FAL_WIZPER_WALLCLOCK_TO_COMPUTE_SEC_FACTOR = 4;
|
|
5308
5363
|
DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS = 4e-4;
|
|
5309
5364
|
}
|
|
5310
5365
|
});
|
|
@@ -5345,6 +5400,10 @@ async function migrateCostTelemetry() {
|
|
|
5345
5400
|
await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_type TEXT`);
|
|
5346
5401
|
} catch {
|
|
5347
5402
|
}
|
|
5403
|
+
try {
|
|
5404
|
+
await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN method TEXT`);
|
|
5405
|
+
} catch {
|
|
5406
|
+
}
|
|
5348
5407
|
await db.execute(`
|
|
5349
5408
|
CREATE TABLE IF NOT EXISTS vendor_usage_log (
|
|
5350
5409
|
id TEXT PRIMARY KEY,
|
|
@@ -5363,6 +5422,10 @@ async function migrateCostTelemetry() {
|
|
|
5363
5422
|
await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_op ON vendor_usage_log(op)`);
|
|
5364
5423
|
await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_probe ON vendor_usage_log(probe_run_id)`);
|
|
5365
5424
|
await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_created ON vendor_usage_log(created_at)`);
|
|
5425
|
+
try {
|
|
5426
|
+
await db.execute(`ALTER TABLE vendor_usage_log ADD COLUMN method TEXT`);
|
|
5427
|
+
} catch {
|
|
5428
|
+
}
|
|
5366
5429
|
await db.execute(`
|
|
5367
5430
|
CREATE TABLE IF NOT EXISTS cost_probe_runs (
|
|
5368
5431
|
id TEXT PRIMARY KEY,
|
|
@@ -5409,8 +5472,8 @@ async function recordKernelSession(r) {
|
|
|
5409
5472
|
const db = getDb();
|
|
5410
5473
|
await db.execute({
|
|
5411
5474
|
sql: `INSERT INTO kernel_session_log
|
|
5412
|
-
(id, kernel_session_id, op, source, probe_run_id, user_id, stealth, headless_sent, proxy_used, proxy_source, proxy_type, fallback, opened_at, closed_at, duration_ms, est_cost_usd_headless, est_cost_usd_headful, error)
|
|
5413
|
-
VALUES (
|
|
5475
|
+
(id, kernel_session_id, op, source, probe_run_id, user_id, stealth, headless_sent, proxy_used, proxy_source, proxy_type, fallback, opened_at, closed_at, duration_ms, est_cost_usd_headless, est_cost_usd_headful, error, method)
|
|
5476
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
5414
5477
|
args: [
|
|
5415
5478
|
(0, import_node_crypto2.randomUUID)(),
|
|
5416
5479
|
r.kernelSessionId ?? null,
|
|
@@ -5429,7 +5492,8 @@ async function recordKernelSession(r) {
|
|
|
5429
5492
|
durationMs,
|
|
5430
5493
|
kernelCostUsd(durationMs, false),
|
|
5431
5494
|
kernelCostUsd(durationMs, true),
|
|
5432
|
-
r.error ?? null
|
|
5495
|
+
r.error ?? null,
|
|
5496
|
+
ctx?.subOp ?? null
|
|
5433
5497
|
]
|
|
5434
5498
|
});
|
|
5435
5499
|
} catch (err) {
|
|
@@ -5443,8 +5507,8 @@ async function recordVendorUsage(r) {
|
|
|
5443
5507
|
const db = getDb();
|
|
5444
5508
|
await db.execute({
|
|
5445
5509
|
sql: `INSERT INTO vendor_usage_log
|
|
5446
|
-
(id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error)
|
|
5447
|
-
VALUES (
|
|
5510
|
+
(id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error, method)
|
|
5511
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
|
5448
5512
|
args: [
|
|
5449
5513
|
(0, import_node_crypto2.randomUUID)(),
|
|
5450
5514
|
ctx?.op ?? null,
|
|
@@ -5455,7 +5519,8 @@ async function recordVendorUsage(r) {
|
|
|
5455
5519
|
r.units,
|
|
5456
5520
|
r.unitType,
|
|
5457
5521
|
vendorCostUsd(r.vendor, r.units),
|
|
5458
|
-
r.error ?? null
|
|
5522
|
+
r.error ?? null,
|
|
5523
|
+
ctx?.subOp ?? null
|
|
5459
5524
|
]
|
|
5460
5525
|
});
|
|
5461
5526
|
} catch (err) {
|
|
@@ -7357,6 +7422,7 @@ var init_llm_parse_with_retry = __esm({
|
|
|
7357
7422
|
DeepInfraLlmClient = class {
|
|
7358
7423
|
apiKey;
|
|
7359
7424
|
model;
|
|
7425
|
+
totalCostUsd = 0;
|
|
7360
7426
|
constructor(apiKey, model = "Qwen/Qwen3.6-35B-A3B") {
|
|
7361
7427
|
this.apiKey = apiKey;
|
|
7362
7428
|
this.model = model;
|
|
@@ -7389,6 +7455,7 @@ ${raw}`
|
|
|
7389
7455
|
OpenRouterLlmClient = class {
|
|
7390
7456
|
apiKey;
|
|
7391
7457
|
model;
|
|
7458
|
+
totalCostUsd = 0;
|
|
7392
7459
|
constructor(apiKey, model = "qwen/qwen3-235b-a22b") {
|
|
7393
7460
|
this.apiKey = apiKey;
|
|
7394
7461
|
this.model = model;
|
|
@@ -7401,6 +7468,9 @@ ${raw}`
|
|
|
7401
7468
|
});
|
|
7402
7469
|
if (!resp.ok) throw new Error(`OpenRouter error ${resp.status}: ${await resp.text()}`);
|
|
7403
7470
|
const data = await resp.json();
|
|
7471
|
+
const cost = data.usage?.cost ?? 0;
|
|
7472
|
+
this.totalCostUsd += cost;
|
|
7473
|
+
void recordVendorUsage({ vendor: "openrouter", model: this.model, units: cost, unitType: "usd" });
|
|
7404
7474
|
return data.choices[0].message.content;
|
|
7405
7475
|
}
|
|
7406
7476
|
async completeJson(prompt) {
|
|
@@ -7426,6 +7496,9 @@ ${raw}`
|
|
|
7426
7496
|
secondary;
|
|
7427
7497
|
lastServedBy = null;
|
|
7428
7498
|
lastPrimaryError = null;
|
|
7499
|
+
get totalCostUsd() {
|
|
7500
|
+
return this.primary.totalCostUsd + this.secondary.totalCostUsd;
|
|
7501
|
+
}
|
|
7429
7502
|
async complete(prompt) {
|
|
7430
7503
|
try {
|
|
7431
7504
|
const result = await this.primary.complete(prompt);
|
|
@@ -7880,6 +7953,390 @@ var init_schemas = __esm({
|
|
|
7880
7953
|
}
|
|
7881
7954
|
});
|
|
7882
7955
|
|
|
7956
|
+
// src/api/rates.ts
|
|
7957
|
+
function browserActiveCostMc(activeMs) {
|
|
7958
|
+
return Math.round(activeMs * MC_PER_BROWSER_MS);
|
|
7959
|
+
}
|
|
7960
|
+
function concurrencySlotBillingInfo() {
|
|
7961
|
+
const billingUrl = process.env.CONCURRENCY_BILLING_URL ?? "https://mcpscraper.dev/billing";
|
|
7962
|
+
return {
|
|
7963
|
+
product: "Extra concurrency slot",
|
|
7964
|
+
price_label: CONCURRENCY_SLOT_PRICE_LABEL,
|
|
7965
|
+
unit_amount_usd: CONCURRENCY_SLOT_PRICE_USD,
|
|
7966
|
+
currency: CONCURRENCY_SLOT_PRICE_CURRENCY,
|
|
7967
|
+
interval: CONCURRENCY_SLOT_PRICE_INTERVAL,
|
|
7968
|
+
billing_url: billingUrl,
|
|
7969
|
+
terminal_command: CONCURRENCY_SLOT_TERMINAL_COMMAND,
|
|
7970
|
+
terminal_command_with_api_key_env: `MCP_SCRAPER_API_KEY=sk_live_your_key ${CONCURRENCY_SLOT_TERMINAL_COMMAND}`
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7973
|
+
function mcToCredits(mc) {
|
|
7974
|
+
return mc / MC_PER_CREDIT;
|
|
7975
|
+
}
|
|
7976
|
+
function insufficientBalanceResponse(balanceMc, requiredMc) {
|
|
7977
|
+
const topupUrl = process.env.TOPUP_URL ?? "https://mcpscraper.dev/billing";
|
|
7978
|
+
const balanceCredits = mcToCredits(balanceMc);
|
|
7979
|
+
const requiredCredits = mcToCredits(requiredMc);
|
|
7980
|
+
return {
|
|
7981
|
+
error: "insufficient_balance",
|
|
7982
|
+
error_code: "insufficient_balance",
|
|
7983
|
+
message: `Insufficient credits. Balance: ${balanceCredits} credits. This call requires ${requiredCredits} credits. Top up at ${topupUrl}`,
|
|
7984
|
+
balance_credits: balanceCredits,
|
|
7985
|
+
required_credits: requiredCredits,
|
|
7986
|
+
topup_url: topupUrl
|
|
7987
|
+
};
|
|
7988
|
+
}
|
|
7989
|
+
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, SCHEDULING_PLANS, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, SITE_AUDIT_LLM_MARGIN_MULTIPLE, VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE, MC_PER_USD;
|
|
7990
|
+
var init_rates = __esm({
|
|
7991
|
+
"src/api/rates.ts"() {
|
|
7992
|
+
"use strict";
|
|
7993
|
+
MC_COSTS = {
|
|
7994
|
+
serp: 100,
|
|
7995
|
+
paa: 200,
|
|
7996
|
+
paa_base: 1e3,
|
|
7997
|
+
page_scrape: 100,
|
|
7998
|
+
url_map: 500,
|
|
7999
|
+
yt_channel: 200,
|
|
8000
|
+
yt_transcription: 200,
|
|
8001
|
+
fb_ad: 200,
|
|
8002
|
+
maps_search: 500,
|
|
8003
|
+
maps_place: 6e3,
|
|
8004
|
+
maps_review: 100,
|
|
8005
|
+
fb_search: 600,
|
|
8006
|
+
fb_transcribe: 200,
|
|
8007
|
+
google_ads_search: 600,
|
|
8008
|
+
google_ads_intel: 200,
|
|
8009
|
+
google_ads_transcribe: 200,
|
|
8010
|
+
instagram_profile: 400,
|
|
8011
|
+
instagram_media: 400,
|
|
8012
|
+
instagram_transcribe: 50,
|
|
8013
|
+
browser_minute: 12e3,
|
|
8014
|
+
reddit_thread: 3e3,
|
|
8015
|
+
reddit_comment: 200,
|
|
8016
|
+
video_analysis: 666667,
|
|
8017
|
+
trustpilot_reviews: 500,
|
|
8018
|
+
trustpilot_review: 100,
|
|
8019
|
+
g2_reviews: 500,
|
|
8020
|
+
g2_review: 150,
|
|
8021
|
+
diff_page: 100
|
|
8022
|
+
};
|
|
8023
|
+
MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
|
|
8024
|
+
BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
|
|
8025
|
+
MC_PER_CREDIT = 1e3;
|
|
8026
|
+
CREDIT_COST_CATALOG = [
|
|
8027
|
+
{
|
|
8028
|
+
key: "serp",
|
|
8029
|
+
label: "SERP search",
|
|
8030
|
+
aliases: ["search_serp", "serp", "google search", "organic results"],
|
|
8031
|
+
credits: mcToCredits(MC_COSTS.serp),
|
|
8032
|
+
unit: "per search",
|
|
8033
|
+
notes: "Returns AI Overview, PAA snippet, videos, forums, and local pack."
|
|
8034
|
+
},
|
|
8035
|
+
{
|
|
8036
|
+
key: "paa",
|
|
8037
|
+
label: "PAA harvest",
|
|
8038
|
+
aliases: ["harvest_paa", "paa", "people also ask", "questions"],
|
|
8039
|
+
credits: mcToCredits(MC_COSTS.paa),
|
|
8040
|
+
unit: `per question (+${mcToCredits(MC_COSTS.paa_base)} credit base)`,
|
|
8041
|
+
notes: `Full SERP feature extraction. Billed ${mcToCredits(MC_COSTS.paa_base)} credit base + ${mcToCredits(MC_COSTS.paa)} per question actually returned (unused estimate refunded).`
|
|
8042
|
+
},
|
|
8043
|
+
{
|
|
8044
|
+
key: "page_scrape",
|
|
8045
|
+
label: "Page crawl / extract",
|
|
8046
|
+
aliases: ["extract_url", "extract_site", "page scrape", "url scrape", "single page", "site crawl"],
|
|
8047
|
+
credits: mcToCredits(MC_COSTS.page_scrape),
|
|
8048
|
+
unit: "per page",
|
|
8049
|
+
notes: "Applies to both single-URL extraction and per-page site crawls."
|
|
8050
|
+
},
|
|
8051
|
+
{
|
|
8052
|
+
key: "url_map",
|
|
8053
|
+
label: "Site URL mapping",
|
|
8054
|
+
aliases: ["map_site_urls", "url map", "site map", "crawl urls"],
|
|
8055
|
+
credits: mcToCredits(MC_COSTS.url_map),
|
|
8056
|
+
unit: "per mapping operation",
|
|
8057
|
+
notes: "Flat rate for the full /map-urls call regardless of URL count discovered."
|
|
8058
|
+
},
|
|
8059
|
+
{
|
|
8060
|
+
key: "yt_channel",
|
|
8061
|
+
label: "YouTube search / channel harvest",
|
|
8062
|
+
aliases: ["youtube_harvest", "youtube search", "youtube channel", "yt_channel"],
|
|
8063
|
+
credits: mcToCredits(MC_COSTS.yt_channel),
|
|
8064
|
+
unit: "per call"
|
|
8065
|
+
},
|
|
8066
|
+
{
|
|
8067
|
+
key: "yt_transcription",
|
|
8068
|
+
label: "YouTube transcription",
|
|
8069
|
+
aliases: ["youtube_transcribe", "youtube transcript", "transcription", "yt_transcription"],
|
|
8070
|
+
credits: mcToCredits(MC_COSTS.yt_transcription),
|
|
8071
|
+
unit: "per minute",
|
|
8072
|
+
notes: "A 5-minute hold is taken, then reconciled to actual video duration."
|
|
8073
|
+
},
|
|
8074
|
+
{
|
|
8075
|
+
key: "fb_ad",
|
|
8076
|
+
label: "Facebook search / ad lookup",
|
|
8077
|
+
aliases: ["facebook_page_intel", "facebook_ad_search", "facebook_ad", "facebook ads", "fb ads"],
|
|
8078
|
+
credits: mcToCredits(MC_COSTS.fb_ad),
|
|
8079
|
+
unit: "per call"
|
|
8080
|
+
},
|
|
8081
|
+
{
|
|
8082
|
+
key: "maps_search",
|
|
8083
|
+
label: "Maps business search",
|
|
8084
|
+
aliases: ["maps_search", "google maps search", "gmb search", "gbp search", "business profiles"],
|
|
8085
|
+
credits: mcToCredits(MC_COSTS.maps_search),
|
|
8086
|
+
unit: "per search",
|
|
8087
|
+
notes: "Returns up to 50 Google Maps business/profile candidates. Use maps_place_intel to hydrate selected businesses."
|
|
8088
|
+
},
|
|
8089
|
+
{
|
|
8090
|
+
key: "maps_place",
|
|
8091
|
+
label: "Maps business lookup",
|
|
8092
|
+
aliases: ["maps_place_intel", "google maps", "maps place", "place intel"],
|
|
8093
|
+
credits: mcToCredits(MC_COSTS.maps_place),
|
|
8094
|
+
unit: "per business",
|
|
8095
|
+
notes: "Base lookup. Reviews billed separately per card at maps_review rate."
|
|
8096
|
+
},
|
|
8097
|
+
{
|
|
8098
|
+
key: "maps_review",
|
|
8099
|
+
label: "Maps review",
|
|
8100
|
+
aliases: ["maps_reviews", "google reviews", "review cards", "reviews"],
|
|
8101
|
+
credits: mcToCredits(MC_COSTS.maps_review),
|
|
8102
|
+
unit: "per review card",
|
|
8103
|
+
notes: "Charged after extraction when includeReviews is true."
|
|
8104
|
+
},
|
|
8105
|
+
{
|
|
8106
|
+
key: "fb_search",
|
|
8107
|
+
label: "Facebook ad library search",
|
|
8108
|
+
aliases: ["facebook_search", "fb_search", "fb ad search"],
|
|
8109
|
+
credits: mcToCredits(MC_COSTS.fb_search),
|
|
8110
|
+
unit: "per search",
|
|
8111
|
+
notes: "Browser automation to search Facebook Ads Library by keyword."
|
|
8112
|
+
},
|
|
8113
|
+
{
|
|
8114
|
+
key: "google_ads_search",
|
|
8115
|
+
label: "Google Ads Transparency search",
|
|
8116
|
+
aliases: ["google_ads_search", "google ads search", "ads transparency search"],
|
|
8117
|
+
credits: mcToCredits(MC_COSTS.google_ads_search),
|
|
8118
|
+
unit: "per search",
|
|
8119
|
+
notes: "Browser automation to find advertisers in Google Ads Transparency Center by domain or name."
|
|
8120
|
+
},
|
|
8121
|
+
{
|
|
8122
|
+
key: "google_ads_intel",
|
|
8123
|
+
label: "Google Ads Transparency advertiser intel",
|
|
8124
|
+
aliases: ["google_ads_page_intel", "google ads intel", "ads transparency intel"],
|
|
8125
|
+
credits: mcToCredits(MC_COSTS.google_ads_intel),
|
|
8126
|
+
unit: "per call",
|
|
8127
|
+
notes: "Lists and hydrates an advertiser's creatives with image URLs and video references."
|
|
8128
|
+
},
|
|
8129
|
+
{
|
|
8130
|
+
key: "google_ads_transcribe",
|
|
8131
|
+
label: "Google ad video transcription",
|
|
8132
|
+
aliases: ["google_ads_transcribe", "google ad transcript"],
|
|
8133
|
+
credits: mcToCredits(MC_COSTS.google_ads_transcribe),
|
|
8134
|
+
unit: "per minute",
|
|
8135
|
+
notes: "A hold is taken, then reconciled to actual video duration."
|
|
8136
|
+
},
|
|
8137
|
+
{
|
|
8138
|
+
key: "fb_transcribe",
|
|
8139
|
+
label: "Facebook video / ad transcription",
|
|
8140
|
+
aliases: ["facebook_transcribe", "facebook_video_transcribe", "fb_transcribe", "fb ad transcript"],
|
|
8141
|
+
credits: mcToCredits(MC_COSTS.fb_transcribe),
|
|
8142
|
+
unit: "per minute",
|
|
8143
|
+
notes: "A hold is taken, then reconciled to actual video duration."
|
|
8144
|
+
},
|
|
8145
|
+
{
|
|
8146
|
+
key: "instagram_profile",
|
|
8147
|
+
label: "Instagram profile content discovery",
|
|
8148
|
+
aliases: ["instagram_profile_content", "instagram profile", "instagram content list", "ig profile"],
|
|
8149
|
+
credits: mcToCredits(MC_COSTS.instagram_profile),
|
|
8150
|
+
unit: "per profile scan",
|
|
8151
|
+
notes: "Browser extraction of public Instagram profile grid links. Complete history may require a logged-in profile."
|
|
8152
|
+
},
|
|
8153
|
+
{
|
|
8154
|
+
key: "instagram_media",
|
|
8155
|
+
label: "Instagram media download",
|
|
8156
|
+
aliases: ["instagram_media_download", "instagram reel download", "instagram post download", "ig media"],
|
|
8157
|
+
credits: mcToCredits(MC_COSTS.instagram_media),
|
|
8158
|
+
unit: "per post or reel",
|
|
8159
|
+
notes: "Extracts post text, image URLs, and reel audio/video tracks, with local downloads when the server can write files."
|
|
8160
|
+
},
|
|
8161
|
+
{
|
|
8162
|
+
key: "instagram_transcribe",
|
|
8163
|
+
label: "Instagram media transcription",
|
|
8164
|
+
aliases: ["instagram transcript", "instagram reel transcript", "ig transcribe"],
|
|
8165
|
+
credits: mcToCredits(MC_COSTS.instagram_transcribe),
|
|
8166
|
+
unit: "per call",
|
|
8167
|
+
notes: "Whisper transcription of selected Instagram audio/video media via fal.ai."
|
|
8168
|
+
},
|
|
8169
|
+
{
|
|
8170
|
+
key: "browser_minute",
|
|
8171
|
+
label: "Interactive browser session",
|
|
8172
|
+
aliases: ["browser_open", "browser agent", "browser_agent", "live browser", "browse", "browser control", "interactive browser"],
|
|
8173
|
+
credits: mcToCredits(MC_COSTS.browser_minute),
|
|
8174
|
+
unit: "per minute of use",
|
|
8175
|
+
notes: "Metered per minute of use for the whole time the browser session is open. Close the session to stop the meter; abandoned sessions are auto-closed after a short idle window."
|
|
8176
|
+
},
|
|
8177
|
+
{
|
|
8178
|
+
key: "reddit_thread",
|
|
8179
|
+
label: "Reddit thread base lookup",
|
|
8180
|
+
aliases: ["reddit", "reddit_thread", "reddit comments", "subreddit", "reddit post"],
|
|
8181
|
+
credits: mcToCredits(MC_COSTS.reddit_thread),
|
|
8182
|
+
unit: "per thread",
|
|
8183
|
+
notes: "Base lookup for the post itself. Comments billed separately per comment at reddit_comment rate. Refunded if the thread cannot be retrieved."
|
|
8184
|
+
},
|
|
8185
|
+
{
|
|
8186
|
+
key: "reddit_comment",
|
|
8187
|
+
label: "Reddit comment",
|
|
8188
|
+
aliases: ["reddit_comment", "reddit comments"],
|
|
8189
|
+
credits: mcToCredits(MC_COSTS.reddit_comment),
|
|
8190
|
+
unit: "per comment",
|
|
8191
|
+
notes: "Charged per comment actually extracted, billed down automatically if the balance runs out mid-thread."
|
|
8192
|
+
},
|
|
8193
|
+
{
|
|
8194
|
+
key: "video_analysis",
|
|
8195
|
+
label: "Video breakdown (frame-by-frame + transcript)",
|
|
8196
|
+
aliases: ["video_frame_analysis", "video breakdown", "video analysis", "analyze video"],
|
|
8197
|
+
credits: mcToCredits(MC_COSTS.video_analysis),
|
|
8198
|
+
unit: "per 120 frames (max 480)",
|
|
8199
|
+
notes: "Full multi-lens video breakdown: samples frames across a video (up to 30 minutes), analyzes each with vision AI, transcribes the audio, then produces summary, pacing/energy, words-per-minute, topic outline, key points, hook analysis, visual style, and a how-to-replicate recipe with a quality-control pass. $1 per 120 frames requested (max 480 = $4); billed down automatically if the video cannot use the requested frames, and refunded fully if the run fails."
|
|
8200
|
+
},
|
|
8201
|
+
{
|
|
8202
|
+
key: "trustpilot_reviews",
|
|
8203
|
+
label: "Trustpilot review harvest",
|
|
8204
|
+
aliases: ["trustpilot_reviews", "trustpilot", "trustpilot reviews"],
|
|
8205
|
+
credits: mcToCredits(MC_COSTS.trustpilot_reviews),
|
|
8206
|
+
unit: "per call",
|
|
8207
|
+
notes: "Base lookup. Reviews billed separately per card at trustpilot_review rate. Refunded if no reviews are found."
|
|
8208
|
+
},
|
|
8209
|
+
{
|
|
8210
|
+
key: "trustpilot_review",
|
|
8211
|
+
label: "Trustpilot review card",
|
|
8212
|
+
aliases: ["trustpilot_review", "trustpilot card"],
|
|
8213
|
+
credits: mcToCredits(MC_COSTS.trustpilot_review),
|
|
8214
|
+
unit: "per review card",
|
|
8215
|
+
notes: "Charged per review actually extracted, billed down automatically if the balance runs out mid-page."
|
|
8216
|
+
},
|
|
8217
|
+
{
|
|
8218
|
+
key: "g2_reviews",
|
|
8219
|
+
label: "G2 review harvest",
|
|
8220
|
+
aliases: ["g2_reviews", "g2", "g2 reviews"],
|
|
8221
|
+
credits: mcToCredits(MC_COSTS.g2_reviews),
|
|
8222
|
+
unit: "per call",
|
|
8223
|
+
notes: "Base lookup. Reviews billed separately per card at g2_review rate. Refunded if no reviews are found."
|
|
8224
|
+
},
|
|
8225
|
+
{
|
|
8226
|
+
key: "g2_review",
|
|
8227
|
+
label: "G2 review card",
|
|
8228
|
+
aliases: ["g2_review", "g2 card"],
|
|
8229
|
+
credits: mcToCredits(MC_COSTS.g2_review),
|
|
8230
|
+
unit: "per review card",
|
|
8231
|
+
notes: "Charged per review actually extracted (each card carries up to 3 Q&A sections), billed down automatically if the balance runs out mid-page."
|
|
8232
|
+
},
|
|
8233
|
+
{
|
|
8234
|
+
key: "diff_page",
|
|
8235
|
+
label: "Page change check",
|
|
8236
|
+
aliases: ["diff_page", "page diff", "change detection"],
|
|
8237
|
+
credits: mcToCredits(MC_COSTS.diff_page),
|
|
8238
|
+
unit: "per check",
|
|
8239
|
+
notes: "Same cost as a single page extract \u2014 one scrape is performed per check, then compared against your last stored snapshot for that URL."
|
|
8240
|
+
}
|
|
8241
|
+
];
|
|
8242
|
+
CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
|
|
8243
|
+
SUBSCRIPTION_TIERS = {
|
|
8244
|
+
"price_1TmiHRS8aAcsk3TGwmSNfNIa": { tier: "starter", label: "Starter", price_id: "price_1TmiHRS8aAcsk3TGwmSNfNIa", monthly_usd: 12, credits_mc: 8e6, concurrency: 3, intro_coupon: "mcp-starter-1dollar-intro-12" },
|
|
8245
|
+
"price_1Tmg1nS8aAcsk3TGe8zcnGTM": { tier: "growth", label: "Growth", price_id: "price_1Tmg1nS8aAcsk3TGe8zcnGTM", monthly_usd: 100, credits_mc: 8e7, concurrency: 10, intro_coupon: null },
|
|
8246
|
+
"price_1Tmg1nS8aAcsk3TGsZw34iXS": { tier: "scale", label: "Scale", price_id: "price_1Tmg1nS8aAcsk3TGsZw34iXS", monthly_usd: 250, credits_mc: 24e7, concurrency: 20, intro_coupon: null }
|
|
8247
|
+
};
|
|
8248
|
+
SUBSCRIPTION_TIER_BY_KEY = Object.fromEntries(
|
|
8249
|
+
Object.values(SUBSCRIPTION_TIERS).map((t) => [t.tier, t])
|
|
8250
|
+
);
|
|
8251
|
+
CONCURRENCY_SLOT_PRICE_USD = 5;
|
|
8252
|
+
CONCURRENCY_SLOT_PRICE_CURRENCY = "usd";
|
|
8253
|
+
CONCURRENCY_SLOT_PRICE_INTERVAL = "month";
|
|
8254
|
+
CONCURRENCY_SLOT_PRICE_LABEL = "$5/month";
|
|
8255
|
+
CONCURRENCY_SLOT_TERMINAL_COMMAND = "npx -y -p mcp-scraper@latest mcp-scraper-cli billing concurrency checkout";
|
|
8256
|
+
FREE_SIGNUP_MC = 1e5;
|
|
8257
|
+
MEMORY_FREE_QUOTA_BYTES = 1e7 * 1;
|
|
8258
|
+
MEMORY_PLANS = {
|
|
8259
|
+
"price_1TnMSTS8aAcsk3TGBgiwuvqL": { plan: "pro", label: "Pro", price_id: "price_1TnMSTS8aAcsk3TGBgiwuvqL", interval: "month", monthly_usd: 19, quota_bytes: 5e9 },
|
|
8260
|
+
"price_1TnMSTS8aAcsk3TGWBVU2agY": { plan: "pro", label: "Pro", price_id: "price_1TnMSTS8aAcsk3TGWBVU2agY", interval: "year", monthly_usd: 19, quota_bytes: 5e9 }
|
|
8261
|
+
};
|
|
8262
|
+
MEMORY_PLAN_QUOTA = { free: MEMORY_FREE_QUOTA_BYTES, pro: 5e9, team: 5e10 };
|
|
8263
|
+
MEMORY_MARGIN_MULTIPLE = 3;
|
|
8264
|
+
MEMORY_PRO_COST_BUDGET_USD = 19 / MEMORY_MARGIN_MULTIPLE;
|
|
8265
|
+
SCHEDULING_PLANS = {
|
|
8266
|
+
"price_1ToXcHS8aAcsk3TGf3cW7zHx": { price_id: "price_1ToXcHS8aAcsk3TGf3cW7zHx", label: "Scheduled Actions", interval: "month", monthly_usd: 10, quota_per_period: 1e3 }
|
|
8267
|
+
};
|
|
8268
|
+
LedgerOperation = {
|
|
8269
|
+
TOPUP: "topup",
|
|
8270
|
+
SUBSCRIPTION: "subscription",
|
|
8271
|
+
SIGNUP_GRANT: "signup_grant",
|
|
8272
|
+
MONTHLY_REFRESH: "monthly_free_refresh",
|
|
8273
|
+
PAA: "paa",
|
|
8274
|
+
PAA_REFUND: "paa_refund",
|
|
8275
|
+
SERP: "serp",
|
|
8276
|
+
REFUND: "refund",
|
|
8277
|
+
TRANSCRIPTION: "transcription",
|
|
8278
|
+
TRANSCRIPTION_HOLD: "transcription_hold",
|
|
8279
|
+
TRANSCRIPTION_REFUND: "transcription_refund",
|
|
8280
|
+
YT_CHANNEL: "yt_channel",
|
|
8281
|
+
FB_AD: "fb_ad",
|
|
8282
|
+
MAPS_SEARCH: "maps_search",
|
|
8283
|
+
MAPS_PLACE: "maps_place",
|
|
8284
|
+
MAPS_REVIEW: "maps_review",
|
|
8285
|
+
MAPS_REVIEW_REFUND: "maps_review_refund",
|
|
8286
|
+
EXTRACT_SITE: "extract_site",
|
|
8287
|
+
EXTRACT_SITE_REFUND: "extract_site_refund",
|
|
8288
|
+
EXTRACT_URL: "page_scrape",
|
|
8289
|
+
URL_MAP: "url_map",
|
|
8290
|
+
EXTRACT_SITE_HOLD: "extract_site_hold",
|
|
8291
|
+
YT_CHANNEL_REFUND: "yt_channel_refund",
|
|
8292
|
+
FB_AD_REFUND: "fb_ad_refund",
|
|
8293
|
+
URL_MAP_REFUND: "url_map_refund",
|
|
8294
|
+
FB_SEARCH: "fb_search",
|
|
8295
|
+
FB_TRANSCRIBE: "fb_transcribe",
|
|
8296
|
+
FB_SEARCH_REFUND: "fb_search_refund",
|
|
8297
|
+
FB_TRANSCRIBE_REFUND: "fb_transcribe_refund",
|
|
8298
|
+
GOOGLE_ADS_SEARCH: "google_ads_search",
|
|
8299
|
+
GOOGLE_ADS_SEARCH_REFUND: "google_ads_search_refund",
|
|
8300
|
+
GOOGLE_ADS_INTEL: "google_ads_intel",
|
|
8301
|
+
GOOGLE_ADS_INTEL_REFUND: "google_ads_intel_refund",
|
|
8302
|
+
GOOGLE_ADS_TRANSCRIBE: "google_ads_transcribe",
|
|
8303
|
+
GOOGLE_ADS_TRANSCRIBE_REFUND: "google_ads_transcribe_refund",
|
|
8304
|
+
INSTAGRAM_PROFILE: "instagram_profile",
|
|
8305
|
+
INSTAGRAM_PROFILE_REFUND: "instagram_profile_refund",
|
|
8306
|
+
INSTAGRAM_MEDIA: "instagram_media",
|
|
8307
|
+
INSTAGRAM_MEDIA_REFUND: "instagram_media_refund",
|
|
8308
|
+
INSTAGRAM_TRANSCRIBE: "instagram_transcribe",
|
|
8309
|
+
INSTAGRAM_TRANSCRIBE_REFUND: "instagram_transcribe_refund",
|
|
8310
|
+
BROWSER_SESSION: "browser_session",
|
|
8311
|
+
REDDIT_THREAD: "reddit_thread",
|
|
8312
|
+
REDDIT_THREAD_REFUND: "reddit_thread_refund",
|
|
8313
|
+
REDDIT_COMMENT: "reddit_comment",
|
|
8314
|
+
REDDIT_COMMENT_REFUND: "reddit_comment_refund",
|
|
8315
|
+
VIDEO_ANALYSIS: "video_analysis",
|
|
8316
|
+
VIDEO_ANALYSIS_REFUND: "video_analysis_refund",
|
|
8317
|
+
VIDEO_ANALYSIS_ADJUST: "video_analysis_adjust",
|
|
8318
|
+
VIDEO_ANALYSIS_LLM: "video_analysis_llm",
|
|
8319
|
+
MEMORY_AI: "memory_ai",
|
|
8320
|
+
MEMORY_AI_REFUND: "memory_ai_refund",
|
|
8321
|
+
TRUSTPILOT_REVIEWS: "trustpilot_reviews",
|
|
8322
|
+
TRUSTPILOT_REVIEWS_REFUND: "trustpilot_reviews_refund",
|
|
8323
|
+
TRUSTPILOT_REVIEW: "trustpilot_review",
|
|
8324
|
+
TRUSTPILOT_REVIEW_REFUND: "trustpilot_review_refund",
|
|
8325
|
+
G2_REVIEWS: "g2_reviews",
|
|
8326
|
+
G2_REVIEWS_REFUND: "g2_reviews_refund",
|
|
8327
|
+
G2_REVIEW: "g2_review",
|
|
8328
|
+
G2_REVIEW_REFUND: "g2_review_refund",
|
|
8329
|
+
DIFF_PAGE: "diff_page",
|
|
8330
|
+
DIFF_PAGE_REFUND: "diff_page_refund",
|
|
8331
|
+
SITE_AUDIT_LLM: "site_audit_llm"
|
|
8332
|
+
};
|
|
8333
|
+
MEMORY_AI_MARGIN_MULTIPLE = 3;
|
|
8334
|
+
SITE_AUDIT_LLM_MARGIN_MULTIPLE = 1.5;
|
|
8335
|
+
VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE = 1.5;
|
|
8336
|
+
MC_PER_USD = SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].credits_mc / SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].monthly_usd;
|
|
8337
|
+
}
|
|
8338
|
+
});
|
|
8339
|
+
|
|
7883
8340
|
// src/services/site-architecture-auditor/prompts/ingest-validate.ts
|
|
7884
8341
|
function buildIngestValidatePrompt(input) {
|
|
7885
8342
|
const sessionPath = `<session-path-from-job>`;
|
|
@@ -8903,6 +9360,9 @@ var init_site_audit_service = __esm({
|
|
|
8903
9360
|
import_p_limit = __toESM(require("p-limit"), 1);
|
|
8904
9361
|
init_llm_parse_with_retry();
|
|
8905
9362
|
init_schemas();
|
|
9363
|
+
init_db();
|
|
9364
|
+
init_rates();
|
|
9365
|
+
init_cost_context();
|
|
8906
9366
|
init_ingest_validate();
|
|
8907
9367
|
init_enrich_competitor();
|
|
8908
9368
|
init_build_graph_per_site();
|
|
@@ -8961,8 +9421,23 @@ var init_site_audit_service = __esm({
|
|
|
8961
9421
|
async getJobsForUser(userId) {
|
|
8962
9422
|
return this.deps.repo.listSiteAuditJobs(userId);
|
|
8963
9423
|
}
|
|
9424
|
+
async runLlmBilled(userId, source, fn) {
|
|
9425
|
+
const before = this.deps.llm.totalCostUsd;
|
|
9426
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "audit_site", userId }, fn);
|
|
9427
|
+
const deltaCostUsd = this.deps.llm.totalCostUsd - before;
|
|
9428
|
+
if (deltaCostUsd <= 0) return result;
|
|
9429
|
+
const mc = Math.round(deltaCostUsd * SITE_AUDIT_LLM_MARGIN_MULTIPLE * MC_PER_USD);
|
|
9430
|
+
if (mc <= 0) return result;
|
|
9431
|
+
const { ok } = await debitMc(userId, mc, LedgerOperation.SITE_AUDIT_LLM, source);
|
|
9432
|
+
if (!ok) {
|
|
9433
|
+
console.warn(`[site-audit-service] SITE_AUDIT_LLM surcharge debit failed for user ${userId} (${source}): insufficient balance for ${mc}mc`);
|
|
9434
|
+
}
|
|
9435
|
+
return result;
|
|
9436
|
+
}
|
|
8964
9437
|
async runIngestPhase(jobId, payload) {
|
|
8965
9438
|
const { request } = payload;
|
|
9439
|
+
const job = await this.deps.repo.getSiteAuditJob(jobId);
|
|
9440
|
+
if (!job) throw new Error(`[site-audit-service] runIngestPhase: job ${jobId} not found`);
|
|
8966
9441
|
const input = {
|
|
8967
9442
|
clientDomain: request.clientDomain,
|
|
8968
9443
|
sfInternalPath: request.sfInternalPath,
|
|
@@ -8977,14 +9452,18 @@ var init_site_audit_service = __esm({
|
|
|
8977
9452
|
businessContext: request.businessContext,
|
|
8978
9453
|
priorAuditMemoryEnabled: request.priorAuditMemoryEnabled
|
|
8979
9454
|
};
|
|
8980
|
-
const result = await
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
9455
|
+
const result = await this.runLlmBilled(
|
|
9456
|
+
job.user_id,
|
|
9457
|
+
`${jobId}:phase1-ingest`,
|
|
9458
|
+
() => llmParseWithRetry(
|
|
9459
|
+
ValidatedFileManifestSchema,
|
|
9460
|
+
(attempt, error) => {
|
|
9461
|
+
void attempt;
|
|
9462
|
+
void error;
|
|
9463
|
+
return buildIngestValidatePrompt(input);
|
|
9464
|
+
},
|
|
9465
|
+
this.deps.llm
|
|
9466
|
+
)
|
|
8988
9467
|
);
|
|
8989
9468
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", result);
|
|
8990
9469
|
}
|
|
@@ -8999,23 +9478,27 @@ var init_site_audit_service = __esm({
|
|
|
8999
9478
|
const competitorDomainsCsvPath = request.competitorDomainsCsvPath;
|
|
9000
9479
|
void competitorDomainsCsvPath;
|
|
9001
9480
|
const competitors = await this._readCompetitorDomains(request);
|
|
9002
|
-
const results = await
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
(
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9481
|
+
const results = await this.runLlmBilled(
|
|
9482
|
+
job.user_id,
|
|
9483
|
+
`${jobId}:phase1-enrich`,
|
|
9484
|
+
() => Promise.all(
|
|
9485
|
+
competitors.map(
|
|
9486
|
+
(domain, idx) => limit(async () => {
|
|
9487
|
+
const competitorIndex = idx + 1;
|
|
9488
|
+
const rawPrompt = buildEnrichCompetitorPrompt({ domain, sessionPath });
|
|
9489
|
+
const prompt = rawPrompt.replace("<competitor-index>", String(competitorIndex));
|
|
9490
|
+
const result = await llmParseWithRetry(
|
|
9491
|
+
EnrichCompetitorOutputSchema,
|
|
9492
|
+
(attempt, error) => {
|
|
9493
|
+
void attempt;
|
|
9494
|
+
void error;
|
|
9495
|
+
return prompt;
|
|
9496
|
+
},
|
|
9497
|
+
this.deps.llm
|
|
9498
|
+
);
|
|
9499
|
+
return result;
|
|
9500
|
+
})
|
|
9501
|
+
)
|
|
9019
9502
|
)
|
|
9020
9503
|
);
|
|
9021
9504
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", { competitors: results });
|
|
@@ -9029,19 +9512,23 @@ var init_site_audit_service = __esm({
|
|
|
9029
9512
|
const clientDomain = request.clientDomain;
|
|
9030
9513
|
const urlInventory = [];
|
|
9031
9514
|
const directedEdgeCount = 0;
|
|
9032
|
-
const orphanAnnotation = await
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9515
|
+
const orphanAnnotation = await this.runLlmBilled(
|
|
9516
|
+
job.user_id,
|
|
9517
|
+
`${jobId}:phase2-build-graph`,
|
|
9518
|
+
() => llmParseWithRetry(
|
|
9519
|
+
OrphanAnnotationOutputSchema,
|
|
9520
|
+
(attempt, error) => {
|
|
9521
|
+
void attempt;
|
|
9522
|
+
void error;
|
|
9523
|
+
return buildGraphPerSitePrompt({
|
|
9524
|
+
siteId: "client",
|
|
9525
|
+
domain: clientDomain,
|
|
9526
|
+
urlInventory,
|
|
9527
|
+
directedEdgeCount
|
|
9528
|
+
});
|
|
9529
|
+
},
|
|
9530
|
+
this.deps.llm
|
|
9531
|
+
)
|
|
9045
9532
|
);
|
|
9046
9533
|
const graphMetrics = await this.deps.python.runGraphMetrics({
|
|
9047
9534
|
directedLinkGraphPath: `${sessionPath}/phase1/directed_link_graph.json`,
|
|
@@ -9086,50 +9573,52 @@ var init_site_audit_service = __esm({
|
|
|
9086
9573
|
}
|
|
9087
9574
|
if (currentBatch.length > 0) batches.push(currentBatch);
|
|
9088
9575
|
const usZipsCsvPath = process.env["US_ZIPS_CSV_PATH"] ?? "/Users/vilovieta/Downloads/sales-magician-api-leads-magician-01c6cff78e31/tools/analytics/data/uszips.csv";
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9576
|
+
await this.runLlmBilled(job.user_id, `${jobId}:phase3-classify`, async () => {
|
|
9577
|
+
for (const batch of batches) {
|
|
9578
|
+
let classified = [];
|
|
9579
|
+
let unclassifiedUrls = batch.map((u) => u.url);
|
|
9580
|
+
try {
|
|
9581
|
+
const pythonResult = await this.deps.python.runLocationClassifier({
|
|
9582
|
+
urls: batch.map((u) => u.url),
|
|
9583
|
+
usZipsCsvPath
|
|
9584
|
+
});
|
|
9585
|
+
classified = pythonResult.classified;
|
|
9586
|
+
unclassifiedUrls = pythonResult.unclassified;
|
|
9587
|
+
} catch (err) {
|
|
9588
|
+
console.warn(
|
|
9589
|
+
"[site-audit-service] runClassifyPhase: python.runLocationClassifier failed (falling back to LLM):",
|
|
9590
|
+
err instanceof Error ? err.message : String(err)
|
|
9591
|
+
);
|
|
9592
|
+
unclassifiedUrls = batch.map((u) => u.url);
|
|
9593
|
+
}
|
|
9594
|
+
for (const c of classified) {
|
|
9595
|
+
allClassifications.push({
|
|
9596
|
+
url: c.url,
|
|
9597
|
+
page_type: c.pageType,
|
|
9598
|
+
confidence: c.confidence > 0.6 ? "high" : c.confidence > 0.4 ? "medium" : "low",
|
|
9599
|
+
_classification_label: "[COMPUTED:python/location-classifier]",
|
|
9600
|
+
topic_cluster: null,
|
|
9601
|
+
location_signals: c.locationSignals
|
|
9602
|
+
});
|
|
9603
|
+
}
|
|
9604
|
+
if (unclassifiedUrls.length > 0) {
|
|
9605
|
+
const unclassifiedBatch = batch.filter((u) => unclassifiedUrls.includes(u.url));
|
|
9606
|
+
const llmResults = await llmParseWithRetry(
|
|
9607
|
+
import_zod10.z.array(PageTypeClassificationRowSchema),
|
|
9608
|
+
(attempt, error) => {
|
|
9609
|
+
void attempt;
|
|
9610
|
+
void error;
|
|
9611
|
+
return buildMetricsClassifyPrompt({
|
|
9612
|
+
urls: unclassifiedBatch,
|
|
9613
|
+
siteDomain: clientDomain
|
|
9614
|
+
});
|
|
9615
|
+
},
|
|
9616
|
+
this.deps.llm
|
|
9617
|
+
);
|
|
9618
|
+
allClassifications.push(...llmResults);
|
|
9619
|
+
}
|
|
9131
9620
|
}
|
|
9132
|
-
}
|
|
9621
|
+
});
|
|
9133
9622
|
void sessionPath;
|
|
9134
9623
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase3-classify", {
|
|
9135
9624
|
classifications: allClassifications,
|
|
@@ -9142,20 +9631,24 @@ var init_site_audit_service = __esm({
|
|
|
9142
9631
|
if (!job) throw new Error(`[site-audit-service] runComparePhase: job ${jobId} not found`);
|
|
9143
9632
|
const request = JSON.parse(job.request);
|
|
9144
9633
|
const clientDomain = request.clientDomain;
|
|
9145
|
-
const result = await
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9634
|
+
const result = await this.runLlmBilled(
|
|
9635
|
+
job.user_id,
|
|
9636
|
+
`${jobId}:phase4-compare`,
|
|
9637
|
+
() => llmParseWithRetry(
|
|
9638
|
+
CompareRecommendOutputSchema,
|
|
9639
|
+
(attempt, error) => {
|
|
9640
|
+
void attempt;
|
|
9641
|
+
void error;
|
|
9642
|
+
return buildCompareRecommendPrompt({
|
|
9643
|
+
clientDomain,
|
|
9644
|
+
clientMoneyPages: [],
|
|
9645
|
+
competitorSites: [],
|
|
9646
|
+
clientUrlMetrics: [],
|
|
9647
|
+
clientPageTypes: []
|
|
9648
|
+
});
|
|
9649
|
+
},
|
|
9650
|
+
this.deps.llm
|
|
9651
|
+
)
|
|
9159
9652
|
);
|
|
9160
9653
|
await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase4-compare", result);
|
|
9161
9654
|
return result;
|
|
@@ -9185,20 +9678,24 @@ var init_site_audit_service = __esm({
|
|
|
9185
9678
|
total: ilesTotal,
|
|
9186
9679
|
components: ilesComponents
|
|
9187
9680
|
};
|
|
9188
|
-
const reportMarkdown = await
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9681
|
+
const reportMarkdown = await this.runLlmBilled(
|
|
9682
|
+
job.user_id,
|
|
9683
|
+
`${jobId}:phase5-synthesize`,
|
|
9684
|
+
() => llmParseWithRetry(
|
|
9685
|
+
import_zod10.z.string(),
|
|
9686
|
+
(attempt, error) => {
|
|
9687
|
+
void attempt;
|
|
9688
|
+
void error;
|
|
9689
|
+
return buildScoreSynthesizePrompt({
|
|
9690
|
+
preComputedArchitectureScore,
|
|
9691
|
+
preComputedIlesScore,
|
|
9692
|
+
rankedBacklog: [],
|
|
9693
|
+
clientDomain,
|
|
9694
|
+
sessionPath
|
|
9695
|
+
});
|
|
9696
|
+
},
|
|
9697
|
+
this.deps.llm
|
|
9698
|
+
)
|
|
9202
9699
|
);
|
|
9203
9700
|
const auditResult = SiteAuditResultSchema.parse({
|
|
9204
9701
|
jobId,
|
|
@@ -9983,364 +10480,6 @@ var init_image_audit = __esm({
|
|
|
9983
10480
|
}
|
|
9984
10481
|
});
|
|
9985
10482
|
|
|
9986
|
-
// src/api/rates.ts
|
|
9987
|
-
function browserActiveCostMc(activeMs) {
|
|
9988
|
-
return Math.round(activeMs * MC_PER_BROWSER_MS);
|
|
9989
|
-
}
|
|
9990
|
-
function concurrencySlotBillingInfo() {
|
|
9991
|
-
const billingUrl = process.env.CONCURRENCY_BILLING_URL ?? "https://mcpscraper.dev/billing";
|
|
9992
|
-
return {
|
|
9993
|
-
product: "Extra concurrency slot",
|
|
9994
|
-
price_label: CONCURRENCY_SLOT_PRICE_LABEL,
|
|
9995
|
-
unit_amount_usd: CONCURRENCY_SLOT_PRICE_USD,
|
|
9996
|
-
currency: CONCURRENCY_SLOT_PRICE_CURRENCY,
|
|
9997
|
-
interval: CONCURRENCY_SLOT_PRICE_INTERVAL,
|
|
9998
|
-
billing_url: billingUrl,
|
|
9999
|
-
terminal_command: CONCURRENCY_SLOT_TERMINAL_COMMAND,
|
|
10000
|
-
terminal_command_with_api_key_env: `MCP_SCRAPER_API_KEY=sk_live_your_key ${CONCURRENCY_SLOT_TERMINAL_COMMAND}`
|
|
10001
|
-
};
|
|
10002
|
-
}
|
|
10003
|
-
function mcToCredits(mc) {
|
|
10004
|
-
return mc / MC_PER_CREDIT;
|
|
10005
|
-
}
|
|
10006
|
-
function insufficientBalanceResponse(balanceMc, requiredMc) {
|
|
10007
|
-
const topupUrl = process.env.TOPUP_URL ?? "https://mcpscraper.dev/billing";
|
|
10008
|
-
const balanceCredits = mcToCredits(balanceMc);
|
|
10009
|
-
const requiredCredits = mcToCredits(requiredMc);
|
|
10010
|
-
return {
|
|
10011
|
-
error: "insufficient_balance",
|
|
10012
|
-
error_code: "insufficient_balance",
|
|
10013
|
-
message: `Insufficient credits. Balance: ${balanceCredits} credits. This call requires ${requiredCredits} credits. Top up at ${topupUrl}`,
|
|
10014
|
-
balance_credits: balanceCredits,
|
|
10015
|
-
required_credits: requiredCredits,
|
|
10016
|
-
topup_url: topupUrl
|
|
10017
|
-
};
|
|
10018
|
-
}
|
|
10019
|
-
var MC_COSTS, MC_PER_BROWSER_MS, BROWSER_OPEN_MIN_BALANCE_MC, MC_PER_CREDIT, CREDIT_COST_CATALOG, CONCURRENCY_PRICE_ID, SUBSCRIPTION_TIERS, SUBSCRIPTION_TIER_BY_KEY, CONCURRENCY_SLOT_PRICE_USD, CONCURRENCY_SLOT_PRICE_CURRENCY, CONCURRENCY_SLOT_PRICE_INTERVAL, CONCURRENCY_SLOT_PRICE_LABEL, CONCURRENCY_SLOT_TERMINAL_COMMAND, FREE_SIGNUP_MC, MEMORY_FREE_QUOTA_BYTES, MEMORY_PLANS, MEMORY_PLAN_QUOTA, MEMORY_MARGIN_MULTIPLE, MEMORY_PRO_COST_BUDGET_USD, SCHEDULING_PLANS, LedgerOperation, MEMORY_AI_MARGIN_MULTIPLE, MC_PER_USD;
|
|
10020
|
-
var init_rates = __esm({
|
|
10021
|
-
"src/api/rates.ts"() {
|
|
10022
|
-
"use strict";
|
|
10023
|
-
MC_COSTS = {
|
|
10024
|
-
serp: 100,
|
|
10025
|
-
paa: 200,
|
|
10026
|
-
paa_base: 1e3,
|
|
10027
|
-
page_scrape: 100,
|
|
10028
|
-
url_map: 500,
|
|
10029
|
-
yt_channel: 200,
|
|
10030
|
-
yt_transcription: 200,
|
|
10031
|
-
fb_ad: 200,
|
|
10032
|
-
maps_search: 500,
|
|
10033
|
-
maps_place: 3e3,
|
|
10034
|
-
maps_review: 100,
|
|
10035
|
-
fb_search: 600,
|
|
10036
|
-
fb_transcribe: 200,
|
|
10037
|
-
google_ads_search: 600,
|
|
10038
|
-
google_ads_intel: 200,
|
|
10039
|
-
google_ads_transcribe: 200,
|
|
10040
|
-
instagram_profile: 400,
|
|
10041
|
-
instagram_media: 400,
|
|
10042
|
-
instagram_transcribe: 50,
|
|
10043
|
-
browser_minute: 12e3,
|
|
10044
|
-
reddit_thread: 2e3,
|
|
10045
|
-
video_analysis: 666667,
|
|
10046
|
-
trustpilot_reviews: 500,
|
|
10047
|
-
trustpilot_review: 100,
|
|
10048
|
-
g2_reviews: 500,
|
|
10049
|
-
g2_review: 150
|
|
10050
|
-
};
|
|
10051
|
-
MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
|
|
10052
|
-
BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
|
|
10053
|
-
MC_PER_CREDIT = 1e3;
|
|
10054
|
-
CREDIT_COST_CATALOG = [
|
|
10055
|
-
{
|
|
10056
|
-
key: "serp",
|
|
10057
|
-
label: "SERP search",
|
|
10058
|
-
aliases: ["search_serp", "serp", "google search", "organic results"],
|
|
10059
|
-
credits: mcToCredits(MC_COSTS.serp),
|
|
10060
|
-
unit: "per search",
|
|
10061
|
-
notes: "Returns AI Overview, PAA snippet, videos, forums, and local pack."
|
|
10062
|
-
},
|
|
10063
|
-
{
|
|
10064
|
-
key: "paa",
|
|
10065
|
-
label: "PAA harvest",
|
|
10066
|
-
aliases: ["harvest_paa", "paa", "people also ask", "questions"],
|
|
10067
|
-
credits: mcToCredits(MC_COSTS.paa),
|
|
10068
|
-
unit: `per question (+${mcToCredits(MC_COSTS.paa_base)} credit base)`,
|
|
10069
|
-
notes: `Full SERP feature extraction. Billed ${mcToCredits(MC_COSTS.paa_base)} credit base + ${mcToCredits(MC_COSTS.paa)} per question actually returned (unused estimate refunded).`
|
|
10070
|
-
},
|
|
10071
|
-
{
|
|
10072
|
-
key: "page_scrape",
|
|
10073
|
-
label: "Page crawl / extract",
|
|
10074
|
-
aliases: ["extract_url", "extract_site", "page scrape", "url scrape", "single page", "site crawl"],
|
|
10075
|
-
credits: mcToCredits(MC_COSTS.page_scrape),
|
|
10076
|
-
unit: "per page",
|
|
10077
|
-
notes: "Applies to both single-URL extraction and per-page site crawls."
|
|
10078
|
-
},
|
|
10079
|
-
{
|
|
10080
|
-
key: "url_map",
|
|
10081
|
-
label: "Site URL mapping",
|
|
10082
|
-
aliases: ["map_site_urls", "url map", "site map", "crawl urls"],
|
|
10083
|
-
credits: mcToCredits(MC_COSTS.url_map),
|
|
10084
|
-
unit: "per mapping operation",
|
|
10085
|
-
notes: "Flat rate for the full /map-urls call regardless of URL count discovered."
|
|
10086
|
-
},
|
|
10087
|
-
{
|
|
10088
|
-
key: "yt_channel",
|
|
10089
|
-
label: "YouTube search / channel harvest",
|
|
10090
|
-
aliases: ["youtube_harvest", "youtube search", "youtube channel", "yt_channel"],
|
|
10091
|
-
credits: mcToCredits(MC_COSTS.yt_channel),
|
|
10092
|
-
unit: "per call"
|
|
10093
|
-
},
|
|
10094
|
-
{
|
|
10095
|
-
key: "yt_transcription",
|
|
10096
|
-
label: "YouTube transcription",
|
|
10097
|
-
aliases: ["youtube_transcribe", "youtube transcript", "transcription", "yt_transcription"],
|
|
10098
|
-
credits: mcToCredits(MC_COSTS.yt_transcription),
|
|
10099
|
-
unit: "per minute",
|
|
10100
|
-
notes: "A 5-minute hold is taken, then reconciled to actual video duration."
|
|
10101
|
-
},
|
|
10102
|
-
{
|
|
10103
|
-
key: "fb_ad",
|
|
10104
|
-
label: "Facebook search / ad lookup",
|
|
10105
|
-
aliases: ["facebook_page_intel", "facebook_ad_search", "facebook_ad", "facebook ads", "fb ads"],
|
|
10106
|
-
credits: mcToCredits(MC_COSTS.fb_ad),
|
|
10107
|
-
unit: "per call"
|
|
10108
|
-
},
|
|
10109
|
-
{
|
|
10110
|
-
key: "maps_search",
|
|
10111
|
-
label: "Maps business search",
|
|
10112
|
-
aliases: ["maps_search", "google maps search", "gmb search", "gbp search", "business profiles"],
|
|
10113
|
-
credits: mcToCredits(MC_COSTS.maps_search),
|
|
10114
|
-
unit: "per search",
|
|
10115
|
-
notes: "Returns up to 50 Google Maps business/profile candidates. Use maps_place_intel to hydrate selected businesses."
|
|
10116
|
-
},
|
|
10117
|
-
{
|
|
10118
|
-
key: "maps_place",
|
|
10119
|
-
label: "Maps business lookup",
|
|
10120
|
-
aliases: ["maps_place_intel", "google maps", "maps place", "place intel"],
|
|
10121
|
-
credits: mcToCredits(MC_COSTS.maps_place),
|
|
10122
|
-
unit: "per business",
|
|
10123
|
-
notes: "Base lookup. Reviews billed separately per card at maps_review rate."
|
|
10124
|
-
},
|
|
10125
|
-
{
|
|
10126
|
-
key: "maps_review",
|
|
10127
|
-
label: "Maps review",
|
|
10128
|
-
aliases: ["maps_reviews", "google reviews", "review cards", "reviews"],
|
|
10129
|
-
credits: mcToCredits(MC_COSTS.maps_review),
|
|
10130
|
-
unit: "per review card",
|
|
10131
|
-
notes: "Charged after extraction when includeReviews is true."
|
|
10132
|
-
},
|
|
10133
|
-
{
|
|
10134
|
-
key: "fb_search",
|
|
10135
|
-
label: "Facebook ad library search",
|
|
10136
|
-
aliases: ["facebook_search", "fb_search", "fb ad search"],
|
|
10137
|
-
credits: mcToCredits(MC_COSTS.fb_search),
|
|
10138
|
-
unit: "per search",
|
|
10139
|
-
notes: "Browser automation to search Facebook Ads Library by keyword."
|
|
10140
|
-
},
|
|
10141
|
-
{
|
|
10142
|
-
key: "google_ads_search",
|
|
10143
|
-
label: "Google Ads Transparency search",
|
|
10144
|
-
aliases: ["google_ads_search", "google ads search", "ads transparency search"],
|
|
10145
|
-
credits: mcToCredits(MC_COSTS.google_ads_search),
|
|
10146
|
-
unit: "per search",
|
|
10147
|
-
notes: "Browser automation to find advertisers in Google Ads Transparency Center by domain or name."
|
|
10148
|
-
},
|
|
10149
|
-
{
|
|
10150
|
-
key: "google_ads_intel",
|
|
10151
|
-
label: "Google Ads Transparency advertiser intel",
|
|
10152
|
-
aliases: ["google_ads_page_intel", "google ads intel", "ads transparency intel"],
|
|
10153
|
-
credits: mcToCredits(MC_COSTS.google_ads_intel),
|
|
10154
|
-
unit: "per call",
|
|
10155
|
-
notes: "Lists and hydrates an advertiser's creatives with image URLs and video references."
|
|
10156
|
-
},
|
|
10157
|
-
{
|
|
10158
|
-
key: "google_ads_transcribe",
|
|
10159
|
-
label: "Google ad video transcription",
|
|
10160
|
-
aliases: ["google_ads_transcribe", "google ad transcript"],
|
|
10161
|
-
credits: mcToCredits(MC_COSTS.google_ads_transcribe),
|
|
10162
|
-
unit: "per minute",
|
|
10163
|
-
notes: "A hold is taken, then reconciled to actual video duration."
|
|
10164
|
-
},
|
|
10165
|
-
{
|
|
10166
|
-
key: "fb_transcribe",
|
|
10167
|
-
label: "Facebook video / ad transcription",
|
|
10168
|
-
aliases: ["facebook_transcribe", "facebook_video_transcribe", "fb_transcribe", "fb ad transcript"],
|
|
10169
|
-
credits: mcToCredits(MC_COSTS.fb_transcribe),
|
|
10170
|
-
unit: "per minute",
|
|
10171
|
-
notes: "A hold is taken, then reconciled to actual video duration."
|
|
10172
|
-
},
|
|
10173
|
-
{
|
|
10174
|
-
key: "instagram_profile",
|
|
10175
|
-
label: "Instagram profile content discovery",
|
|
10176
|
-
aliases: ["instagram_profile_content", "instagram profile", "instagram content list", "ig profile"],
|
|
10177
|
-
credits: mcToCredits(MC_COSTS.instagram_profile),
|
|
10178
|
-
unit: "per profile scan",
|
|
10179
|
-
notes: "Browser extraction of public Instagram profile grid links. Complete history may require a logged-in profile."
|
|
10180
|
-
},
|
|
10181
|
-
{
|
|
10182
|
-
key: "instagram_media",
|
|
10183
|
-
label: "Instagram media download",
|
|
10184
|
-
aliases: ["instagram_media_download", "instagram reel download", "instagram post download", "ig media"],
|
|
10185
|
-
credits: mcToCredits(MC_COSTS.instagram_media),
|
|
10186
|
-
unit: "per post or reel",
|
|
10187
|
-
notes: "Extracts post text, image URLs, and reel audio/video tracks, with local downloads when the server can write files."
|
|
10188
|
-
},
|
|
10189
|
-
{
|
|
10190
|
-
key: "instagram_transcribe",
|
|
10191
|
-
label: "Instagram media transcription",
|
|
10192
|
-
aliases: ["instagram transcript", "instagram reel transcript", "ig transcribe"],
|
|
10193
|
-
credits: mcToCredits(MC_COSTS.instagram_transcribe),
|
|
10194
|
-
unit: "per call",
|
|
10195
|
-
notes: "Whisper transcription of selected Instagram audio/video media via fal.ai."
|
|
10196
|
-
},
|
|
10197
|
-
{
|
|
10198
|
-
key: "browser_minute",
|
|
10199
|
-
label: "Interactive browser session",
|
|
10200
|
-
aliases: ["browser_open", "browser agent", "browser_agent", "live browser", "browse", "browser control", "interactive browser"],
|
|
10201
|
-
credits: mcToCredits(MC_COSTS.browser_minute),
|
|
10202
|
-
unit: "per minute of use",
|
|
10203
|
-
notes: "Metered per minute of use for the whole time the browser session is open. Close the session to stop the meter; abandoned sessions are auto-closed after a short idle window."
|
|
10204
|
-
},
|
|
10205
|
-
{
|
|
10206
|
-
key: "reddit_thread",
|
|
10207
|
-
label: "Reddit thread + comments",
|
|
10208
|
-
aliases: ["reddit", "reddit_thread", "reddit comments", "subreddit", "reddit post"],
|
|
10209
|
-
credits: mcToCredits(MC_COSTS.reddit_thread),
|
|
10210
|
-
unit: "per thread",
|
|
10211
|
-
notes: "Captures a Reddit post and its comment tree. Charged on success; refunded if the thread cannot be retrieved."
|
|
10212
|
-
},
|
|
10213
|
-
{
|
|
10214
|
-
key: "video_analysis",
|
|
10215
|
-
label: "Video breakdown (frame-by-frame + transcript)",
|
|
10216
|
-
aliases: ["video_frame_analysis", "video breakdown", "video analysis", "analyze video"],
|
|
10217
|
-
credits: mcToCredits(MC_COSTS.video_analysis),
|
|
10218
|
-
unit: "per 120 frames (max 480)",
|
|
10219
|
-
notes: "Full multi-lens video breakdown: samples frames across a video (up to 30 minutes), analyzes each with vision AI, transcribes the audio, then produces summary, pacing/energy, words-per-minute, topic outline, key points, hook analysis, visual style, and a how-to-replicate recipe with a quality-control pass. $1 per 120 frames requested (max 480 = $4); billed down automatically if the video cannot use the requested frames, and refunded fully if the run fails."
|
|
10220
|
-
},
|
|
10221
|
-
{
|
|
10222
|
-
key: "trustpilot_reviews",
|
|
10223
|
-
label: "Trustpilot review harvest",
|
|
10224
|
-
aliases: ["trustpilot_reviews", "trustpilot", "trustpilot reviews"],
|
|
10225
|
-
credits: mcToCredits(MC_COSTS.trustpilot_reviews),
|
|
10226
|
-
unit: "per call",
|
|
10227
|
-
notes: "Base lookup. Reviews billed separately per card at trustpilot_review rate. Refunded if no reviews are found."
|
|
10228
|
-
},
|
|
10229
|
-
{
|
|
10230
|
-
key: "trustpilot_review",
|
|
10231
|
-
label: "Trustpilot review card",
|
|
10232
|
-
aliases: ["trustpilot_review", "trustpilot card"],
|
|
10233
|
-
credits: mcToCredits(MC_COSTS.trustpilot_review),
|
|
10234
|
-
unit: "per review card",
|
|
10235
|
-
notes: "Charged per review actually extracted, billed down automatically if the balance runs out mid-page."
|
|
10236
|
-
},
|
|
10237
|
-
{
|
|
10238
|
-
key: "g2_reviews",
|
|
10239
|
-
label: "G2 review harvest",
|
|
10240
|
-
aliases: ["g2_reviews", "g2", "g2 reviews"],
|
|
10241
|
-
credits: mcToCredits(MC_COSTS.g2_reviews),
|
|
10242
|
-
unit: "per call",
|
|
10243
|
-
notes: "Base lookup. Reviews billed separately per card at g2_review rate. Refunded if no reviews are found."
|
|
10244
|
-
},
|
|
10245
|
-
{
|
|
10246
|
-
key: "g2_review",
|
|
10247
|
-
label: "G2 review card",
|
|
10248
|
-
aliases: ["g2_review", "g2 card"],
|
|
10249
|
-
credits: mcToCredits(MC_COSTS.g2_review),
|
|
10250
|
-
unit: "per review card",
|
|
10251
|
-
notes: "Charged per review actually extracted (each card carries up to 3 Q&A sections), billed down automatically if the balance runs out mid-page."
|
|
10252
|
-
}
|
|
10253
|
-
];
|
|
10254
|
-
CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
|
|
10255
|
-
SUBSCRIPTION_TIERS = {
|
|
10256
|
-
"price_1TmiHRS8aAcsk3TGwmSNfNIa": { tier: "starter", label: "Starter", price_id: "price_1TmiHRS8aAcsk3TGwmSNfNIa", monthly_usd: 12, credits_mc: 8e6, concurrency: 3, intro_coupon: "mcp-starter-1dollar-intro-12" },
|
|
10257
|
-
"price_1Tmg1nS8aAcsk3TGe8zcnGTM": { tier: "growth", label: "Growth", price_id: "price_1Tmg1nS8aAcsk3TGe8zcnGTM", monthly_usd: 100, credits_mc: 8e7, concurrency: 10, intro_coupon: null },
|
|
10258
|
-
"price_1Tmg1nS8aAcsk3TGsZw34iXS": { tier: "scale", label: "Scale", price_id: "price_1Tmg1nS8aAcsk3TGsZw34iXS", monthly_usd: 250, credits_mc: 24e7, concurrency: 20, intro_coupon: null }
|
|
10259
|
-
};
|
|
10260
|
-
SUBSCRIPTION_TIER_BY_KEY = Object.fromEntries(
|
|
10261
|
-
Object.values(SUBSCRIPTION_TIERS).map((t) => [t.tier, t])
|
|
10262
|
-
);
|
|
10263
|
-
CONCURRENCY_SLOT_PRICE_USD = 5;
|
|
10264
|
-
CONCURRENCY_SLOT_PRICE_CURRENCY = "usd";
|
|
10265
|
-
CONCURRENCY_SLOT_PRICE_INTERVAL = "month";
|
|
10266
|
-
CONCURRENCY_SLOT_PRICE_LABEL = "$5/month";
|
|
10267
|
-
CONCURRENCY_SLOT_TERMINAL_COMMAND = "npx -y -p mcp-scraper@latest mcp-scraper-cli billing concurrency checkout";
|
|
10268
|
-
FREE_SIGNUP_MC = 1e5;
|
|
10269
|
-
MEMORY_FREE_QUOTA_BYTES = 1e7 * 1;
|
|
10270
|
-
MEMORY_PLANS = {
|
|
10271
|
-
"price_1TnMSTS8aAcsk3TGBgiwuvqL": { plan: "pro", label: "Pro", price_id: "price_1TnMSTS8aAcsk3TGBgiwuvqL", interval: "month", monthly_usd: 19, quota_bytes: 5e9 },
|
|
10272
|
-
"price_1TnMSTS8aAcsk3TGWBVU2agY": { plan: "pro", label: "Pro", price_id: "price_1TnMSTS8aAcsk3TGWBVU2agY", interval: "year", monthly_usd: 19, quota_bytes: 5e9 }
|
|
10273
|
-
};
|
|
10274
|
-
MEMORY_PLAN_QUOTA = { free: MEMORY_FREE_QUOTA_BYTES, pro: 5e9, team: 5e10 };
|
|
10275
|
-
MEMORY_MARGIN_MULTIPLE = 3;
|
|
10276
|
-
MEMORY_PRO_COST_BUDGET_USD = 19 / MEMORY_MARGIN_MULTIPLE;
|
|
10277
|
-
SCHEDULING_PLANS = {
|
|
10278
|
-
"price_1ToXcHS8aAcsk3TGf3cW7zHx": { price_id: "price_1ToXcHS8aAcsk3TGf3cW7zHx", label: "Scheduled Actions", interval: "month", monthly_usd: 10, quota_per_period: 1e3 }
|
|
10279
|
-
};
|
|
10280
|
-
LedgerOperation = {
|
|
10281
|
-
TOPUP: "topup",
|
|
10282
|
-
SUBSCRIPTION: "subscription",
|
|
10283
|
-
SIGNUP_GRANT: "signup_grant",
|
|
10284
|
-
MONTHLY_REFRESH: "monthly_free_refresh",
|
|
10285
|
-
PAA: "paa",
|
|
10286
|
-
PAA_REFUND: "paa_refund",
|
|
10287
|
-
SERP: "serp",
|
|
10288
|
-
REFUND: "refund",
|
|
10289
|
-
TRANSCRIPTION: "transcription",
|
|
10290
|
-
TRANSCRIPTION_HOLD: "transcription_hold",
|
|
10291
|
-
TRANSCRIPTION_REFUND: "transcription_refund",
|
|
10292
|
-
YT_CHANNEL: "yt_channel",
|
|
10293
|
-
FB_AD: "fb_ad",
|
|
10294
|
-
MAPS_SEARCH: "maps_search",
|
|
10295
|
-
MAPS_PLACE: "maps_place",
|
|
10296
|
-
MAPS_REVIEW: "maps_review",
|
|
10297
|
-
MAPS_REVIEW_REFUND: "maps_review_refund",
|
|
10298
|
-
EXTRACT_SITE: "extract_site",
|
|
10299
|
-
EXTRACT_SITE_REFUND: "extract_site_refund",
|
|
10300
|
-
EXTRACT_URL: "page_scrape",
|
|
10301
|
-
URL_MAP: "url_map",
|
|
10302
|
-
EXTRACT_SITE_HOLD: "extract_site_hold",
|
|
10303
|
-
YT_CHANNEL_REFUND: "yt_channel_refund",
|
|
10304
|
-
FB_AD_REFUND: "fb_ad_refund",
|
|
10305
|
-
URL_MAP_REFUND: "url_map_refund",
|
|
10306
|
-
FB_SEARCH: "fb_search",
|
|
10307
|
-
FB_TRANSCRIBE: "fb_transcribe",
|
|
10308
|
-
FB_SEARCH_REFUND: "fb_search_refund",
|
|
10309
|
-
FB_TRANSCRIBE_REFUND: "fb_transcribe_refund",
|
|
10310
|
-
GOOGLE_ADS_SEARCH: "google_ads_search",
|
|
10311
|
-
GOOGLE_ADS_SEARCH_REFUND: "google_ads_search_refund",
|
|
10312
|
-
GOOGLE_ADS_INTEL: "google_ads_intel",
|
|
10313
|
-
GOOGLE_ADS_INTEL_REFUND: "google_ads_intel_refund",
|
|
10314
|
-
GOOGLE_ADS_TRANSCRIBE: "google_ads_transcribe",
|
|
10315
|
-
GOOGLE_ADS_TRANSCRIBE_REFUND: "google_ads_transcribe_refund",
|
|
10316
|
-
INSTAGRAM_PROFILE: "instagram_profile",
|
|
10317
|
-
INSTAGRAM_PROFILE_REFUND: "instagram_profile_refund",
|
|
10318
|
-
INSTAGRAM_MEDIA: "instagram_media",
|
|
10319
|
-
INSTAGRAM_MEDIA_REFUND: "instagram_media_refund",
|
|
10320
|
-
INSTAGRAM_TRANSCRIBE: "instagram_transcribe",
|
|
10321
|
-
INSTAGRAM_TRANSCRIBE_REFUND: "instagram_transcribe_refund",
|
|
10322
|
-
BROWSER_SESSION: "browser_session",
|
|
10323
|
-
REDDIT_THREAD: "reddit_thread",
|
|
10324
|
-
REDDIT_THREAD_REFUND: "reddit_thread_refund",
|
|
10325
|
-
VIDEO_ANALYSIS: "video_analysis",
|
|
10326
|
-
VIDEO_ANALYSIS_REFUND: "video_analysis_refund",
|
|
10327
|
-
VIDEO_ANALYSIS_ADJUST: "video_analysis_adjust",
|
|
10328
|
-
MEMORY_AI: "memory_ai",
|
|
10329
|
-
MEMORY_AI_REFUND: "memory_ai_refund",
|
|
10330
|
-
TRUSTPILOT_REVIEWS: "trustpilot_reviews",
|
|
10331
|
-
TRUSTPILOT_REVIEWS_REFUND: "trustpilot_reviews_refund",
|
|
10332
|
-
TRUSTPILOT_REVIEW: "trustpilot_review",
|
|
10333
|
-
TRUSTPILOT_REVIEW_REFUND: "trustpilot_review_refund",
|
|
10334
|
-
G2_REVIEWS: "g2_reviews",
|
|
10335
|
-
G2_REVIEWS_REFUND: "g2_reviews_refund",
|
|
10336
|
-
G2_REVIEW: "g2_review",
|
|
10337
|
-
G2_REVIEW_REFUND: "g2_review_refund"
|
|
10338
|
-
};
|
|
10339
|
-
MEMORY_AI_MARGIN_MULTIPLE = 3;
|
|
10340
|
-
MC_PER_USD = SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].credits_mc / SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].monthly_usd;
|
|
10341
|
-
}
|
|
10342
|
-
});
|
|
10343
|
-
|
|
10344
10483
|
// src/api/site-extract-repository.ts
|
|
10345
10484
|
var site_extract_repository_exports = {};
|
|
10346
10485
|
__export(site_extract_repository_exports, {
|
|
@@ -11560,6 +11699,8 @@ var init_internal_memory_routes = __esm({
|
|
|
11560
11699
|
import_hono2 = require("hono");
|
|
11561
11700
|
init_db();
|
|
11562
11701
|
init_rates();
|
|
11702
|
+
init_cost_telemetry();
|
|
11703
|
+
init_cost_context();
|
|
11563
11704
|
internalMemoryApp = new import_hono2.Hono();
|
|
11564
11705
|
internalMemoryApp.post("/ai-debit", async (c) => {
|
|
11565
11706
|
const secret2 = c.req.header("x-internal-secret");
|
|
@@ -11572,6 +11713,11 @@ var init_internal_memory_routes = __esm({
|
|
|
11572
11713
|
}
|
|
11573
11714
|
const user = await getUserByEmail(body.identity.trim().toLowerCase());
|
|
11574
11715
|
if (!user) return c.json({ ok: false, reason: "no_user" }, 200);
|
|
11716
|
+
if (body.costUsd > 0) {
|
|
11717
|
+
await runWithCostContext({ ...currentCostContext(), op: "memory_ai", userId: user.id }, async () => {
|
|
11718
|
+
void recordVendorUsage({ vendor: "mcp_memory_ai", model: null, units: body.costUsd, unitType: "usd", error: null });
|
|
11719
|
+
});
|
|
11720
|
+
}
|
|
11575
11721
|
const mc = Math.round(body.costUsd * MEMORY_AI_MARGIN_MULTIPLE * MC_PER_USD);
|
|
11576
11722
|
if (mc <= 0) return c.json({ ok: true, mc: 0, balance_mc: null }, 200);
|
|
11577
11723
|
const { ok, balance_mc } = await debitMc(user.id, mc, LedgerOperation.MEMORY_AI, `${body.source ?? "ai"}:${body.op ?? ""}`);
|
|
@@ -12834,6 +12980,563 @@ var init_youtube_harvest = __esm({
|
|
|
12834
12980
|
}
|
|
12835
12981
|
});
|
|
12836
12982
|
|
|
12983
|
+
// src/locations.ts
|
|
12984
|
+
var LOCATIONS;
|
|
12985
|
+
var init_locations = __esm({
|
|
12986
|
+
"src/locations.ts"() {
|
|
12987
|
+
"use strict";
|
|
12988
|
+
LOCATIONS = {
|
|
12989
|
+
"austin": "Austin,Texas,United States",
|
|
12990
|
+
"new york": "New York,New York,United States",
|
|
12991
|
+
"new york city": "New York,New York,United States",
|
|
12992
|
+
"nyc": "New York,New York,United States",
|
|
12993
|
+
"los angeles": "Los Angeles,California,United States",
|
|
12994
|
+
"la": "Los Angeles,California,United States",
|
|
12995
|
+
"chicago": "Chicago,Illinois,United States",
|
|
12996
|
+
"houston": "Houston,Texas,United States",
|
|
12997
|
+
"phoenix": "Phoenix,Arizona,United States",
|
|
12998
|
+
"philadelphia": "Philadelphia,Pennsylvania,United States",
|
|
12999
|
+
"philly": "Philadelphia,Pennsylvania,United States",
|
|
13000
|
+
"san antonio": "San Antonio,Texas,United States",
|
|
13001
|
+
"dallas": "Dallas,Texas,United States",
|
|
13002
|
+
"miami": "Miami,Florida,United States",
|
|
13003
|
+
"seattle": "Seattle,Washington,United States",
|
|
13004
|
+
"denver": "Denver,Colorado,United States",
|
|
13005
|
+
"loveland": "Loveland,Colorado,United States",
|
|
13006
|
+
"loveland co": "Loveland,Colorado,United States",
|
|
13007
|
+
"fort collins": "Fort Collins,Colorado,United States",
|
|
13008
|
+
"boulder": "Boulder,Colorado,United States",
|
|
13009
|
+
"colorado springs": "Colorado Springs,Colorado,United States",
|
|
13010
|
+
"boston": "Boston,Massachusetts,United States",
|
|
13011
|
+
"atlanta": "Atlanta,Georgia,United States",
|
|
13012
|
+
"san francisco": "San Francisco,California,United States",
|
|
13013
|
+
"sf": "San Francisco,California,United States",
|
|
13014
|
+
"portland": "Portland,Oregon,United States",
|
|
13015
|
+
"las vegas": "Las Vegas,Nevada,United States",
|
|
13016
|
+
"minneapolis": "Minneapolis,Minnesota,United States",
|
|
13017
|
+
"detroit": "Detroit,Michigan,United States",
|
|
13018
|
+
"nashville": "Nashville,Tennessee,United States",
|
|
13019
|
+
"charlotte": "Charlotte,North Carolina,United States",
|
|
13020
|
+
"orlando": "Orlando,Florida,United States",
|
|
13021
|
+
"san diego": "San Diego,California,United States",
|
|
13022
|
+
"baltimore": "Baltimore,Maryland,United States",
|
|
13023
|
+
"sacramento": "Sacramento,California,United States",
|
|
13024
|
+
"columbus": "Columbus,Ohio,United States",
|
|
13025
|
+
"indianapolis": "Indianapolis,Indiana,United States",
|
|
13026
|
+
"san jose": "San Jose,California,United States",
|
|
13027
|
+
"fort worth": "Fort Worth,Texas,United States",
|
|
13028
|
+
"jacksonville": "Jacksonville,Florida,United States",
|
|
13029
|
+
"memphis": "Memphis,Tennessee,United States",
|
|
13030
|
+
"louisville": "Louisville,Kentucky,United States",
|
|
13031
|
+
"raleigh": "Raleigh,North Carolina,United States",
|
|
13032
|
+
"richmond": "Richmond,Virginia,United States",
|
|
13033
|
+
"salt lake city": "Salt Lake City,Utah,United States",
|
|
13034
|
+
"toronto": "Toronto,Ontario,Canada",
|
|
13035
|
+
"vancouver": "Vancouver,British Columbia,Canada",
|
|
13036
|
+
"montreal": "Montreal,Quebec,Canada",
|
|
13037
|
+
"calgary": "Calgary,Alberta,Canada",
|
|
13038
|
+
"ottawa": "Ottawa,Ontario,Canada",
|
|
13039
|
+
"london": "London,England,United Kingdom",
|
|
13040
|
+
"manchester": "Manchester,England,United Kingdom",
|
|
13041
|
+
"birmingham": "Birmingham,England,United Kingdom",
|
|
13042
|
+
"edinburgh": "Edinburgh,Scotland,United Kingdom",
|
|
13043
|
+
"glasgow": "Glasgow,Scotland,United Kingdom",
|
|
13044
|
+
"leeds": "Leeds,England,United Kingdom",
|
|
13045
|
+
"sydney": "Sydney,New South Wales,Australia",
|
|
13046
|
+
"melbourne": "Melbourne,Victoria,Australia",
|
|
13047
|
+
"brisbane": "Brisbane,Queensland,Australia",
|
|
13048
|
+
"perth": "Perth,Western Australia,Australia",
|
|
13049
|
+
"adelaide": "Adelaide,South Australia,Australia",
|
|
13050
|
+
"dublin": "Dublin,Leinster,Ireland"
|
|
13051
|
+
};
|
|
13052
|
+
}
|
|
13053
|
+
});
|
|
13054
|
+
|
|
13055
|
+
// src/uule.ts
|
|
13056
|
+
function encodeVarint(value) {
|
|
13057
|
+
const bytes = [];
|
|
13058
|
+
let remaining = value;
|
|
13059
|
+
do {
|
|
13060
|
+
let byte = remaining & 127;
|
|
13061
|
+
remaining >>>= 7;
|
|
13062
|
+
if (remaining > 0) byte |= 128;
|
|
13063
|
+
bytes.push(byte);
|
|
13064
|
+
} while (remaining > 0);
|
|
13065
|
+
return bytes;
|
|
13066
|
+
}
|
|
13067
|
+
function encodeUule(name) {
|
|
13068
|
+
const locationBytes = Buffer.from(name, "utf8");
|
|
13069
|
+
const payload = Buffer.concat([
|
|
13070
|
+
Buffer.from([8, 2, 16, 32, 34]),
|
|
13071
|
+
Buffer.from(encodeVarint(locationBytes.length)),
|
|
13072
|
+
locationBytes
|
|
13073
|
+
]);
|
|
13074
|
+
return `w+${payload.toString("base64")}`;
|
|
13075
|
+
}
|
|
13076
|
+
function normalizeLocation(input) {
|
|
13077
|
+
const raw = input.toLowerCase().trim();
|
|
13078
|
+
if (LOCATIONS[raw]) return LOCATIONS[raw];
|
|
13079
|
+
const beforeComma = raw.split(",")[0].trim();
|
|
13080
|
+
if (beforeComma !== raw && LOCATIONS[beforeComma]) return LOCATIONS[beforeComma];
|
|
13081
|
+
const withoutState = raw.replace(/\s+[a-z]{2}$/, "").trim();
|
|
13082
|
+
if (withoutState !== raw && LOCATIONS[withoutState]) return LOCATIONS[withoutState];
|
|
13083
|
+
return input;
|
|
13084
|
+
}
|
|
13085
|
+
var init_uule = __esm({
|
|
13086
|
+
"src/uule.ts"() {
|
|
13087
|
+
"use strict";
|
|
13088
|
+
init_locations();
|
|
13089
|
+
}
|
|
13090
|
+
});
|
|
13091
|
+
|
|
13092
|
+
// src/kernel-proxy-resolver.ts
|
|
13093
|
+
function proxyIdSuffix2(proxyId) {
|
|
13094
|
+
return proxyId ? proxyId.slice(-6) : null;
|
|
13095
|
+
}
|
|
13096
|
+
function resolution(source, proxyMode, proxyId, target, error, disposable = false) {
|
|
13097
|
+
const proxyType = source === "disabled" ? "none" : source === "configured_fallback" ? "configured" : source === "isp_created" ? "isp" : source === "location_created" || source === "location_reused" ? "residential" : "unknown";
|
|
13098
|
+
return {
|
|
13099
|
+
kernelProxyId: proxyId,
|
|
13100
|
+
...disposable && proxyId ? { disposableProxyId: proxyId } : {},
|
|
13101
|
+
resolution: {
|
|
13102
|
+
source,
|
|
13103
|
+
proxyType,
|
|
13104
|
+
proxyMode,
|
|
13105
|
+
proxyIdPresent: Boolean(proxyId),
|
|
13106
|
+
proxyIdSuffix: proxyIdSuffix2(proxyId),
|
|
13107
|
+
target,
|
|
13108
|
+
error,
|
|
13109
|
+
disposable
|
|
13110
|
+
}
|
|
13111
|
+
};
|
|
13112
|
+
}
|
|
13113
|
+
function normalizeStateName(value) {
|
|
13114
|
+
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
13115
|
+
}
|
|
13116
|
+
function normalizeCountryName(value) {
|
|
13117
|
+
return value.trim().toLowerCase().replace(/\./g, "").replace(/\s+/g, " ");
|
|
13118
|
+
}
|
|
13119
|
+
function isUnitedStates(country) {
|
|
13120
|
+
if (!country) return true;
|
|
13121
|
+
const normalized = normalizeCountryName(country);
|
|
13122
|
+
return normalized === "united states" || normalized === "united states of america" || normalized === "usa" || normalized === "us";
|
|
13123
|
+
}
|
|
13124
|
+
function stateCodeFor(region) {
|
|
13125
|
+
const trimmed = region.trim();
|
|
13126
|
+
if (/^[A-Za-z]{2}$/.test(trimmed)) return trimmed.toUpperCase();
|
|
13127
|
+
return US_STATE_CODES[normalizeStateName(trimmed)] ?? null;
|
|
13128
|
+
}
|
|
13129
|
+
function kernelCityIdentifierCandidates(city) {
|
|
13130
|
+
const ascii = city.normalize("NFKD").replace(/[^\x00-\x7F]/g, "").toLowerCase();
|
|
13131
|
+
const words = ascii.split(/[^a-z0-9]+/).filter(Boolean);
|
|
13132
|
+
const underscored = words.join("_");
|
|
13133
|
+
const compact = words.join("");
|
|
13134
|
+
return Array.from(new Set([underscored, compact].filter(Boolean)));
|
|
13135
|
+
}
|
|
13136
|
+
function proxyName(country, state, city) {
|
|
13137
|
+
return city ? `mcp-serp-residential-${country.toLowerCase()}-${state.toLowerCase()}-${city}` : `mcp-serp-residential-${country.toLowerCase()}-${state.toLowerCase()}`;
|
|
13138
|
+
}
|
|
13139
|
+
function freshProxyName(baseName, attemptIndex) {
|
|
13140
|
+
const stamp = `${Date.now()}-${attemptIndex ?? 0}-${Math.random().toString(36).slice(2, 8)}`;
|
|
13141
|
+
return `${baseName}-fresh-${stamp}`;
|
|
13142
|
+
}
|
|
13143
|
+
function zipProxyName(zip) {
|
|
13144
|
+
return `mcp-serp-residential-us-zip-${zip}`;
|
|
13145
|
+
}
|
|
13146
|
+
function parseKernelLocationProxyTarget(location2, gl) {
|
|
13147
|
+
if (!location2 || gl.toLowerCase() !== "us") return null;
|
|
13148
|
+
const canonicalLocation = normalizeLocation(location2);
|
|
13149
|
+
let parts = canonicalLocation.split(",").map((part) => part.trim()).filter(Boolean);
|
|
13150
|
+
if (parts.length > 1 && isUnitedStates(parts[parts.length - 1])) {
|
|
13151
|
+
parts = parts.slice(0, -1);
|
|
13152
|
+
}
|
|
13153
|
+
if (parts.length === 1) {
|
|
13154
|
+
const stateOnly = stateCodeFor(parts[0]);
|
|
13155
|
+
if (!stateOnly) return null;
|
|
13156
|
+
return {
|
|
13157
|
+
canonicalLocation,
|
|
13158
|
+
level: "state",
|
|
13159
|
+
country: "US",
|
|
13160
|
+
state: stateOnly,
|
|
13161
|
+
city: "",
|
|
13162
|
+
cityCandidates: [],
|
|
13163
|
+
proxyName: proxyName("US", stateOnly),
|
|
13164
|
+
config: {
|
|
13165
|
+
country: "US",
|
|
13166
|
+
state: stateOnly
|
|
13167
|
+
}
|
|
13168
|
+
};
|
|
13169
|
+
}
|
|
13170
|
+
const [city = "", region = ""] = parts;
|
|
13171
|
+
if (!city || !region) return null;
|
|
13172
|
+
const state = stateCodeFor(region);
|
|
13173
|
+
if (!state) return null;
|
|
13174
|
+
const cityCandidates = kernelCityIdentifierCandidates(city);
|
|
13175
|
+
const primaryCity = cityCandidates[0];
|
|
13176
|
+
if (!primaryCity) return null;
|
|
13177
|
+
return {
|
|
13178
|
+
canonicalLocation,
|
|
13179
|
+
level: "city",
|
|
13180
|
+
country: "US",
|
|
13181
|
+
state,
|
|
13182
|
+
city: primaryCity,
|
|
13183
|
+
cityCandidates,
|
|
13184
|
+
proxyName: proxyName("US", state, primaryCity),
|
|
13185
|
+
config: {
|
|
13186
|
+
country: "US",
|
|
13187
|
+
state,
|
|
13188
|
+
city: primaryCity
|
|
13189
|
+
}
|
|
13190
|
+
};
|
|
13191
|
+
}
|
|
13192
|
+
function cityZipKey(target) {
|
|
13193
|
+
return `${target.city}|${target.state}`;
|
|
13194
|
+
}
|
|
13195
|
+
function knownZipFor(target, explicitZip) {
|
|
13196
|
+
if (explicitZip && /^\d{5}$/.test(explicitZip)) return explicitZip;
|
|
13197
|
+
return US_CITY_CENTER_ZIPS[cityZipKey(target)] ?? null;
|
|
13198
|
+
}
|
|
13199
|
+
function zipTarget(target, zip) {
|
|
13200
|
+
return {
|
|
13201
|
+
...target,
|
|
13202
|
+
level: "zip",
|
|
13203
|
+
zip,
|
|
13204
|
+
proxyName: zipProxyName(zip),
|
|
13205
|
+
config: {
|
|
13206
|
+
country: target.country,
|
|
13207
|
+
state: target.state,
|
|
13208
|
+
zip
|
|
13209
|
+
}
|
|
13210
|
+
};
|
|
13211
|
+
}
|
|
13212
|
+
function withProxyName(target, name) {
|
|
13213
|
+
return {
|
|
13214
|
+
...target,
|
|
13215
|
+
proxyName: name
|
|
13216
|
+
};
|
|
13217
|
+
}
|
|
13218
|
+
function configMatches(config, target, city) {
|
|
13219
|
+
if (target.level === "zip") {
|
|
13220
|
+
return config?.country?.toUpperCase() === target.country && config?.zip === target.zip;
|
|
13221
|
+
}
|
|
13222
|
+
return config?.country?.toUpperCase() === target.country && config?.state?.toUpperCase() === target.state && (city ? config?.city === city : !config?.city);
|
|
13223
|
+
}
|
|
13224
|
+
function findExistingTargetProxy(proxies, target) {
|
|
13225
|
+
return proxies.find((proxy) => proxy.type === "residential" && proxy.status !== "unavailable" && Boolean(proxy.id) && (proxy.name === target.proxyName || configMatches(proxy.config, target, target.level === "city" ? target.city : void 0))) ?? null;
|
|
13226
|
+
}
|
|
13227
|
+
function findExistingProxy(proxies, target) {
|
|
13228
|
+
for (const city of target.cityCandidates) {
|
|
13229
|
+
const name = proxyName(target.country, target.state, city);
|
|
13230
|
+
const found = proxies.find((proxy) => proxy.type === "residential" && proxy.status !== "unavailable" && Boolean(proxy.id) && (proxy.name === name || configMatches(proxy.config, target, city)));
|
|
13231
|
+
if (found) return found;
|
|
13232
|
+
}
|
|
13233
|
+
return null;
|
|
13234
|
+
}
|
|
13235
|
+
function stateTarget(target) {
|
|
13236
|
+
return {
|
|
13237
|
+
...target,
|
|
13238
|
+
level: "state",
|
|
13239
|
+
proxyName: proxyName(target.country, target.state),
|
|
13240
|
+
config: {
|
|
13241
|
+
country: target.country,
|
|
13242
|
+
state: target.state
|
|
13243
|
+
}
|
|
13244
|
+
};
|
|
13245
|
+
}
|
|
13246
|
+
function findExistingStateProxy(proxies, target) {
|
|
13247
|
+
const name = proxyName(target.country, target.state);
|
|
13248
|
+
return proxies.find((proxy) => proxy.type === "residential" && proxy.status !== "unavailable" && Boolean(proxy.id) && (proxy.name === name || configMatches(proxy.config, target))) ?? null;
|
|
13249
|
+
}
|
|
13250
|
+
function escalatedTargetLevel(target, attemptIndex) {
|
|
13251
|
+
return stateTarget(target);
|
|
13252
|
+
}
|
|
13253
|
+
function errorText2(err) {
|
|
13254
|
+
return err instanceof Error ? err.message : String(err);
|
|
13255
|
+
}
|
|
13256
|
+
function freshTargetCandidates(target, explicitZip, attemptIndex) {
|
|
13257
|
+
const out = [];
|
|
13258
|
+
const zip = knownZipFor(target, explicitZip);
|
|
13259
|
+
if (zip) {
|
|
13260
|
+
const targetZip = zipTarget(target, zip);
|
|
13261
|
+
out.push(withProxyName(targetZip, freshProxyName(targetZip.proxyName, attemptIndex)));
|
|
13262
|
+
}
|
|
13263
|
+
for (const city of target.cityCandidates) {
|
|
13264
|
+
const cityTarget = {
|
|
13265
|
+
...target,
|
|
13266
|
+
level: "city",
|
|
13267
|
+
city,
|
|
13268
|
+
proxyName: proxyName(target.country, target.state, city),
|
|
13269
|
+
config: {
|
|
13270
|
+
country: target.country,
|
|
13271
|
+
state: target.state,
|
|
13272
|
+
city
|
|
13273
|
+
}
|
|
13274
|
+
};
|
|
13275
|
+
out.push(withProxyName(cityTarget, freshProxyName(cityTarget.proxyName, attemptIndex)));
|
|
13276
|
+
}
|
|
13277
|
+
const fallbackTarget = stateTarget(target);
|
|
13278
|
+
out.push(withProxyName(fallbackTarget, freshProxyName(fallbackTarget.proxyName, attemptIndex)));
|
|
13279
|
+
return out;
|
|
13280
|
+
}
|
|
13281
|
+
async function createFreshLocationProxy(kernel, options, target) {
|
|
13282
|
+
const createErrors = [];
|
|
13283
|
+
for (const candidate of freshTargetCandidates(target, options.proxyZip, options.attemptIndex)) {
|
|
13284
|
+
try {
|
|
13285
|
+
const created = await kernel.proxies.create({
|
|
13286
|
+
type: "residential",
|
|
13287
|
+
name: candidate.proxyName,
|
|
13288
|
+
config: candidate.level === "zip" ? { country: candidate.country, zip: candidate.zip } : candidate.config
|
|
13289
|
+
});
|
|
13290
|
+
if (created.id) {
|
|
13291
|
+
return resolution("location_created", options.proxyMode, created.id, candidate, null, true);
|
|
13292
|
+
}
|
|
13293
|
+
createErrors.push(`${candidate.proxyName}: Kernel did not return a proxy id`);
|
|
13294
|
+
} catch (err) {
|
|
13295
|
+
createErrors.push(`${candidate.proxyName}: ${errorText2(err)}`);
|
|
13296
|
+
}
|
|
13297
|
+
}
|
|
13298
|
+
if (process.env.KERNEL_PROXY_TYPE !== "residential") {
|
|
13299
|
+
const ispTarget = stateTarget(target);
|
|
13300
|
+
try {
|
|
13301
|
+
const created = await kernel.proxies.create({
|
|
13302
|
+
type: "isp",
|
|
13303
|
+
name: freshProxyName(`mcp-serp-isp-${ispTarget.country.toLowerCase()}-${(ispTarget.state ?? "").toLowerCase()}`, options.attemptIndex),
|
|
13304
|
+
config: { country: ispTarget.country, ...ispTarget.state ? { state: ispTarget.state } : {} }
|
|
13305
|
+
});
|
|
13306
|
+
if (created.id) {
|
|
13307
|
+
return resolution("isp_created", options.proxyMode, created.id, ispTarget, null, true);
|
|
13308
|
+
}
|
|
13309
|
+
createErrors.push("isp: Kernel did not return a proxy id");
|
|
13310
|
+
} catch (err) {
|
|
13311
|
+
createErrors.push(`isp: ${errorText2(err)}`);
|
|
13312
|
+
}
|
|
13313
|
+
}
|
|
13314
|
+
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, createErrors.join(" | "));
|
|
13315
|
+
}
|
|
13316
|
+
async function deleteKernelProxyId(kernelApiKey, proxyId) {
|
|
13317
|
+
if (!kernelApiKey || !proxyId) return;
|
|
13318
|
+
const kernel = new import_sdk6.default({ apiKey: kernelApiKey });
|
|
13319
|
+
await kernel.proxies.delete(proxyId);
|
|
13320
|
+
}
|
|
13321
|
+
async function resolveKernelProxyId(options) {
|
|
13322
|
+
if (options.proxyMode === "none") {
|
|
13323
|
+
return resolution("disabled", options.proxyMode, void 0, null, null);
|
|
13324
|
+
}
|
|
13325
|
+
if (options.proxyMode === "configured") {
|
|
13326
|
+
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, null, null);
|
|
13327
|
+
}
|
|
13328
|
+
const target = parseKernelLocationProxyTarget(options.location, options.gl);
|
|
13329
|
+
if (!target || !options.kernelApiKey) {
|
|
13330
|
+
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, target ? null : "location could not be normalized to a US city/state proxy target");
|
|
13331
|
+
}
|
|
13332
|
+
const kernel = new import_sdk6.default({ apiKey: options.kernelApiKey });
|
|
13333
|
+
try {
|
|
13334
|
+
const attemptIndex = options.attemptIndex ?? 0;
|
|
13335
|
+
if (options.fresh) {
|
|
13336
|
+
return await createFreshLocationProxy(kernel, options, target);
|
|
13337
|
+
}
|
|
13338
|
+
if (attemptIndex >= 1) {
|
|
13339
|
+
const escalatedTarget = escalatedTargetLevel(target, attemptIndex);
|
|
13340
|
+
const createErrors2 = [];
|
|
13341
|
+
try {
|
|
13342
|
+
const created = await kernel.proxies.create({
|
|
13343
|
+
type: "residential",
|
|
13344
|
+
name: escalatedTarget.proxyName,
|
|
13345
|
+
config: escalatedTarget.config
|
|
13346
|
+
});
|
|
13347
|
+
if (created.id) {
|
|
13348
|
+
return resolution("location_created", options.proxyMode, created.id, escalatedTarget, null);
|
|
13349
|
+
}
|
|
13350
|
+
createErrors2.push(`${escalatedTarget.state}: Kernel did not return a proxy id`);
|
|
13351
|
+
} catch (err) {
|
|
13352
|
+
createErrors2.push(`${escalatedTarget.state}: ${errorText2(err)}`);
|
|
13353
|
+
}
|
|
13354
|
+
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, escalatedTarget, createErrors2.join(" | "));
|
|
13355
|
+
}
|
|
13356
|
+
const proxies = await kernel.proxies.list();
|
|
13357
|
+
const zip = knownZipFor(target, options.proxyZip);
|
|
13358
|
+
const createErrors = [];
|
|
13359
|
+
if (zip) {
|
|
13360
|
+
const targetZip = zipTarget(target, zip);
|
|
13361
|
+
const existingZip = findExistingTargetProxy(proxies, targetZip);
|
|
13362
|
+
if (existingZip?.id) {
|
|
13363
|
+
return resolution("location_reused", options.proxyMode, existingZip.id, targetZip, null);
|
|
13364
|
+
}
|
|
13365
|
+
try {
|
|
13366
|
+
const created = await kernel.proxies.create({
|
|
13367
|
+
type: "residential",
|
|
13368
|
+
name: targetZip.proxyName,
|
|
13369
|
+
config: {
|
|
13370
|
+
country: targetZip.country,
|
|
13371
|
+
zip
|
|
13372
|
+
}
|
|
13373
|
+
});
|
|
13374
|
+
if (created.id) {
|
|
13375
|
+
return resolution("location_created", options.proxyMode, created.id, targetZip, null);
|
|
13376
|
+
}
|
|
13377
|
+
createErrors.push(`${zip}: Kernel did not return a proxy id`);
|
|
13378
|
+
} catch (err) {
|
|
13379
|
+
createErrors.push(`${zip}: ${errorText2(err)}`);
|
|
13380
|
+
}
|
|
13381
|
+
}
|
|
13382
|
+
const existing = findExistingProxy(proxies, target);
|
|
13383
|
+
if (existing?.id) {
|
|
13384
|
+
return resolution("location_reused", options.proxyMode, existing.id, target, createErrors.join(" | ") || null);
|
|
13385
|
+
}
|
|
13386
|
+
for (const city of target.cityCandidates) {
|
|
13387
|
+
try {
|
|
13388
|
+
const created = await kernel.proxies.create({
|
|
13389
|
+
type: "residential",
|
|
13390
|
+
name: proxyName(target.country, target.state, city),
|
|
13391
|
+
config: {
|
|
13392
|
+
country: target.country,
|
|
13393
|
+
state: target.state,
|
|
13394
|
+
city
|
|
13395
|
+
}
|
|
13396
|
+
});
|
|
13397
|
+
if (created.id) {
|
|
13398
|
+
return resolution("location_created", options.proxyMode, created.id, {
|
|
13399
|
+
...target,
|
|
13400
|
+
level: "city",
|
|
13401
|
+
city,
|
|
13402
|
+
proxyName: proxyName(target.country, target.state, city),
|
|
13403
|
+
config: {
|
|
13404
|
+
country: target.country,
|
|
13405
|
+
state: target.state,
|
|
13406
|
+
city
|
|
13407
|
+
}
|
|
13408
|
+
}, null);
|
|
13409
|
+
}
|
|
13410
|
+
createErrors.push(`${city}: Kernel did not return a proxy id`);
|
|
13411
|
+
} catch (err) {
|
|
13412
|
+
createErrors.push(`${city}: ${errorText2(err)}`);
|
|
13413
|
+
}
|
|
13414
|
+
}
|
|
13415
|
+
const fallbackTarget = stateTarget(target);
|
|
13416
|
+
const existingState = findExistingStateProxy(proxies, fallbackTarget);
|
|
13417
|
+
if (existingState?.id) {
|
|
13418
|
+
return resolution("location_reused", options.proxyMode, existingState.id, fallbackTarget, createErrors.join(" | "));
|
|
13419
|
+
}
|
|
13420
|
+
try {
|
|
13421
|
+
const created = await kernel.proxies.create({
|
|
13422
|
+
type: "residential",
|
|
13423
|
+
name: fallbackTarget.proxyName,
|
|
13424
|
+
config: fallbackTarget.config
|
|
13425
|
+
});
|
|
13426
|
+
if (created.id) {
|
|
13427
|
+
return resolution("location_created", options.proxyMode, created.id, fallbackTarget, createErrors.join(" | "));
|
|
13428
|
+
}
|
|
13429
|
+
createErrors.push(`${fallbackTarget.state}: Kernel did not return a proxy id`);
|
|
13430
|
+
} catch (err) {
|
|
13431
|
+
createErrors.push(`${fallbackTarget.state}: ${errorText2(err)}`);
|
|
13432
|
+
}
|
|
13433
|
+
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, createErrors.join(" | "));
|
|
13434
|
+
} catch (err) {
|
|
13435
|
+
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, errorText2(err));
|
|
13436
|
+
}
|
|
13437
|
+
}
|
|
13438
|
+
var import_sdk6, US_STATE_CODES, US_CITY_CENTER_ZIPS;
|
|
13439
|
+
var init_kernel_proxy_resolver = __esm({
|
|
13440
|
+
"src/kernel-proxy-resolver.ts"() {
|
|
13441
|
+
"use strict";
|
|
13442
|
+
import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
|
|
13443
|
+
init_uule();
|
|
13444
|
+
US_STATE_CODES = {
|
|
13445
|
+
alabama: "AL",
|
|
13446
|
+
alaska: "AK",
|
|
13447
|
+
arizona: "AZ",
|
|
13448
|
+
arkansas: "AR",
|
|
13449
|
+
california: "CA",
|
|
13450
|
+
colorado: "CO",
|
|
13451
|
+
connecticut: "CT",
|
|
13452
|
+
delaware: "DE",
|
|
13453
|
+
florida: "FL",
|
|
13454
|
+
georgia: "GA",
|
|
13455
|
+
hawaii: "HI",
|
|
13456
|
+
idaho: "ID",
|
|
13457
|
+
illinois: "IL",
|
|
13458
|
+
indiana: "IN",
|
|
13459
|
+
iowa: "IA",
|
|
13460
|
+
kansas: "KS",
|
|
13461
|
+
kentucky: "KY",
|
|
13462
|
+
louisiana: "LA",
|
|
13463
|
+
maine: "ME",
|
|
13464
|
+
maryland: "MD",
|
|
13465
|
+
massachusetts: "MA",
|
|
13466
|
+
michigan: "MI",
|
|
13467
|
+
minnesota: "MN",
|
|
13468
|
+
mississippi: "MS",
|
|
13469
|
+
missouri: "MO",
|
|
13470
|
+
montana: "MT",
|
|
13471
|
+
nebraska: "NE",
|
|
13472
|
+
nevada: "NV",
|
|
13473
|
+
"new hampshire": "NH",
|
|
13474
|
+
"new jersey": "NJ",
|
|
13475
|
+
"new mexico": "NM",
|
|
13476
|
+
"new york": "NY",
|
|
13477
|
+
"north carolina": "NC",
|
|
13478
|
+
"north dakota": "ND",
|
|
13479
|
+
ohio: "OH",
|
|
13480
|
+
oklahoma: "OK",
|
|
13481
|
+
oregon: "OR",
|
|
13482
|
+
pennsylvania: "PA",
|
|
13483
|
+
"rhode island": "RI",
|
|
13484
|
+
"south carolina": "SC",
|
|
13485
|
+
"south dakota": "SD",
|
|
13486
|
+
tennessee: "TN",
|
|
13487
|
+
texas: "TX",
|
|
13488
|
+
utah: "UT",
|
|
13489
|
+
vermont: "VT",
|
|
13490
|
+
virginia: "VA",
|
|
13491
|
+
washington: "WA",
|
|
13492
|
+
"west virginia": "WV",
|
|
13493
|
+
wisconsin: "WI",
|
|
13494
|
+
wyoming: "WY"
|
|
13495
|
+
};
|
|
13496
|
+
US_CITY_CENTER_ZIPS = {
|
|
13497
|
+
"atlanta|GA": "30303",
|
|
13498
|
+
"austin|TX": "78701",
|
|
13499
|
+
"baltimore|MD": "21201",
|
|
13500
|
+
"boston|MA": "02108",
|
|
13501
|
+
"boulder|CO": "80302",
|
|
13502
|
+
"charlotte|NC": "28202",
|
|
13503
|
+
"chicago|IL": "60601",
|
|
13504
|
+
"colorado_springs|CO": "80903",
|
|
13505
|
+
"columbus|OH": "43215",
|
|
13506
|
+
"dallas|TX": "75201",
|
|
13507
|
+
"denver|CO": "80202",
|
|
13508
|
+
"detroit|MI": "48226",
|
|
13509
|
+
"fort_collins|CO": "80524",
|
|
13510
|
+
"fort_worth|TX": "76102",
|
|
13511
|
+
"houston|TX": "77002",
|
|
13512
|
+
"indianapolis|IN": "46204",
|
|
13513
|
+
"jacksonville|FL": "32202",
|
|
13514
|
+
"las_vegas|NV": "89101",
|
|
13515
|
+
"los_angeles|CA": "90012",
|
|
13516
|
+
"louisville|KY": "40202",
|
|
13517
|
+
"loveland|CO": "80537",
|
|
13518
|
+
"memphis|TN": "38103",
|
|
13519
|
+
"miami|FL": "33131",
|
|
13520
|
+
"minneapolis|MN": "55401",
|
|
13521
|
+
"nashville|TN": "37203",
|
|
13522
|
+
"new_york|NY": "10001",
|
|
13523
|
+
"orlando|FL": "32801",
|
|
13524
|
+
"philadelphia|PA": "19103",
|
|
13525
|
+
"phoenix|AZ": "85004",
|
|
13526
|
+
"portland|OR": "97205",
|
|
13527
|
+
"raleigh|NC": "27601",
|
|
13528
|
+
"richmond|VA": "23219",
|
|
13529
|
+
"sacramento|CA": "95814",
|
|
13530
|
+
"salt_lake_city|UT": "84101",
|
|
13531
|
+
"san_antonio|TX": "78205",
|
|
13532
|
+
"san_diego|CA": "92101",
|
|
13533
|
+
"san_francisco|CA": "94103",
|
|
13534
|
+
"san_jose|CA": "95113",
|
|
13535
|
+
"seattle|WA": "98101"
|
|
13536
|
+
};
|
|
13537
|
+
}
|
|
13538
|
+
});
|
|
13539
|
+
|
|
12837
13540
|
// src/youtube/CaptionFetcher.ts
|
|
12838
13541
|
async function fetchViaYoutubeTranscript(videoId) {
|
|
12839
13542
|
try {
|
|
@@ -12846,6 +13549,11 @@ async function fetchViaYoutubeTranscript(videoId) {
|
|
|
12846
13549
|
}));
|
|
12847
13550
|
const text = chunks.map((c) => c.text).join(" ");
|
|
12848
13551
|
const last = chunks[chunks.length - 1];
|
|
13552
|
+
const now = Date.now();
|
|
13553
|
+
await runWithCostContext(
|
|
13554
|
+
{ ...currentCostContext(), op: "yt_transcription", subOp: "youtube-transcript" },
|
|
13555
|
+
() => recordKernelSession({ kernelSessionId: null, source: "yt_caption_tier", headlessSent: null, openedAtMs: now, closedAtMs: now })
|
|
13556
|
+
);
|
|
12849
13557
|
return { videoId, text, chunks, durationMs: last ? last.timestamp[1] * 1e3 : 0, method: "youtube-transcript" };
|
|
12850
13558
|
} catch {
|
|
12851
13559
|
return null;
|
|
@@ -12897,162 +13605,214 @@ function parseTimedtextXml(xml) {
|
|
|
12897
13605
|
}
|
|
12898
13606
|
return results;
|
|
12899
13607
|
}
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
13608
|
+
function isBotBlockStatus(status) {
|
|
13609
|
+
return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
|
|
13610
|
+
}
|
|
13611
|
+
async function probePlayer(page, videoId) {
|
|
13612
|
+
return page.evaluate(
|
|
13613
|
+
async ({ vid, clients }) => {
|
|
13614
|
+
let lastStatus = null;
|
|
13615
|
+
for (const client2 of clients) {
|
|
13616
|
+
const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
|
|
13617
|
+
method: "POST",
|
|
13618
|
+
headers: { "Content-Type": "application/json", "User-Agent": client2.userAgent },
|
|
13619
|
+
body: JSON.stringify({
|
|
13620
|
+
context: { client: { clientName: client2.name, clientVersion: client2.version } },
|
|
13621
|
+
videoId: vid
|
|
13622
|
+
})
|
|
13623
|
+
}).catch(() => null);
|
|
13624
|
+
if (!resp?.ok) {
|
|
13625
|
+
lastStatus = resp ? `player_http_${resp.status}` : "player_fetch_failed";
|
|
13626
|
+
continue;
|
|
13627
|
+
}
|
|
13628
|
+
const data = await resp.json();
|
|
13629
|
+
lastStatus = data?.playabilityStatus?.status ?? null;
|
|
13630
|
+
const audioFormats = (data?.streamingData?.adaptiveFormats ?? []).filter((f) => typeof f.url === "string" && f.url && typeof f.mimeType === "string" && f.mimeType.startsWith("audio/")).sort((a, b) => (Number(a.contentLength) || Infinity) - (Number(b.contentLength) || Infinity));
|
|
13631
|
+
const pick = audioFormats[0] ?? null;
|
|
13632
|
+
let captions = null;
|
|
13633
|
+
const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
|
|
13634
|
+
if (tracks.length > 0) {
|
|
13635
|
+
const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
|
|
13636
|
+
const sep = track.baseUrl.includes("?") ? "&" : "?";
|
|
13637
|
+
const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
|
|
13638
|
+
if (xmlResp?.ok) captions = await xmlResp.text();
|
|
13639
|
+
}
|
|
13640
|
+
if (!captions && !pick) continue;
|
|
13641
|
+
return {
|
|
13642
|
+
captions,
|
|
13643
|
+
status: lastStatus,
|
|
13644
|
+
audio: pick ? {
|
|
13645
|
+
url: pick.url,
|
|
13646
|
+
mimeType: pick.mimeType,
|
|
13647
|
+
bitrate: pick.bitrate ?? null,
|
|
13648
|
+
contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
|
|
13649
|
+
approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
|
|
13650
|
+
} : null
|
|
13651
|
+
};
|
|
13652
|
+
}
|
|
13653
|
+
return { captions: null, audio: null, status: lastStatus };
|
|
13654
|
+
},
|
|
13655
|
+
{ vid: videoId, clients: INNERTUBE_CLIENTS }
|
|
13656
|
+
).catch(() => null);
|
|
13657
|
+
}
|
|
13658
|
+
async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
|
|
13659
|
+
if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
|
|
13660
|
+
console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
|
|
13661
|
+
return null;
|
|
13662
|
+
}
|
|
13663
|
+
const sep = audio.url.includes("?") ? "&" : "?";
|
|
13664
|
+
const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
|
|
13665
|
+
const result = await page.evaluate(async (u) => {
|
|
13666
|
+
try {
|
|
13667
|
+
const resp = await fetch(u);
|
|
13668
|
+
if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
|
|
13669
|
+
const blob = await resp.blob();
|
|
13670
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
13671
|
+
const reader = new FileReader();
|
|
13672
|
+
reader.onload = () => resolve(reader.result);
|
|
13673
|
+
reader.onerror = () => reject(reader.error);
|
|
13674
|
+
reader.readAsDataURL(blob);
|
|
12926
13675
|
});
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
if (entries.length === 0) return null;
|
|
12940
|
-
const chunks = entries.map((e) => ({
|
|
12941
|
-
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
12942
|
-
text: e.text
|
|
12943
|
-
}));
|
|
12944
|
-
const text = chunks.map((c) => c.text).join(" ");
|
|
12945
|
-
const last = chunks[chunks.length - 1];
|
|
12946
|
-
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
12947
|
-
} catch {
|
|
13676
|
+
return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
|
|
13677
|
+
} catch (e) {
|
|
13678
|
+
return { ok: false, error: String(e) };
|
|
13679
|
+
}
|
|
13680
|
+
}, url).catch((e) => ({ ok: false, error: String(e) }));
|
|
13681
|
+
if (!result.ok || !result.base64) {
|
|
13682
|
+
console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
|
|
13683
|
+
return null;
|
|
13684
|
+
}
|
|
13685
|
+
const bytes = Buffer.from(result.base64, "base64");
|
|
13686
|
+
if (audio.contentLength && bytes.length < audio.contentLength) {
|
|
13687
|
+
console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
|
|
12948
13688
|
return null;
|
|
12949
|
-
} finally {
|
|
12950
|
-
await driver.close();
|
|
12951
13689
|
}
|
|
13690
|
+
return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
|
|
12952
13691
|
}
|
|
12953
|
-
async function
|
|
12954
|
-
const
|
|
12955
|
-
|
|
12956
|
-
const video = document.querySelector("video");
|
|
12957
|
-
if (!video) {
|
|
12958
|
-
resolve({ ok: false, error: "no video element" });
|
|
12959
|
-
return;
|
|
12960
|
-
}
|
|
12961
|
-
video.muted = false;
|
|
12962
|
-
video.volume = 1;
|
|
12963
|
-
try {
|
|
12964
|
-
const ctx = new AudioContext();
|
|
12965
|
-
const src = ctx.createMediaElementSource(video);
|
|
12966
|
-
const dest = ctx.createMediaStreamDestination();
|
|
12967
|
-
src.connect(dest);
|
|
12968
|
-
src.connect(ctx.destination);
|
|
12969
|
-
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg";
|
|
12970
|
-
const recorder = new MediaRecorder(dest.stream, { mimeType, audioBitsPerSecond: 48e3 });
|
|
12971
|
-
const chunks = [];
|
|
12972
|
-
recorder.ondataavailable = (e) => {
|
|
12973
|
-
if (e.data.size > 0) chunks.push(e.data);
|
|
12974
|
-
};
|
|
12975
|
-
recorder.onstop = async () => {
|
|
12976
|
-
const blob = new Blob(chunks, { type: mimeType });
|
|
12977
|
-
const ab = await blob.arrayBuffer();
|
|
12978
|
-
const arr = new Uint8Array(ab);
|
|
12979
|
-
let binary = "";
|
|
12980
|
-
const chunkSize = 8192;
|
|
12981
|
-
for (let i = 0; i < arr.length; i += chunkSize) {
|
|
12982
|
-
binary += String.fromCharCode(...arr.slice(i, Math.min(i + chunkSize, arr.length)));
|
|
12983
|
-
}
|
|
12984
|
-
resolve({ ok: true, base64: btoa(binary), size: ab.byteLength, mimeType });
|
|
12985
|
-
};
|
|
12986
|
-
recorder.start(1e3);
|
|
12987
|
-
video.playbackRate = 2;
|
|
12988
|
-
video.play().catch(() => void 0);
|
|
12989
|
-
setTimeout(() => {
|
|
12990
|
-
recorder.stop();
|
|
12991
|
-
ctx.close();
|
|
12992
|
-
}, recordSecs * 1e3);
|
|
12993
|
-
} catch (e) {
|
|
12994
|
-
resolve({ ok: false, error: String(e) });
|
|
12995
|
-
}
|
|
12996
|
-
});
|
|
12997
|
-
}, WHISPER_RECORD_SECONDS);
|
|
12998
|
-
if (!result.ok || !result.base64 || !result.mimeType) return null;
|
|
12999
|
-
return { bytes: Buffer.from(result.base64, "base64"), mimeType: result.mimeType };
|
|
13000
|
-
}
|
|
13001
|
-
async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start) {
|
|
13002
|
-
const driver = new BrowserDriver();
|
|
13692
|
+
async function transcribeWithWizper(videoId, audio, startMs) {
|
|
13693
|
+
const falKey = process.env.FAL_KEY;
|
|
13694
|
+
if (!falKey) return null;
|
|
13003
13695
|
try {
|
|
13004
|
-
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
13005
|
-
const page = driver.getPage();
|
|
13006
|
-
await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
|
|
13007
|
-
await page.waitForFunction(
|
|
13008
|
-
() => !!window.ytcfg,
|
|
13009
|
-
{ timeout: 2e4 }
|
|
13010
|
-
);
|
|
13011
|
-
const playStatus = await page.evaluate(() => {
|
|
13012
|
-
const w = window;
|
|
13013
|
-
return w.ytInitialPlayerResponse?.playabilityStatus?.status;
|
|
13014
|
-
});
|
|
13015
|
-
if (playStatus !== "OK") {
|
|
13016
|
-
await driver.close();
|
|
13017
|
-
return null;
|
|
13018
|
-
}
|
|
13019
|
-
const audioInfo = await captureAudioViaMediaRecorder(page);
|
|
13020
|
-
await driver.close();
|
|
13021
|
-
if (!audioInfo || audioInfo.bytes.length < 1e4) return null;
|
|
13022
13696
|
import_client3.fal.config({ credentials: falKey });
|
|
13023
|
-
const ext =
|
|
13024
|
-
const audioFile = new File([new Uint8Array(
|
|
13697
|
+
const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
|
|
13698
|
+
const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
|
|
13025
13699
|
const uploadedUrl = await import_client3.fal.storage.upload(audioFile);
|
|
13700
|
+
const subscribeStartMs = Date.now();
|
|
13026
13701
|
const result = await import_client3.fal.subscribe("fal-ai/wizper", {
|
|
13027
|
-
input: { audio_url: uploadedUrl, task: "transcribe", language: "en" },
|
|
13702
|
+
input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
|
|
13028
13703
|
logs: false,
|
|
13029
13704
|
pollInterval: 3e3
|
|
13030
13705
|
});
|
|
13706
|
+
const subscribeDurationMs = Date.now() - subscribeStartMs;
|
|
13031
13707
|
const data = result.data;
|
|
13032
13708
|
const chunks = (data.chunks ?? []).map((c) => ({
|
|
13033
13709
|
timestamp: c.timestamp,
|
|
13034
13710
|
text: c.text.trim()
|
|
13035
13711
|
}));
|
|
13036
|
-
|
|
13037
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
|
|
13712
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
|
|
13038
13713
|
const text = data.text ?? chunks.map((c) => c.text).join(" ");
|
|
13039
13714
|
if (!text) return null;
|
|
13040
|
-
return { videoId, text, chunks, durationMs: Date.now() -
|
|
13041
|
-
} catch {
|
|
13042
|
-
|
|
13715
|
+
return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
|
|
13716
|
+
} catch (err) {
|
|
13717
|
+
console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13043
13718
|
return null;
|
|
13044
13719
|
}
|
|
13045
13720
|
}
|
|
13046
|
-
async function
|
|
13721
|
+
async function resolveResidentialProxy(kernelApiKey) {
|
|
13722
|
+
try {
|
|
13723
|
+
const resolved = await resolveKernelProxyId({
|
|
13724
|
+
kernelApiKey,
|
|
13725
|
+
proxyMode: "location",
|
|
13726
|
+
location: YT_RESIDENTIAL_LOCATION,
|
|
13727
|
+
gl: "us",
|
|
13728
|
+
fresh: true
|
|
13729
|
+
});
|
|
13730
|
+
return { kernelProxyId: resolved.kernelProxyId, disposableProxyId: resolved.disposableProxyId };
|
|
13731
|
+
} catch (err) {
|
|
13732
|
+
console.warn(`[CaptionFetcher] residential proxy resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
13733
|
+
return {};
|
|
13734
|
+
}
|
|
13735
|
+
}
|
|
13736
|
+
async function probeAndExtract(videoId, opts) {
|
|
13047
13737
|
const kernelApiKey = browserServiceApiKey();
|
|
13048
|
-
|
|
13049
|
-
const
|
|
13050
|
-
|
|
13738
|
+
if (!kernelApiKey) return null;
|
|
13739
|
+
const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
|
|
13740
|
+
const driver = new BrowserDriver();
|
|
13741
|
+
return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
|
|
13742
|
+
let entries = [];
|
|
13743
|
+
let audio = null;
|
|
13744
|
+
let retryWithFreshIp = false;
|
|
13745
|
+
try {
|
|
13746
|
+
await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
|
|
13747
|
+
const page = driver.getPage();
|
|
13748
|
+
await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
|
|
13749
|
+
await page.waitForFunction(
|
|
13750
|
+
() => !!window.ytcfg,
|
|
13751
|
+
{ timeout: 2e4 }
|
|
13752
|
+
);
|
|
13753
|
+
const probe = await probePlayer(page, videoId);
|
|
13754
|
+
if (!probe) return null;
|
|
13755
|
+
if (probe.captions && !opts?.skipCaptions) {
|
|
13756
|
+
entries = parseTimedtextJson3(probe.captions);
|
|
13757
|
+
if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
|
|
13758
|
+
}
|
|
13759
|
+
if (entries.length === 0) {
|
|
13760
|
+
if (!probe.audio) {
|
|
13761
|
+
retryWithFreshIp = isBotBlockStatus(probe.status);
|
|
13762
|
+
console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
|
|
13763
|
+
return { entries, audio: null, retryWithFreshIp };
|
|
13764
|
+
}
|
|
13765
|
+
audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
|
|
13766
|
+
if (!audio) return { entries, audio: null, retryWithFreshIp: true };
|
|
13767
|
+
}
|
|
13768
|
+
} catch (err) {
|
|
13769
|
+
console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13770
|
+
return null;
|
|
13771
|
+
} finally {
|
|
13772
|
+
const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
|
|
13773
|
+
await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
|
|
13774
|
+
}
|
|
13775
|
+
return { entries, audio, retryWithFreshIp };
|
|
13776
|
+
});
|
|
13777
|
+
}
|
|
13778
|
+
async function extractWithProxyEscalation(videoId, opts) {
|
|
13779
|
+
const direct = await probeAndExtract(videoId, opts);
|
|
13780
|
+
if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
|
|
13781
|
+
const shouldEscalate = direct === null || direct.retryWithFreshIp;
|
|
13782
|
+
if (!shouldEscalate) return direct;
|
|
13783
|
+
const kernelApiKey = browserServiceApiKey();
|
|
13784
|
+
if (!kernelApiKey) return direct;
|
|
13785
|
+
const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
|
|
13786
|
+
if (!kernelProxyId) return direct;
|
|
13787
|
+
try {
|
|
13788
|
+
console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
|
|
13789
|
+
const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
|
|
13790
|
+
return viaProxy ?? direct;
|
|
13791
|
+
} finally {
|
|
13792
|
+
if (disposableProxyId) {
|
|
13793
|
+
await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
|
|
13794
|
+
(err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
|
|
13795
|
+
);
|
|
13796
|
+
}
|
|
13797
|
+
}
|
|
13798
|
+
}
|
|
13799
|
+
async function fetchViaKernelSession(videoId) {
|
|
13051
13800
|
const start = Date.now();
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13801
|
+
const outcome = await extractWithProxyEscalation(videoId);
|
|
13802
|
+
if (!outcome) return null;
|
|
13803
|
+
if (outcome.entries.length > 0) {
|
|
13804
|
+
const chunks = outcome.entries.map((e) => ({
|
|
13805
|
+
timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
|
|
13806
|
+
text: e.text
|
|
13807
|
+
}));
|
|
13808
|
+
const text = chunks.map((c) => c.text).join(" ");
|
|
13809
|
+
return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
|
|
13810
|
+
}
|
|
13811
|
+
if (outcome.audio) {
|
|
13812
|
+
return runWithCostContext(
|
|
13813
|
+
{ ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
|
|
13814
|
+
() => transcribeWithWizper(videoId, outcome.audio, start)
|
|
13815
|
+
);
|
|
13056
13816
|
}
|
|
13057
13817
|
return null;
|
|
13058
13818
|
}
|
|
@@ -13074,21 +13834,29 @@ function finalizeCaptions(result) {
|
|
|
13074
13834
|
async function fetchCaptions(videoId) {
|
|
13075
13835
|
const primary = await fetchViaYoutubeTranscript(videoId);
|
|
13076
13836
|
if (primary) return finalizeCaptions(primary);
|
|
13077
|
-
const
|
|
13078
|
-
if (
|
|
13079
|
-
|
|
13080
|
-
|
|
13081
|
-
|
|
13837
|
+
const result = await fetchViaKernelSession(videoId);
|
|
13838
|
+
if (result) return finalizeCaptions(result);
|
|
13839
|
+
throw new Error(
|
|
13840
|
+
`Could not transcribe ${videoId}. It has no caption track, and its audio stream could not be downloaded \u2014 YouTube permits a single media request per IP, so streams that do not fit in one response are unreachable. Captioned videos of any length still work.`
|
|
13841
|
+
);
|
|
13082
13842
|
}
|
|
13083
|
-
var import_client3,
|
|
13843
|
+
var import_client3, INNERTUBE_CLIENTS, MAX_AUDIO_BYTES, YT_RESIDENTIAL_LOCATION;
|
|
13084
13844
|
var init_CaptionFetcher = __esm({
|
|
13085
13845
|
"src/youtube/CaptionFetcher.ts"() {
|
|
13086
13846
|
"use strict";
|
|
13087
13847
|
init_BrowserDriver();
|
|
13088
13848
|
init_browser_service_env();
|
|
13849
|
+
init_kernel_proxy_resolver();
|
|
13089
13850
|
import_client3 = require("@fal-ai/client");
|
|
13090
13851
|
init_cost_telemetry();
|
|
13091
|
-
|
|
13852
|
+
init_cost_context();
|
|
13853
|
+
INNERTUBE_CLIENTS = [
|
|
13854
|
+
{ name: "ANDROID", version: "20.10.38", userAgent: "com.google.android.youtube/20.10.38 (Linux; U; Android 14)" },
|
|
13855
|
+
{ name: "IOS", version: "20.10.4", userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)" },
|
|
13856
|
+
{ name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
|
|
13857
|
+
];
|
|
13858
|
+
MAX_AUDIO_BYTES = 96 * 1024 * 1024;
|
|
13859
|
+
YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
|
|
13092
13860
|
}
|
|
13093
13861
|
});
|
|
13094
13862
|
|
|
@@ -13113,7 +13881,7 @@ var init_api_auth = __esm({
|
|
|
13113
13881
|
});
|
|
13114
13882
|
|
|
13115
13883
|
// src/api/server-schemas.ts
|
|
13116
|
-
var import_zod13, HarvestBodySchema, ExtractUrlBodySchema, MapUrlsBodySchema, ExtractSiteBodySchema, YoutubeHarvestBodySchema, YoutubeTranscribeBodySchema;
|
|
13884
|
+
var import_zod13, HarvestBodySchema, ExtractUrlBodySchema, DiffPageBodySchema, MapUrlsBodySchema, ExtractSiteBodySchema, YoutubeHarvestBodySchema, YoutubeTranscribeBodySchema;
|
|
13117
13885
|
var init_server_schemas = __esm({
|
|
13118
13886
|
"src/api/server-schemas.ts"() {
|
|
13119
13887
|
"use strict";
|
|
@@ -13144,6 +13912,11 @@ var init_server_schemas = __esm({
|
|
|
13144
13912
|
depositToVault: import_zod13.z.boolean().optional(),
|
|
13145
13913
|
vaultName: import_zod13.z.string().trim().min(1).max(120).optional()
|
|
13146
13914
|
});
|
|
13915
|
+
DiffPageBodySchema = import_zod13.z.object({
|
|
13916
|
+
url: import_zod13.z.string().min(1, "url is required"),
|
|
13917
|
+
allowLocal: import_zod13.z.boolean().optional(),
|
|
13918
|
+
resetBaseline: import_zod13.z.boolean().optional()
|
|
13919
|
+
});
|
|
13147
13920
|
MapUrlsBodySchema = import_zod13.z.object({
|
|
13148
13921
|
url: import_zod13.z.string().min(1, "url is required"),
|
|
13149
13922
|
maxUrls: import_zod13.z.number().int().min(1).max(2e3).optional(),
|
|
@@ -13419,6 +14192,7 @@ var init_youtube_routes = __esm({
|
|
|
13419
14192
|
init_api_auth();
|
|
13420
14193
|
init_server_schemas();
|
|
13421
14194
|
init_concurrency_gates();
|
|
14195
|
+
init_cost_context();
|
|
13422
14196
|
youtubeApp = new import_hono3.Hono();
|
|
13423
14197
|
youtubeApp.post("/harvest", createApiKeyAuth(), async (c) => {
|
|
13424
14198
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -13496,7 +14270,7 @@ var init_youtube_routes = __esm({
|
|
|
13496
14270
|
const { ok: trOk, balance_mc: trBal } = await debitMc(user.id, holdMc, LedgerOperation.TRANSCRIPTION_HOLD, id);
|
|
13497
14271
|
if (!trOk) return c.json(insufficientBalanceResponse(trBal, holdMc), 402);
|
|
13498
14272
|
debited = true;
|
|
13499
|
-
const result = await fetchCaptions(id);
|
|
14273
|
+
const result = await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", userId: user.id }, () => fetchCaptions(id));
|
|
13500
14274
|
const lastChunk = result.chunks?.[result.chunks.length - 1];
|
|
13501
14275
|
const chunkSecs = lastChunk && Number.isFinite(lastChunk.timestamp[1]) ? lastChunk.timestamp[1] : 0;
|
|
13502
14276
|
const durationSecs = Number.isFinite(result.durationMs) ? result.durationMs / 1e3 : 0;
|
|
@@ -14317,755 +15091,198 @@ function extractCollatedResults(payload) {
|
|
|
14317
15091
|
page_name: r.page_name ?? snapshot?.page_name ?? "",
|
|
14318
15092
|
is_active: Boolean(r.is_active),
|
|
14319
15093
|
collation_count: typeof r.collation_count === "number" ? r.collation_count : null,
|
|
14320
|
-
snapshot
|
|
14321
|
-
});
|
|
14322
|
-
}
|
|
14323
|
-
}
|
|
14324
|
-
return results;
|
|
14325
|
-
}
|
|
14326
|
-
async function collectAdLibraryResults(page, url, maxResults, opts = {}) {
|
|
14327
|
-
const captureMs = opts.captureMs ?? 3e4;
|
|
14328
|
-
const collected = [];
|
|
14329
|
-
const seen = /* @__PURE__ */ new Set();
|
|
14330
|
-
const handler = (resp) => {
|
|
14331
|
-
if (!resp.url().includes("/api/graphql")) return;
|
|
14332
|
-
const friendlyName = (resp.request().postData() ?? "").match(/fb_api_req_friendly_name=([^&]+)/)?.[1];
|
|
14333
|
-
if (friendlyName !== AD_LIBRARY_QUERY) return;
|
|
14334
|
-
void resp.text().then((text) => {
|
|
14335
|
-
for (const payload of parseFbGraphqlJson(text)) {
|
|
14336
|
-
for (const result of extractCollatedResults(payload)) {
|
|
14337
|
-
if (seen.has(result.ad_archive_id)) continue;
|
|
14338
|
-
seen.add(result.ad_archive_id);
|
|
14339
|
-
collected.push(result);
|
|
14340
|
-
}
|
|
14341
|
-
}
|
|
14342
|
-
}).catch(() => void 0);
|
|
14343
|
-
};
|
|
14344
|
-
page.on("response", handler);
|
|
14345
|
-
try {
|
|
14346
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
14347
|
-
const deadline = Date.now() + captureMs;
|
|
14348
|
-
let lastCount = -1;
|
|
14349
|
-
let stableRounds = 0;
|
|
14350
|
-
while (Date.now() < deadline && collected.length < maxResults) {
|
|
14351
|
-
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)).catch(() => void 0);
|
|
14352
|
-
await page.waitForTimeout(2e3);
|
|
14353
|
-
if (collected.length === lastCount) {
|
|
14354
|
-
stableRounds++;
|
|
14355
|
-
if (stableRounds >= 2 && collected.length > 0) break;
|
|
14356
|
-
} else {
|
|
14357
|
-
stableRounds = 0;
|
|
14358
|
-
}
|
|
14359
|
-
lastCount = collected.length;
|
|
14360
|
-
}
|
|
14361
|
-
} finally {
|
|
14362
|
-
page.off("response", handler);
|
|
14363
|
-
}
|
|
14364
|
-
return collected.slice(0, maxResults);
|
|
14365
|
-
}
|
|
14366
|
-
function advertisersFromResults(results, maxResults) {
|
|
14367
|
-
const byPage = /* @__PURE__ */ new Map();
|
|
14368
|
-
for (const r of results) {
|
|
14369
|
-
if (!r.page_id || !r.page_name) continue;
|
|
14370
|
-
const collation = typeof r.collation_count === "number" && r.collation_count > 0 ? r.collation_count : 0;
|
|
14371
|
-
const existing = byPage.get(r.page_id);
|
|
14372
|
-
if (existing) {
|
|
14373
|
-
existing.resultCount++;
|
|
14374
|
-
existing.maxCollation = Math.max(existing.maxCollation, collation);
|
|
14375
|
-
} else {
|
|
14376
|
-
byPage.set(r.page_id, { pageName: r.page_name, pageId: r.page_id, sampleLibraryId: r.ad_archive_id, maxCollation: collation, resultCount: 1 });
|
|
14377
|
-
}
|
|
14378
|
-
}
|
|
14379
|
-
return [...byPage.values()].map((e) => ({ pageName: e.pageName, pageId: e.pageId, sampleLibraryId: e.sampleLibraryId, adCount: Math.max(e.maxCollation, e.resultCount) })).sort((a, b) => b.adCount - a.adCount).slice(0, maxResults);
|
|
14380
|
-
}
|
|
14381
|
-
var AD_LIBRARY_QUERY;
|
|
14382
|
-
var init_FacebookAdGraphql = __esm({
|
|
14383
|
-
"src/extractor/FacebookAdGraphql.ts"() {
|
|
14384
|
-
"use strict";
|
|
14385
|
-
AD_LIBRARY_QUERY = "AdLibrarySearchPaginationQuery";
|
|
14386
|
-
}
|
|
14387
|
-
});
|
|
14388
|
-
|
|
14389
|
-
// src/extractor/FacebookOrganicVideoExtractor.ts
|
|
14390
|
-
function htmlDecode(value) {
|
|
14391
|
-
return value.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
14392
|
-
}
|
|
14393
|
-
function decodeEscapedValue(raw) {
|
|
14394
|
-
let out = raw;
|
|
14395
|
-
try {
|
|
14396
|
-
out = JSON.parse(`"${raw.replace(/"/g, '\\"')}"`);
|
|
14397
|
-
} catch {
|
|
14398
|
-
out = raw.replace(/\\u0025/g, "%").replace(/\\u0026/g, "&").replace(/\\u003D/g, "=").replace(/\\u003d/g, "=").replace(/\\"/g, '"').replace(/\\\//g, "/");
|
|
14399
|
-
}
|
|
14400
|
-
return htmlDecode(out);
|
|
14401
|
-
}
|
|
14402
|
-
function facebookVideoIdFromUrl(url) {
|
|
14403
|
-
try {
|
|
14404
|
-
const parsed = new URL(url);
|
|
14405
|
-
const path6 = parsed.pathname;
|
|
14406
|
-
const direct = path6.match(/\/(?:reel|videos|watch)\/(?:[^/]+\/)?(\d{8,})/i);
|
|
14407
|
-
if (direct) return direct[1];
|
|
14408
|
-
const queryVideoId = parsed.searchParams.get("v") ?? parsed.searchParams.get("video_id");
|
|
14409
|
-
if (queryVideoId && /^\d{8,}$/.test(queryVideoId)) return queryVideoId;
|
|
14410
|
-
const numericTail = path6.match(/\/(\d{8,})\/?$/);
|
|
14411
|
-
if (numericTail) return numericTail[1];
|
|
14412
|
-
} catch {
|
|
14413
|
-
}
|
|
14414
|
-
return null;
|
|
14415
|
-
}
|
|
14416
|
-
function parseEfg(url) {
|
|
14417
|
-
try {
|
|
14418
|
-
const efg = new URL(url).searchParams.get("efg");
|
|
14419
|
-
if (!efg) return null;
|
|
14420
|
-
return JSON.parse(Buffer.from(efg, "base64").toString("utf8"));
|
|
14421
|
-
} catch {
|
|
14422
|
-
return null;
|
|
14423
|
-
}
|
|
14424
|
-
}
|
|
14425
|
-
function manifestDurationSec(htmlWindow) {
|
|
14426
|
-
const decoded = decodeEscapedValue(htmlWindow);
|
|
14427
|
-
const match = decoded.match(/mediaPresentationDuration="PT([\d.]+)S"/);
|
|
14428
|
-
return match ? Number(match[1]) : null;
|
|
14429
|
-
}
|
|
14430
|
-
function metadataContent(html, key) {
|
|
14431
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14432
|
-
const re = new RegExp(`<meta\\s+[^>]*(?:property|name)=["']${escaped}["'][^>]*content=["']([^"']+)["'][^>]*>`, "i");
|
|
14433
|
-
const m = html.match(re);
|
|
14434
|
-
return m ? htmlDecode(m[1]).trim() : null;
|
|
14435
|
-
}
|
|
14436
|
-
function ownerNameFromHtml(html) {
|
|
14437
|
-
const ogTitle = metadataContent(html, "og:title");
|
|
14438
|
-
const pipe = ogTitle?.split("|").map((s) => s.trim()).filter(Boolean);
|
|
14439
|
-
if (pipe && pipe.length > 1) return pipe[pipe.length - 1] ?? null;
|
|
14440
|
-
return null;
|
|
14441
|
-
}
|
|
14442
|
-
function extractFacebookOrganicVideo(html, pageUrl, sourceUrl = pageUrl, quality = "best") {
|
|
14443
|
-
const videoId = facebookVideoIdFromUrl(pageUrl) ?? facebookVideoIdFromUrl(sourceUrl);
|
|
14444
|
-
const manifestIndex = videoId ? html.indexOf(`dash_mpd_debug.mpd?v=${videoId}`) : -1;
|
|
14445
|
-
const searchWindow = manifestIndex >= 0 ? html.slice(Math.max(0, manifestIndex - 5e4), Math.min(html.length, manifestIndex + 5e4)) : html;
|
|
14446
|
-
const targetDuration = manifestDurationSec(searchWindow);
|
|
14447
|
-
const candidates = [];
|
|
14448
|
-
const seen = /* @__PURE__ */ new Set();
|
|
14449
|
-
const re = /browser_native_(sd|hd)_url\\?"\s*:\s*\\?"([^"<]+?)\\?"/g;
|
|
14450
|
-
let match;
|
|
14451
|
-
while ((match = re.exec(searchWindow)) !== null) {
|
|
14452
|
-
const url = decodeEscapedValue(match[2]);
|
|
14453
|
-
if (!url || seen.has(url)) continue;
|
|
14454
|
-
seen.add(url);
|
|
14455
|
-
const parsed = parseEfg(url);
|
|
14456
|
-
let bitrate = null;
|
|
14457
|
-
try {
|
|
14458
|
-
const value = new URL(url).searchParams.get("bitrate");
|
|
14459
|
-
bitrate = value ? Number(value) : null;
|
|
14460
|
-
} catch {
|
|
14461
|
-
}
|
|
14462
|
-
candidates.push({
|
|
14463
|
-
url,
|
|
14464
|
-
quality: match[1],
|
|
14465
|
-
bitrate: Number.isFinite(bitrate) ? bitrate : null,
|
|
14466
|
-
durationSec: typeof parsed?.duration_s === "number" ? parsed.duration_s : null,
|
|
14467
|
-
assetId: typeof parsed?.xpv_asset_id === "number" ? parsed.xpv_asset_id : null,
|
|
14468
|
-
vencodeTag: typeof parsed?.vencode_tag === "string" ? parsed.vencode_tag : null
|
|
14469
|
-
});
|
|
14470
|
-
}
|
|
14471
|
-
const targetCandidates = candidates.filter((candidate) => {
|
|
14472
|
-
if (!targetDuration || candidate.durationSec == null) return true;
|
|
14473
|
-
return Math.abs(candidate.durationSec - targetDuration) <= 2;
|
|
14474
|
-
});
|
|
14475
|
-
const selectable = targetCandidates.length ? targetCandidates : candidates;
|
|
14476
|
-
const selected = [...selectable].filter((candidate) => quality === "best" || candidate.quality === quality).sort((a, b) => {
|
|
14477
|
-
const aq = a.quality === "hd" ? 1 : 0;
|
|
14478
|
-
const bq = b.quality === "hd" ? 1 : 0;
|
|
14479
|
-
if (quality === "best" && aq !== bq) return bq - aq;
|
|
14480
|
-
return (b.bitrate ?? 0) - (a.bitrate ?? 0);
|
|
14481
|
-
})[0];
|
|
14482
|
-
if (!selected) {
|
|
14483
|
-
throw new Error("No progressive Facebook video URL was found in the rendered page state");
|
|
14484
|
-
}
|
|
14485
|
-
return {
|
|
14486
|
-
sourceUrl,
|
|
14487
|
-
pageUrl,
|
|
14488
|
-
videoId,
|
|
14489
|
-
title: metadataContent(html, "og:title") ?? metadataContent(html, "twitter:title"),
|
|
14490
|
-
description: metadataContent(html, "og:description") ?? metadataContent(html, "description") ?? metadataContent(html, "twitter:description"),
|
|
14491
|
-
ownerName: ownerNameFromHtml(html),
|
|
14492
|
-
videoUrl: selected.url,
|
|
14493
|
-
selectedQuality: selected.quality,
|
|
14494
|
-
bitrate: selected.bitrate,
|
|
14495
|
-
durationSec: targetDuration ?? selected.durationSec,
|
|
14496
|
-
assetId: selected.assetId,
|
|
14497
|
-
vencodeTag: selected.vencodeTag,
|
|
14498
|
-
candidates: selectable,
|
|
14499
|
-
extractedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14500
|
-
};
|
|
14501
|
-
}
|
|
14502
|
-
async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "best") {
|
|
14503
|
-
await page.waitForLoadState("domcontentloaded", { timeout: 15e3 }).catch(() => {
|
|
14504
|
-
});
|
|
14505
|
-
await page.waitForTimeout(1500);
|
|
14506
|
-
const html = await page.content();
|
|
14507
|
-
return extractFacebookOrganicVideo(html, page.url(), sourceUrl, quality);
|
|
14508
|
-
}
|
|
14509
|
-
var init_FacebookOrganicVideoExtractor = __esm({
|
|
14510
|
-
"src/extractor/FacebookOrganicVideoExtractor.ts"() {
|
|
14511
|
-
"use strict";
|
|
14512
|
-
}
|
|
14513
|
-
});
|
|
14514
|
-
|
|
14515
|
-
// src/locations.ts
|
|
14516
|
-
var LOCATIONS;
|
|
14517
|
-
var init_locations = __esm({
|
|
14518
|
-
"src/locations.ts"() {
|
|
14519
|
-
"use strict";
|
|
14520
|
-
LOCATIONS = {
|
|
14521
|
-
"austin": "Austin,Texas,United States",
|
|
14522
|
-
"new york": "New York,New York,United States",
|
|
14523
|
-
"new york city": "New York,New York,United States",
|
|
14524
|
-
"nyc": "New York,New York,United States",
|
|
14525
|
-
"los angeles": "Los Angeles,California,United States",
|
|
14526
|
-
"la": "Los Angeles,California,United States",
|
|
14527
|
-
"chicago": "Chicago,Illinois,United States",
|
|
14528
|
-
"houston": "Houston,Texas,United States",
|
|
14529
|
-
"phoenix": "Phoenix,Arizona,United States",
|
|
14530
|
-
"philadelphia": "Philadelphia,Pennsylvania,United States",
|
|
14531
|
-
"philly": "Philadelphia,Pennsylvania,United States",
|
|
14532
|
-
"san antonio": "San Antonio,Texas,United States",
|
|
14533
|
-
"dallas": "Dallas,Texas,United States",
|
|
14534
|
-
"miami": "Miami,Florida,United States",
|
|
14535
|
-
"seattle": "Seattle,Washington,United States",
|
|
14536
|
-
"denver": "Denver,Colorado,United States",
|
|
14537
|
-
"loveland": "Loveland,Colorado,United States",
|
|
14538
|
-
"loveland co": "Loveland,Colorado,United States",
|
|
14539
|
-
"fort collins": "Fort Collins,Colorado,United States",
|
|
14540
|
-
"boulder": "Boulder,Colorado,United States",
|
|
14541
|
-
"colorado springs": "Colorado Springs,Colorado,United States",
|
|
14542
|
-
"boston": "Boston,Massachusetts,United States",
|
|
14543
|
-
"atlanta": "Atlanta,Georgia,United States",
|
|
14544
|
-
"san francisco": "San Francisco,California,United States",
|
|
14545
|
-
"sf": "San Francisco,California,United States",
|
|
14546
|
-
"portland": "Portland,Oregon,United States",
|
|
14547
|
-
"las vegas": "Las Vegas,Nevada,United States",
|
|
14548
|
-
"minneapolis": "Minneapolis,Minnesota,United States",
|
|
14549
|
-
"detroit": "Detroit,Michigan,United States",
|
|
14550
|
-
"nashville": "Nashville,Tennessee,United States",
|
|
14551
|
-
"charlotte": "Charlotte,North Carolina,United States",
|
|
14552
|
-
"orlando": "Orlando,Florida,United States",
|
|
14553
|
-
"san diego": "San Diego,California,United States",
|
|
14554
|
-
"baltimore": "Baltimore,Maryland,United States",
|
|
14555
|
-
"sacramento": "Sacramento,California,United States",
|
|
14556
|
-
"columbus": "Columbus,Ohio,United States",
|
|
14557
|
-
"indianapolis": "Indianapolis,Indiana,United States",
|
|
14558
|
-
"san jose": "San Jose,California,United States",
|
|
14559
|
-
"fort worth": "Fort Worth,Texas,United States",
|
|
14560
|
-
"jacksonville": "Jacksonville,Florida,United States",
|
|
14561
|
-
"memphis": "Memphis,Tennessee,United States",
|
|
14562
|
-
"louisville": "Louisville,Kentucky,United States",
|
|
14563
|
-
"raleigh": "Raleigh,North Carolina,United States",
|
|
14564
|
-
"richmond": "Richmond,Virginia,United States",
|
|
14565
|
-
"salt lake city": "Salt Lake City,Utah,United States",
|
|
14566
|
-
"toronto": "Toronto,Ontario,Canada",
|
|
14567
|
-
"vancouver": "Vancouver,British Columbia,Canada",
|
|
14568
|
-
"montreal": "Montreal,Quebec,Canada",
|
|
14569
|
-
"calgary": "Calgary,Alberta,Canada",
|
|
14570
|
-
"ottawa": "Ottawa,Ontario,Canada",
|
|
14571
|
-
"london": "London,England,United Kingdom",
|
|
14572
|
-
"manchester": "Manchester,England,United Kingdom",
|
|
14573
|
-
"birmingham": "Birmingham,England,United Kingdom",
|
|
14574
|
-
"edinburgh": "Edinburgh,Scotland,United Kingdom",
|
|
14575
|
-
"glasgow": "Glasgow,Scotland,United Kingdom",
|
|
14576
|
-
"leeds": "Leeds,England,United Kingdom",
|
|
14577
|
-
"sydney": "Sydney,New South Wales,Australia",
|
|
14578
|
-
"melbourne": "Melbourne,Victoria,Australia",
|
|
14579
|
-
"brisbane": "Brisbane,Queensland,Australia",
|
|
14580
|
-
"perth": "Perth,Western Australia,Australia",
|
|
14581
|
-
"adelaide": "Adelaide,South Australia,Australia",
|
|
14582
|
-
"dublin": "Dublin,Leinster,Ireland"
|
|
14583
|
-
};
|
|
14584
|
-
}
|
|
14585
|
-
});
|
|
14586
|
-
|
|
14587
|
-
// src/uule.ts
|
|
14588
|
-
function encodeVarint(value) {
|
|
14589
|
-
const bytes = [];
|
|
14590
|
-
let remaining = value;
|
|
14591
|
-
do {
|
|
14592
|
-
let byte = remaining & 127;
|
|
14593
|
-
remaining >>>= 7;
|
|
14594
|
-
if (remaining > 0) byte |= 128;
|
|
14595
|
-
bytes.push(byte);
|
|
14596
|
-
} while (remaining > 0);
|
|
14597
|
-
return bytes;
|
|
14598
|
-
}
|
|
14599
|
-
function encodeUule(name) {
|
|
14600
|
-
const locationBytes = Buffer.from(name, "utf8");
|
|
14601
|
-
const payload = Buffer.concat([
|
|
14602
|
-
Buffer.from([8, 2, 16, 32, 34]),
|
|
14603
|
-
Buffer.from(encodeVarint(locationBytes.length)),
|
|
14604
|
-
locationBytes
|
|
14605
|
-
]);
|
|
14606
|
-
return `w+${payload.toString("base64")}`;
|
|
14607
|
-
}
|
|
14608
|
-
function normalizeLocation(input) {
|
|
14609
|
-
const raw = input.toLowerCase().trim();
|
|
14610
|
-
if (LOCATIONS[raw]) return LOCATIONS[raw];
|
|
14611
|
-
const beforeComma = raw.split(",")[0].trim();
|
|
14612
|
-
if (beforeComma !== raw && LOCATIONS[beforeComma]) return LOCATIONS[beforeComma];
|
|
14613
|
-
const withoutState = raw.replace(/\s+[a-z]{2}$/, "").trim();
|
|
14614
|
-
if (withoutState !== raw && LOCATIONS[withoutState]) return LOCATIONS[withoutState];
|
|
14615
|
-
return input;
|
|
14616
|
-
}
|
|
14617
|
-
var init_uule = __esm({
|
|
14618
|
-
"src/uule.ts"() {
|
|
14619
|
-
"use strict";
|
|
14620
|
-
init_locations();
|
|
14621
|
-
}
|
|
14622
|
-
});
|
|
14623
|
-
|
|
14624
|
-
// src/kernel-proxy-resolver.ts
|
|
14625
|
-
function proxyIdSuffix2(proxyId) {
|
|
14626
|
-
return proxyId ? proxyId.slice(-6) : null;
|
|
14627
|
-
}
|
|
14628
|
-
function resolution(source, proxyMode, proxyId, target, error, disposable = false) {
|
|
14629
|
-
const proxyType = source === "disabled" ? "none" : source === "configured_fallback" ? "configured" : source === "isp_created" ? "isp" : source === "location_created" || source === "location_reused" ? "residential" : "unknown";
|
|
14630
|
-
return {
|
|
14631
|
-
kernelProxyId: proxyId,
|
|
14632
|
-
...disposable && proxyId ? { disposableProxyId: proxyId } : {},
|
|
14633
|
-
resolution: {
|
|
14634
|
-
source,
|
|
14635
|
-
proxyType,
|
|
14636
|
-
proxyMode,
|
|
14637
|
-
proxyIdPresent: Boolean(proxyId),
|
|
14638
|
-
proxyIdSuffix: proxyIdSuffix2(proxyId),
|
|
14639
|
-
target,
|
|
14640
|
-
error,
|
|
14641
|
-
disposable
|
|
14642
|
-
}
|
|
14643
|
-
};
|
|
14644
|
-
}
|
|
14645
|
-
function normalizeStateName(value) {
|
|
14646
|
-
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
14647
|
-
}
|
|
14648
|
-
function normalizeCountryName(value) {
|
|
14649
|
-
return value.trim().toLowerCase().replace(/\./g, "").replace(/\s+/g, " ");
|
|
14650
|
-
}
|
|
14651
|
-
function isUnitedStates(country) {
|
|
14652
|
-
if (!country) return true;
|
|
14653
|
-
const normalized = normalizeCountryName(country);
|
|
14654
|
-
return normalized === "united states" || normalized === "united states of america" || normalized === "usa" || normalized === "us";
|
|
14655
|
-
}
|
|
14656
|
-
function stateCodeFor(region) {
|
|
14657
|
-
const trimmed = region.trim();
|
|
14658
|
-
if (/^[A-Za-z]{2}$/.test(trimmed)) return trimmed.toUpperCase();
|
|
14659
|
-
return US_STATE_CODES[normalizeStateName(trimmed)] ?? null;
|
|
14660
|
-
}
|
|
14661
|
-
function kernelCityIdentifierCandidates(city) {
|
|
14662
|
-
const ascii = city.normalize("NFKD").replace(/[^\x00-\x7F]/g, "").toLowerCase();
|
|
14663
|
-
const words = ascii.split(/[^a-z0-9]+/).filter(Boolean);
|
|
14664
|
-
const underscored = words.join("_");
|
|
14665
|
-
const compact = words.join("");
|
|
14666
|
-
return Array.from(new Set([underscored, compact].filter(Boolean)));
|
|
14667
|
-
}
|
|
14668
|
-
function proxyName(country, state, city) {
|
|
14669
|
-
return city ? `mcp-serp-residential-${country.toLowerCase()}-${state.toLowerCase()}-${city}` : `mcp-serp-residential-${country.toLowerCase()}-${state.toLowerCase()}`;
|
|
14670
|
-
}
|
|
14671
|
-
function freshProxyName(baseName, attemptIndex) {
|
|
14672
|
-
const stamp = `${Date.now()}-${attemptIndex ?? 0}-${Math.random().toString(36).slice(2, 8)}`;
|
|
14673
|
-
return `${baseName}-fresh-${stamp}`;
|
|
14674
|
-
}
|
|
14675
|
-
function zipProxyName(zip) {
|
|
14676
|
-
return `mcp-serp-residential-us-zip-${zip}`;
|
|
14677
|
-
}
|
|
14678
|
-
function parseKernelLocationProxyTarget(location2, gl) {
|
|
14679
|
-
if (!location2 || gl.toLowerCase() !== "us") return null;
|
|
14680
|
-
const canonicalLocation = normalizeLocation(location2);
|
|
14681
|
-
let parts = canonicalLocation.split(",").map((part) => part.trim()).filter(Boolean);
|
|
14682
|
-
if (parts.length > 1 && isUnitedStates(parts[parts.length - 1])) {
|
|
14683
|
-
parts = parts.slice(0, -1);
|
|
14684
|
-
}
|
|
14685
|
-
if (parts.length === 1) {
|
|
14686
|
-
const stateOnly = stateCodeFor(parts[0]);
|
|
14687
|
-
if (!stateOnly) return null;
|
|
14688
|
-
return {
|
|
14689
|
-
canonicalLocation,
|
|
14690
|
-
level: "state",
|
|
14691
|
-
country: "US",
|
|
14692
|
-
state: stateOnly,
|
|
14693
|
-
city: "",
|
|
14694
|
-
cityCandidates: [],
|
|
14695
|
-
proxyName: proxyName("US", stateOnly),
|
|
14696
|
-
config: {
|
|
14697
|
-
country: "US",
|
|
14698
|
-
state: stateOnly
|
|
14699
|
-
}
|
|
14700
|
-
};
|
|
14701
|
-
}
|
|
14702
|
-
const [city = "", region = ""] = parts;
|
|
14703
|
-
if (!city || !region) return null;
|
|
14704
|
-
const state = stateCodeFor(region);
|
|
14705
|
-
if (!state) return null;
|
|
14706
|
-
const cityCandidates = kernelCityIdentifierCandidates(city);
|
|
14707
|
-
const primaryCity = cityCandidates[0];
|
|
14708
|
-
if (!primaryCity) return null;
|
|
14709
|
-
return {
|
|
14710
|
-
canonicalLocation,
|
|
14711
|
-
level: "city",
|
|
14712
|
-
country: "US",
|
|
14713
|
-
state,
|
|
14714
|
-
city: primaryCity,
|
|
14715
|
-
cityCandidates,
|
|
14716
|
-
proxyName: proxyName("US", state, primaryCity),
|
|
14717
|
-
config: {
|
|
14718
|
-
country: "US",
|
|
14719
|
-
state,
|
|
14720
|
-
city: primaryCity
|
|
14721
|
-
}
|
|
14722
|
-
};
|
|
14723
|
-
}
|
|
14724
|
-
function cityZipKey(target) {
|
|
14725
|
-
return `${target.city}|${target.state}`;
|
|
14726
|
-
}
|
|
14727
|
-
function knownZipFor(target, explicitZip) {
|
|
14728
|
-
if (explicitZip && /^\d{5}$/.test(explicitZip)) return explicitZip;
|
|
14729
|
-
return US_CITY_CENTER_ZIPS[cityZipKey(target)] ?? null;
|
|
14730
|
-
}
|
|
14731
|
-
function zipTarget(target, zip) {
|
|
14732
|
-
return {
|
|
14733
|
-
...target,
|
|
14734
|
-
level: "zip",
|
|
14735
|
-
zip,
|
|
14736
|
-
proxyName: zipProxyName(zip),
|
|
14737
|
-
config: {
|
|
14738
|
-
country: target.country,
|
|
14739
|
-
state: target.state,
|
|
14740
|
-
zip
|
|
14741
|
-
}
|
|
14742
|
-
};
|
|
14743
|
-
}
|
|
14744
|
-
function withProxyName(target, name) {
|
|
14745
|
-
return {
|
|
14746
|
-
...target,
|
|
14747
|
-
proxyName: name
|
|
14748
|
-
};
|
|
14749
|
-
}
|
|
14750
|
-
function configMatches(config, target, city) {
|
|
14751
|
-
if (target.level === "zip") {
|
|
14752
|
-
return config?.country?.toUpperCase() === target.country && config?.zip === target.zip;
|
|
14753
|
-
}
|
|
14754
|
-
return config?.country?.toUpperCase() === target.country && config?.state?.toUpperCase() === target.state && (city ? config?.city === city : !config?.city);
|
|
14755
|
-
}
|
|
14756
|
-
function findExistingTargetProxy(proxies, target) {
|
|
14757
|
-
return proxies.find((proxy) => proxy.type === "residential" && proxy.status !== "unavailable" && Boolean(proxy.id) && (proxy.name === target.proxyName || configMatches(proxy.config, target, target.level === "city" ? target.city : void 0))) ?? null;
|
|
14758
|
-
}
|
|
14759
|
-
function findExistingProxy(proxies, target) {
|
|
14760
|
-
for (const city of target.cityCandidates) {
|
|
14761
|
-
const name = proxyName(target.country, target.state, city);
|
|
14762
|
-
const found = proxies.find((proxy) => proxy.type === "residential" && proxy.status !== "unavailable" && Boolean(proxy.id) && (proxy.name === name || configMatches(proxy.config, target, city)));
|
|
14763
|
-
if (found) return found;
|
|
14764
|
-
}
|
|
14765
|
-
return null;
|
|
14766
|
-
}
|
|
14767
|
-
function stateTarget(target) {
|
|
14768
|
-
return {
|
|
14769
|
-
...target,
|
|
14770
|
-
level: "state",
|
|
14771
|
-
proxyName: proxyName(target.country, target.state),
|
|
14772
|
-
config: {
|
|
14773
|
-
country: target.country,
|
|
14774
|
-
state: target.state
|
|
14775
|
-
}
|
|
14776
|
-
};
|
|
14777
|
-
}
|
|
14778
|
-
function findExistingStateProxy(proxies, target) {
|
|
14779
|
-
const name = proxyName(target.country, target.state);
|
|
14780
|
-
return proxies.find((proxy) => proxy.type === "residential" && proxy.status !== "unavailable" && Boolean(proxy.id) && (proxy.name === name || configMatches(proxy.config, target))) ?? null;
|
|
14781
|
-
}
|
|
14782
|
-
function escalatedTargetLevel(target, attemptIndex) {
|
|
14783
|
-
return stateTarget(target);
|
|
14784
|
-
}
|
|
14785
|
-
function errorText2(err) {
|
|
14786
|
-
return err instanceof Error ? err.message : String(err);
|
|
14787
|
-
}
|
|
14788
|
-
function freshTargetCandidates(target, explicitZip, attemptIndex) {
|
|
14789
|
-
const out = [];
|
|
14790
|
-
const zip = knownZipFor(target, explicitZip);
|
|
14791
|
-
if (zip) {
|
|
14792
|
-
const targetZip = zipTarget(target, zip);
|
|
14793
|
-
out.push(withProxyName(targetZip, freshProxyName(targetZip.proxyName, attemptIndex)));
|
|
14794
|
-
}
|
|
14795
|
-
for (const city of target.cityCandidates) {
|
|
14796
|
-
const cityTarget = {
|
|
14797
|
-
...target,
|
|
14798
|
-
level: "city",
|
|
14799
|
-
city,
|
|
14800
|
-
proxyName: proxyName(target.country, target.state, city),
|
|
14801
|
-
config: {
|
|
14802
|
-
country: target.country,
|
|
14803
|
-
state: target.state,
|
|
14804
|
-
city
|
|
14805
|
-
}
|
|
14806
|
-
};
|
|
14807
|
-
out.push(withProxyName(cityTarget, freshProxyName(cityTarget.proxyName, attemptIndex)));
|
|
14808
|
-
}
|
|
14809
|
-
const fallbackTarget = stateTarget(target);
|
|
14810
|
-
out.push(withProxyName(fallbackTarget, freshProxyName(fallbackTarget.proxyName, attemptIndex)));
|
|
14811
|
-
return out;
|
|
14812
|
-
}
|
|
14813
|
-
async function createFreshLocationProxy(kernel, options, target) {
|
|
14814
|
-
const createErrors = [];
|
|
14815
|
-
for (const candidate of freshTargetCandidates(target, options.proxyZip, options.attemptIndex)) {
|
|
14816
|
-
try {
|
|
14817
|
-
const created = await kernel.proxies.create({
|
|
14818
|
-
type: "residential",
|
|
14819
|
-
name: candidate.proxyName,
|
|
14820
|
-
config: candidate.level === "zip" ? { country: candidate.country, zip: candidate.zip } : candidate.config
|
|
15094
|
+
snapshot
|
|
14821
15095
|
});
|
|
14822
|
-
if (created.id) {
|
|
14823
|
-
return resolution("location_created", options.proxyMode, created.id, candidate, null, true);
|
|
14824
|
-
}
|
|
14825
|
-
createErrors.push(`${candidate.proxyName}: Kernel did not return a proxy id`);
|
|
14826
|
-
} catch (err) {
|
|
14827
|
-
createErrors.push(`${candidate.proxyName}: ${errorText2(err)}`);
|
|
14828
15096
|
}
|
|
14829
15097
|
}
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
15098
|
+
return results;
|
|
15099
|
+
}
|
|
15100
|
+
async function collectAdLibraryResults(page, url, maxResults, opts = {}) {
|
|
15101
|
+
const captureMs = opts.captureMs ?? 3e4;
|
|
15102
|
+
const collected = [];
|
|
15103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15104
|
+
const handler = (resp) => {
|
|
15105
|
+
if (!resp.url().includes("/api/graphql")) return;
|
|
15106
|
+
const friendlyName = (resp.request().postData() ?? "").match(/fb_api_req_friendly_name=([^&]+)/)?.[1];
|
|
15107
|
+
if (friendlyName !== AD_LIBRARY_QUERY) return;
|
|
15108
|
+
void resp.text().then((text) => {
|
|
15109
|
+
for (const payload of parseFbGraphqlJson(text)) {
|
|
15110
|
+
for (const result of extractCollatedResults(payload)) {
|
|
15111
|
+
if (seen.has(result.ad_archive_id)) continue;
|
|
15112
|
+
seen.add(result.ad_archive_id);
|
|
15113
|
+
collected.push(result);
|
|
15114
|
+
}
|
|
14840
15115
|
}
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
15116
|
+
}).catch(() => void 0);
|
|
15117
|
+
};
|
|
15118
|
+
page.on("response", handler);
|
|
15119
|
+
try {
|
|
15120
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
15121
|
+
const deadline = Date.now() + captureMs;
|
|
15122
|
+
let lastCount = -1;
|
|
15123
|
+
let stableRounds = 0;
|
|
15124
|
+
while (Date.now() < deadline && collected.length < maxResults) {
|
|
15125
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)).catch(() => void 0);
|
|
15126
|
+
await page.waitForTimeout(2e3);
|
|
15127
|
+
if (collected.length === lastCount) {
|
|
15128
|
+
stableRounds++;
|
|
15129
|
+
if (stableRounds >= 2 && collected.length > 0) break;
|
|
15130
|
+
} else {
|
|
15131
|
+
stableRounds = 0;
|
|
15132
|
+
}
|
|
15133
|
+
lastCount = collected.length;
|
|
14844
15134
|
}
|
|
15135
|
+
} finally {
|
|
15136
|
+
page.off("response", handler);
|
|
14845
15137
|
}
|
|
14846
|
-
return
|
|
15138
|
+
return collected.slice(0, maxResults);
|
|
14847
15139
|
}
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
const
|
|
14851
|
-
|
|
15140
|
+
function advertisersFromResults(results, maxResults) {
|
|
15141
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
15142
|
+
for (const r of results) {
|
|
15143
|
+
if (!r.page_id || !r.page_name) continue;
|
|
15144
|
+
const collation = typeof r.collation_count === "number" && r.collation_count > 0 ? r.collation_count : 0;
|
|
15145
|
+
const existing = byPage.get(r.page_id);
|
|
15146
|
+
if (existing) {
|
|
15147
|
+
existing.resultCount++;
|
|
15148
|
+
existing.maxCollation = Math.max(existing.maxCollation, collation);
|
|
15149
|
+
} else {
|
|
15150
|
+
byPage.set(r.page_id, { pageName: r.page_name, pageId: r.page_id, sampleLibraryId: r.ad_archive_id, maxCollation: collation, resultCount: 1 });
|
|
15151
|
+
}
|
|
15152
|
+
}
|
|
15153
|
+
return [...byPage.values()].map((e) => ({ pageName: e.pageName, pageId: e.pageId, sampleLibraryId: e.sampleLibraryId, adCount: Math.max(e.maxCollation, e.resultCount) })).sort((a, b) => b.adCount - a.adCount).slice(0, maxResults);
|
|
14852
15154
|
}
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
15155
|
+
var AD_LIBRARY_QUERY;
|
|
15156
|
+
var init_FacebookAdGraphql = __esm({
|
|
15157
|
+
"src/extractor/FacebookAdGraphql.ts"() {
|
|
15158
|
+
"use strict";
|
|
15159
|
+
AD_LIBRARY_QUERY = "AdLibrarySearchPaginationQuery";
|
|
14856
15160
|
}
|
|
14857
|
-
|
|
14858
|
-
|
|
15161
|
+
});
|
|
15162
|
+
|
|
15163
|
+
// src/extractor/FacebookOrganicVideoExtractor.ts
|
|
15164
|
+
function htmlDecode(value) {
|
|
15165
|
+
return value.replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
15166
|
+
}
|
|
15167
|
+
function decodeEscapedValue(raw) {
|
|
15168
|
+
let out = raw;
|
|
15169
|
+
try {
|
|
15170
|
+
out = JSON.parse(`"${raw.replace(/"/g, '\\"')}"`);
|
|
15171
|
+
} catch {
|
|
15172
|
+
out = raw.replace(/\\u0025/g, "%").replace(/\\u0026/g, "&").replace(/\\u003D/g, "=").replace(/\\u003d/g, "=").replace(/\\"/g, '"').replace(/\\\//g, "/");
|
|
14859
15173
|
}
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
15174
|
+
return htmlDecode(out);
|
|
15175
|
+
}
|
|
15176
|
+
function facebookVideoIdFromUrl(url) {
|
|
15177
|
+
try {
|
|
15178
|
+
const parsed = new URL(url);
|
|
15179
|
+
const path6 = parsed.pathname;
|
|
15180
|
+
const direct = path6.match(/\/(?:reel|videos|watch)\/(?:[^/]+\/)?(\d{8,})/i);
|
|
15181
|
+
if (direct) return direct[1];
|
|
15182
|
+
const queryVideoId = parsed.searchParams.get("v") ?? parsed.searchParams.get("video_id");
|
|
15183
|
+
if (queryVideoId && /^\d{8,}$/.test(queryVideoId)) return queryVideoId;
|
|
15184
|
+
const numericTail = path6.match(/\/(\d{8,})\/?$/);
|
|
15185
|
+
if (numericTail) return numericTail[1];
|
|
15186
|
+
} catch {
|
|
14863
15187
|
}
|
|
14864
|
-
|
|
15188
|
+
return null;
|
|
15189
|
+
}
|
|
15190
|
+
function parseEfg(url) {
|
|
14865
15191
|
try {
|
|
14866
|
-
const
|
|
14867
|
-
if (
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
});
|
|
14906
|
-
if (created.id) {
|
|
14907
|
-
return resolution("location_created", options.proxyMode, created.id, targetZip, null);
|
|
14908
|
-
}
|
|
14909
|
-
createErrors.push(`${zip}: Kernel did not return a proxy id`);
|
|
14910
|
-
} catch (err) {
|
|
14911
|
-
createErrors.push(`${zip}: ${errorText2(err)}`);
|
|
14912
|
-
}
|
|
14913
|
-
}
|
|
14914
|
-
const existing = findExistingProxy(proxies, target);
|
|
14915
|
-
if (existing?.id) {
|
|
14916
|
-
return resolution("location_reused", options.proxyMode, existing.id, target, createErrors.join(" | ") || null);
|
|
14917
|
-
}
|
|
14918
|
-
for (const city of target.cityCandidates) {
|
|
14919
|
-
try {
|
|
14920
|
-
const created = await kernel.proxies.create({
|
|
14921
|
-
type: "residential",
|
|
14922
|
-
name: proxyName(target.country, target.state, city),
|
|
14923
|
-
config: {
|
|
14924
|
-
country: target.country,
|
|
14925
|
-
state: target.state,
|
|
14926
|
-
city
|
|
14927
|
-
}
|
|
14928
|
-
});
|
|
14929
|
-
if (created.id) {
|
|
14930
|
-
return resolution("location_created", options.proxyMode, created.id, {
|
|
14931
|
-
...target,
|
|
14932
|
-
level: "city",
|
|
14933
|
-
city,
|
|
14934
|
-
proxyName: proxyName(target.country, target.state, city),
|
|
14935
|
-
config: {
|
|
14936
|
-
country: target.country,
|
|
14937
|
-
state: target.state,
|
|
14938
|
-
city
|
|
14939
|
-
}
|
|
14940
|
-
}, null);
|
|
14941
|
-
}
|
|
14942
|
-
createErrors.push(`${city}: Kernel did not return a proxy id`);
|
|
14943
|
-
} catch (err) {
|
|
14944
|
-
createErrors.push(`${city}: ${errorText2(err)}`);
|
|
14945
|
-
}
|
|
14946
|
-
}
|
|
14947
|
-
const fallbackTarget = stateTarget(target);
|
|
14948
|
-
const existingState = findExistingStateProxy(proxies, fallbackTarget);
|
|
14949
|
-
if (existingState?.id) {
|
|
14950
|
-
return resolution("location_reused", options.proxyMode, existingState.id, fallbackTarget, createErrors.join(" | "));
|
|
14951
|
-
}
|
|
15192
|
+
const efg = new URL(url).searchParams.get("efg");
|
|
15193
|
+
if (!efg) return null;
|
|
15194
|
+
return JSON.parse(Buffer.from(efg, "base64").toString("utf8"));
|
|
15195
|
+
} catch {
|
|
15196
|
+
return null;
|
|
15197
|
+
}
|
|
15198
|
+
}
|
|
15199
|
+
function manifestDurationSec(htmlWindow) {
|
|
15200
|
+
const decoded = decodeEscapedValue(htmlWindow);
|
|
15201
|
+
const match = decoded.match(/mediaPresentationDuration="PT([\d.]+)S"/);
|
|
15202
|
+
return match ? Number(match[1]) : null;
|
|
15203
|
+
}
|
|
15204
|
+
function metadataContent(html, key) {
|
|
15205
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15206
|
+
const re = new RegExp(`<meta\\s+[^>]*(?:property|name)=["']${escaped}["'][^>]*content=["']([^"']+)["'][^>]*>`, "i");
|
|
15207
|
+
const m = html.match(re);
|
|
15208
|
+
return m ? htmlDecode(m[1]).trim() : null;
|
|
15209
|
+
}
|
|
15210
|
+
function ownerNameFromHtml(html) {
|
|
15211
|
+
const ogTitle = metadataContent(html, "og:title");
|
|
15212
|
+
const pipe = ogTitle?.split("|").map((s) => s.trim()).filter(Boolean);
|
|
15213
|
+
if (pipe && pipe.length > 1) return pipe[pipe.length - 1] ?? null;
|
|
15214
|
+
return null;
|
|
15215
|
+
}
|
|
15216
|
+
function extractFacebookOrganicVideo(html, pageUrl, sourceUrl = pageUrl, quality = "best") {
|
|
15217
|
+
const videoId = facebookVideoIdFromUrl(pageUrl) ?? facebookVideoIdFromUrl(sourceUrl);
|
|
15218
|
+
const manifestIndex = videoId ? html.indexOf(`dash_mpd_debug.mpd?v=${videoId}`) : -1;
|
|
15219
|
+
const searchWindow = manifestIndex >= 0 ? html.slice(Math.max(0, manifestIndex - 5e4), Math.min(html.length, manifestIndex + 5e4)) : html;
|
|
15220
|
+
const targetDuration = manifestDurationSec(searchWindow);
|
|
15221
|
+
const candidates = [];
|
|
15222
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15223
|
+
const re = /browser_native_(sd|hd)_url\\?"\s*:\s*\\?"([^"<]+?)\\?"/g;
|
|
15224
|
+
let match;
|
|
15225
|
+
while ((match = re.exec(searchWindow)) !== null) {
|
|
15226
|
+
const url = decodeEscapedValue(match[2]);
|
|
15227
|
+
if (!url || seen.has(url)) continue;
|
|
15228
|
+
seen.add(url);
|
|
15229
|
+
const parsed = parseEfg(url);
|
|
15230
|
+
let bitrate = null;
|
|
14952
15231
|
try {
|
|
14953
|
-
const
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
config: fallbackTarget.config
|
|
14957
|
-
});
|
|
14958
|
-
if (created.id) {
|
|
14959
|
-
return resolution("location_created", options.proxyMode, created.id, fallbackTarget, createErrors.join(" | "));
|
|
14960
|
-
}
|
|
14961
|
-
createErrors.push(`${fallbackTarget.state}: Kernel did not return a proxy id`);
|
|
14962
|
-
} catch (err) {
|
|
14963
|
-
createErrors.push(`${fallbackTarget.state}: ${errorText2(err)}`);
|
|
15232
|
+
const value = new URL(url).searchParams.get("bitrate");
|
|
15233
|
+
bitrate = value ? Number(value) : null;
|
|
15234
|
+
} catch {
|
|
14964
15235
|
}
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
15236
|
+
candidates.push({
|
|
15237
|
+
url,
|
|
15238
|
+
quality: match[1],
|
|
15239
|
+
bitrate: Number.isFinite(bitrate) ? bitrate : null,
|
|
15240
|
+
durationSec: typeof parsed?.duration_s === "number" ? parsed.duration_s : null,
|
|
15241
|
+
assetId: typeof parsed?.xpv_asset_id === "number" ? parsed.xpv_asset_id : null,
|
|
15242
|
+
vencodeTag: typeof parsed?.vencode_tag === "string" ? parsed.vencode_tag : null
|
|
15243
|
+
});
|
|
15244
|
+
}
|
|
15245
|
+
const targetCandidates = candidates.filter((candidate) => {
|
|
15246
|
+
if (!targetDuration || candidate.durationSec == null) return true;
|
|
15247
|
+
return Math.abs(candidate.durationSec - targetDuration) <= 2;
|
|
15248
|
+
});
|
|
15249
|
+
const selectable = targetCandidates.length ? targetCandidates : candidates;
|
|
15250
|
+
const selected = [...selectable].filter((candidate) => quality === "best" || candidate.quality === quality).sort((a, b) => {
|
|
15251
|
+
const aq = a.quality === "hd" ? 1 : 0;
|
|
15252
|
+
const bq = b.quality === "hd" ? 1 : 0;
|
|
15253
|
+
if (quality === "best" && aq !== bq) return bq - aq;
|
|
15254
|
+
return (b.bitrate ?? 0) - (a.bitrate ?? 0);
|
|
15255
|
+
})[0];
|
|
15256
|
+
if (!selected) {
|
|
15257
|
+
throw new Error("No progressive Facebook video URL was found in the rendered page state");
|
|
14968
15258
|
}
|
|
15259
|
+
return {
|
|
15260
|
+
sourceUrl,
|
|
15261
|
+
pageUrl,
|
|
15262
|
+
videoId,
|
|
15263
|
+
title: metadataContent(html, "og:title") ?? metadataContent(html, "twitter:title"),
|
|
15264
|
+
description: metadataContent(html, "og:description") ?? metadataContent(html, "description") ?? metadataContent(html, "twitter:description"),
|
|
15265
|
+
ownerName: ownerNameFromHtml(html),
|
|
15266
|
+
videoUrl: selected.url,
|
|
15267
|
+
selectedQuality: selected.quality,
|
|
15268
|
+
bitrate: selected.bitrate,
|
|
15269
|
+
durationSec: targetDuration ?? selected.durationSec,
|
|
15270
|
+
assetId: selected.assetId,
|
|
15271
|
+
vencodeTag: selected.vencodeTag,
|
|
15272
|
+
candidates: selectable,
|
|
15273
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15274
|
+
};
|
|
14969
15275
|
}
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
15276
|
+
async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "best") {
|
|
15277
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 15e3 }).catch(() => {
|
|
15278
|
+
});
|
|
15279
|
+
await page.waitForTimeout(1500);
|
|
15280
|
+
const html = await page.content();
|
|
15281
|
+
return extractFacebookOrganicVideo(html, page.url(), sourceUrl, quality);
|
|
15282
|
+
}
|
|
15283
|
+
var init_FacebookOrganicVideoExtractor = __esm({
|
|
15284
|
+
"src/extractor/FacebookOrganicVideoExtractor.ts"() {
|
|
14973
15285
|
"use strict";
|
|
14974
|
-
import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
|
|
14975
|
-
init_uule();
|
|
14976
|
-
US_STATE_CODES = {
|
|
14977
|
-
alabama: "AL",
|
|
14978
|
-
alaska: "AK",
|
|
14979
|
-
arizona: "AZ",
|
|
14980
|
-
arkansas: "AR",
|
|
14981
|
-
california: "CA",
|
|
14982
|
-
colorado: "CO",
|
|
14983
|
-
connecticut: "CT",
|
|
14984
|
-
delaware: "DE",
|
|
14985
|
-
florida: "FL",
|
|
14986
|
-
georgia: "GA",
|
|
14987
|
-
hawaii: "HI",
|
|
14988
|
-
idaho: "ID",
|
|
14989
|
-
illinois: "IL",
|
|
14990
|
-
indiana: "IN",
|
|
14991
|
-
iowa: "IA",
|
|
14992
|
-
kansas: "KS",
|
|
14993
|
-
kentucky: "KY",
|
|
14994
|
-
louisiana: "LA",
|
|
14995
|
-
maine: "ME",
|
|
14996
|
-
maryland: "MD",
|
|
14997
|
-
massachusetts: "MA",
|
|
14998
|
-
michigan: "MI",
|
|
14999
|
-
minnesota: "MN",
|
|
15000
|
-
mississippi: "MS",
|
|
15001
|
-
missouri: "MO",
|
|
15002
|
-
montana: "MT",
|
|
15003
|
-
nebraska: "NE",
|
|
15004
|
-
nevada: "NV",
|
|
15005
|
-
"new hampshire": "NH",
|
|
15006
|
-
"new jersey": "NJ",
|
|
15007
|
-
"new mexico": "NM",
|
|
15008
|
-
"new york": "NY",
|
|
15009
|
-
"north carolina": "NC",
|
|
15010
|
-
"north dakota": "ND",
|
|
15011
|
-
ohio: "OH",
|
|
15012
|
-
oklahoma: "OK",
|
|
15013
|
-
oregon: "OR",
|
|
15014
|
-
pennsylvania: "PA",
|
|
15015
|
-
"rhode island": "RI",
|
|
15016
|
-
"south carolina": "SC",
|
|
15017
|
-
"south dakota": "SD",
|
|
15018
|
-
tennessee: "TN",
|
|
15019
|
-
texas: "TX",
|
|
15020
|
-
utah: "UT",
|
|
15021
|
-
vermont: "VT",
|
|
15022
|
-
virginia: "VA",
|
|
15023
|
-
washington: "WA",
|
|
15024
|
-
"west virginia": "WV",
|
|
15025
|
-
wisconsin: "WI",
|
|
15026
|
-
wyoming: "WY"
|
|
15027
|
-
};
|
|
15028
|
-
US_CITY_CENTER_ZIPS = {
|
|
15029
|
-
"atlanta|GA": "30303",
|
|
15030
|
-
"austin|TX": "78701",
|
|
15031
|
-
"baltimore|MD": "21201",
|
|
15032
|
-
"boston|MA": "02108",
|
|
15033
|
-
"boulder|CO": "80302",
|
|
15034
|
-
"charlotte|NC": "28202",
|
|
15035
|
-
"chicago|IL": "60601",
|
|
15036
|
-
"colorado_springs|CO": "80903",
|
|
15037
|
-
"columbus|OH": "43215",
|
|
15038
|
-
"dallas|TX": "75201",
|
|
15039
|
-
"denver|CO": "80202",
|
|
15040
|
-
"detroit|MI": "48226",
|
|
15041
|
-
"fort_collins|CO": "80524",
|
|
15042
|
-
"fort_worth|TX": "76102",
|
|
15043
|
-
"houston|TX": "77002",
|
|
15044
|
-
"indianapolis|IN": "46204",
|
|
15045
|
-
"jacksonville|FL": "32202",
|
|
15046
|
-
"las_vegas|NV": "89101",
|
|
15047
|
-
"los_angeles|CA": "90012",
|
|
15048
|
-
"louisville|KY": "40202",
|
|
15049
|
-
"loveland|CO": "80537",
|
|
15050
|
-
"memphis|TN": "38103",
|
|
15051
|
-
"miami|FL": "33131",
|
|
15052
|
-
"minneapolis|MN": "55401",
|
|
15053
|
-
"nashville|TN": "37203",
|
|
15054
|
-
"new_york|NY": "10001",
|
|
15055
|
-
"orlando|FL": "32801",
|
|
15056
|
-
"philadelphia|PA": "19103",
|
|
15057
|
-
"phoenix|AZ": "85004",
|
|
15058
|
-
"portland|OR": "97205",
|
|
15059
|
-
"raleigh|NC": "27601",
|
|
15060
|
-
"richmond|VA": "23219",
|
|
15061
|
-
"sacramento|CA": "95814",
|
|
15062
|
-
"salt_lake_city|UT": "84101",
|
|
15063
|
-
"san_antonio|TX": "78205",
|
|
15064
|
-
"san_diego|CA": "92101",
|
|
15065
|
-
"san_francisco|CA": "94103",
|
|
15066
|
-
"san_jose|CA": "95113",
|
|
15067
|
-
"seattle|WA": "98101"
|
|
15068
|
-
};
|
|
15069
15286
|
}
|
|
15070
15287
|
});
|
|
15071
15288
|
|
|
@@ -15198,8 +15415,7 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
|
|
|
15198
15415
|
const text = data.text ?? "";
|
|
15199
15416
|
const chunks = data.chunks ?? [];
|
|
15200
15417
|
const durationMs = Date.now() - startMs;
|
|
15201
|
-
|
|
15202
|
-
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
|
|
15418
|
+
void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
|
|
15203
15419
|
return {
|
|
15204
15420
|
text,
|
|
15205
15421
|
chunks,
|
|
@@ -16755,6 +16971,7 @@ var init_reddit_routes = __esm({
|
|
|
16755
16971
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
16756
16972
|
let debited = false;
|
|
16757
16973
|
let refunded = false;
|
|
16974
|
+
let commentDebitMc = 0;
|
|
16758
16975
|
try {
|
|
16759
16976
|
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, body.url);
|
|
16760
16977
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
|
|
@@ -17016,6 +17233,8 @@ var init_video_routes = __esm({
|
|
|
17016
17233
|
init_rates();
|
|
17017
17234
|
init_api_auth();
|
|
17018
17235
|
init_memory();
|
|
17236
|
+
init_cost_telemetry();
|
|
17237
|
+
init_cost_context();
|
|
17019
17238
|
videoApp = new import_hono9.Hono();
|
|
17020
17239
|
AnalyzeBodySchema = import_zod20.z.object({
|
|
17021
17240
|
sourceUrl: import_zod20.z.string().trim().url("sourceUrl must be a direct video file URL"),
|
|
@@ -17092,6 +17311,21 @@ var init_video_routes = __esm({
|
|
|
17092
17311
|
effectiveFrames: st.frameCount ?? null
|
|
17093
17312
|
};
|
|
17094
17313
|
}
|
|
17314
|
+
if ((st.status === "done" || st.status === "failed") && typeof st.costUsd === "number" && st.costUsd > 0) {
|
|
17315
|
+
const llmOp = `${LedgerOperation.VIDEO_ANALYSIS_LLM}:${runId}`;
|
|
17316
|
+
if (!await ledgerExistsForOperation(user.id, llmOp)) {
|
|
17317
|
+
await runWithCostContext({ ...currentCostContext(), op: "video_analysis", userId: user.id }, async () => {
|
|
17318
|
+
void recordVendorUsage({ vendor: "mcp_memory_video", model: null, units: st.costUsd, unitType: "usd", error: null });
|
|
17319
|
+
});
|
|
17320
|
+
const surchargeMc = Math.round(st.costUsd * VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE * MC_PER_USD);
|
|
17321
|
+
if (surchargeMc > 0) {
|
|
17322
|
+
const { ok } = await debitMc(user.id, surchargeMc, llmOp, `real-cost-surcharge:${runId}`);
|
|
17323
|
+
if (!ok) {
|
|
17324
|
+
console.warn(`[video-routes] VIDEO_ANALYSIS_LLM surcharge debit failed for user ${user.id} (run ${runId}): insufficient balance for ${surchargeMc}mc`);
|
|
17325
|
+
}
|
|
17326
|
+
}
|
|
17327
|
+
}
|
|
17328
|
+
}
|
|
17095
17329
|
return c.json({
|
|
17096
17330
|
ok: true,
|
|
17097
17331
|
runId,
|
|
@@ -18330,7 +18564,7 @@ var init_trustpilot_routes = __esm({
|
|
|
18330
18564
|
let pagesFetched = 0;
|
|
18331
18565
|
try {
|
|
18332
18566
|
await driver.launch({
|
|
18333
|
-
headless:
|
|
18567
|
+
headless: false,
|
|
18334
18568
|
kernelApiKey: browserServiceApiKey(),
|
|
18335
18569
|
viewport: { width: 1280, height: 900 },
|
|
18336
18570
|
locale: "en-US"
|
|
@@ -18536,7 +18770,7 @@ var init_g2_routes = __esm({
|
|
|
18536
18770
|
const driver = new BrowserDriver();
|
|
18537
18771
|
try {
|
|
18538
18772
|
await driver.launch({
|
|
18539
|
-
headless:
|
|
18773
|
+
headless: false,
|
|
18540
18774
|
kernelApiKey: browserServiceApiKey(),
|
|
18541
18775
|
kernelProxyId: proxyId,
|
|
18542
18776
|
viewport: { width: 1280, height: 900 },
|
|
@@ -19330,6 +19564,55 @@ ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${memSection}$
|
|
|
19330
19564
|
}
|
|
19331
19565
|
return { ...textResult, structuredContent };
|
|
19332
19566
|
}
|
|
19567
|
+
function formatDiffPage(raw, input) {
|
|
19568
|
+
const parsed = parseData(raw);
|
|
19569
|
+
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
19570
|
+
const d = parsed.data;
|
|
19571
|
+
const url = d.url ?? input.url;
|
|
19572
|
+
const title = d.title ?? "Untitled";
|
|
19573
|
+
const statusLine = d.status === "baseline" ? d.isReset ? "\u{1F195} **Baseline reset** \u2014 prior history discarded, this check is now the new baseline." : "\u{1F195} **Baseline captured** \u2014 first check for this URL, nothing to compare yet." : d.status === "unchanged" ? `\u2705 **No changes** since ${d.previousCheckedAt ?? "the last check"}.` : `\u{1F504} **Changed** since ${d.previousCheckedAt ?? "the last check"} \u2014 +${d.summary.linesAdded}/-${d.summary.linesRemoved} lines (${d.summary.percentChanged ?? 0}% of content).`;
|
|
19574
|
+
const previewHunks = d.hunks.slice(0, DIFF_PAGE_PREVIEW_HUNKS);
|
|
19575
|
+
const diffBlock = previewHunks.length ? [
|
|
19576
|
+
"\n## Diff",
|
|
19577
|
+
"```diff",
|
|
19578
|
+
...previewHunks.flatMap((h) => h.lines.map((l) => `${h.type === "added" ? "+" : "-"} ${l}`)),
|
|
19579
|
+
"```",
|
|
19580
|
+
previewHunks.length < d.hunks.length ? `*(showing ${previewHunks.length} of ${d.hunks.length} returned hunks)*` : ""
|
|
19581
|
+
].filter(Boolean).join("\n") : "";
|
|
19582
|
+
const truncationNotes = [
|
|
19583
|
+
d.contentTruncated ? "\n\u26A0\uFE0F Page content exceeded the storable cap and was truncated before hashing/diffing \u2014 changes past that point are not visible to this comparison." : "",
|
|
19584
|
+
d.hunksTruncatedReason ? `
|
|
19585
|
+
\u26A0\uFE0F ${d.hunksTruncatedReason}` : ""
|
|
19586
|
+
].filter(Boolean).join("\n");
|
|
19587
|
+
const tips = `
|
|
19588
|
+
---
|
|
19589
|
+
\u{1F4A1} **Tips**
|
|
19590
|
+
- Call \`diff_page\` again anytime to re-check \u2014 this tool is on-demand, not automatic.
|
|
19591
|
+
- Want the page's current content with nothing to compare? use \`extract_url\`.`;
|
|
19592
|
+
const full = `# Page Diff: ${url}
|
|
19593
|
+
**${title}**
|
|
19594
|
+
|
|
19595
|
+
${statusLine}${diffBlock}${truncationNotes}${tips}`;
|
|
19596
|
+
return {
|
|
19597
|
+
...oneBlock(full),
|
|
19598
|
+
structuredContent: {
|
|
19599
|
+
url,
|
|
19600
|
+
title: d.title,
|
|
19601
|
+
status: d.status,
|
|
19602
|
+
isReset: d.isReset,
|
|
19603
|
+
previousCheckedAt: d.previousCheckedAt,
|
|
19604
|
+
currentCheckedAt: d.currentCheckedAt,
|
|
19605
|
+
contentHash: d.contentHash,
|
|
19606
|
+
previousContentHash: d.previousContentHash,
|
|
19607
|
+
summary: d.summary,
|
|
19608
|
+
hunks: d.hunks,
|
|
19609
|
+
contentTruncated: d.contentTruncated,
|
|
19610
|
+
hunksTruncated: d.hunksTruncated,
|
|
19611
|
+
hunksTruncatedReason: d.hunksTruncatedReason,
|
|
19612
|
+
totalChangedLineCount: d.totalChangedLineCount
|
|
19613
|
+
}
|
|
19614
|
+
};
|
|
19615
|
+
}
|
|
19333
19616
|
async function formatMapSiteUrls(raw, input, ctx) {
|
|
19334
19617
|
const parsed = parseData(raw);
|
|
19335
19618
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
@@ -21054,7 +21337,7 @@ ${rows}` : ""
|
|
|
21054
21337
|
}
|
|
21055
21338
|
};
|
|
21056
21339
|
}
|
|
21057
|
-
var import_node_fs6, import_node_os6, import_node_path8, INLINE_BUDGET_BYTES, STRUCTURED_ARRAY_CAP, reportSavingEnabled, BULK_PAGE_THRESHOLD, BULK_URL_THRESHOLD;
|
|
21340
|
+
var import_node_fs6, import_node_os6, import_node_path8, INLINE_BUDGET_BYTES, STRUCTURED_ARRAY_CAP, reportSavingEnabled, BULK_PAGE_THRESHOLD, BULK_URL_THRESHOLD, DIFF_PAGE_PREVIEW_HUNKS;
|
|
21058
21341
|
var init_mcp_response_formatter = __esm({
|
|
21059
21342
|
"src/mcp/mcp-response-formatter.ts"() {
|
|
21060
21343
|
"use strict";
|
|
@@ -21073,6 +21356,7 @@ var init_mcp_response_formatter = __esm({
|
|
|
21073
21356
|
reportSavingEnabled = true;
|
|
21074
21357
|
BULK_PAGE_THRESHOLD = 25;
|
|
21075
21358
|
BULK_URL_THRESHOLD = 500;
|
|
21359
|
+
DIFF_PAGE_PREVIEW_HUNKS = 20;
|
|
21076
21360
|
}
|
|
21077
21361
|
});
|
|
21078
21362
|
|
|
@@ -27003,7 +27287,7 @@ var PACKAGE_VERSION;
|
|
|
27003
27287
|
var init_version = __esm({
|
|
27004
27288
|
"src/version.ts"() {
|
|
27005
27289
|
"use strict";
|
|
27006
|
-
PACKAGE_VERSION = "0.4.
|
|
27290
|
+
PACKAGE_VERSION = "0.4.12";
|
|
27007
27291
|
}
|
|
27008
27292
|
});
|
|
27009
27293
|
|
|
@@ -27142,7 +27426,7 @@ var init_output_schema_registry = __esm({
|
|
|
27142
27426
|
});
|
|
27143
27427
|
|
|
27144
27428
|
// src/mcp/mcp-tool-schemas.ts
|
|
27145
|
-
var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema;
|
|
27429
|
+
var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema;
|
|
27146
27430
|
var init_mcp_tool_schemas = __esm({
|
|
27147
27431
|
"src/mcp/mcp-tool-schemas.ts"() {
|
|
27148
27432
|
"use strict";
|
|
@@ -27170,6 +27454,11 @@ var init_mcp_tool_schemas = __esm({
|
|
|
27170
27454
|
depositToVault: import_zod33.z.boolean().default(false).describe("Save the full page content into the user's MCP Memory vault server-side, embedded for semantic recall \u2014 the full body is NOT returned to chat."),
|
|
27171
27455
|
vaultName: import_zod33.z.string().trim().min(1).max(120).optional().describe("Optional vault to deposit into. Defaults to the user's personal vault.")
|
|
27172
27456
|
};
|
|
27457
|
+
DiffPageInputSchema = {
|
|
27458
|
+
url: import_zod33.z.string().url().describe("Public http/https URL to check for changes since the last diff_page call."),
|
|
27459
|
+
allowLocal: import_zod33.z.boolean().default(false).describe("Allow localhost and private-network URLs. Local development only."),
|
|
27460
|
+
resetBaseline: import_zod33.z.boolean().default(false).describe("Discard any previously stored snapshot for this URL and capture the current content as a fresh baseline instead of diffing against history. Use when you deliberately want to restart change tracking.")
|
|
27461
|
+
};
|
|
27173
27462
|
MapSiteUrlsInputSchema = {
|
|
27174
27463
|
url: import_zod33.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
27175
27464
|
maxUrls: import_zod33.z.number().int().min(1).max(1e4).optional().describe("Maximum URLs to discover. Use 100 for normal maps, up to 10000 for a full inventory. Large maps (over 500 URLs) write the complete inventory to a local file and return only a summary plus the file path instead of the full list inline.")
|
|
@@ -27563,6 +27852,29 @@ var init_mcp_tool_schemas = __esm({
|
|
|
27563
27852
|
error: import_zod33.z.string().optional()
|
|
27564
27853
|
}).optional()
|
|
27565
27854
|
};
|
|
27855
|
+
DiffPageOutputSchema = {
|
|
27856
|
+
url: import_zod33.z.string(),
|
|
27857
|
+
title: NullableString,
|
|
27858
|
+
status: import_zod33.z.enum(["baseline", "unchanged", "changed"]).describe('"baseline" = first-ever check for this URL, or resetBaseline was used \u2014 nothing to compare against. "unchanged" = content hash matched the stored snapshot. "changed" = a diff was computed.'),
|
|
27859
|
+
isReset: import_zod33.z.boolean().describe("True only when resetBaseline discarded a real prior snapshot \u2014 distinguishes an explicit reset from a URL's true first-ever check."),
|
|
27860
|
+
previousCheckedAt: NullableString.describe("ISO timestamp of the snapshot this was compared against, or null if there was none."),
|
|
27861
|
+
currentCheckedAt: import_zod33.z.string().describe("ISO timestamp of this check, now stored as the new snapshot."),
|
|
27862
|
+
contentHash: import_zod33.z.string().describe("sha256 of the full (untruncated) page content just captured."),
|
|
27863
|
+
previousContentHash: NullableString,
|
|
27864
|
+
summary: import_zod33.z.object({
|
|
27865
|
+
linesAdded: import_zod33.z.number().int().min(0),
|
|
27866
|
+
linesRemoved: import_zod33.z.number().int().min(0),
|
|
27867
|
+
percentChanged: import_zod33.z.number().min(0).max(100).nullable().describe('Proportion of changed lines relative to the larger of the two versions. Null when status is "baseline".')
|
|
27868
|
+
}),
|
|
27869
|
+
hunks: import_zod33.z.array(import_zod33.z.object({
|
|
27870
|
+
type: import_zod33.z.enum(["added", "removed"]).describe("Unchanged context lines are omitted \u2014 only added/removed lines are returned."),
|
|
27871
|
+
lines: import_zod33.z.array(import_zod33.z.string())
|
|
27872
|
+
})).describe("Ordered added/removed line hunks, capped for response size \u2014 see hunksTruncated."),
|
|
27873
|
+
contentTruncated: import_zod33.z.boolean().describe("True if the scraped page exceeded the 250,000-character storable cap and was truncated before hashing/diffing/storing \u2014 changes past that point are invisible to this comparison."),
|
|
27874
|
+
hunksTruncated: import_zod33.z.boolean().describe("True if the hunks list above was capped for response size \u2014 see hunksTruncatedReason."),
|
|
27875
|
+
hunksTruncatedReason: NullableString,
|
|
27876
|
+
totalChangedLineCount: import_zod33.z.number().int().min(0).describe("Total changed lines found before any hunksTruncated capping was applied.")
|
|
27877
|
+
};
|
|
27566
27878
|
ExtractSiteOutputSchema = {
|
|
27567
27879
|
url: import_zod33.z.string(),
|
|
27568
27880
|
pageCount: import_zod33.z.number().int().min(0),
|
|
@@ -28575,6 +28887,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
28575
28887
|
outputSchema: recordOutputSchema("extract_url", ExtractUrlOutputSchema),
|
|
28576
28888
|
annotations: liveWebToolAnnotations("Single URL Extract")
|
|
28577
28889
|
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
28890
|
+
server.registerTool("diff_page", {
|
|
28891
|
+
title: "Page Change Check",
|
|
28892
|
+
description: "Check whether a public URL has changed since you last checked it with this tool: scrapes the current page, diffs it against your last stored snapshot for that URL, and returns what was added or removed (or confirms no change). Stores the new snapshot as the baseline for next time \u2014 on-demand only, no automatic recurring checks. Use extract_url instead when you just want the page's current content with nothing to compare against.",
|
|
28893
|
+
inputSchema: DiffPageInputSchema,
|
|
28894
|
+
outputSchema: recordOutputSchema("diff_page", DiffPageOutputSchema),
|
|
28895
|
+
annotations: liveWebToolAnnotations("Page Change Check")
|
|
28896
|
+
}, async (input) => formatDiffPage(await executor.diffPage(input), input));
|
|
28578
28897
|
server.registerTool("map_site_urls", {
|
|
28579
28898
|
title: "Site URL Map",
|
|
28580
28899
|
description: `Map/crawl a public website for a sitemap, URL inventory, or broken-link scan. Returns internal URLs with HTTP status; ${fileBehavior("maps over 500 URLs are written to a local CSV file instead of inlined.", "large results are stored as a retrievable artifact \u2014 you get an inline summary plus an artifactId for report_artifact_read.")}`,
|
|
@@ -29006,6 +29325,9 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
29006
29325
|
extractUrl(input) {
|
|
29007
29326
|
return this.call("/extract-url", input);
|
|
29008
29327
|
}
|
|
29328
|
+
diffPage(input) {
|
|
29329
|
+
return this.call("/diff-page", input);
|
|
29330
|
+
}
|
|
29009
29331
|
mapSiteUrls(input) {
|
|
29010
29332
|
return this.call("/map-urls", input);
|
|
29011
29333
|
}
|
|
@@ -37392,6 +37714,77 @@ var init_site_audit_worker = __esm({
|
|
|
37392
37714
|
}
|
|
37393
37715
|
});
|
|
37394
37716
|
|
|
37717
|
+
// src/api/page-diff.ts
|
|
37718
|
+
function sha256Hex(value) {
|
|
37719
|
+
return (0, import_node_crypto14.createHash)("sha256").update(value).digest("hex");
|
|
37720
|
+
}
|
|
37721
|
+
function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
|
|
37722
|
+
if (value.length <= maxChars) return { value, truncated: false };
|
|
37723
|
+
return { value: value.slice(0, maxChars), truncated: true };
|
|
37724
|
+
}
|
|
37725
|
+
function changeLines(change) {
|
|
37726
|
+
const lines = change.value.split("\n");
|
|
37727
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
37728
|
+
return lines;
|
|
37729
|
+
}
|
|
37730
|
+
function diffPageContent(oldContent, newContent) {
|
|
37731
|
+
const changes = (0, import_diff.diffLines)(oldContent, newContent);
|
|
37732
|
+
const rawHunks = [];
|
|
37733
|
+
let linesAdded = 0;
|
|
37734
|
+
let linesRemoved = 0;
|
|
37735
|
+
let totalChangedLineCount = 0;
|
|
37736
|
+
for (const change of changes) {
|
|
37737
|
+
if (!change.added && !change.removed) continue;
|
|
37738
|
+
const lines = changeLines(change);
|
|
37739
|
+
totalChangedLineCount += lines.length;
|
|
37740
|
+
if (change.added) linesAdded += lines.length;
|
|
37741
|
+
if (change.removed) linesRemoved += lines.length;
|
|
37742
|
+
rawHunks.push({ type: change.added ? "added" : "removed", lines });
|
|
37743
|
+
}
|
|
37744
|
+
let hunksTruncated = false;
|
|
37745
|
+
const hunksWithinCount = rawHunks.length > MAX_DIFF_HUNKS ? rawHunks.slice(0, MAX_DIFF_HUNKS) : rawHunks;
|
|
37746
|
+
if (rawHunks.length > MAX_DIFF_HUNKS) hunksTruncated = true;
|
|
37747
|
+
const hunks = [];
|
|
37748
|
+
let emittedLines = 0;
|
|
37749
|
+
for (const hunk of hunksWithinCount) {
|
|
37750
|
+
if (emittedLines >= MAX_DIFF_LINES_PER_RESPONSE) {
|
|
37751
|
+
hunksTruncated = true;
|
|
37752
|
+
break;
|
|
37753
|
+
}
|
|
37754
|
+
const remaining = MAX_DIFF_LINES_PER_RESPONSE - emittedLines;
|
|
37755
|
+
if (hunk.lines.length > remaining) {
|
|
37756
|
+
hunks.push({ type: hunk.type, lines: hunk.lines.slice(0, remaining) });
|
|
37757
|
+
emittedLines += remaining;
|
|
37758
|
+
hunksTruncated = true;
|
|
37759
|
+
break;
|
|
37760
|
+
}
|
|
37761
|
+
hunks.push(hunk);
|
|
37762
|
+
emittedLines += hunk.lines.length;
|
|
37763
|
+
}
|
|
37764
|
+
const denominator = Math.max(oldContent.split("\n").length, newContent.split("\n").length, 1);
|
|
37765
|
+
const percentChanged = totalChangedLineCount > 0 ? Math.min(100, Math.round(totalChangedLineCount / denominator * 1e3) / 10) : 0;
|
|
37766
|
+
return {
|
|
37767
|
+
hunks,
|
|
37768
|
+
linesAdded,
|
|
37769
|
+
linesRemoved,
|
|
37770
|
+
percentChanged,
|
|
37771
|
+
hunksTruncated,
|
|
37772
|
+
hunksTruncatedReason: hunksTruncated ? `Showing ${emittedLines} of ${totalChangedLineCount} changed lines (capped at ${MAX_DIFF_HUNKS} hunks / ${MAX_DIFF_LINES_PER_RESPONSE} lines). Use contentHash/previousContentHash to confirm the full scope of change.` : null,
|
|
37773
|
+
totalChangedLineCount
|
|
37774
|
+
};
|
|
37775
|
+
}
|
|
37776
|
+
var import_node_crypto14, import_diff, MAX_SNAPSHOT_CONTENT_CHARS, MAX_DIFF_HUNKS, MAX_DIFF_LINES_PER_RESPONSE;
|
|
37777
|
+
var init_page_diff = __esm({
|
|
37778
|
+
"src/api/page-diff.ts"() {
|
|
37779
|
+
"use strict";
|
|
37780
|
+
import_node_crypto14 = require("crypto");
|
|
37781
|
+
import_diff = require("diff");
|
|
37782
|
+
MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
|
|
37783
|
+
MAX_DIFF_HUNKS = 200;
|
|
37784
|
+
MAX_DIFF_LINES_PER_RESPONSE = 2e3;
|
|
37785
|
+
}
|
|
37786
|
+
});
|
|
37787
|
+
|
|
37395
37788
|
// src/api/scrape-vault-sink.ts
|
|
37396
37789
|
async function depositScrapeToVault(user, opts) {
|
|
37397
37790
|
try {
|
|
@@ -37826,14 +38219,17 @@ async function processJob(job) {
|
|
|
37826
38219
|
running++;
|
|
37827
38220
|
try {
|
|
37828
38221
|
const opts = typeof job.options === "string" ? JSON.parse(job.options) : job.options;
|
|
37829
|
-
const result = await
|
|
37830
|
-
|
|
37831
|
-
|
|
37832
|
-
|
|
37833
|
-
|
|
37834
|
-
|
|
37835
|
-
|
|
37836
|
-
|
|
38222
|
+
const result = await runWithCostContext(
|
|
38223
|
+
{ op: opts.serpOnly ? "serp" : "paa", userId: Number(job.user_id) },
|
|
38224
|
+
() => harvest({
|
|
38225
|
+
...opts,
|
|
38226
|
+
kernelApiKey: browserServiceApiKey(),
|
|
38227
|
+
headless: true,
|
|
38228
|
+
format: "json",
|
|
38229
|
+
outputDir: "/tmp/paa-output-api",
|
|
38230
|
+
onAttemptEvent: createHarvestAttemptRecorder(job.id, job.user_id)
|
|
38231
|
+
})
|
|
38232
|
+
);
|
|
37837
38233
|
await completeJob(job.id, result);
|
|
37838
38234
|
const attempts = await listHarvestAttempts(job.id, job.user_id);
|
|
37839
38235
|
if (!opts.serpOnly && typeof opts.billingHoldMc === "number") {
|
|
@@ -37899,6 +38295,7 @@ var init_worker = __esm({
|
|
|
37899
38295
|
init_rates();
|
|
37900
38296
|
init_harvest_problems();
|
|
37901
38297
|
init_harvest_attempt_events();
|
|
38298
|
+
init_cost_context();
|
|
37902
38299
|
MAX_CONCURRENT = 2;
|
|
37903
38300
|
running = 0;
|
|
37904
38301
|
}
|
|
@@ -37977,9 +38374,12 @@ function opForCostPath(p) {
|
|
|
37977
38374
|
}
|
|
37978
38375
|
if (p.startsWith("/instagram/")) return "instagram";
|
|
37979
38376
|
if (p === "/reddit/thread") return "reddit_thread";
|
|
38377
|
+
if (p.startsWith("/trustpilot/")) return "trustpilot_reviews";
|
|
38378
|
+
if (p.startsWith("/g2/")) return "g2_reviews";
|
|
37980
38379
|
if (p.startsWith("/api/internal/site-architecture-auditor")) return "audit_site";
|
|
37981
38380
|
if (p.startsWith("/api/internal/memory")) return "memory_ai";
|
|
37982
38381
|
if (p.startsWith("/agent/")) return "browser_session";
|
|
38382
|
+
if (p.startsWith("/video/")) return "video_analysis";
|
|
37983
38383
|
return null;
|
|
37984
38384
|
}
|
|
37985
38385
|
function maybeProvisionInBackground(user, vaultCount) {
|
|
@@ -38089,6 +38489,7 @@ var init_server = __esm({
|
|
|
38089
38489
|
import_stripe2 = __toESM(require("stripe"), 1);
|
|
38090
38490
|
init_rates();
|
|
38091
38491
|
init_server_schemas();
|
|
38492
|
+
init_page_diff();
|
|
38092
38493
|
init_scrape_vault_sink();
|
|
38093
38494
|
init_scrape_blob_cleanup();
|
|
38094
38495
|
init_schemas3();
|
|
@@ -38811,16 +39212,20 @@ var init_server = __esm({
|
|
|
38811
39212
|
AbortSignal.timeout(syncTimeoutMs)
|
|
38812
39213
|
]);
|
|
38813
39214
|
try {
|
|
38814
|
-
const
|
|
38815
|
-
|
|
38816
|
-
|
|
38817
|
-
|
|
38818
|
-
|
|
38819
|
-
|
|
38820
|
-
|
|
38821
|
-
|
|
38822
|
-
|
|
38823
|
-
|
|
39215
|
+
const harvestCtx = currentCostContext();
|
|
39216
|
+
const result = await runWithCostContext(
|
|
39217
|
+
{ ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
|
|
39218
|
+
() => harvest({
|
|
39219
|
+
...options,
|
|
39220
|
+
kernelApiKey: browserServiceApiKey(),
|
|
39221
|
+
headless: true,
|
|
39222
|
+
format: "json",
|
|
39223
|
+
outputDir: "/tmp/paa-output-api",
|
|
39224
|
+
signal: syncSignal,
|
|
39225
|
+
softDeadlineMs: Date.now() + Math.floor(syncTimeoutMs * 0.8),
|
|
39226
|
+
onAttemptEvent: recordAttempt
|
|
39227
|
+
})
|
|
39228
|
+
);
|
|
38824
39229
|
await completeJob(jobId, result);
|
|
38825
39230
|
const attempts = await listHarvestAttempts(jobId, user.id);
|
|
38826
39231
|
if (!options.serpOnly) {
|
|
@@ -38988,6 +39393,91 @@ var init_server = __esm({
|
|
|
38988
39393
|
await releaseConcurrencyGate(gate.lockId);
|
|
38989
39394
|
}
|
|
38990
39395
|
});
|
|
39396
|
+
app.post("/diff-page", auth2, async (c) => {
|
|
39397
|
+
const raw = await c.req.json().catch(() => ({}));
|
|
39398
|
+
const bodyResult = DiffPageBodySchema.safeParse(raw);
|
|
39399
|
+
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
39400
|
+
const { url, allowLocal, resetBaseline } = bodyResult.data;
|
|
39401
|
+
if (!allowLocal) {
|
|
39402
|
+
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
39403
|
+
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
39404
|
+
} else {
|
|
39405
|
+
try {
|
|
39406
|
+
new URL(url);
|
|
39407
|
+
} catch {
|
|
39408
|
+
return c.json({ error: "Invalid URL" }, 400);
|
|
39409
|
+
}
|
|
39410
|
+
}
|
|
39411
|
+
const canonicalUrl = (() => {
|
|
39412
|
+
try {
|
|
39413
|
+
return new URL(url).href;
|
|
39414
|
+
} catch {
|
|
39415
|
+
return url;
|
|
39416
|
+
}
|
|
39417
|
+
})();
|
|
39418
|
+
const user = c.get("user");
|
|
39419
|
+
const gate = await acquireConcurrencyGate(user, "diff_page", {
|
|
39420
|
+
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
39421
|
+
metadata: { url: canonicalUrl }
|
|
39422
|
+
});
|
|
39423
|
+
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
39424
|
+
let debited = false;
|
|
39425
|
+
try {
|
|
39426
|
+
const { ok: dpOk, balance_mc: dpBal } = await debitMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, new URL(canonicalUrl).hostname);
|
|
39427
|
+
if (!dpOk) return c.json(insufficientBalanceResponse(dpBal, MC_COSTS.diff_page), 402);
|
|
39428
|
+
debited = true;
|
|
39429
|
+
const existingSnapshot = await getPageSnapshot(user.id, canonicalUrl);
|
|
39430
|
+
const previous = resetBaseline ? null : existingSnapshot;
|
|
39431
|
+
const kernelApiKey = browserServiceApiKey();
|
|
39432
|
+
const result = await extractKpo({ url: canonicalUrl, kernelApiKey });
|
|
39433
|
+
const currentContent = result.bodyMarkdown ?? "";
|
|
39434
|
+
const currentHash = sha256Hex(currentContent);
|
|
39435
|
+
const { value: storedContent, truncated: contentTruncated } = truncateForStorage(currentContent);
|
|
39436
|
+
const currentCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
39437
|
+
await upsertPageSnapshot(user.id, canonicalUrl, {
|
|
39438
|
+
contentHash: currentHash,
|
|
39439
|
+
content: storedContent,
|
|
39440
|
+
title: result.title ?? null,
|
|
39441
|
+
contentBytes: Buffer.byteLength(currentContent, "utf8"),
|
|
39442
|
+
truncated: contentTruncated
|
|
39443
|
+
});
|
|
39444
|
+
let status;
|
|
39445
|
+
let diff = { hunks: [], linesAdded: 0, linesRemoved: 0, percentChanged: 0, hunksTruncated: false, hunksTruncatedReason: null, totalChangedLineCount: 0 };
|
|
39446
|
+
if (!previous) {
|
|
39447
|
+
status = "baseline";
|
|
39448
|
+
diff.percentChanged = null;
|
|
39449
|
+
} else if (previous.contentHash === currentHash) {
|
|
39450
|
+
status = "unchanged";
|
|
39451
|
+
} else {
|
|
39452
|
+
status = "changed";
|
|
39453
|
+
diff = diffPageContent(previous.content, storedContent);
|
|
39454
|
+
}
|
|
39455
|
+
await logRequestEvent({ userId: user.id, source: "diff_page", status: "done", query: canonicalUrl, result: { status } });
|
|
39456
|
+
return c.json({
|
|
39457
|
+
url: canonicalUrl,
|
|
39458
|
+
title: result.title ?? null,
|
|
39459
|
+
status,
|
|
39460
|
+
isReset: !!resetBaseline && !!existingSnapshot,
|
|
39461
|
+
previousCheckedAt: previous?.checkedAt ?? null,
|
|
39462
|
+
currentCheckedAt,
|
|
39463
|
+
contentHash: currentHash,
|
|
39464
|
+
previousContentHash: previous?.contentHash ?? null,
|
|
39465
|
+
summary: { linesAdded: diff.linesAdded, linesRemoved: diff.linesRemoved, percentChanged: diff.percentChanged },
|
|
39466
|
+
hunks: diff.hunks,
|
|
39467
|
+
contentTruncated,
|
|
39468
|
+
hunksTruncated: diff.hunksTruncated,
|
|
39469
|
+
hunksTruncatedReason: diff.hunksTruncatedReason,
|
|
39470
|
+
totalChangedLineCount: diff.totalChangedLineCount
|
|
39471
|
+
});
|
|
39472
|
+
} catch (err) {
|
|
39473
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39474
|
+
if (debited) await creditMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, "failed call");
|
|
39475
|
+
await logRequestEvent({ userId: user.id, source: "diff_page", status: "failed", query: canonicalUrl, error: msg });
|
|
39476
|
+
return c.json({ error: msg }, 500);
|
|
39477
|
+
} finally {
|
|
39478
|
+
await releaseConcurrencyGate(gate.lockId);
|
|
39479
|
+
}
|
|
39480
|
+
});
|
|
38991
39481
|
app.post("/map-urls", auth2, async (c) => {
|
|
38992
39482
|
const raw = await c.req.json().catch(() => ({}));
|
|
38993
39483
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|