@detiq/crawler 1.1.20 → 1.1.22

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/Dockerfile CHANGED
@@ -36,6 +36,11 @@ COPY packages/vscode-extension/package.json packages/vscode-extension/
36
36
  COPY packages/web/package.json packages/web/
37
37
  RUN npm ci --include=dev
38
38
 
39
+ # Patch Stagehand's act() schema to add `additionalProperties: false` on the
40
+ # inner anyOf[0] object. OpenAI strict mode (v1/responses) requires it on every
41
+ # nested object; Stagehand ≤3.6 omits it, causing a 400 "invalid_json_schema".
42
+ RUN node -e "const fs=require('fs');['packages/crawler/node_modules/@browserbasehq/stagehand/dist/cjs/lib/inference.js','packages/crawler/node_modules/@browserbasehq/stagehand/dist/esm/lib/inference.js'].forEach(f=>{if(!fs.existsSync(f)){console.log('skip',f);return;}let c=fs.readFileSync(f,'utf8');if(c.includes('.strict()')){console.log('already patched',f);return;}c=c.replace(/\\.nullable\\(\\)\\n(\\s+)\\.describe\\(\"The element to act on/g,'\.strict()\n\$1.nullable()\n\$1.describe(\"The element to act on');fs.writeFileSync(f,c);console.log('patched',f);});"
43
+
39
44
  # Re-install chromium after npm ci so the browser revision matches the
40
45
  # exact playwright version resolved by the lockfile (not the base image's
41
46
  # bundled version, which may differ if the lockfile was updated).
package/dist/crawler.js CHANGED
@@ -73,7 +73,7 @@ const _crawlLLMCache = /* @__PURE__ */ new Map();
73
73
  async function getCachedCrawlLLM(tenantId) {
74
74
  if (_crawlLLMCache.has(tenantId)) return _crawlLLMCache.get(tenantId);
75
75
  const cfg = await getTaskAgentLLMConfig(tenantId, "crawl").catch(() => null);
76
- _crawlLLMCache.set(tenantId, cfg);
76
+ if (cfg) _crawlLLMCache.set(tenantId, cfg);
77
77
  return cfg;
78
78
  }
79
79
  function getGitInfo() {
@@ -88,7 +88,7 @@ function getGitInfo() {
88
88
  const ALLOWED_NUMERIC_PATHS = /* @__PURE__ */ new Set(["/2fa", "/404", "/500", "/403", "/401"]);
89
89
  const UUID_PATH_SEGMENT_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
90
90
  const CUID_PATH_SEGMENT_RE = /^c[a-z0-9]{20,}$/i;
91
- const NUMERIC_ID_PATH_SEGMENT_RE = /^\d[\d-]*$/;
91
+ const NUMERIC_ID_PATH_SEGMENT_RE = /^\d{4,}$/;
92
92
  const COMPOUND_NUMERIC_ID_PATH_SEGMENT_RE = /^\d+-\d+$/;
93
93
  function normalizePathForNumericGuard(pathname) {
94
94
  const normalized = pathname.replace(/\/+$/, "") || "/";
@@ -374,6 +374,7 @@ const TwoFASchema = z.object({
374
374
  has2FA: z.boolean(),
375
375
  fieldLabel: z.string().optional()
376
376
  });
377
+ const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
377
378
  async function crawlProject(opts, onProgress) {
378
379
  const tuning = await getCrawlTuning();
379
380
  if (opts.screenshotDir) await fs.mkdir(opts.screenshotDir, { recursive: true });
@@ -1597,6 +1598,7 @@ async function crawlProject(opts, onProgress) {
1597
1598
  };
1598
1599
  const edges = [];
1599
1600
  const pendingTransitions = [];
1601
+ const pendingSummaryWrites = [];
1600
1602
  const discovered = [];
1601
1603
  const performanceMetrics = [];
1602
1604
  const seenDomHashes = new Set(opts.knownDomHashes ?? []);
@@ -1681,7 +1683,6 @@ async function crawlProject(opts, onProgress) {
1681
1683
  const name = pathParts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).replace(/[-_]/g, " ")).join(" \u2014 ") || "Home";
1682
1684
  const screen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, name, url, opts.jobId, discoveryMethodFor(url));
1683
1685
  visited.set(canonUrl, screen.id);
1684
- const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
1685
1686
  const lwBodyText = await workerPage.evaluate(() => document.body?.innerText ?? "").catch(() => "");
1686
1687
  if (INLINE_AUTH_WALL_RE.test(lwBodyText)) {
1687
1688
  logger.warn({ url, screenId: screen.id }, "[lite-worker] Inline session-expired content detected \u2014 marking requiresAuth");
@@ -2875,9 +2876,9 @@ async function crawlProject(opts, onProgress) {
2875
2876
  })();
2876
2877
  }
2877
2878
  }
