@effect-uai/firecrawl 0.9.0
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/LICENSE +21 -0
- package/dist/FirecrawlRead.d.mts +52 -0
- package/dist/FirecrawlRead.d.mts.map +1 -0
- package/dist/FirecrawlRead.mjs +134 -0
- package/dist/FirecrawlRead.mjs.map +1 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/models.d.mts +10 -0
- package/dist/models.d.mts.map +1 -0
- package/dist/models.mjs +1 -0
- package/package.json +58 -0
- package/src/FirecrawlRead.ts +212 -0
- package/src/index.ts +2 -0
- package/src/models.ts +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Betalyra Sociedade Unipessoal Lda.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FirecrawlProxy } from "./models.mjs";
|
|
2
|
+
import { Context, Duration, Effect, Layer, Redacted } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { CommonReadRequest, ReadResponse, WebRead } from "@effect-uai/core/WebRead";
|
|
6
|
+
|
|
7
|
+
//#region src/FirecrawlRead.d.ts
|
|
8
|
+
declare namespace FirecrawlRead_d_exports {
|
|
9
|
+
export { Config, FirecrawlRead, FirecrawlReadRequest, FirecrawlReadService, layer, make };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Firecrawl-typed read request. Widens {@link CommonReadRequest} with
|
|
13
|
+
* Firecrawl's own scrape knobs; the common fields map onto Firecrawl's wire
|
|
14
|
+
* body (see the codec below).
|
|
15
|
+
*/
|
|
16
|
+
type FirecrawlReadRequest = CommonReadRequest & {
|
|
17
|
+
/** Keep only the main content, dropping nav/header/footer/sidebar. */readonly onlyMainContent?: boolean; /** Restrict extraction to these HTML tags / selectors. */
|
|
18
|
+
readonly includeTags?: ReadonlyArray<string>; /** Drop these HTML tags / selectors before extraction. */
|
|
19
|
+
readonly excludeTags?: ReadonlyArray<string>; /** Wait this long for the page to settle before scraping (JS-heavy sites). */
|
|
20
|
+
readonly waitFor?: Duration.Duration; /** Emulate a mobile device. */
|
|
21
|
+
readonly mobile?: boolean; /** Proxy tier - see {@link FirecrawlProxy}. */
|
|
22
|
+
readonly proxy?: FirecrawlProxy;
|
|
23
|
+
};
|
|
24
|
+
type FirecrawlReadService = {
|
|
25
|
+
readonly read: (request: FirecrawlReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>;
|
|
26
|
+
};
|
|
27
|
+
declare const FirecrawlRead_base: Context.ServiceClass<FirecrawlRead, "@betalyra/effect-uai/providers/firecrawl/FirecrawlRead", FirecrawlReadService>;
|
|
28
|
+
/**
|
|
29
|
+
* Provider-typed service tag. Yield this for Firecrawl-typed reads (its own
|
|
30
|
+
* scrape knobs); yield the generic `WebRead` tag for provider-portable code.
|
|
31
|
+
* Both are registered by {@link layer}.
|
|
32
|
+
*/
|
|
33
|
+
declare class FirecrawlRead extends FirecrawlRead_base {}
|
|
34
|
+
type Config = {
|
|
35
|
+
readonly apiKey: Redacted.Redacted;
|
|
36
|
+
readonly baseUrl?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Build a `FirecrawlReadService` value. For Layer-based setup, prefer
|
|
40
|
+
* {@link layer}.
|
|
41
|
+
*/
|
|
42
|
+
declare const make: (cfg: Config) => Effect.Effect<FirecrawlReadService, never, HttpClient.HttpClient>;
|
|
43
|
+
/**
|
|
44
|
+
* Layer registering both the provider-typed `FirecrawlRead` tag and the
|
|
45
|
+
* generic `WebRead` tag over one implementation. A `CommonReadRequest` is
|
|
46
|
+
* structurally a `FirecrawlReadRequest` with the extra knobs unset, so the
|
|
47
|
+
* generic registration forwards directly.
|
|
48
|
+
*/
|
|
49
|
+
declare const layer: (cfg: Config) => Layer.Layer<FirecrawlRead | WebRead, never, HttpClient.HttpClient>;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { Config, FirecrawlRead, FirecrawlReadRequest, FirecrawlReadService, layer, make, FirecrawlRead_d_exports as t };
|
|
52
|
+
//# sourceMappingURL=FirecrawlRead.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FirecrawlRead.d.mts","names":[],"sources":["../src/FirecrawlRead.ts"],"mappings":";;;;;;;;;;;;;;;KAoBY,oBAAA,GAAuB,iBAAA;iFAExB,eAAA;WAEA,WAAA,GAAc,aAAA;WAEd,WAAA,GAAc,aAAA,UANO;EAAA,SAQrB,OAAA,GAAU,QAAA,CAAS,QAAA,EARK;EAAA,SAUxB,MAAA,YAJc;EAAA,SAMd,KAAA,GAAQ,cAAA;AAAA;AAAA,KAGP,oBAAA;EAAA,SACD,IAAA,GAAO,OAAA,EAAS,oBAAA,KAAyB,MAAA,CAAO,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,OAAA;AAAA;AAAA,cACvF,kBAAA;;;;;;cAOY,aAAA,SAAsB,kBAAA;AAAA,KAIvB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;AAfX;;;cAyJa,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,oBAAA,SAA6B,UAAA,CAAW,UAAA;;;;;;;cAY5C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,aAAA,GAAgB,OAAA,SAAgB,UAAA,CAAW,UAAA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { Context, Duration, Effect, Layer, Redacted, Schema } from "effect";
|
|
3
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
|
+
import * as AiError from "@effect-uai/core/AiError";
|
|
5
|
+
import { WebRead } from "@effect-uai/core/WebRead";
|
|
6
|
+
//#region src/FirecrawlRead.ts
|
|
7
|
+
var FirecrawlRead_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
FirecrawlRead: () => FirecrawlRead,
|
|
9
|
+
layer: () => layer,
|
|
10
|
+
make: () => make
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Provider-typed service tag. Yield this for Firecrawl-typed reads (its own
|
|
14
|
+
* scrape knobs); yield the generic `WebRead` tag for provider-portable code.
|
|
15
|
+
* Both are registered by {@link layer}.
|
|
16
|
+
*/
|
|
17
|
+
var FirecrawlRead = class extends Context.Service()("@betalyra/effect-uai/providers/firecrawl/FirecrawlRead") {};
|
|
18
|
+
const buildBody = (request) => {
|
|
19
|
+
const contentFormat = request.format ?? "markdown";
|
|
20
|
+
return {
|
|
21
|
+
url: request.url,
|
|
22
|
+
formats: [contentFormat, "links"],
|
|
23
|
+
...request.onlyMainContent !== void 0 && { onlyMainContent: request.onlyMainContent },
|
|
24
|
+
...request.includeTags !== void 0 && { includeTags: request.includeTags },
|
|
25
|
+
...request.excludeTags !== void 0 && { excludeTags: request.excludeTags },
|
|
26
|
+
...request.waitFor !== void 0 && { waitFor: Duration.toMillis(request.waitFor) },
|
|
27
|
+
...request.timeout !== void 0 && { timeout: Duration.toMillis(request.timeout) },
|
|
28
|
+
...request.mobile !== void 0 && { mobile: request.mobile },
|
|
29
|
+
...request.proxy !== void 0 && { proxy: request.proxy }
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const WireMetadata = Schema.Struct({
|
|
33
|
+
title: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)]))),
|
|
34
|
+
sourceURL: Schema.optional(Schema.NullOr(Schema.String)),
|
|
35
|
+
statusCode: Schema.optional(Schema.Number)
|
|
36
|
+
});
|
|
37
|
+
const WireData = Schema.Struct({
|
|
38
|
+
markdown: Schema.optional(Schema.NullOr(Schema.String)),
|
|
39
|
+
html: Schema.optional(Schema.NullOr(Schema.String)),
|
|
40
|
+
rawHtml: Schema.optional(Schema.NullOr(Schema.String)),
|
|
41
|
+
links: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
42
|
+
metadata: Schema.optional(WireMetadata)
|
|
43
|
+
});
|
|
44
|
+
const WireResponse = Schema.Struct({
|
|
45
|
+
success: Schema.optional(Schema.Boolean),
|
|
46
|
+
data: WireData
|
|
47
|
+
});
|
|
48
|
+
const pickTitle = (title) => typeof title === "string" ? title : Array.isArray(title) ? title[0] : void 0;
|
|
49
|
+
const toResponse = (wire, request) => {
|
|
50
|
+
const { data } = wire;
|
|
51
|
+
const content = ((request.format ?? "markdown") === "html" ? data.html : data.markdown) ?? "";
|
|
52
|
+
const title = pickTitle(data.metadata?.title);
|
|
53
|
+
const links = data.links ?? void 0;
|
|
54
|
+
return {
|
|
55
|
+
url: data.metadata?.sourceURL ?? request.url,
|
|
56
|
+
content,
|
|
57
|
+
...title !== void 0 && { title },
|
|
58
|
+
...links !== void 0 && links.length > 0 && { links },
|
|
59
|
+
raw: wire
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const transportFailure = (cause) => new AiError.Unavailable({
|
|
63
|
+
provider: "firecrawl",
|
|
64
|
+
raw: cause
|
|
65
|
+
});
|
|
66
|
+
const httpStatusError = (status, body) => {
|
|
67
|
+
const provider = "firecrawl";
|
|
68
|
+
const raw = body;
|
|
69
|
+
if (status === 429) return new AiError.RateLimited({
|
|
70
|
+
provider,
|
|
71
|
+
raw
|
|
72
|
+
});
|
|
73
|
+
if (status === 408 || status === 504) return new AiError.Timeout({
|
|
74
|
+
provider,
|
|
75
|
+
raw
|
|
76
|
+
});
|
|
77
|
+
if (status === 401) return new AiError.AuthFailed({
|
|
78
|
+
provider,
|
|
79
|
+
subtype: "auth",
|
|
80
|
+
raw
|
|
81
|
+
});
|
|
82
|
+
if (status === 403) return new AiError.AuthFailed({
|
|
83
|
+
provider,
|
|
84
|
+
subtype: "permission",
|
|
85
|
+
raw
|
|
86
|
+
});
|
|
87
|
+
if (status === 402) return new AiError.AuthFailed({
|
|
88
|
+
provider,
|
|
89
|
+
subtype: "billing",
|
|
90
|
+
raw
|
|
91
|
+
});
|
|
92
|
+
if (status >= 500) return new AiError.Unavailable({
|
|
93
|
+
provider,
|
|
94
|
+
status,
|
|
95
|
+
raw
|
|
96
|
+
});
|
|
97
|
+
return new AiError.InvalidRequest({
|
|
98
|
+
provider,
|
|
99
|
+
raw
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.firecrawl.dev";
|
|
103
|
+
const postScrape = (cfg, body) => Effect.gen(function* () {
|
|
104
|
+
const client = yield* HttpClient.HttpClient;
|
|
105
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v2/scrape`).pipe(HttpClientRequest.setHeader("Authorization", `Bearer ${Redacted.value(cfg.apiKey)}`), HttpClientRequest.bodyJsonUnsafe(body));
|
|
106
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
107
|
+
if (response.status >= 400) {
|
|
108
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
109
|
+
return yield* httpStatusError(response.status, text);
|
|
110
|
+
}
|
|
111
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure));
|
|
112
|
+
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(Effect.mapError(transportFailure));
|
|
113
|
+
});
|
|
114
|
+
const readImpl = (cfg) => (request) => Effect.map(postScrape(cfg, buildBody(request)), (wire) => toResponse(wire, request));
|
|
115
|
+
/**
|
|
116
|
+
* Build a `FirecrawlReadService` value. For Layer-based setup, prefer
|
|
117
|
+
* {@link layer}.
|
|
118
|
+
*/
|
|
119
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({ read: (request) => readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)) }));
|
|
120
|
+
/**
|
|
121
|
+
* Layer registering both the provider-typed `FirecrawlRead` tag and the
|
|
122
|
+
* generic `WebRead` tag over one implementation. A `CommonReadRequest` is
|
|
123
|
+
* structurally a `FirecrawlReadRequest` with the extra knobs unset, so the
|
|
124
|
+
* generic registration forwards directly.
|
|
125
|
+
*/
|
|
126
|
+
const layer = (cfg) => {
|
|
127
|
+
const typed = Layer.effect(FirecrawlRead, make(cfg));
|
|
128
|
+
const generic = Layer.effect(WebRead, Effect.map(make(cfg), (s) => ({ read: (request) => s.read(request) })));
|
|
129
|
+
return Layer.merge(typed, generic);
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
export { FirecrawlRead, layer, make, FirecrawlRead_exports as t };
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=FirecrawlRead.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FirecrawlRead.mjs","names":[],"sources":["../src/FirecrawlRead.ts"],"sourcesContent":["import { Context, Duration, Effect, Layer, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n type CommonReadRequest,\n type ReadResponse,\n WebRead,\n type WebReadService,\n} from \"@effect-uai/core/WebRead\"\nimport type { FirecrawlProxy } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Firecrawl-typed read request. Widens {@link CommonReadRequest} with\n * Firecrawl's own scrape knobs; the common fields map onto Firecrawl's wire\n * body (see the codec below).\n */\nexport type FirecrawlReadRequest = CommonReadRequest & {\n /** Keep only the main content, dropping nav/header/footer/sidebar. */\n readonly onlyMainContent?: boolean\n /** Restrict extraction to these HTML tags / selectors. */\n readonly includeTags?: ReadonlyArray<string>\n /** Drop these HTML tags / selectors before extraction. */\n readonly excludeTags?: ReadonlyArray<string>\n /** Wait this long for the page to settle before scraping (JS-heavy sites). */\n readonly waitFor?: Duration.Duration\n /** Emulate a mobile device. */\n readonly mobile?: boolean\n /** Proxy tier - see {@link FirecrawlProxy}. */\n readonly proxy?: FirecrawlProxy\n}\n\nexport type FirecrawlReadService = {\n readonly read: (request: FirecrawlReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>\n}\n\n/**\n * Provider-typed service tag. Yield this for Firecrawl-typed reads (its own\n * scrape knobs); yield the generic `WebRead` tag for provider-portable code.\n * Both are registered by {@link layer}.\n */\nexport class FirecrawlRead extends Context.Service<FirecrawlRead, FirecrawlReadService>()(\n \"@betalyra/effect-uai/providers/firecrawl/FirecrawlRead\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n}\n\n// ---------------------------------------------------------------------------\n// Codec - request body\n// ---------------------------------------------------------------------------\n\ntype WireBody = {\n readonly url: string\n readonly formats: ReadonlyArray<string>\n readonly onlyMainContent?: boolean\n readonly includeTags?: ReadonlyArray<string>\n readonly excludeTags?: ReadonlyArray<string>\n readonly waitFor?: number\n readonly timeout?: number\n readonly mobile?: boolean\n readonly proxy?: string\n}\n\n// `markdown` / `html` map one-to-one onto Firecrawl's format names. `links` is\n// always requested so {@link ReadResponse.links} can be populated; it does not\n// change the per-page billing.\nconst buildBody = (request: FirecrawlReadRequest): WireBody => {\n const contentFormat = request.format ?? \"markdown\"\n return {\n url: request.url,\n formats: [contentFormat, \"links\"],\n ...(request.onlyMainContent !== undefined && { onlyMainContent: request.onlyMainContent }),\n ...(request.includeTags !== undefined && { includeTags: request.includeTags }),\n ...(request.excludeTags !== undefined && { excludeTags: request.excludeTags }),\n ...(request.waitFor !== undefined && { waitFor: Duration.toMillis(request.waitFor) }),\n ...(request.timeout !== undefined && { timeout: Duration.toMillis(request.timeout) }),\n ...(request.mobile !== undefined && { mobile: request.mobile }),\n ...(request.proxy !== undefined && { proxy: request.proxy }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Codec - response\n// ---------------------------------------------------------------------------\n\nconst WireMetadata = Schema.Struct({\n // Firecrawl returns `title` as a string, an array (repeated meta tags), or\n // null; normalize to a single string in `toResponse`.\n title: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)]))),\n sourceURL: Schema.optional(Schema.NullOr(Schema.String)),\n statusCode: Schema.optional(Schema.Number),\n})\n\nconst WireData = Schema.Struct({\n markdown: Schema.optional(Schema.NullOr(Schema.String)),\n html: Schema.optional(Schema.NullOr(Schema.String)),\n rawHtml: Schema.optional(Schema.NullOr(Schema.String)),\n links: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),\n metadata: Schema.optional(WireMetadata),\n})\n\nconst WireResponse = Schema.Struct({\n success: Schema.optional(Schema.Boolean),\n data: WireData,\n})\ntype WireResponse = typeof WireResponse.Type\n\nconst pickTitle = (title: string | ReadonlyArray<string> | null | undefined): string | undefined =>\n typeof title === \"string\" ? title : Array.isArray(title) ? title[0] : undefined\n\nconst toResponse = (wire: WireResponse, request: FirecrawlReadRequest): ReadResponse => {\n const { data } = wire\n const format = request.format ?? \"markdown\"\n const content = (format === \"html\" ? data.html : data.markdown) ?? \"\"\n const title = pickTitle(data.metadata?.title)\n const links = data.links ?? undefined\n return {\n url: data.metadata?.sourceURL ?? request.url,\n content,\n ...(title !== undefined && { title }),\n ...(links !== undefined && links.length > 0 && { links }),\n raw: wire,\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"firecrawl\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"firecrawl\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.firecrawl.dev\"\n\nconst postScrape = (\n cfg: Config,\n body: WireBody,\n): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v2/scrape`).pipe(\n HttpClientRequest.setHeader(\"Authorization\", `Bearer ${Redacted.value(cfg.apiKey)}`),\n HttpClientRequest.bodyJsonUnsafe(body),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst readImpl =\n (cfg: Config) =>\n (\n request: FirecrawlReadRequest,\n ): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.map(postScrape(cfg, buildBody(request)), (wire) => toResponse(wire, request))\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `FirecrawlReadService` value. For Layer-based setup, prefer\n * {@link layer}.\n */\nexport const make = (\n cfg: Config,\n): Effect.Effect<FirecrawlReadService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({\n read: (request) =>\n readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n }))\n\n/**\n * Layer registering both the provider-typed `FirecrawlRead` tag and the\n * generic `WebRead` tag over one implementation. A `CommonReadRequest` is\n * structurally a `FirecrawlReadRequest` with the extra knobs unset, so the\n * generic registration forwards directly.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<FirecrawlRead | WebRead, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(FirecrawlRead, make(cfg))\n const generic = Layer.effect(\n WebRead,\n Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/models.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Firecrawl's proxy tier for a scrape. Higher tiers survive more aggressive
|
|
4
|
+
* anti-bot protection at higher cost. Provider-specific, so it lives on
|
|
5
|
+
* `FirecrawlReadRequest`, not the cross-provider `CommonReadRequest`.
|
|
6
|
+
*/
|
|
7
|
+
type FirecrawlProxy = "basic" | "stealth" | "auto" | (string & {});
|
|
8
|
+
//#endregion
|
|
9
|
+
export { FirecrawlProxy };
|
|
10
|
+
//# sourceMappingURL=models.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAKA;;;;KAAY,cAAA"}
|
package/dist/models.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effect-uai/firecrawl",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Firecrawl provider for @effect-uai/core (web read).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"effect",
|
|
8
|
+
"firecrawl",
|
|
9
|
+
"scrape",
|
|
10
|
+
"web-read"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/betalyra/effect-uai/tree/main/packages/providers/firecrawl",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/betalyra/effect-uai/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Betalyra",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/betalyra/effect-uai",
|
|
21
|
+
"directory": "packages/providers/firecrawl"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./FirecrawlRead": {
|
|
38
|
+
"types": "./dist/FirecrawlRead.d.mts",
|
|
39
|
+
"import": "./dist/FirecrawlRead.mjs"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"effect": "4.0.0-beta.57",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"@effect-uai/core": "0.9.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@effect-uai/core": ">=0.2.0 <1",
|
|
52
|
+
"effect": "4.0.0-beta.57"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsdown",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Context, Duration, Effect, Layer, Redacted, Schema } from "effect"
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
|
+
import * as AiError from "@effect-uai/core/AiError"
|
|
4
|
+
import {
|
|
5
|
+
type CommonReadRequest,
|
|
6
|
+
type ReadResponse,
|
|
7
|
+
WebRead,
|
|
8
|
+
type WebReadService,
|
|
9
|
+
} from "@effect-uai/core/WebRead"
|
|
10
|
+
import type { FirecrawlProxy } from "./models.js"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Public types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Firecrawl-typed read request. Widens {@link CommonReadRequest} with
|
|
18
|
+
* Firecrawl's own scrape knobs; the common fields map onto Firecrawl's wire
|
|
19
|
+
* body (see the codec below).
|
|
20
|
+
*/
|
|
21
|
+
export type FirecrawlReadRequest = CommonReadRequest & {
|
|
22
|
+
/** Keep only the main content, dropping nav/header/footer/sidebar. */
|
|
23
|
+
readonly onlyMainContent?: boolean
|
|
24
|
+
/** Restrict extraction to these HTML tags / selectors. */
|
|
25
|
+
readonly includeTags?: ReadonlyArray<string>
|
|
26
|
+
/** Drop these HTML tags / selectors before extraction. */
|
|
27
|
+
readonly excludeTags?: ReadonlyArray<string>
|
|
28
|
+
/** Wait this long for the page to settle before scraping (JS-heavy sites). */
|
|
29
|
+
readonly waitFor?: Duration.Duration
|
|
30
|
+
/** Emulate a mobile device. */
|
|
31
|
+
readonly mobile?: boolean
|
|
32
|
+
/** Proxy tier - see {@link FirecrawlProxy}. */
|
|
33
|
+
readonly proxy?: FirecrawlProxy
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type FirecrawlReadService = {
|
|
37
|
+
readonly read: (request: FirecrawlReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Provider-typed service tag. Yield this for Firecrawl-typed reads (its own
|
|
42
|
+
* scrape knobs); yield the generic `WebRead` tag for provider-portable code.
|
|
43
|
+
* Both are registered by {@link layer}.
|
|
44
|
+
*/
|
|
45
|
+
export class FirecrawlRead extends Context.Service<FirecrawlRead, FirecrawlReadService>()(
|
|
46
|
+
"@betalyra/effect-uai/providers/firecrawl/FirecrawlRead",
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
export type Config = {
|
|
50
|
+
readonly apiKey: Redacted.Redacted
|
|
51
|
+
readonly baseUrl?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Codec - request body
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
type WireBody = {
|
|
59
|
+
readonly url: string
|
|
60
|
+
readonly formats: ReadonlyArray<string>
|
|
61
|
+
readonly onlyMainContent?: boolean
|
|
62
|
+
readonly includeTags?: ReadonlyArray<string>
|
|
63
|
+
readonly excludeTags?: ReadonlyArray<string>
|
|
64
|
+
readonly waitFor?: number
|
|
65
|
+
readonly timeout?: number
|
|
66
|
+
readonly mobile?: boolean
|
|
67
|
+
readonly proxy?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// `markdown` / `html` map one-to-one onto Firecrawl's format names. `links` is
|
|
71
|
+
// always requested so {@link ReadResponse.links} can be populated; it does not
|
|
72
|
+
// change the per-page billing.
|
|
73
|
+
const buildBody = (request: FirecrawlReadRequest): WireBody => {
|
|
74
|
+
const contentFormat = request.format ?? "markdown"
|
|
75
|
+
return {
|
|
76
|
+
url: request.url,
|
|
77
|
+
formats: [contentFormat, "links"],
|
|
78
|
+
...(request.onlyMainContent !== undefined && { onlyMainContent: request.onlyMainContent }),
|
|
79
|
+
...(request.includeTags !== undefined && { includeTags: request.includeTags }),
|
|
80
|
+
...(request.excludeTags !== undefined && { excludeTags: request.excludeTags }),
|
|
81
|
+
...(request.waitFor !== undefined && { waitFor: Duration.toMillis(request.waitFor) }),
|
|
82
|
+
...(request.timeout !== undefined && { timeout: Duration.toMillis(request.timeout) }),
|
|
83
|
+
...(request.mobile !== undefined && { mobile: request.mobile }),
|
|
84
|
+
...(request.proxy !== undefined && { proxy: request.proxy }),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Codec - response
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const WireMetadata = Schema.Struct({
|
|
93
|
+
// Firecrawl returns `title` as a string, an array (repeated meta tags), or
|
|
94
|
+
// null; normalize to a single string in `toResponse`.
|
|
95
|
+
title: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)]))),
|
|
96
|
+
sourceURL: Schema.optional(Schema.NullOr(Schema.String)),
|
|
97
|
+
statusCode: Schema.optional(Schema.Number),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const WireData = Schema.Struct({
|
|
101
|
+
markdown: Schema.optional(Schema.NullOr(Schema.String)),
|
|
102
|
+
html: Schema.optional(Schema.NullOr(Schema.String)),
|
|
103
|
+
rawHtml: Schema.optional(Schema.NullOr(Schema.String)),
|
|
104
|
+
links: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))),
|
|
105
|
+
metadata: Schema.optional(WireMetadata),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const WireResponse = Schema.Struct({
|
|
109
|
+
success: Schema.optional(Schema.Boolean),
|
|
110
|
+
data: WireData,
|
|
111
|
+
})
|
|
112
|
+
type WireResponse = typeof WireResponse.Type
|
|
113
|
+
|
|
114
|
+
const pickTitle = (title: string | ReadonlyArray<string> | null | undefined): string | undefined =>
|
|
115
|
+
typeof title === "string" ? title : Array.isArray(title) ? title[0] : undefined
|
|
116
|
+
|
|
117
|
+
const toResponse = (wire: WireResponse, request: FirecrawlReadRequest): ReadResponse => {
|
|
118
|
+
const { data } = wire
|
|
119
|
+
const format = request.format ?? "markdown"
|
|
120
|
+
const content = (format === "html" ? data.html : data.markdown) ?? ""
|
|
121
|
+
const title = pickTitle(data.metadata?.title)
|
|
122
|
+
const links = data.links ?? undefined
|
|
123
|
+
return {
|
|
124
|
+
url: data.metadata?.sourceURL ?? request.url,
|
|
125
|
+
content,
|
|
126
|
+
...(title !== undefined && { title }),
|
|
127
|
+
...(links !== undefined && links.length > 0 && { links }),
|
|
128
|
+
raw: wire,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// HTTP
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
const transportFailure = (cause: unknown): AiError.AiError =>
|
|
137
|
+
new AiError.Unavailable({ provider: "firecrawl", raw: cause })
|
|
138
|
+
|
|
139
|
+
const httpStatusError = (status: number, body: string): AiError.AiError => {
|
|
140
|
+
const provider = "firecrawl"
|
|
141
|
+
const raw = body
|
|
142
|
+
if (status === 429) return new AiError.RateLimited({ provider, raw })
|
|
143
|
+
if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
|
|
144
|
+
if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
|
|
145
|
+
if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
|
|
146
|
+
if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
|
|
147
|
+
if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
|
|
148
|
+
return new AiError.InvalidRequest({ provider, raw })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.firecrawl.dev"
|
|
152
|
+
|
|
153
|
+
const postScrape = (
|
|
154
|
+
cfg: Config,
|
|
155
|
+
body: WireBody,
|
|
156
|
+
): Effect.Effect<WireResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const client = yield* HttpClient.HttpClient
|
|
159
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v2/scrape`).pipe(
|
|
160
|
+
HttpClientRequest.setHeader("Authorization", `Bearer ${Redacted.value(cfg.apiKey)}`),
|
|
161
|
+
HttpClientRequest.bodyJsonUnsafe(body),
|
|
162
|
+
)
|
|
163
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
|
|
164
|
+
if (response.status >= 400) {
|
|
165
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
|
|
166
|
+
return yield* httpStatusError(response.status, text)
|
|
167
|
+
}
|
|
168
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
169
|
+
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(
|
|
170
|
+
Effect.mapError(transportFailure),
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const readImpl =
|
|
175
|
+
(cfg: Config) =>
|
|
176
|
+
(
|
|
177
|
+
request: FirecrawlReadRequest,
|
|
178
|
+
): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
179
|
+
Effect.map(postScrape(cfg, buildBody(request)), (wire) => toResponse(wire, request))
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Constructors
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build a `FirecrawlReadService` value. For Layer-based setup, prefer
|
|
187
|
+
* {@link layer}.
|
|
188
|
+
*/
|
|
189
|
+
export const make = (
|
|
190
|
+
cfg: Config,
|
|
191
|
+
): Effect.Effect<FirecrawlReadService, never, HttpClient.HttpClient> =>
|
|
192
|
+
Effect.map(HttpClient.HttpClient.asEffect(), (client) => ({
|
|
193
|
+
read: (request) =>
|
|
194
|
+
readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
195
|
+
}))
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Layer registering both the provider-typed `FirecrawlRead` tag and the
|
|
199
|
+
* generic `WebRead` tag over one implementation. A `CommonReadRequest` is
|
|
200
|
+
* structurally a `FirecrawlReadRequest` with the extra knobs unset, so the
|
|
201
|
+
* generic registration forwards directly.
|
|
202
|
+
*/
|
|
203
|
+
export const layer = (
|
|
204
|
+
cfg: Config,
|
|
205
|
+
): Layer.Layer<FirecrawlRead | WebRead, never, HttpClient.HttpClient> => {
|
|
206
|
+
const typed = Layer.effect(FirecrawlRead, make(cfg))
|
|
207
|
+
const generic = Layer.effect(
|
|
208
|
+
WebRead,
|
|
209
|
+
Effect.map(make(cfg), (s): WebReadService => ({ read: (request) => s.read(request) })),
|
|
210
|
+
)
|
|
211
|
+
return Layer.merge(typed, generic)
|
|
212
|
+
}
|
package/src/index.ts
ADDED
package/src/models.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firecrawl's proxy tier for a scrape. Higher tiers survive more aggressive
|
|
3
|
+
* anti-bot protection at higher cost. Provider-specific, so it lives on
|
|
4
|
+
* `FirecrawlReadRequest`, not the cross-provider `CommonReadRequest`.
|
|
5
|
+
*/
|
|
6
|
+
export type FirecrawlProxy =
|
|
7
|
+
| "basic"
|
|
8
|
+
| "stealth"
|
|
9
|
+
| "auto"
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
11
|
+
| (string & {})
|