mcp-scraper 0.2.19 → 0.2.21

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.
Files changed (77) hide show
  1. package/README.md +36 -5
  2. package/dist/bin/api-server.cjs +1072 -349
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/browser-agent-stdio-server.cjs +1274 -75
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +4 -2
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +659 -210
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +108 -9
  12. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +2104 -566
  14. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js +7 -4
  16. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  17. package/dist/bin/mcp-scraper-install.cjs +3 -3
  18. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  19. package/dist/bin/mcp-scraper-install.js +2 -2
  20. package/dist/bin/mcp-stdio-server.cjs +726 -387
  21. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  22. package/dist/bin/mcp-stdio-server.js +3 -2
  23. package/dist/bin/mcp-stdio-server.js.map +1 -1
  24. package/dist/bin/paa-harvest.cjs +5 -3
  25. package/dist/bin/paa-harvest.cjs.map +1 -1
  26. package/dist/bin/paa-harvest.js +5 -2
  27. package/dist/bin/paa-harvest.js.map +1 -1
  28. package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
  29. package/dist/chunk-DBQDG7EH.js.map +1 -0
  30. package/dist/chunk-DUEW4EOO.js +214 -0
  31. package/dist/chunk-DUEW4EOO.js.map +1 -0
  32. package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
  33. package/dist/chunk-H2R232HK.js.map +1 -0
  34. package/dist/chunk-HS6OKELW.js +1786 -0
  35. package/dist/chunk-HS6OKELW.js.map +1 -0
  36. package/dist/{chunk-CQTAKXBN.js → chunk-IPW4LFOT.js} +11 -116
  37. package/dist/chunk-IPW4LFOT.js.map +1 -0
  38. package/dist/chunk-LFATOGDF.js +111 -0
  39. package/dist/chunk-LFATOGDF.js.map +1 -0
  40. package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
  41. package/dist/chunk-MLNCKQ2B.js.map +1 -0
  42. package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
  43. package/dist/chunk-O5J7E4VX.js.map +1 -0
  44. package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
  45. package/dist/chunk-WN7PBKMV.js +28 -0
  46. package/dist/chunk-WN7PBKMV.js.map +1 -0
  47. package/dist/chunk-XKUDVN2E.js +7 -0
  48. package/dist/chunk-XKUDVN2E.js.map +1 -0
  49. package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
  50. package/dist/index.cjs +5 -3
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +2 -2
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +3 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/{server-CRUSZA2Q.js → server-24XIQ6W6.js} +248 -40
  57. package/dist/server-24XIQ6W6.js.map +1 -0
  58. package/dist/{worker-OZSWIS3F.js → worker-SLQ375UG.js} +8 -5
  59. package/dist/{worker-OZSWIS3F.js.map → worker-SLQ375UG.js.map} +1 -1
  60. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
  61. package/docs/mcp-tool-craft-lint.generated.md +51 -0
  62. package/docs/mcp-tool-manifest.generated.json +1534 -0
  63. package/docs/specs/local-competitive-audit-spec.md +1 -2
  64. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
  65. package/package.json +4 -1
  66. package/dist/chunk-4UEVYYDW.js.map +0 -1
  67. package/dist/chunk-76P4DKWR.js +0 -793
  68. package/dist/chunk-76P4DKWR.js.map +0 -1
  69. package/dist/chunk-CQTAKXBN.js.map +0 -1
  70. package/dist/chunk-F44RBOJ5.js.map +0 -1
  71. package/dist/chunk-MCBOZ2JF.js.map +0 -1
  72. package/dist/chunk-RU2HZD55.js +0 -7
  73. package/dist/chunk-RU2HZD55.js.map +0 -1
  74. package/dist/chunk-TL7YTFLH.js.map +0 -1
  75. package/dist/server-CRUSZA2Q.js.map +0 -1
  76. /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
  77. /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
