mcp-scraper 0.38.2 → 0.40.1

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 (111) hide show
  1. package/README.md +5 -2
  2. package/package.json +5 -6
  3. package/dist/bin/api-server.cjs +0 -58752
  4. package/dist/bin/api-server.cjs.map +0 -1
  5. package/dist/bin/api-server.d.cts +0 -1
  6. package/dist/bin/api-server.d.ts +0 -1
  7. package/dist/bin/api-server.js +0 -38
  8. package/dist/bin/api-server.js.map +0 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +0 -2671
  10. package/dist/bin/mcp-scraper-cli.cjs.map +0 -1
  11. package/dist/bin/mcp-scraper-cli.d.cts +0 -1
  12. package/dist/bin/mcp-scraper-cli.d.ts +0 -1
  13. package/dist/bin/mcp-scraper-cli.js +0 -742
  14. package/dist/bin/mcp-scraper-cli.js.map +0 -1
  15. package/dist/bin/mcp-scraper-install.cjs +0 -129
  16. package/dist/bin/mcp-scraper-install.cjs.map +0 -1
  17. package/dist/bin/mcp-scraper-install.d.cts +0 -1
  18. package/dist/bin/mcp-scraper-install.d.ts +0 -1
  19. package/dist/bin/mcp-scraper-install.js +0 -27
  20. package/dist/bin/mcp-scraper-install.js.map +0 -1
  21. package/dist/bin/mcp-stdio-server.cjs +0 -12264
  22. package/dist/bin/mcp-stdio-server.cjs.map +0 -1
  23. package/dist/bin/mcp-stdio-server.d.cts +0 -1
  24. package/dist/bin/mcp-stdio-server.d.ts +0 -1
  25. package/dist/bin/mcp-stdio-server.js +0 -135
  26. package/dist/bin/mcp-stdio-server.js.map +0 -1
  27. package/dist/bin/paa-harvest.cjs +0 -3808
  28. package/dist/bin/paa-harvest.cjs.map +0 -1
  29. package/dist/bin/paa-harvest.d.cts +0 -1
  30. package/dist/bin/paa-harvest.d.ts +0 -1
  31. package/dist/bin/paa-harvest.js +0 -44
  32. package/dist/bin/paa-harvest.js.map +0 -1
  33. package/dist/chunk-345BQXZH.js +0 -712
  34. package/dist/chunk-345BQXZH.js.map +0 -1
  35. package/dist/chunk-44HZLHDV.js +0 -52
  36. package/dist/chunk-44HZLHDV.js.map +0 -1
  37. package/dist/chunk-AZRPG43B.js +0 -617
  38. package/dist/chunk-AZRPG43B.js.map +0 -1
  39. package/dist/chunk-CB5C3BPB.js +0 -135
  40. package/dist/chunk-CB5C3BPB.js.map +0 -1
  41. package/dist/chunk-EGJKUB4Q.js +0 -276
  42. package/dist/chunk-EGJKUB4Q.js.map +0 -1
  43. package/dist/chunk-FQI5PFE7.js +0 -1866
  44. package/dist/chunk-FQI5PFE7.js.map +0 -1
  45. package/dist/chunk-FRYT3ID4.js +0 -684
  46. package/dist/chunk-FRYT3ID4.js.map +0 -1
  47. package/dist/chunk-G3P3ZDB4.js +0 -69
  48. package/dist/chunk-G3P3ZDB4.js.map +0 -1
  49. package/dist/chunk-K443GQY5.js +0 -24
  50. package/dist/chunk-K443GQY5.js.map +0 -1
  51. package/dist/chunk-N7KUTTCC.js +0 -3007
  52. package/dist/chunk-N7KUTTCC.js.map +0 -1
  53. package/dist/chunk-NGM237OO.js +0 -3410
  54. package/dist/chunk-NGM237OO.js.map +0 -1
  55. package/dist/chunk-NKCCGADE.js +0 -11285
  56. package/dist/chunk-NKCCGADE.js.map +0 -1
  57. package/dist/chunk-NNW3O6ZD.js +0 -108
  58. package/dist/chunk-NNW3O6ZD.js.map +0 -1
  59. package/dist/chunk-QZXKQB7Y.js +0 -414
  60. package/dist/chunk-QZXKQB7Y.js.map +0 -1
  61. package/dist/chunk-SFRMFGQ6.js +0 -158
  62. package/dist/chunk-SFRMFGQ6.js.map +0 -1
  63. package/dist/chunk-YCI2PNCS.js +0 -499
  64. package/dist/chunk-YCI2PNCS.js.map +0 -1
  65. package/dist/chunk-YODBNTTN.js +0 -7
  66. package/dist/chunk-YODBNTTN.js.map +0 -1
  67. package/dist/db-C5KVCOYT.js +0 -239
  68. package/dist/db-C5KVCOYT.js.map +0 -1
  69. package/dist/extract-bundle-KUBX6N6Z.js +0 -568
  70. package/dist/extract-bundle-KUBX6N6Z.js.map +0 -1
  71. package/dist/index.cjs +0 -4160
  72. package/dist/index.cjs.map +0 -1
  73. package/dist/index.d.cts +0 -413
  74. package/dist/index.d.ts +0 -413
  75. package/dist/index.js +0 -338
  76. package/dist/index.js.map +0 -1
  77. package/dist/location-data-repository-TTWF3OTM.js +0 -35
  78. package/dist/location-data-repository-TTWF3OTM.js.map +0 -1
  79. package/dist/server-5EX6XBIA.js +0 -33596
  80. package/dist/server-5EX6XBIA.js.map +0 -1
  81. package/dist/site-extract-repository-XSPJTCIL.js +0 -62
  82. package/dist/site-extract-repository-XSPJTCIL.js.map +0 -1
  83. package/dist/worker-XCPU4YSN.js +0 -142
  84. package/dist/worker-XCPU4YSN.js.map +0 -1
  85. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +0 -58
  86. package/docs/adr/0002-hybrid-smart-rag-vault-retrieval.md +0 -62
  87. package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +0 -22
  88. package/docs/adr/README.md +0 -13
  89. package/docs/final-tooling-spec.md +0 -206
  90. package/docs/hosted-location-data.md +0 -108
  91. package/docs/kernel-proxy-future-enhancements.md +0 -80
  92. package/docs/mcp-tool-craft-lint.generated.md +0 -183
  93. package/docs/mcp-tool-design-guide.md +0 -225
  94. package/docs/mcp-tool-manifest.generated.json +0 -22871
  95. package/docs/mcp-tool-quality-spec.md +0 -240
  96. package/docs/oauth-legal-review.md +0 -38
  97. package/docs/seo-crawl-report-spec.md +0 -287
  98. package/docs/specs/api-forge-spec.md +0 -234
  99. package/docs/specs/connected-services-control-plane-decoupling-spec.md +0 -1044
  100. package/docs/specs/deferred-work-spec.md +0 -86
  101. package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +0 -1689
  102. package/docs/specs/kernel-stealth-captcha-test-matrix.md +0 -278
  103. package/docs/specs/main-mcp-integration-ownership-spec.md +0 -1164
  104. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +0 -1602
  105. package/docs/specs/meta-ad-creative-media-resolution-spec.md +0 -31
  106. package/docs/specs/multimodal-image-memory-architecture-spec.md +0 -1022
  107. package/docs/specs/oauth-mcp-spec.md +0 -213
  108. package/docs/specs/query-fanout-transport-contract-fix.md +0 -45
  109. package/docs/specs/relationship-workspace-ai-behavior-plan.md +0 -26
  110. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +0 -995
  111. package/docs/tool-catalog-spec.md +0 -388