2878
- const INLINE_AUTH_WALL_RE = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
2879
+ const INLINE_AUTH_WALL_RE2 = /your login session (was|has been) expired|session (has )?expired[^.]*click|please (log ?in|sign ?in) to continue|you (are|were) logged out|your session has timed out/i;
2879
2880
  const pageBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
2880
- if (INLINE_AUTH_WALL_RE.test(pageBodyText)) {
2881
+ if (INLINE_AUTH_WALL_RE2.test(pageBodyText)) {
2881
2882
  logger.warn({ url, screenId: screen.id }, "[crawler] Inline session-expired content detected \u2014 skipping DOM snapshot, marking requiresAuth");
2882
2883
  AppBrain.updateScreenRequiresAuth(opts.tenantId, screen.id, true).catch(() => {
2883
2884
  });
@@ -2932,6 +2933,7 @@ async function crawlProject(opts, onProgress) {
2932
2933
  const summaryPromise = (async () => {
2933
2934
  try {
2934
2935
  const llmCfg = await getCachedCrawlLLM(opts.tenantId);
2936
+ if (!llmCfg) return void 0;
2935
2937
  const snippet = extracted.markdown.slice(0, 1200);
2936
2938
  if (snippet.trim().length < 80) return void 0;
2937
2939
  const summarySeed = deterministicSeed(`${opts.tenantId}:${opts.projectId ?? ""}:page-summary:${url}`);
@@ -2972,10 +2974,15 @@ ${snippet}`,
2972
2974
  markdown: fitMarkdown ?? extracted.markdown,
2973
2975
  heading: extracted.title || name
2974
2976
  }).catch((err) => console.warn("[crawler] markdown extraction save failed:", err));
2975
- summaryPromise.then((aiSummary) => {
2976
- if (aiSummary) AppBrain.updateScreenAiSummary(opts.tenantId, screen.id, aiSummary).catch(() => {
2977
- });
2978
- });
2977
+ pendingSummaryWrites.push(
2978
+ summaryPromise.then((aiSummary) => {
2979
+ if (aiSummary) AppBrain.updateScreenAiSummary(opts.tenantId, screen.id, aiSummary).catch((e) => {
2980
+ logger.warn({ screenId: screen.id, err: String(e?.message ?? e) }, "[crawler] aiSummary DB save failed");
2981
+ });
2982
+ }).catch((e) => {
2983
+ logger.warn({ screenId: screen.id, err: String(e?.message ?? e) }, "[crawler] aiSummary generation failed");
2984
+ })
2985
+ );
2979
2986
  }
2980
2987
  const structuredJson = summarizeStructuredElements(atElements);
2981
2988
  const artifact = buildCrawlArtifactEnvelope({
@@ -3413,7 +3420,7 @@ ${snippet}`,
3413
3420
  if (liteWorkerPromises.length > 0) await Promise.all(liteWorkerPromises);
3414
3421
  if (opts.discoveryMode) {
3415
3422
  return {
3416
- discoveredUrls: Array.from(visited.values()),
3423
+ discoveredUrls: Array.from(visited.values()).filter((v) => v.startsWith("http")),
3417
3424
  screensFound: 0,
3418
3425
  screensErrored: 0,
3419
3426
  screensSkippedToNextSegment: 0,
@@ -3614,6 +3621,13 @@ ${snippet}`,
3614
3621
  await AppBrain.saveElement(opts.tenantId, opts.projectId, existingScreenId, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
3615
3622
  });
3616
3623
  }
3624
+ } else {
3625
+ const p3aBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
3626
+ if (INLINE_AUTH_WALL_RE.test(p3aBodyText)) {
3627
+ logger.warn({ url: revisitUrl, screenId: existingScreenId }, "[P3a] Re-visit got 0 elements \u2014 inline session-expired content, marking requiresAuth");
3628
+ AppBrain.updateScreenRequiresAuth(opts.tenantId, existingScreenId, true).catch(() => {
3629
+ });
3630
+ }
3617
3631
  }
3618
3632
  const revisitLinks = await discoverLinks(stagehand, origin, page, linkLLM ?? void 0, false, rawPage ?? void 0).catch(() => []);
3619
3633
  for (const href of revisitLinks) {
@@ -3753,6 +3767,14 @@ ${snippet}`,
3753
3767
  await AppBrain.saveElement(opts.tenantId, opts.projectId, gapScreen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark).catch(() => {
3754
3768
  });
3755
3769
  }
3770
+ if (gapElems.length === 0) {
3771
+ const gapBodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
3772
+ if (INLINE_AUTH_WALL_RE.test(gapBodyText)) {
3773
+ logger.warn({ url: gapUrl, screenId: gapScreen.id }, "[P6] Gap URL got 0 elements \u2014 inline session-expired content, marking requiresAuth");
3774
+ AppBrain.updateScreenRequiresAuth(opts.tenantId, gapScreen.id, true).catch(() => {
3775
+ });
3776
+ }
3777
+ }
3756
3778
  visited.set(canonicalize(gapUrl), gapScreen.id);
3757
3779
  discovered.push({ url: gapUrl, screenId: gapScreen.id, elements: gapElems.length, version: gapScreen.version });
3758
3780
  for (const lm of gapLinkMaps) {
@@ -4134,6 +4156,12 @@ ${snippet}`,
4134
4156
  }
4135
4157
  const screensSucceeded = discovered.filter((d) => !!d.screenId).length;
4136
4158
  const screensErrored = discovered.filter((d) => !!d.error && !d.screenId).length;
4159
+ if (pendingSummaryWrites.length > 0) {
4160
+ await Promise.race([
4161
+ Promise.allSettled(pendingSummaryWrites),
4162
+ new Promise((resolve) => setTimeout(resolve, Math.min(6e4, pendingSummaryWrites.length * 3e3)))
4163
+ ]);
4164
+ }
4137
4165
  if (screensSucceeded >= 3) {
4138
4166
  const screensWithElements = discovered.filter((d) => (d.elements ?? 0) > 0 && d.screenId).length;
4139
4167
  if (screensWithElements === 0) {
@@ -5867,6 +5895,16 @@ async function captureAuthDiscoveryScreen(page, opts, requestedUrl, source, ordi
5867
5895
  await AppBrain.updateScreenDomSnapshot(opts.tenantId, screen.id, authDomPath, authDomCloudUrl).catch(() => {
5868
5896
  });
5869
5897
  }
5898
+ const authDiscoveryElements = await readScreen(page).catch(() => []);
5899
+ if (authDiscoveryElements.length > 0) {
5900
+ await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {
5901
+ });
5902
+ for (const el of authDiscoveryElements) {
5903
+ await AppBrain.saveElement(opts.tenantId, opts.projectId, screen.id, el.label, el.role, el.expectedData, void 0, void 0, true, void 0, void 0, void 0).catch(() => {
5904
+ });
5905
+ }
5906
+ logger.info({ screenId: screen.id, elements: authDiscoveryElements.length }, "[crawler] auth-discovery: saved elements");
5907
+ }
5870
5908
  await AppBrain.saveCrawlEvidence(opts.tenantId, opts.projectId, {
5871
5909
  screenId: screen.id,
5872
5910
  jobId: opts.jobId,