@@ -16,9 +16,11 @@ import {
16
16
  harvestProblemResponse,
17
17
  insufficientBalanceResponse,
18
18
  serializeHarvestProblem
19
- } from "./chunk-BSYPATSM.js";
19
+ } from "./chunk-SXTXMFEQ.js";
20
20
  import {
21
21
  SiteAuditJobRowSchema,
22
+ advanceWorkflowRunStep,
23
+ appendWorkflowArtifacts,
22
24
  cancelJob,
23
25
  checkRateLimit,
24
26
  claimMonthlyFreeRefresh,
@@ -48,6 +50,7 @@ import {
48
50
  getUserStats,
49
51
  getWorkflowArtifact,
50
52
  getWorkflowRun,
53
+ getWorkflowRunStepState,
51
54
  getWorkflowSchedule,
52
55
  ledgerExistsForOperation,
53
56
  ledgerExistsForStripePI,
@@ -74,47 +77,62 @@ import {
74
77
  setPassword,
75
78
  setStripeCustomerId,
76
79
  verifyPassword
77
- } from "./chunk-TL7YTFLH.js";
80
+ } from "./chunk-DBQDG7EH.js";
78
81
  import {
79
82
  BrowserDriver,
80
- MapsPlaceOptionsSchema,
81
- MapsSearchOptionsSchema,
82
83
  MapsSelectors,
83
- RawMapsAboutAttributeSchema,
84
- RawMapsHoursRowSchema,
85
- RawMapsOverviewSchema,
86
- RawMapsReviewStatsSchema,
87
- browserServiceApiKey,
88
- browserServiceProxyId,
89
84
  buildYouTubeChannelVideosUrl,
90
85
  deleteKernelProxyId,
91
86
  harvest,
92
87
  resolveKernelProxyId
93
- } from "./chunk-CQTAKXBN.js";
88
+ } from "./chunk-IPW4LFOT.js";
94
89
  import {
95
90
  CaptureSerpPageSnapshotsInputSchema,
91
+ CaptureSerpPageSnapshotsOutputSchema,
96
92
  CaptureSerpSnapshotInputSchema,
93
+ CaptureSerpSnapshotOutputSchema,
97
94
  HttpMcpToolExecutor,
98
95
  buildPaaExtractorMcpServer,
99
96
  configureReportSaving,
97
+ formatCaptureSerpPageSnapshots,
98
+ formatCaptureSerpSnapshot,
100
99
  harvestTimeoutBudget,
101
100
  liveWebToolAnnotations,
102
101
  outputBaseDir
103
- } from "./chunk-4UEVYYDW.js";
102
+ } from "./chunk-O5J7E4VX.js";
104
103
  import {
105
104
  CaptchaError,
106
105
  RECAPTCHA_INSTRUCTIONS,
107
106
  sanitizeVendorName
108
107
  } from "./chunk-M2S27J6Z.js";
108
+ import {
109
+ browserServiceApiKey,
110
+ browserServiceProfileName,
111
+ browserServiceProfileSaveChanges,
112
+ browserServiceProxyId
113
+ } from "./chunk-WN7PBKMV.js";
109
114
  import {
110
115
  csvRecords,
111
116
  listWorkflowDefinitions,
112
117
  rowsToCsv,
113
118
  runWorkflow,
119
+ runWorkflowStep,
114
120
  workflowDefinition,
115
- workflowOutputBaseDir
116
- } from "./chunk-F44RBOJ5.js";
117
- import "./chunk-RU2HZD55.js";
121
+ workflowOutputBaseDir,
122
+ workflowStepCount,
123
+ workflowSupportsSteps
124
+ } from "./chunk-H2R232HK.js";
125
+ import {
126
+ DEFAULT_MAPS_PROXY_MODE,
127
+ DEFAULT_PROXY_MODE,
128
+ MapsPlaceOptionsSchema,
129
+ MapsSearchOptionsSchema,
130
+ RawMapsAboutAttributeSchema,
131
+ RawMapsHoursRowSchema,
132
+ RawMapsOverviewSchema,
133
+ RawMapsReviewStatsSchema
134
+ } from "./chunk-LFATOGDF.js";
135
+ import "./chunk-XKUDVN2E.js";
118
136
 
119
137
  // src/api/outbound-sanitize.ts
120
138
  var KEY_RENAMES = {
@@ -2463,6 +2481,10 @@ var posts = [
2463
2481
  }
2464
2482
  ];
2465
2483
 
2484
+ // src/attribution-pixel.ts
2485
+ var ATTRIBUTION_PIXEL_SRC = "https://hyros-attribution-crm.vercel.app/api/track/sdk?token=35c27ba33dd0c6cc640f9fef415ee3dc9ead67fefe99d269be602ee269900e25";
2486
+ var ATTRIBUTION_PIXEL_SCRIPT = `<script src="${ATTRIBUTION_PIXEL_SRC}" async></script>`;
2487
+
2466
2488
  // src/blog/template.ts