@@ -1,158 +0,0 @@
1
- import {
2
- CaptchaError,
3
- LocationMismatchError,
4
- RequestAbortedError
5
- } from "./chunk-44HZLHDV.js";
6
- import {
7
- finishHarvestAttempt,
8
- startHarvestAttempt
9
- } from "./chunk-N7KUTTCC.js";
10
-
11
- // src/api/harvest-problems.ts
12
- function errorMessage(err) {
13
- return err instanceof Error ? err.message : String(err);
14
- }
15
- function looksLikeTimeout(err, message) {
16
- if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) return true;
17
- return /timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message);
18
- }
19
- function looksLikeCaptcha(message) {
20
- return /captcha|recaptcha|unusual traffic|google\.com\/sorry|blocked/i.test(message);
21
- }
22
- function looksLikeProxyTunnelFailure(message) {
23
- return /ERR_TUNNEL_CONNECTION_FAILED|ERR_PROXY_CONNECTION_FAILED|ERR_SOCKS_CONNECTION_FAILED|tunnel connection failed|proxy connection failed|transport error: proxy/i.test(message);
24
- }
25
- function looksLikeProxyUnavailable(message) {
26
- return /proxy unavailable|proxy_unavailable|connection_test_failed|did not return a proxy id|configured fallback/i.test(message);
27
- }
28
- function classifyHarvestProblem(err) {
29
- const message = errorMessage(err);
30
- if (err instanceof RequestAbortedError) {
31
- return {
32
- error_code: "request_aborted",
33
- error_type: "request_aborted",
34
- message,
35
- retryable: true,
36
- httpStatus: 408,
37
- terminalStatus: "cancelled"
38
- };
39
- }
40
- if (err instanceof CaptchaError || looksLikeCaptcha(message)) {
41
- return {
42
- error_code: "captcha_exhausted",
43
- error_type: "captcha",
44
- message,
45
- retryable: true,
46
- httpStatus: 503,
47
- terminalStatus: "failed"
48
- };
49
- }
50
- if (err instanceof LocationMismatchError) {
51
- return {
52
- error_code: "location_mismatch",
53
- error_type: "location_mismatch",
54
- message,
55
- retryable: true,
56
- httpStatus: 503,
57
- terminalStatus: "failed"
58
- };
59
- }
60
- if (looksLikeProxyTunnelFailure(message)) {
61
- return {
62
- error_code: "proxy_tunnel_failed",
63
- error_type: "proxy_tunnel_failed",
64
- message,
65
- retryable: true,
66
- httpStatus: 503,
67
- terminalStatus: "failed"
68
- };
69
- }
70
- if (looksLikeProxyUnavailable(message)) {
71
- return {
72
- error_code: "proxy_unavailable",
73
- error_type: "proxy_unavailable",
74
- message,
75
- retryable: true,
76
- httpStatus: 503,
77
- terminalStatus: "failed"
78
- };
79
- }
80
- if (looksLikeTimeout(err, message)) {
81
- return {
82
- error_code: "harvest_timeout",
83
- error_type: "timeout",
84
- message,
85
- retryable: true,
86
- httpStatus: 504,
87
- terminalStatus: "failed"
88
- };
89
- }
90
- return {
91
- error_code: "extraction_failed",
92
- error_type: "extraction",
93
- message,
94
- retryable: false,
95
- httpStatus: 500,
96
- terminalStatus: "failed"
97
- };
98
- }
99
- function serializeHarvestProblem(problem) {
100
- return JSON.stringify({
101
- error_code: problem.error_code,
102
- error_type: problem.error_type,
103
- message: problem.message,
104
- retryable: problem.retryable
105
- });
106
- }
107
- function harvestProblemResponse(problem) {
108
- return {
109
- error: problem.message,
110
- error_code: problem.error_code,
111
- error_type: problem.error_type,
112
- retryable: problem.retryable
113
- };
114
- }
115
-
116
- // src/api/harvest-attempt-events.ts
117
- function createHarvestAttemptRecorder(jobId, userId) {
118
- return async (event) => {
119
- if (event.type === "started") {
120
- await startHarvestAttempt({
121
- jobId,
122
- userId,
123
- attemptNumber: event.attemptNumber,
124
- maxAttempts: event.maxAttempts,
125
- query: event.query,
126
- location: event.location,
127
- maxQuestions: event.maxQuestions,
128
- startedAt: event.startedAt
129
- });
130
- return;
131
- }
132
- await finishHarvestAttempt({
133
- jobId,
134
- attemptNumber: event.attemptNumber,
135
- outcome: event.outcome,
136
- kernelSessionId: event.kernelSessionId,
137
- questionCount: event.questionCount,
138
- durationMs: event.durationMs,
139
- error: event.error,
140
- willRetry: event.willRetry,
141
- kernelDeleteStarted: event.cleanup.kernelDeleteStarted,
142
- kernelDeleteSucceeded: event.cleanup.kernelDeleteSucceeded,
143
- kernelDeleteError: event.cleanup.kernelDeleteError,
144
- browserCloseSucceeded: event.cleanup.browserCloseSucceeded,
145
- browserCloseError: event.cleanup.browserCloseError,
146
- debug: event.debug,
147
- completedAt: event.completedAt
148
- });
149
- };
150
- }
151
-
152
- export {
153
- classifyHarvestProblem,
154
- serializeHarvestProblem,
155
- harvestProblemResponse,
156
- createHarvestAttemptRecorder
157
- };
158
- //# sourceMappingURL=chunk-SFRMFGQ6.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/api/harvest-problems.ts","../src/api/harvest-attempt-events.ts"],"sourcesContent":["import { CaptchaError, LocationMismatchError, RequestAbortedError } from '../errors.js'\n\nexport type HarvestProblemCode =\n | 'request_aborted'\n | 'captcha_exhausted'\n | 'location_mismatch'\n | 'proxy_tunnel_failed'\n | 'proxy_unavailable'\n | 'harvest_timeout'\n | 'extraction_failed'\n\nexport interface HarvestProblem {\n error_code: HarvestProblemCode\n error_type: string\n message: string\n retryable: boolean\n httpStatus: number\n terminalStatus: 'cancelled' | 'failed'\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\nfunction looksLikeTimeout(err: unknown, message: string): boolean {\n if (err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError')) return true\n return /timeout|timed out|Timeout \\d+ms exceeded|deadline/i.test(message)\n}\n\nfunction looksLikeCaptcha(message: string): boolean {\n return /captcha|recaptcha|unusual traffic|google\\.com\\/sorry|blocked/i.test(message)\n}\n\nfunction looksLikeProxyTunnelFailure(message: string): boolean {\n return /ERR_TUNNEL_CONNECTION_FAILED|ERR_PROXY_CONNECTION_FAILED|ERR_SOCKS_CONNECTION_FAILED|tunnel connection failed|proxy connection failed|transport error: proxy/i.test(message)\n}\n\nfunction looksLikeProxyUnavailable(message: string): boolean {\n return /proxy unavailable|proxy_unavailable|connection_test_failed|did not return a proxy id|configured fallback/i.test(message)\n}\n\nexport function classifyHarvestProblem(err: unknown): HarvestProblem {\n const message = errorMessage(err)\n\n if (err instanceof RequestAbortedError) {\n return {\n error_code: 'request_aborted',\n error_type: 'request_aborted',\n message,\n retryable: true,\n httpStatus: 408,\n terminalStatus: 'cancelled',\n }\n }\n\n if (err instanceof CaptchaError || looksLikeCaptcha(message)) {\n return {\n error_code: 'captcha_exhausted',\n error_type: 'captcha',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (err instanceof LocationMismatchError) {\n return {\n error_code: 'location_mismatch',\n error_type: 'location_mismatch',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (looksLikeProxyTunnelFailure(message)) {\n return {\n error_code: 'proxy_tunnel_failed',\n error_type: 'proxy_tunnel_failed',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (looksLikeProxyUnavailable(message)) {\n return {\n error_code: 'proxy_unavailable',\n error_type: 'proxy_unavailable',\n message,\n retryable: true,\n httpStatus: 503,\n terminalStatus: 'failed',\n }\n }\n\n if (looksLikeTimeout(err, message)) {\n return {\n error_code: 'harvest_timeout',\n error_type: 'timeout',\n message,\n retryable: true,\n httpStatus: 504,\n terminalStatus: 'failed',\n }\n }\n\n return {\n error_code: 'extraction_failed',\n error_type: 'extraction',\n message,\n retryable: false,\n httpStatus: 500,\n terminalStatus: 'failed',\n }\n}\n\nexport function serializeHarvestProblem(problem: HarvestProblem): string {\n return JSON.stringify({\n error_code: problem.error_code,\n error_type: problem.error_type,\n message: problem.message,\n retryable: problem.retryable,\n })\n}\n\nexport function harvestProblemResponse(problem: HarvestProblem): {\n error: string\n error_code: HarvestProblemCode\n error_type: string\n retryable: boolean\n} {\n return {\n error: problem.message,\n error_code: problem.error_code,\n error_type: problem.error_type,\n retryable: problem.retryable,\n }\n}\n","import type { HarvestAttemptLogEvent } from '../harvest.js'\nimport { finishHarvestAttempt, startHarvestAttempt } from './db.js'\n\nexport function createHarvestAttemptRecorder(jobId: string, userId: number | bigint) {\n return async (event: HarvestAttemptLogEvent): Promise<void> => {\n if (event.type === 'started') {\n await startHarvestAttempt({\n jobId,\n userId,\n attemptNumber: event.attemptNumber,\n maxAttempts: event.maxAttempts,\n query: event.query,\n location: event.location,\n maxQuestions: event.maxQuestions,\n startedAt: event.startedAt,\n })\n return\n }\n\n await finishHarvestAttempt({\n jobId,\n attemptNumber: event.attemptNumber,\n outcome: event.outcome,\n kernelSessionId: event.kernelSessionId,\n questionCount: event.questionCount,\n durationMs: event.durationMs,\n error: event.error,\n willRetry: event.willRetry,\n kernelDeleteStarted: event.cleanup.kernelDeleteStarted,\n kernelDeleteSucceeded: event.cleanup.kernelDeleteSucceeded,\n kernelDeleteError: event.cleanup.kernelDeleteError,\n browserCloseSucceeded: event.cleanup.browserCloseSucceeded,\n browserCloseError: event.cleanup.browserCloseError,\n debug: event.debug,\n completedAt: event.completedAt,\n })\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,499 +0,0 @@
1
- import {
2
- LedgerOperation
3
- } from "./chunk-AZRPG43B.js";
4
- import {
5
- sanitizeVendorName
6
- } from "./chunk-44HZLHDV.js";
7
- import {
8
- CREDIT_LOT_TTL,
9
- getDb,
10
- settleDebitMcIdempotent
11
- } from "./chunk-N7KUTTCC.js";
12
-
13
- // src/api/site-extract-repository.ts
14
- function extractJobLimitInfo(job) {
15
- const effectiveMaxPages = Math.max(1, Number(job.options.effectiveMaxPages ?? job.options.maxPages ?? 1));
16
- const requestedMaxPages = Math.max(effectiveMaxPages, Number(job.options.requestedMaxPages ?? effectiveMaxPages));
17
- const creditLimited = requestedMaxPages > effectiveMaxPages;
18
- return {
19
- requestedMaxPages,
20
- effectiveMaxPages,
21
- creditLimited,
22
- // If discovery exhausted below the funded cap, the smaller hold did not
23
- // actually truncate this crawl. Hitting the cap is conservatively partial.
24
- creditTruncated: creditLimited && job.totalUrls >= effectiveMaxPages
25
- };
26
- }
27
- function terminalExtractJobStatus(progress, creditTruncated = false) {
28
- if (progress.successfulUrls === 0) return "failed";
29
- if (progress.failedUrls > 0 || progress.remainingUrls > 0 || creditTruncated) return "partial";
30
- return "complete";
31
- }
32
- function rowToJob(r) {
33
- const totalUrls = Number(r.total_urls ?? 0);
34
- const legacyProgress = r.attempted_urls == null;
35
- const attemptedUrls = Number(legacyProgress ? r.done_urls ?? 0 : r.attempted_urls);
36
- const successfulUrls = Number(legacyProgress ? r.done_urls ?? 0 : r.successful_urls ?? 0);
37
- const failedUrls = Number(r.failed_urls ?? Math.max(0, attemptedUrls - successfulUrls));
38
- return {
39
- id: String(r.id),
40
- userId: r.user_id != null ? Number(r.user_id) : null,
41
- idempotencyKey: r.idempotency_key != null ? String(r.idempotency_key) : null,
42
- requestFingerprint: r.request_fingerprint != null ? String(r.request_fingerprint) : null,
43
- status: r.status != null ? String(r.status) : "pending",
44
- startUrl: String(r.start_url ?? ""),
45
- options: r.options ? JSON.parse(String(r.options)) : {},
46
- totalUrls,
47
- doneUrls: attemptedUrls,
48
- attemptedUrls,
49
- successfulUrls,
50
- failedUrls,
51
- remainingUrls: Math.max(0, totalUrls - attemptedUrls),
52
- artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,
53
- error: r.error != null ? String(r.error) : null,
54
- billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,
55
- createdAt: String(r.created_at ?? ""),
56
- updatedAt: String(r.updated_at ?? "")
57
- };
58
- }
59
- async function createExtractJob(jobId, userId, startUrl, options) {
60
- const db = getDb();
61
- await db.execute({
62
- sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)
63
- VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,
64
- args: [jobId, userId, startUrl, JSON.stringify(options)]
65
- });
66
- }
67
- async function getExtractJobByIdempotencyKey(userId, idempotencyKey) {
68
- const db = getDb();
69
- const res = await db.execute({
70
- sql: `SELECT * FROM site_extract_jobs WHERE user_id = ? AND idempotency_key = ? LIMIT 1`,
71
- args: [userId, idempotencyKey]
72
- });
73
- return res.rows[0] ? rowToJob(res.rows[0]) : null;
74
- }
75
- async function createOrGetExtractJob(input) {
76
- const db = getDb();
77
- const inserted = await db.execute({
78
- sql: `INSERT OR IGNORE INTO site_extract_jobs
79
- (id, user_id, status, start_url, options, idempotency_key, request_fingerprint, created_at, updated_at)
80
- VALUES (?, ?, 'pending', ?, ?, ?, ?, datetime('now'), datetime('now'))`,
81
- args: [
82
- input.jobId,
83
- input.userId,
84
- input.startUrl,
85
- JSON.stringify(input.options),
86
- input.idempotencyKey,
87
- input.requestFingerprint
88
- ]
89
- });
90
- const job = await getExtractJobByIdempotencyKey(input.userId, input.idempotencyKey);
91
- if (!job) throw new Error("idempotent extract job was not persisted");
92
- return {
93
- job,
94
- created: inserted.rowsAffected === 1,
95
- conflict: job.requestFingerprint !== input.requestFingerprint
96
- };
97
- }
98
- async function getExtractJob(jobId) {
99
- const db = getDb();
100
- const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] });
101
- return res.rows[0] ? rowToJob(res.rows[0]) : null;
102
- }
103
- async function listExtractJobs(userId) {
104
- const db = getDb();
105
- const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50`, args: [userId] });
106
- return res.rows.map((r) => rowToJob(r));
107
- }
108
- async function listUnsettledExtractJobs(limit = 25) {
109
- const db = getDb();
110
- const res = await db.execute({
111
- sql: `SELECT * FROM site_extract_jobs
112
- WHERE billed_mc IS NULL AND status IN ('complete', 'partial', 'failed')
113
- AND (next_settlement_at IS NULL OR next_settlement_at <= datetime('now'))
114
- ORDER BY updated_at ASC LIMIT ?`,
115
- args: [Math.max(1, Math.min(100, Math.round(limit)))]
116
- });
117
- return res.rows.map((row) => rowToJob(row));
118
- }
119
- async function listFundedPendingExtractJobs(limit = 25, staleMinutes = 2) {
120
- const db = getDb();
121
- const safeStaleMinutes = Math.max(1, Math.min(60, Math.round(staleMinutes)));
122
- const res = await db.execute({
123
- sql: `SELECT j.* FROM site_extract_jobs j
124
- JOIN billing_debits d
125
- ON d.idempotency_key = json_extract(j.options, '$.debitKey')
126
- AND d.user_id = j.user_id
127
- AND d.status = 'applied'
128
- WHERE j.status = 'pending'
129
- AND j.billed_mc IS NULL
130
- AND (j.next_dispatch_at IS NULL OR j.next_dispatch_at <= datetime('now'))
131
- AND j.updated_at <= datetime('now', ?)
132
- ORDER BY j.updated_at ASC LIMIT ?`,
133
- args: [`-${safeStaleMinutes} minutes`, Math.max(1, Math.min(100, Math.round(limit)))]
134
- });
135
- return res.rows.map((row) => rowToJob(row));
136
- }
137
- async function markExtractJobDispatchAttempt(jobId) {
138
- const db = getDb();
139
- await db.execute({
140
- sql: `UPDATE site_extract_jobs
141
- SET dispatch_attempts = dispatch_attempts + 1,
142
- updated_at = datetime('now'),
143
- next_dispatch_at = datetime('now', '+5 minutes'),
144
- dispatch_error = NULL,
145
- status = CASE
146
- WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours') THEN 'failed'
147
- ELSE status
148
- END,
149
- error = CASE
150
- WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours')
151
- THEN 'Background crawl was acknowledged repeatedly but never started; the hold will be refunded.'
152
- ELSE error
153
- END
154
- WHERE id = ? AND status = 'pending'`,
155
- args: [jobId]
156
- });
157
- return getExtractJob(jobId);
158
- }
159
- async function recordExtractJobDispatchFailure(jobId, error) {
160
- const db = getDb();
161
- const safeError = sanitizeVendorName(error).slice(0, 1e3);
162
- await db.execute({
163
- sql: `UPDATE site_extract_jobs SET
164
- dispatch_attempts = dispatch_attempts + 1,
165
- dispatch_error = ?,
166
- next_dispatch_at = datetime('now', '+5 minutes'),
167
- updated_at = datetime('now'),
168
- status = CASE
169
- WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours') THEN 'failed'
170
- ELSE status
171
- END,
172
- error = CASE
173
- WHEN dispatch_attempts + 1 >= 10 OR created_at <= datetime('now', '-6 hours')
174
- THEN 'Background crawl could not be dispatched after repeated attempts; the hold will be refunded.'
175
- ELSE error
176
- END
177
- WHERE id = ? AND status = 'pending'`,
178
- args: [safeError, jobId]
179
- });
180
- return getExtractJob(jobId);
181
- }
182
- async function recordExtractSettlementFailure(jobId, error) {
183
- const db = getDb();
184
- await db.execute({
185
- sql: `UPDATE site_extract_jobs SET
186
- settlement_attempts = settlement_attempts + 1,
187
- settlement_error = ?,
188
- next_settlement_at = datetime('now', '+15 minutes'),
189
- updated_at = datetime('now')
190
- WHERE id = ? AND billed_mc IS NULL`,
191
- args: [sanitizeVendorName(error).slice(0, 1e3), jobId]
192
- });
193
- }
194
- async function listStaleRunningExtractJobs(limit = 25, staleMinutes = 60) {
195
- const db = getDb();
196
- const safeStaleMinutes = Math.max(15, Math.min(24 * 60, Math.round(staleMinutes)));
197
- const res = await db.execute({
198
- sql: `SELECT j.* FROM site_extract_jobs j
199
- LEFT JOIN billing_debits d
200
- ON d.idempotency_key = json_extract(j.options, '$.debitKey')
201
- AND d.user_id = j.user_id
202
- AND d.status = 'applied'
203
- WHERE j.status = 'running'
204
- AND j.billed_mc IS NULL
205
- AND (
206
- json_extract(j.options, '$.debitKey') IS NULL
207
- OR d.idempotency_key IS NOT NULL
208
- )
209
- AND j.updated_at <= datetime('now', ?)
210
- ORDER BY j.updated_at ASC LIMIT ?`,
211
- args: [`-${safeStaleMinutes} minutes`, Math.max(1, Math.min(100, Math.round(limit)))]
212
- });
213
- return res.rows.map((row) => rowToJob(row));
214
- }
215
- async function failStaleRunningExtractJob(jobId, staleMinutes = 60) {
216
- const db = getDb();
217
- const safeStaleMinutes = Math.max(15, Math.min(24 * 60, Math.round(staleMinutes)));
218
- const result = await db.execute({
219
- sql: `UPDATE site_extract_jobs
220
- SET status = 'failed',
221
- error = 'Background crawl stopped without a heartbeat; completed pages were preserved and the unused hold will be refunded.',
222
- updated_at = datetime('now')
223
- WHERE id = ?
224
- AND status = 'running'
225
- AND billed_mc IS NULL
226
- AND updated_at <= datetime('now', ?)`,
227
- args: [jobId, `-${safeStaleMinutes} minutes`]
228
- });
229
- return result.rowsAffected === 1;
230
- }
231
- async function claimFailedExtractJobForRefinalize(jobId) {
232
- const db = getDb();
233
- const claimed = await db.execute({
234
- sql: `UPDATE site_extract_jobs
235
- SET status = 'running', updated_at = datetime('now')
236
- WHERE id = ? AND status = 'failed'`,
237
- args: [jobId]
238
- });
239
- if (claimed.rowsAffected !== 1) return null;
240
- return getExtractJob(jobId);
241
- }
242
- async function abandonExtractSettlement(jobId, error) {
243
- const db = getDb();
244
- await db.execute({
245
- sql: `UPDATE site_extract_jobs SET billed_mc = 0, settlement_error = ?, updated_at = datetime('now')
246
- WHERE id = ? AND billed_mc IS NULL`,
247
- args: [sanitizeVendorName(error).slice(0, 1e3), jobId]
248
- });
249
- }
250
- async function setExtractJobTotal(jobId, totalUrls) {
251
- const db = getDb();
252
- await db.execute({
253
- sql: `UPDATE site_extract_jobs
254
- SET status = 'running', total_urls = MAX(total_urls, ?), updated_at = datetime('now')
255
- WHERE id = ? AND status IN ('pending', 'running')`,
256
- args: [totalUrls, jobId]
257
- });
258
- }
259
- async function saveExtractPages(jobId, pages) {
260
- if (pages.length === 0) return;
261
- const db = getDb();
262
- await db.batch([
263
- ...pages.map((p) => {
264
- const { discoveryLinks: _ephemeralDiscoveryLinks, ...storedPage } = p;
265
- return {
266
- sql: `INSERT INTO site_extract_pages (job_id, url, page)
267
- SELECT ?, ?, ?
268
- WHERE EXISTS (
269
- SELECT 1 FROM site_extract_jobs
270
- WHERE id = ? AND status IN ('pending', 'running')
271
- )
272
- ON CONFLICT(job_id, url) DO UPDATE SET page = excluded.page
273
- WHERE NOT CASE
274
- WHEN json_valid(site_extract_pages.page) = 0 THEN 0
275
- WHEN json_extract(site_extract_pages.page, '$.extractionStatus') IS NOT NULL
276
- THEN json_extract(site_extract_pages.page, '$.extractionStatus') = 'successful'
277
- ELSE COALESCE(
278
- json_extract(site_extract_pages.page, '$.status') >= 200
279
- AND json_extract(site_extract_pages.page, '$.status') < 300
280
- AND json_extract(site_extract_pages.page, '$.wordCount') > 0,
281
- 0
282
- )
283
- END
284
- OR CASE
285
- WHEN json_valid(excluded.page) = 0 THEN 0
286
- WHEN json_extract(excluded.page, '$.extractionStatus') IS NOT NULL
287
- THEN json_extract(excluded.page, '$.extractionStatus') = 'successful'
288
- ELSE COALESCE(
289
- json_extract(excluded.page, '$.status') >= 200
290
- AND json_extract(excluded.page, '$.status') < 300
291
- AND json_extract(excluded.page, '$.wordCount') > 0,
292
- 0
293
- )
294
- END`,
295
- args: [jobId, p.archivedUrl ?? p.url, JSON.stringify(storedPage), jobId]
296
- };
297
- }),
298
- {
299
- sql: `UPDATE site_extract_jobs SET
300
- done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?),
301
- attempted_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?),
302
- successful_urls = (
303
- SELECT COUNT(*) FROM site_extract_pages
304
- WHERE job_id = ? AND CASE
305
- WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
306
- THEN json_extract(page, '$.extractionStatus') = 'successful'
307
- ELSE json_extract(page, '$.status') >= 200
308
- AND json_extract(page, '$.status') < 300
309
- AND json_extract(page, '$.wordCount') > 0
310
- END
311
- ),
312
- failed_urls = (
313
- SELECT COUNT(*) FROM site_extract_pages
314
- WHERE job_id = ? AND NOT CASE
315
- WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
316
- THEN json_extract(page, '$.extractionStatus') = 'successful'
317
- ELSE COALESCE(
318
- json_extract(page, '$.status') >= 200
319
- AND json_extract(page, '$.status') < 300
320
- AND json_extract(page, '$.wordCount') > 0,
321
- 0
322
- )
323
- END
324
- ),
325
- updated_at = datetime('now')
326
- WHERE id = ? AND status IN ('pending', 'running')`,
327
- args: [jobId, jobId, jobId, jobId, jobId]
328
- }
329
- ], "write");
330
- }
331
- async function getExtractedPages(jobId) {
332
- const db = getDb();
333
- const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
334
- return res.rows.map((r) => JSON.parse(String(r.page)));
335
- }
336
- async function getExtractedImageLinks(jobId, limit = 181) {
337
- const db = getDb();
338
- const safeLimit = Math.max(1, Math.min(5001, Math.round(limit)));
339
- const res = await db.execute({
340
- sql: `SELECT DISTINCT CAST(images.value AS TEXT) AS url
341
- FROM site_extract_pages p,
342
- json_each(CASE WHEN json_valid(p.page) THEN p.page ELSE '{}' END, '$.imageLinks') images
343
- WHERE p.job_id = ?
344
- AND images.type = 'text'
345
- AND length(CAST(images.value AS TEXT)) BETWEEN 1 AND 4096
346
- LIMIT ?`,
347
- args: [jobId, safeLimit]
348
- });
349
- return res.rows.map((row) => String(row.url));
350
- }
351
- async function countSuccessfulPages(jobId) {
352
- const db = getDb();
353
- const res = await db.execute({
354
- sql: `SELECT COUNT(*) AS n FROM site_extract_pages
355
- WHERE job_id = ? AND CASE
356
- WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
357
- THEN json_extract(page, '$.extractionStatus') = 'successful'
358
- ELSE COALESCE(
359
- json_extract(page, '$.status') >= 200
360
- AND json_extract(page, '$.status') < 300
361
- AND json_extract(page, '$.wordCount') > 0,
362
- 0
363
- )
364
- END`,
365
- args: [jobId]
366
- });
367
- return Number(res.rows[0]?.n ?? 0);
368
- }
369
- async function getExtractedUrls(jobId) {
370
- const db = getDb();
371
- const res = await db.execute({ sql: `SELECT url FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
372
- return new Set(res.rows.map((r) => String(r.url)));
373
- }
374
- async function finishExtractJob(jobId, artifacts, status, error = null, allowFailedRecovery = false) {
375
- const db = getDb();
376
- const result = await db.execute({
377
- sql: `UPDATE site_extract_jobs
378
- SET status = ?, artifacts = ?, error = ?, updated_at = datetime('now')
379
- WHERE id = ? AND (status IN ('pending', 'running') OR (? = 1 AND status = 'failed'))`,
380
- args: [status, JSON.stringify(artifacts ?? []), error ? sanitizeVendorName(error).slice(0, 2e3) : null, jobId, allowFailedRecovery ? 1 : 0]
381
- });
382
- return result.rowsAffected === 1;
383
- }
384
- async function completeExtractJob(jobId, artifacts) {
385
- await finishExtractJob(jobId, artifacts, "complete");
386
- }
387
- async function failExtractJob(jobId, error) {
388
- const db = getDb();
389
- await db.execute({
390
- sql: `UPDATE site_extract_jobs
391
- SET status = 'failed', error = ?, updated_at = datetime('now')
392
- WHERE id = ? AND status IN ('pending', 'running')`,
393
- args: [sanitizeVendorName(error).slice(0, 2e3), jobId]
394
- });
395
- }
396
- async function failUnfundedExtractJob(jobId, error) {
397
- const db = getDb();
398
- await db.execute({
399
- sql: `UPDATE site_extract_jobs
400
- SET status = 'failed', billed_mc = 0, error = ?, updated_at = datetime('now')
401
- WHERE id = ? AND status = 'pending' AND billed_mc IS NULL`,
402
- args: [sanitizeVendorName(error).slice(0, 2e3), jobId]
403
- });
404
- }
405
- async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference) {
406
- const db = getDb();
407
- const job = await getExtractJob(jobId);
408
- if (!job || job.billedMc != null) return;
409
- const debitKey = typeof job.options.debitKey === "string" ? job.options.debitKey : null;
410
- if (debitKey) {
411
- const settlement = await settleDebitMcIdempotent(
412
- userId,
413
- debitKey,
414
- Math.max(0, netChargeMc),
415
- LedgerOperation.EXTRACT_SITE_REFUND,
416
- reference,
417
- "extract_site"
418
- );
419
- await db.execute({
420
- sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`,
421
- args: [settlement.final_amount_mc, jobId]
422
- });
423
- return;
424
- }
425
- const safeRefundMc = Math.max(0, refundMc);
426
- const safeNetChargeMc = Math.max(0, netChargeMc);
427
- await db.batch([
428
- {
429
- sql: `UPDATE site_extract_jobs SET billed_mc = -1 WHERE id = ? AND billed_mc IS NULL`,
430
- args: [jobId]
431
- },
432
- {
433
- sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, expires_at)
434
- SELECT ?, ?, ?, ?, datetime(j.created_at, ?)
435
- FROM site_extract_jobs j
436
- WHERE j.id = ?
437
- AND j.billed_mc = -1
438
- AND ? > 0
439
- AND changes() = 1
440
- AND datetime(j.created_at, ?) > datetime('now')`,
441
- args: [
442
- userId,
443
- safeRefundMc,
444
- safeRefundMc,
445
- LedgerOperation.EXTRACT_SITE_REFUND,
446
- CREDIT_LOT_TTL,
447
- jobId,
448
- safeRefundMc,
449
- CREDIT_LOT_TTL
450
- ]
451
- },
452
- {
453
- sql: `UPDATE users SET balance_mc = balance_mc + ?
454
- WHERE id = ? AND ? > 0 AND changes() = 1`,
455
- args: [safeRefundMc, userId, safeRefundMc]
456
- },
457
- {
458
- sql: `INSERT INTO ledger (user_id, amount_mc, operation, description)
459
- SELECT ?, ?, ?, ? WHERE ? > 0 AND changes() = 1`,
460
- args: [userId, safeRefundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference, safeRefundMc]
461
- },
462
- {
463
- sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now')
464
- WHERE id = ? AND billed_mc = -1`,
465
- args: [safeNetChargeMc, jobId]
466
- }
467
- ], "write");
468
- }
469
-
470
- export {
471
- extractJobLimitInfo,
472
- terminalExtractJobStatus,
473
- createExtractJob,
474
- getExtractJobByIdempotencyKey,
475
- createOrGetExtractJob,
476
- getExtractJob,
477
- listExtractJobs,
478
- listUnsettledExtractJobs,
479
- listFundedPendingExtractJobs,
480
- markExtractJobDispatchAttempt,
481
- recordExtractJobDispatchFailure,
482
- recordExtractSettlementFailure,
483
- listStaleRunningExtractJobs,
484
- failStaleRunningExtractJob,
485
- claimFailedExtractJobForRefinalize,
486
- abandonExtractSettlement,
487
- setExtractJobTotal,
488
- saveExtractPages,
489
- getExtractedPages,
490
- getExtractedImageLinks,
491
- countSuccessfulPages,
492
- getExtractedUrls,
493
- finishExtractJob,
494
- completeExtractJob,
495
- failExtractJob,
496
- failUnfundedExtractJob,
497
- settleExtractJob
498
- };
499
- //# sourceMappingURL=chunk-YCI2PNCS.js.map