2467
2489
  function faqJsonLd(post) {
2468
2490
  const firstFive = post.sections.flatMap((s) => s.cards).slice(0, 5);
@@ -3257,6 +3279,7 @@ function renderPost(post) {
3257
3279
  <script type="application/ld+json">${faqJsonLd(post)}</script>
3258
3280
  <script type="application/ld+json">${breadcrumbJsonLd(post)}</script>
3259
3281
 
3282
+ ${ATTRIBUTION_PIXEL_SCRIPT}
3260
3283
  <style>${CSS}</style>
3261
3284
  </head>
3262
3285
  <body>
@@ -7245,6 +7268,7 @@ var YouTubeHarvestOptionsSchema = z10.object({
7245
7268
  downloadMp3: z10.boolean().default(false),
7246
7269
  outputDir: z10.string().default("./yt-output"),
7247
7270
  kernelApiKey: z10.string().optional(),
7271
+ kernelProxyId: z10.string().optional(),
7248
7272
  headless: z10.boolean().default(false)
7249
7273
  }).refine(
7250
7274
  (o) => o.mode === "search" ? !!o.query : !!o.channelHandle,
@@ -7498,6 +7522,7 @@ var YouTubeExtractor = class {
7498
7522
  const config = {
7499
7523
  headless: options.headless,
7500
7524
  kernelApiKey: options.kernelApiKey,
7525
+ kernelProxyId: options.kernelProxyId,
7501
7526
  viewport: { width: 1280, height: 800 },
7502
7527
  locale: "en-US"
7503
7528
  };
@@ -7697,7 +7722,11 @@ async function ytHarvest(rawOptions) {
7697
7722
  throw new Error("A browser backend API key is required \u2014 YouTube harvesting requires a stealth session.");
7698
7723
  }
7699
7724
  const raw = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
7700
- const options = YouTubeHarvestOptionsSchema.parse({ kernelApiKey, ...raw });
7725
+ const options = YouTubeHarvestOptionsSchema.parse({
7726
+ kernelApiKey,
7727
+ kernelProxyId: browserServiceProxyId(),
7728
+ ...raw
7729
+ });
7701
7730
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
7702
7731
  try {
7703
7732
  const result = await extractOnce(options);
@@ -7789,10 +7818,11 @@ function parseTimedtextXml(xml) {
7789
7818
  async function fetchViaKernelInnertube(videoId) {
7790
7819
  const kernelApiKey = browserServiceApiKey();
7791
7820
  if (!kernelApiKey) return null;
7821
+ const kernelProxyId = browserServiceProxyId();
7792
7822
  const driver = new BrowserDriver();
7793
7823
  const start = Date.now();
7794
7824
  try {
7795
- await driver.launch({ kernelApiKey, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
7825
+ await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
7796
7826
  const page = driver.getPage();
7797
7827
  await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
7798
7828
  await page.waitForFunction(
@@ -7887,10 +7917,10 @@ async function captureAudioViaMediaRecorder(page) {
7887
7917
  if (!result.ok || !result.base64 || !result.mimeType) return null;
7888
7918
  return { bytes: Buffer.from(result.base64, "base64"), mimeType: result.mimeType };
7889
7919
  }
7890
- async function attemptKernelWhisper(videoId, kernelApiKey, falKey, start) {
7920
+ async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start) {
7891
7921
  const driver = new BrowserDriver();
7892
7922
  try {
7893
- await driver.launch({ kernelApiKey, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
7923
+ await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
7894
7924
  const page = driver.getPage();
7895
7925
  await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
7896
7926
  await page.waitForFunction(
@@ -7932,12 +7962,13 @@ async function attemptKernelWhisper(videoId, kernelApiKey, falKey, start) {
7932
7962
  }
7933
7963
  async function fetchViaKernelWhisper(videoId) {
7934
7964
  const kernelApiKey = browserServiceApiKey();
7965
+ const kernelProxyId = browserServiceProxyId();
7935
7966
  const falKey = process.env.FAL_KEY;
7936
7967
  if (!kernelApiKey || !falKey) return null;
7937
7968
  const start = Date.now();
7938
7969
  for (let attempt = 0; attempt < 4; attempt++) {
7939
7970
  if (attempt > 0) await new Promise((r) => setTimeout(r, 2e3 * attempt));
7940
- const result = await attemptKernelWhisper(videoId, kernelApiKey, falKey, start);
7971
+ const result = await attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start);
7941
7972
  if (result) return result;
7942
7973
  }
7943
7974
  return null;
@@ -8341,6 +8372,7 @@ function buildTranscriptHtml(result) {
8341
8372
  (ch) => `<div class="chunk"><span class="ts">${fmtSecs(ch.timestamp[0])} \u2192 ${fmtSecs(ch.timestamp[1])}</span><span class="ct">${esc(ch.text.trim())}</span></div>`
8342
8373
  ).join("\n");
8343
8374
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8375
+ ${ATTRIBUTION_PIXEL_SCRIPT}
8344
8376
  <style>body{font-family:system-ui,sans-serif;max-width:800px;margin:1.5rem auto;padding:0 1rem;color:#111;line-height:1.7}
8345
8377
  h1{font-size:1.2rem;margin-bottom:.25rem}.meta{color:#666;font-size:.85rem;margin-bottom:1.5rem}
8346
8378
  .full-text{background:#f5f5f3;border-radius:8px;padding:1rem 1.25rem;font-size:.875rem;white-space:pre-wrap;margin-bottom:2rem;word-break:break-word}
@@ -9396,9 +9428,8 @@ async function kernelLaunchOptsResidential() {
9396
9428
  try {
9397
9429
  const resolution = await resolveKernelProxyId({
9398
9430
  kernelApiKey: browserServiceApiKey(),
9399
- proxyMode: "location",
9431
+ proxyMode: DEFAULT_PROXY_MODE,
9400
9432
  configuredKernelProxyId: browserServiceProxyId(),
9401
- location: "New York, NY",
9402
9433
  gl: "us"
9403
9434
  });
9404
9435
  if (resolution.kernelProxyId) proxyId = resolution.kernelProxyId;
@@ -10845,7 +10876,7 @@ var DirectoryWorkflowOptionsSchema = z15.object({
10845
10876
  saveCsv: z15.boolean().default(true),
10846
10877
  gl: z15.string().length(2).default("us"),
10847
10878
  hl: z15.string().length(2).default("en"),
10848
- proxyMode: z15.enum(["location", "configured", "none"]).default("location"),
10879
+ proxyMode: z15.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
10849
10880
  proxyZip: z15.string().regex(/^\d{5}$/).optional(),
10850
10881
  debug: z15.boolean().default(false),
10851
10882
  headless: z15.boolean().default(true),
@@ -11302,6 +11333,88 @@ async function executeWorkflowRun(input) {
11302
11333
  throw err;
11303
11334
  }
11304
11335
  }
11336
+ async function runWorkflowStepAndPersist(input) {
11337
+ const runId = input.run.id;
11338
+ const workflowId = input.run.workflow_id;
11339
+ await markWorkflowRunRunning(runId);
11340
+ let result;
11341
+ try {
11342
+ result = await runWorkflowStep(workflowId, input.workflowInput, {
11343
+ stepIndex: input.stepIndex,
11344
+ state: input.priorState ?? void 0,
11345
+ options: {
11346
+ apiKey: input.user.api_key,
11347
+ apiUrl: input.apiUrl,
11348
+ outputDir: hostedWorkflowOutputDir(),
11349
+ runId,
11350
+ headers: input.concurrencyLockId ? { "x-mcp-scraper-concurrency-lock": input.concurrencyLockId } : void 0
11351
+ }
11352
+ });
11353
+ } catch (err) {
11354
+ const message = err instanceof Error ? err.message : String(err);
11355
+ await failWorkflowRunRecord(runId, message, { error: message });
11356
+ const failedRun = await getWorkflowRun(runId, input.user.id);
11357
+ if (failedRun) await deliverWorkflowWebhook({ userId: input.user.id, run: failedRun, scheduleId: failedRun.schedule_id, webhookUrl: input.webhookUrl ?? null, summary: null, artifacts: [] });
11358
+ throw err;
11359
+ }
11360
+ if (result.artifacts.length) {
11361
+ await appendWorkflowArtifacts(runId, input.user.id, result.artifacts.map((a) => ({
11362
+ kind: a.kind,
11363
+ label: a.label,
11364
+ path: a.path,
11365
+ bytes: a.bytes,
11366
+ rows: a.rows,
11367
+ content: a.content
11368
+ })));
11369
+ }
11370
+ const logEntry = {
11371
+ stepIndex: result.stepIndex,
11372
+ stepId: result.stepId,
11373
+ stepTitle: result.stepTitle,
11374
+ output: result.output,
11375
+ warnings: result.warnings,
11376
+ at: (/* @__PURE__ */ new Date()).toISOString()
11377
+ };
11378
+ const status = result.isLast ? result.summary?.status ?? "succeeded" : "running";
11379
+ await advanceWorkflowRunStep(runId, {
11380
+ stepIndex: result.stepIndex + 1,
11381
+ totalSteps: result.totalSteps,
11382
+ state: result.state,
11383
+ status,
11384
+ logEntry,
11385
+ appendLog: input.priorLog
11386
+ });
11387
+ if (result.isLast && result.summary) {
11388
+ const manifest = await readManifestFromSummary(result.summary);
11389
+ await completeWorkflowRunRecord(runId, result.summary.status, manifest ?? { summary: result.summary });
11390
+ }
11391
+ const run = await getWorkflowRun(runId, input.user.id);
11392
+ const artifacts = await listWorkflowArtifacts(runId, input.user.id);
11393
+ if (result.isLast && run) {
11394
+ await deliverWorkflowWebhook({ userId: input.user.id, run, scheduleId: run.schedule_id, webhookUrl: input.webhookUrl ?? null, summary: result.summary, artifacts });
11395
+ }
11396
+ const definition = workflowDefinition(workflowId);
11397
+ const nextIndex = result.stepIndex + 1;
11398
+ const nextStep = result.isLast ? null : {
11399
+ index: nextIndex,
11400
+ id: definition.steps?.[nextIndex]?.id ?? null,
11401
+ title: definition.steps?.[nextIndex]?.title ?? null
11402
+ };
11403
+ return {
11404
+ run: run ? exposeRun(input.c, run, artifacts) : null,
11405
+ summary: result.isLast ? result.summary : null,
11406
+ step: {
11407
+ index: result.stepIndex,
11408
+ id: result.stepId,
11409
+ title: result.stepTitle,
11410
+ totalSteps: result.totalSteps,
11411
+ output: result.output,
11412
+ warnings: result.warnings
11413
+ },
11414
+ nextStep,
11415
+ done: result.isLast
11416
+ };
11417
+ }
11305
11418
  workflowApp.get("/definitions", createApiKeyAuth(), (c) => {
11306
11419
  return c.json({ workflows: listWorkflowDefinitions() });
11307
11420
  });
@@ -11324,7 +11437,29 @@ workflowApp.post("/run", createApiKeyAuth(), async (c) => {
11324
11437
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11325
11438
  let run = null;
11326
11439
  try {
11327
- run = await createWorkflowRun({ userId: user.id, workflowId: parsed.data.workflowId, workflowInput: input });
11440
+ const stepwise = workflowSupportsSteps(parsed.data.workflowId);
11441
+ run = await createWorkflowRun({
11442
+ userId: user.id,
11443
+ workflowId: parsed.data.workflowId,
11444
+ workflowInput: input,
11445
+ webhookUrl,
11446
+ totalSteps: stepwise ? workflowStepCount(parsed.data.workflowId) : 1
11447
+ });
11448
+ if (stepwise) {
11449
+ const response = await runWorkflowStepAndPersist({
11450
+ c,
11451
+ user,
11452
+ run,
11453
+ workflowInput: input,
11454
+ stepIndex: 0,
11455
+ priorState: null,
11456
+ priorLog: [],
11457
+ apiUrl: originFromUrl(c.req.url),
11458
+ webhookUrl,
11459
+ concurrencyLockId: gate.lockId
11460
+ });
11461
+ return c.json(response);
11462
+ }
11328
11463
  const executed = await executeWorkflowRun({
11329
11464
  runId: run.id,
11330
11465
  user,
@@ -11334,7 +11469,7 @@ workflowApp.post("/run", createApiKeyAuth(), async (c) => {
11334
11469
  webhookUrl,
11335
11470
  concurrencyLockId: gate.lockId
11336
11471
  });
11337
- return c.json({ run: exposeRun(c, executed.run, executed.artifacts), summary: executed.summary });
11472
+ return c.json({ run: exposeRun(c, executed.run, executed.artifacts), summary: executed.summary, done: true });
11338
11473
  } catch (err) {
11339
11474
  const failed = run ? await getWorkflowRun(run.id, user.id) : null;
11340
11475
  return c.json({ run: failed, error: err instanceof Error ? err.message : String(err) }, 500);
@@ -11342,6 +11477,50 @@ workflowApp.post("/run", createApiKeyAuth(), async (c) => {
11342
11477
  await releaseConcurrencyGate(gate.lockId);
11343
11478
  }
11344
11479
  });
11480
+ var TERMINAL_RUN_STATUSES = ["succeeded", "partial", "failed", "cancelled"];
11481
+ workflowApp.post("/runs/:id/step", createApiKeyAuth(), async (c) => {
11482
+ const user = c.get("user");
11483
+ const run = await getWorkflowRun(c.req.param("id"), user.id);
11484
+ if (!run) return c.json({ error: "Run not found" }, 404);
11485
+ if (!workflowSupportsSteps(run.workflow_id)) {
11486
+ return c.json({ error: `Workflow "${run.workflow_id}" does not support stepwise execution` }, 400);
11487
+ }
11488
+ const artifactsNow = await listWorkflowArtifacts(run.id, user.id);
11489
+ if (TERMINAL_RUN_STATUSES.includes(run.status)) {
11490
+ return c.json({ run: exposeRun(c, run, artifactsNow), done: true, error: `Run is already ${run.status}` }, 409);
11491
+ }
11492
+ const stepState = await getWorkflowRunStepState(run.id, user.id);
11493
+ const stepIndex = stepState?.stepIndex ?? run.step_index;
11494
+ const totalSteps = workflowStepCount(run.workflow_id);
11495
+ if (stepIndex >= totalSteps) {
11496
+ return c.json({ run: exposeRun(c, run, artifactsNow), done: true, error: "No remaining steps" }, 409);
11497
+ }
11498
+ const gate = await acquireConcurrencyGate(user, "workflow_run", {
11499
+ ttlSeconds: 60 * 60,
11500
+ metadata: { workflowId: run.workflow_id, step: stepIndex }
11501
+ });
11502
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11503
+ try {
11504
+ const response = await runWorkflowStepAndPersist({
11505
+ c,
11506
+ user,
11507
+ run,
11508
+ workflowInput: run.input,
11509
+ stepIndex,
11510
+ priorState: stepState?.state ?? null,
11511
+ priorLog: stepState?.stepLog ?? run.step_log,
11512
+ apiUrl: originFromUrl(c.req.url),
11513
+ webhookUrl: run.webhook_url,
11514
+ concurrencyLockId: gate.lockId
11515
+ });
11516
+ return c.json(response);
11517
+ } catch (err) {
11518
+ const failed = await getWorkflowRun(run.id, user.id);
11519
+ return c.json({ run: failed, error: err instanceof Error ? err.message : String(err) }, 500);
11520
+ } finally {
11521
+ await releaseConcurrencyGate(gate.lockId);
11522
+ }
11523
+ });
11345
11524
  workflowApp.get("/runs", createApiKeyAuth(), async (c) => {
11346
11525
  const user = c.get("user");
11347
11526
  const runs = await listWorkflowRuns(user.id);
@@ -11360,14 +11539,17 @@ workflowApp.get("/runs/:id/artifacts/:artifactId", createApiKeyAuth(), async (c)
11360
11539
  if (!run) return c.json({ error: "Run not found" }, 404);
11361
11540
  const artifact = await getWorkflowArtifact(c.req.param("artifactId"), user.id);
11362
11541
  if (!artifact || artifact.run_id !== run.id) return c.json({ error: "Artifact not found" }, 404);
11542
+ const filename = artifact.path.split("/").pop() || artifact.id;
11543
+ const headers = {
11544
+ "Content-Type": artifact.content_type,
11545
+ "Content-Disposition": `attachment; filename="${filename}"`
11546
+ };
11547
+ if (artifact.content != null) {
11548
+ return new Response(artifact.content, { headers });
11549
+ }
11363
11550
  try {
11364
11551
  const content = await readFile2(artifact.path);
11365
- return new Response(content, {
11366
- headers: {
11367
- "Content-Type": artifact.content_type,
11368
- "Content-Disposition": `attachment; filename="${artifact.path.split("/").pop() || artifact.id}"`
11369
- }
11370
- });
11552
+ return new Response(content, { headers });
11371
11553
  } catch {
11372
11554
  return c.json({ error: "Artifact file is no longer available" }, 410);
11373
11555
  }
@@ -11905,7 +12087,7 @@ var SerpIntelligenceCaptureBodySchema = z17.object({
11905
12087
  gl: z17.string().trim().length(2).default("us"),
11906
12088
  hl: z17.string().trim().length(2).default("en"),
11907
12089
  device: z17.enum(SerpIntelligenceDeviceValues).default("desktop"),
11908
- proxyMode: z17.enum(SerpIntelligenceProxyModeValues).default("location"),
12090
+ proxyMode: z17.enum(SerpIntelligenceProxyModeValues).default(DEFAULT_PROXY_MODE),
11909
12091
  proxyZip: z17.string().regex(/^\d{5}$/).optional(),
11910
12092
  pages: z17.number().int().min(1).max(2).default(1),
11911
12093
  debug: z17.boolean().default(false),
@@ -12437,16 +12619,18 @@ var mcpApp = new Hono9();
12437
12619
  function registerSerpIntelligenceCaptureTools(server, executor) {
12438
12620
  server.registerTool("capture_serp_snapshot", {
12439
12621
  title: "SERP Intelligence Snapshot",
12440
- description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location, infer gl/hl, use proxyMode location for localized US residential evidence; location mode creates fresh proxy IDs across retries, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear or location evidence is wrong. Use configured only for the static residential proxy, and none only for direct-network debugging. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
12622
+ description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location and infer gl/hl. Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting for the highest general success rate. Use proxyMode location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
12441
12623
  inputSchema: CaptureSerpSnapshotInputSchema,
12624
+ outputSchema: CaptureSerpSnapshotOutputSchema,
12442
12625
  annotations: liveWebToolAnnotations("SERP Intelligence Snapshot")
12443
- }, async (input) => executor.captureSerpSnapshot(input));
12626
+ }, async (input) => formatCaptureSerpSnapshot(await executor.captureSerpSnapshot(input), input));
12444
12627
  server.registerTool("capture_serp_page_snapshots", {
12445
12628
  title: "SERP Intelligence Page Snapshots",
12446
12629
  description: "Capture public ranking-page evidence through POST /serp-intelligence/page-snapshots, the same product page snapshot path used by Phoenix. Provide urls for simple captures or targets when preserving organic, AI citation, local-pack, configured target, or site-subject source metadata. Private IPs, localhost, file URLs, and internal URLs are rejected by the service. Use timeoutMs for slow pages and debug true for sanitized proxy/browser diagnostics.",
12447
12630
  inputSchema: CaptureSerpPageSnapshotsInputSchema,
12631
+ outputSchema: CaptureSerpPageSnapshotsOutputSchema,
12448
12632
  annotations: liveWebToolAnnotations("SERP Intelligence Page Snapshots")
12449
- }, async (input) => executor.captureSerpPageSnapshots(input));
12633
+ }, async (input) => formatCaptureSerpPageSnapshots(await executor.captureSerpPageSnapshots(input), input));
12450
12634
  }
12451
12635
  mcpApp.all("/", async (c) => {
12452
12636
  try {
@@ -12639,14 +12823,35 @@ function client() {
12639
12823
  if (!apiKey) throw new Error("Browser backend API key is required");
12640
12824
  return new Kernel3({ apiKey });
12641
12825
  }
12826
+ function isProfileConflict(err) {
12827
+ const message = err instanceof Error ? err.message : String(err);
12828
+ return /\b409\b|conflict|already exists/i.test(message);
12829
+ }
12830
+ async function ensureProfile(k, name) {
12831
+ try {
12832
+ await k.profiles.create({ name });
12833
+ } catch (err) {
12834
+ if (!isProfileConflict(err)) throw err;
12835
+ }
12836
+ }
12642
12837
  async function createSession(opts = {}) {
12643
12838
  const k = client();
12644
12839
  const resolvedProxyId = opts.proxyId ?? browserServiceProxyId();
12840
+ const resolvedProfileName = opts.profileName ?? browserServiceProfileName();
12841
+ const resolvedSaveProfileChanges = opts.saveProfileChanges ?? browserServiceProfileSaveChanges();
12842
+ if (resolvedProfileName && resolvedSaveProfileChanges === true) {
12843
+ await ensureProfile(k, resolvedProfileName);
12844
+ }
12645
12845
  const browser = await k.browsers.create({
12646
12846
  stealth: opts.stealth ?? true,
12647
12847
  timeout_seconds: opts.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS,
12648
12848
  ...resolvedProxyId ? { proxy_id: resolvedProxyId } : {},
12649
- ...opts.profileName ? { profile: { name: opts.profileName } } : {}
12849
+ ...resolvedProfileName ? {
12850
+ profile: {
12851
+ name: resolvedProfileName,
12852
+ ...typeof resolvedSaveProfileChanges === "boolean" ? { save_changes: resolvedSaveProfileChanges } : {}
12853
+ }
12854
+ } : {}
12650
12855
  });
12651
12856
  const runtimeSessionId = browser.session_id;
12652
12857
  if (opts.disableDefaultProxy) {
@@ -13029,6 +13234,7 @@ function buildBrowserAgentRoutes() {
13029
13234
  timeoutSeconds,
13030
13235
  proxyId: typeof body.proxy_id === "string" ? body.proxy_id : void 0,
13031
13236
  profileName: typeof body.profile === "string" ? body.profile : void 0,
13237
+ saveProfileChanges: typeof body.save_profile_changes === "boolean" ? body.save_profile_changes : void 0,
13032
13238
  disableDefaultProxy: body.disable_default_proxy === true,
13033
13239
  viewport: body.viewport && typeof body.viewport === "object" ? body.viewport : void 0
13034
13240
  });
@@ -13388,6 +13594,7 @@ function renderConsoleHtml(initialSessionId) {
13388
13594
  .gate button { width: 100%; }
13389
13595
  .muted { color: #6b7689; font-size: 12px; }
13390
13596
  </style>
13597
+ ${ATTRIBUTION_PIXEL_SCRIPT}
13391
13598
  </head>
13392
13599
  <body>
13393
13600
  <div id="app"></div>
@@ -14103,7 +14310,7 @@ app.post("/harvest", auth2, async (c) => {
14103
14310
  gl: body.gl ?? "us",
14104
14311
  hl: body.hl ?? "en",
14105
14312
  device: body.device ?? "desktop",
14106
- proxyMode: body.proxyMode ?? "location",
14313
+ proxyMode: body.proxyMode ?? DEFAULT_PROXY_MODE,
14107
14314
  proxyZip: body.proxyZip,
14108
14315
  debug: body.debug ?? false,
14109
14316
  serpOnly: body.serpOnly ?? false,
@@ -14139,7 +14346,7 @@ app.post("/harvest/sync", auth2, async (c) => {
14139
14346
  gl: body.gl ?? "us",
14140
14347
  hl: body.hl ?? "en",
14141
14348
  device: body.device ?? "desktop",
14142
- proxyMode: body.proxyMode ?? "location",
14349
+ proxyMode: body.proxyMode ?? DEFAULT_PROXY_MODE,
14143
14350
  proxyZip: body.proxyZip,
14144
14351
  debug: body.debug ?? false,
14145
14352
  serpOnly: body.serpOnly ?? false,
@@ -14605,7 +14812,7 @@ app.get("/cron/tick", async (c) => {
14605
14812
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
14606
14813
  return c.json({ error: "Unauthorized" }, 401);
14607
14814
  }
14608
- const { drainQueue } = await import("./worker-OZSWIS3F.js");
14815
+ const { drainQueue } = await import("./worker-SLQ375UG.js");
14609
14816
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
14610
14817
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
14611
14818
  const [results, sweepResult] = await Promise.all([
@@ -14684,6 +14891,7 @@ app.get("/font-test", (c) => {
14684
14891
  .i6 em{font-family:"GTF";font-weight:700;font-style:italic}
14685
14892
  .i7 em{font-family:"GTF";font-weight:900;font-style:italic}
14686
14893
  </style>
14894
+ ${ATTRIBUTION_PIXEL_SCRIPT}
14687
14895
  </head>
14688
14896
  <body>
14689
14897
 
@@ -14733,4 +14941,4 @@ app.get("/blog/:slug/", (c) => {
14733
14941
  export {
14734
14942
  app
14735
14943
  };
14736
- //# sourceMappingURL=server-CRUSZA2Q.js.map
14944
+ //# sourceMappingURL=server-24XIQ6W6.js.map