@effect-uai/firecrawl 0.10.0 → 0.11.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/dist/FirecrawlRead.d.mts +31 -2
- package/dist/FirecrawlRead.d.mts.map +1 -1
- package/dist/FirecrawlRead.mjs +42 -1
- package/dist/FirecrawlRead.mjs.map +1 -1
- package/package.json +4 -4
- package/src/FirecrawlRead.ts +114 -1
package/dist/FirecrawlRead.d.mts
CHANGED
|
@@ -6,7 +6,7 @@ import { CommonReadRequest, ReadResponse, WebRead } from "@effect-uai/core/WebRe
|
|
|
6
6
|
|
|
7
7
|
//#region src/FirecrawlRead.d.ts
|
|
8
8
|
declare namespace FirecrawlRead_d_exports {
|
|
9
|
-
export { Config, FirecrawlRead, FirecrawlReadRequest, FirecrawlReadService, layer, make };
|
|
9
|
+
export { Config, FirecrawlMapRequest, FirecrawlRead, FirecrawlReadRequest, FirecrawlReadService, MapLink, MapResponse, layer, make };
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Firecrawl-typed read request. Widens {@link CommonReadRequest} with
|
|
@@ -21,8 +21,37 @@ type FirecrawlReadRequest = CommonReadRequest & {
|
|
|
21
21
|
readonly mobile?: boolean; /** Proxy tier - see {@link FirecrawlProxy}. */
|
|
22
22
|
readonly proxy?: FirecrawlProxy;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Firecrawl-typed map (sitemap / URL discovery) request. Given a base `url`,
|
|
26
|
+
* Firecrawl returns the site's URLs without scraping each one - handy for
|
|
27
|
+
* locating specific pages (pricing, docs) before a targeted read. All fields
|
|
28
|
+
* but `url` are optional.
|
|
29
|
+
*/
|
|
30
|
+
type FirecrawlMapRequest = {
|
|
31
|
+
readonly url: string; /** Order results by relevance to this query. */
|
|
32
|
+
readonly search?: string; /** Cap the number of links returned. */
|
|
33
|
+
readonly limit?: number; /** How to use the site's sitemap: `only` (sitemap alone), `include`, or `skip`. */
|
|
34
|
+
readonly sitemap?: "only" | "include" | "skip"; /** Include links on subdomains of the base URL. */
|
|
35
|
+
readonly includeSubdomains?: boolean; /** Abort the map if it runs past this duration. */
|
|
36
|
+
readonly timeout?: Duration.Duration;
|
|
37
|
+
};
|
|
38
|
+
/** One discovered URL, with page metadata where Firecrawl has it. */
|
|
39
|
+
type MapLink = {
|
|
40
|
+
readonly url: string;
|
|
41
|
+
readonly title?: string;
|
|
42
|
+
readonly description?: string;
|
|
43
|
+
};
|
|
44
|
+
type MapResponse = {
|
|
45
|
+
readonly links: ReadonlyArray<MapLink>;
|
|
46
|
+
readonly raw: unknown;
|
|
47
|
+
};
|
|
24
48
|
type FirecrawlReadService = {
|
|
25
49
|
readonly read: (request: FirecrawlReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>;
|
|
50
|
+
/**
|
|
51
|
+
* Discover a site's URLs (sitemap-style) without scraping. Firecrawl-specific;
|
|
52
|
+
* not on the generic `WebRead` tag, whose contract is a single-URL read.
|
|
53
|
+
*/
|
|
54
|
+
readonly map: (request: FirecrawlMapRequest) => Effect.Effect<MapResponse, AiError.AiError>;
|
|
26
55
|
};
|
|
27
56
|
declare const FirecrawlRead_base: Context.ServiceClass<FirecrawlRead, "@betalyra/effect-uai/providers/firecrawl/FirecrawlRead", FirecrawlReadService>;
|
|
28
57
|
/**
|
|
@@ -48,5 +77,5 @@ declare const make: (cfg: Config) => Effect.Effect<FirecrawlReadService, never,
|
|
|
48
77
|
*/
|
|
49
78
|
declare const layer: (cfg: Config) => Layer.Layer<FirecrawlRead | WebRead, never, HttpClient.HttpClient>;
|
|
50
79
|
//#endregion
|
|
51
|
-
export { Config, FirecrawlRead, FirecrawlReadRequest, FirecrawlReadService, layer, make, FirecrawlRead_d_exports as t };
|
|
80
|
+
export { Config, FirecrawlMapRequest, FirecrawlRead, FirecrawlReadRequest, FirecrawlReadService, MapLink, MapResponse, layer, make, FirecrawlRead_d_exports as t };
|
|
52
81
|
//# sourceMappingURL=FirecrawlRead.d.mts.map
|
|
@@ -1 +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,
|
|
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;WAEd,OAAA,GAAU,QAAA,CAAS,QAAA,EARlB;EAAA,SAUD,MAAA;WAEA,KAAA,GAAQ,cAAA;AAAA;;;;;;;KASP,mBAAA;EAAA,SACD,GAAA,UAlBc;EAAA,SAoBd,MAAA,WAlBc;EAAA,SAoBd,KAAA,WAlBU;EAAA,SAoBV,OAAA,gCAlBA;EAAA,SAoBA,iBAAA,YAlBQ;EAAA,SAoBR,OAAA,GAAU,QAAA,CAAS,QAAA;AAAA;AAX9B;AAAA,KAeY,OAAA;EAAA,SACD,GAAA;EAAA,SACA,KAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGC,WAAA;EAAA,SACD,KAAA,EAAO,aAAA,CAAc,OAAA;EAAA,SACrB,GAAA;AAAA;AAAA,KAGC,oBAAA;EAAA,SACD,IAAA,GAAO,OAAA,EAAS,oBAAA,KAAyB,MAAA,CAAO,MAAA,CAAO,YAAA,EAAc,OAAA,CAAQ,OAAA;EAhBlD;;AAItC;;EAJsC,SAqB3B,GAAA,GAAM,OAAA,EAAS,mBAAA,KAAwB,MAAA,CAAO,MAAA,CAAO,WAAA,EAAa,OAAA,CAAQ,OAAA;AAAA;AAAA,cACpF,kBAAA;;;;;AAZD;cAmBa,aAAA,SAAsB,kBAAA;AAAA,KAIvB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA;AAAA;;;;AApBX;cAwOa,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,oBAAA,SAA6B,UAAA,CAAW,UAAA;;;;;;;cAc5C,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,aAAA,GAAgB,OAAA,SAAgB,UAAA,CAAW,UAAA"}
|
package/dist/FirecrawlRead.mjs
CHANGED
|
@@ -29,6 +29,14 @@ const buildBody = (request) => {
|
|
|
29
29
|
...request.proxy !== void 0 && { proxy: request.proxy }
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
|
+
const buildMapBody = (request) => ({
|
|
33
|
+
url: request.url,
|
|
34
|
+
...request.search !== void 0 && { search: request.search },
|
|
35
|
+
...request.limit !== void 0 && { limit: request.limit },
|
|
36
|
+
...request.sitemap !== void 0 && { sitemap: request.sitemap },
|
|
37
|
+
...request.includeSubdomains !== void 0 && { includeSubdomains: request.includeSubdomains },
|
|
38
|
+
...request.timeout !== void 0 && { timeout: Duration.toMillis(request.timeout) }
|
|
39
|
+
});
|
|
32
40
|
const WireMetadata = Schema.Struct({
|
|
33
41
|
title: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Array(Schema.String)]))),
|
|
34
42
|
sourceURL: Schema.optional(Schema.NullOr(Schema.String)),
|
|
@@ -59,6 +67,24 @@ const toResponse = (wire, request) => {
|
|
|
59
67
|
raw: wire
|
|
60
68
|
};
|
|
61
69
|
};
|
|
70
|
+
const WireMapLink = Schema.Struct({
|
|
71
|
+
url: Schema.String,
|
|
72
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
73
|
+
description: Schema.optional(Schema.NullOr(Schema.String))
|
|
74
|
+
});
|
|
75
|
+
const WireMapResponse = Schema.Struct({
|
|
76
|
+
success: Schema.optional(Schema.Boolean),
|
|
77
|
+
links: Schema.Array(Schema.Union([WireMapLink, Schema.String]))
|
|
78
|
+
});
|
|
79
|
+
const toMapLink = (link) => typeof link === "string" ? { url: link } : {
|
|
80
|
+
url: link.url,
|
|
81
|
+
...link.title != null && { title: link.title },
|
|
82
|
+
...link.description != null && { description: link.description }
|
|
83
|
+
};
|
|
84
|
+
const toMapResponse = (wire) => ({
|
|
85
|
+
links: wire.links.map(toMapLink),
|
|
86
|
+
raw: wire
|
|
87
|
+
});
|
|
62
88
|
const transportFailure = (cause) => new AiError.Unavailable({
|
|
63
89
|
provider: "firecrawl",
|
|
64
90
|
raw: cause
|
|
@@ -112,11 +138,26 @@ const postScrape = (cfg, body) => Effect.gen(function* () {
|
|
|
112
138
|
return yield* Schema.decodeUnknownEffect(WireResponse)(json).pipe(Effect.mapError(transportFailure));
|
|
113
139
|
});
|
|
114
140
|
const readImpl = (cfg) => (request) => Effect.map(postScrape(cfg, buildBody(request)), (wire) => toResponse(wire, request));
|
|
141
|
+
const postMap = (cfg, body) => Effect.gen(function* () {
|
|
142
|
+
const client = yield* HttpClient.HttpClient;
|
|
143
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v2/map`).pipe(HttpClientRequest.setHeader("Authorization", `Bearer ${Redacted.value(cfg.apiKey)}`), HttpClientRequest.bodyJsonUnsafe(body));
|
|
144
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
|
|
145
|
+
if (response.status >= 400) {
|
|
146
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
|
|
147
|
+
return yield* httpStatusError(response.status, text);
|
|
148
|
+
}
|
|
149
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure));
|
|
150
|
+
return yield* Schema.decodeUnknownEffect(WireMapResponse)(json).pipe(Effect.mapError(transportFailure));
|
|
151
|
+
});
|
|
152
|
+
const mapImpl = (cfg) => (request) => Effect.map(postMap(cfg, buildMapBody(request)), toMapResponse);
|
|
115
153
|
/**
|
|
116
154
|
* Build a `FirecrawlReadService` value. For Layer-based setup, prefer
|
|
117
155
|
* {@link layer}.
|
|
118
156
|
*/
|
|
119
|
-
const make = (cfg) => Effect.map(HttpClient.HttpClient
|
|
157
|
+
const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => ({
|
|
158
|
+
read: (request) => readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
159
|
+
map: (request) => mapImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client))
|
|
160
|
+
}));
|
|
120
161
|
/**
|
|
121
162
|
* Layer registering both the provider-typed `FirecrawlRead` tag and the
|
|
122
163
|
* generic `WebRead` tag over one implementation. A `CommonReadRequest` is
|
|
@@ -1 +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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"}
|
|
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\n/**\n * Firecrawl-typed map (sitemap / URL discovery) request. Given a base `url`,\n * Firecrawl returns the site's URLs without scraping each one - handy for\n * locating specific pages (pricing, docs) before a targeted read. All fields\n * but `url` are optional.\n */\nexport type FirecrawlMapRequest = {\n readonly url: string\n /** Order results by relevance to this query. */\n readonly search?: string\n /** Cap the number of links returned. */\n readonly limit?: number\n /** How to use the site's sitemap: `only` (sitemap alone), `include`, or `skip`. */\n readonly sitemap?: \"only\" | \"include\" | \"skip\"\n /** Include links on subdomains of the base URL. */\n readonly includeSubdomains?: boolean\n /** Abort the map if it runs past this duration. */\n readonly timeout?: Duration.Duration\n}\n\n/** One discovered URL, with page metadata where Firecrawl has it. */\nexport type MapLink = {\n readonly url: string\n readonly title?: string\n readonly description?: string\n}\n\nexport type MapResponse = {\n readonly links: ReadonlyArray<MapLink>\n readonly raw: unknown\n}\n\nexport type FirecrawlReadService = {\n readonly read: (request: FirecrawlReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>\n /**\n * Discover a site's URLs (sitemap-style) without scraping. Firecrawl-specific;\n * not on the generic `WebRead` tag, whose contract is a single-URL read.\n */\n readonly map: (request: FirecrawlMapRequest) => Effect.Effect<MapResponse, 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\ntype WireMapBody = {\n readonly url: string\n readonly search?: string\n readonly limit?: number\n readonly sitemap?: string\n readonly includeSubdomains?: boolean\n readonly timeout?: number\n}\n\nconst buildMapBody = (request: FirecrawlMapRequest): WireMapBody => ({\n url: request.url,\n ...(request.search !== undefined && { search: request.search }),\n ...(request.limit !== undefined && { limit: request.limit }),\n ...(request.sitemap !== undefined && { sitemap: request.sitemap }),\n ...(request.includeSubdomains !== undefined && { includeSubdomains: request.includeSubdomains }),\n ...(request.timeout !== undefined && { timeout: Duration.toMillis(request.timeout) }),\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// v2 returns link objects; older responses returned bare URL strings. Accept\n// both and normalize a string to `{ url }` in `toMapResponse`.\nconst WireMapLink = Schema.Struct({\n url: Schema.String,\n title: Schema.optional(Schema.NullOr(Schema.String)),\n description: Schema.optional(Schema.NullOr(Schema.String)),\n})\n\nconst WireMapResponse = Schema.Struct({\n success: Schema.optional(Schema.Boolean),\n links: Schema.Array(Schema.Union([WireMapLink, Schema.String])),\n})\ntype WireMapResponse = typeof WireMapResponse.Type\n\nconst toMapLink = (link: typeof WireMapLink.Type | string): MapLink =>\n typeof link === \"string\"\n ? { url: link }\n : {\n url: link.url,\n ...(link.title != null && { title: link.title }),\n ...(link.description != null && { description: link.description }),\n }\n\nconst toMapResponse = (wire: WireMapResponse): MapResponse => ({\n links: wire.links.map(toMapLink),\n raw: wire,\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\nconst postMap = (\n cfg: Config,\n body: WireMapBody,\n): Effect.Effect<WireMapResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v2/map`).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(WireMapResponse)(json).pipe(\n Effect.mapError(transportFailure),\n )\n })\n\nconst mapImpl =\n (cfg: Config) =>\n (\n request: FirecrawlMapRequest,\n ): Effect.Effect<MapResponse, AiError.AiError, HttpClient.HttpClient> =>\n Effect.map(postMap(cfg, buildMapBody(request)), toMapResponse)\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, (client) => ({\n read: (request) =>\n readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),\n map: (request) =>\n mapImpl(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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/firecrawl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Firecrawl provider for @effect-uai/core (web read).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"effect": "4.0.0-beta.
|
|
46
|
+
"effect": "4.0.0-beta.94",
|
|
47
47
|
"typescript": "^6.0.3",
|
|
48
|
-
"@effect-uai/core": "0.
|
|
48
|
+
"@effect-uai/core": "0.11.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@effect-uai/core": ">=0.2.0 <1",
|
|
52
|
-
"effect": "4.0.0-beta.
|
|
52
|
+
"effect": ">=4.0.0-beta.94 <5.0.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsdown",
|
package/src/FirecrawlRead.ts
CHANGED
|
@@ -33,8 +33,45 @@ export type FirecrawlReadRequest = CommonReadRequest & {
|
|
|
33
33
|
readonly proxy?: FirecrawlProxy
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Firecrawl-typed map (sitemap / URL discovery) request. Given a base `url`,
|
|
38
|
+
* Firecrawl returns the site's URLs without scraping each one - handy for
|
|
39
|
+
* locating specific pages (pricing, docs) before a targeted read. All fields
|
|
40
|
+
* but `url` are optional.
|
|
41
|
+
*/
|
|
42
|
+
export type FirecrawlMapRequest = {
|
|
43
|
+
readonly url: string
|
|
44
|
+
/** Order results by relevance to this query. */
|
|
45
|
+
readonly search?: string
|
|
46
|
+
/** Cap the number of links returned. */
|
|
47
|
+
readonly limit?: number
|
|
48
|
+
/** How to use the site's sitemap: `only` (sitemap alone), `include`, or `skip`. */
|
|
49
|
+
readonly sitemap?: "only" | "include" | "skip"
|
|
50
|
+
/** Include links on subdomains of the base URL. */
|
|
51
|
+
readonly includeSubdomains?: boolean
|
|
52
|
+
/** Abort the map if it runs past this duration. */
|
|
53
|
+
readonly timeout?: Duration.Duration
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One discovered URL, with page metadata where Firecrawl has it. */
|
|
57
|
+
export type MapLink = {
|
|
58
|
+
readonly url: string
|
|
59
|
+
readonly title?: string
|
|
60
|
+
readonly description?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type MapResponse = {
|
|
64
|
+
readonly links: ReadonlyArray<MapLink>
|
|
65
|
+
readonly raw: unknown
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
export type FirecrawlReadService = {
|
|
37
69
|
readonly read: (request: FirecrawlReadRequest) => Effect.Effect<ReadResponse, AiError.AiError>
|
|
70
|
+
/**
|
|
71
|
+
* Discover a site's URLs (sitemap-style) without scraping. Firecrawl-specific;
|
|
72
|
+
* not on the generic `WebRead` tag, whose contract is a single-URL read.
|
|
73
|
+
*/
|
|
74
|
+
readonly map: (request: FirecrawlMapRequest) => Effect.Effect<MapResponse, AiError.AiError>
|
|
38
75
|
}
|
|
39
76
|
|
|
40
77
|
/**
|
|
@@ -85,6 +122,24 @@ const buildBody = (request: FirecrawlReadRequest): WireBody => {
|
|
|
85
122
|
}
|
|
86
123
|
}
|
|
87
124
|
|
|
125
|
+
type WireMapBody = {
|
|
126
|
+
readonly url: string
|
|
127
|
+
readonly search?: string
|
|
128
|
+
readonly limit?: number
|
|
129
|
+
readonly sitemap?: string
|
|
130
|
+
readonly includeSubdomains?: boolean
|
|
131
|
+
readonly timeout?: number
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const buildMapBody = (request: FirecrawlMapRequest): WireMapBody => ({
|
|
135
|
+
url: request.url,
|
|
136
|
+
...(request.search !== undefined && { search: request.search }),
|
|
137
|
+
...(request.limit !== undefined && { limit: request.limit }),
|
|
138
|
+
...(request.sitemap !== undefined && { sitemap: request.sitemap }),
|
|
139
|
+
...(request.includeSubdomains !== undefined && { includeSubdomains: request.includeSubdomains }),
|
|
140
|
+
...(request.timeout !== undefined && { timeout: Duration.toMillis(request.timeout) }),
|
|
141
|
+
})
|
|
142
|
+
|
|
88
143
|
// ---------------------------------------------------------------------------
|
|
89
144
|
// Codec - response
|
|
90
145
|
// ---------------------------------------------------------------------------
|
|
@@ -129,6 +184,34 @@ const toResponse = (wire: WireResponse, request: FirecrawlReadRequest): ReadResp
|
|
|
129
184
|
}
|
|
130
185
|
}
|
|
131
186
|
|
|
187
|
+
// v2 returns link objects; older responses returned bare URL strings. Accept
|
|
188
|
+
// both and normalize a string to `{ url }` in `toMapResponse`.
|
|
189
|
+
const WireMapLink = Schema.Struct({
|
|
190
|
+
url: Schema.String,
|
|
191
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
192
|
+
description: Schema.optional(Schema.NullOr(Schema.String)),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const WireMapResponse = Schema.Struct({
|
|
196
|
+
success: Schema.optional(Schema.Boolean),
|
|
197
|
+
links: Schema.Array(Schema.Union([WireMapLink, Schema.String])),
|
|
198
|
+
})
|
|
199
|
+
type WireMapResponse = typeof WireMapResponse.Type
|
|
200
|
+
|
|
201
|
+
const toMapLink = (link: typeof WireMapLink.Type | string): MapLink =>
|
|
202
|
+
typeof link === "string"
|
|
203
|
+
? { url: link }
|
|
204
|
+
: {
|
|
205
|
+
url: link.url,
|
|
206
|
+
...(link.title != null && { title: link.title }),
|
|
207
|
+
...(link.description != null && { description: link.description }),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const toMapResponse = (wire: WireMapResponse): MapResponse => ({
|
|
211
|
+
links: wire.links.map(toMapLink),
|
|
212
|
+
raw: wire,
|
|
213
|
+
})
|
|
214
|
+
|
|
132
215
|
// ---------------------------------------------------------------------------
|
|
133
216
|
// HTTP
|
|
134
217
|
// ---------------------------------------------------------------------------
|
|
@@ -178,6 +261,34 @@ const readImpl =
|
|
|
178
261
|
): Effect.Effect<ReadResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
179
262
|
Effect.map(postScrape(cfg, buildBody(request)), (wire) => toResponse(wire, request))
|
|
180
263
|
|
|
264
|
+
const postMap = (
|
|
265
|
+
cfg: Config,
|
|
266
|
+
body: WireMapBody,
|
|
267
|
+
): Effect.Effect<WireMapResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
268
|
+
Effect.gen(function* () {
|
|
269
|
+
const client = yield* HttpClient.HttpClient
|
|
270
|
+
const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/v2/map`).pipe(
|
|
271
|
+
HttpClientRequest.setHeader("Authorization", `Bearer ${Redacted.value(cfg.apiKey)}`),
|
|
272
|
+
HttpClientRequest.bodyJsonUnsafe(body),
|
|
273
|
+
)
|
|
274
|
+
const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
|
|
275
|
+
if (response.status >= 400) {
|
|
276
|
+
const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
|
|
277
|
+
return yield* httpStatusError(response.status, text)
|
|
278
|
+
}
|
|
279
|
+
const json = yield* response.json.pipe(Effect.mapError(transportFailure))
|
|
280
|
+
return yield* Schema.decodeUnknownEffect(WireMapResponse)(json).pipe(
|
|
281
|
+
Effect.mapError(transportFailure),
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const mapImpl =
|
|
286
|
+
(cfg: Config) =>
|
|
287
|
+
(
|
|
288
|
+
request: FirecrawlMapRequest,
|
|
289
|
+
): Effect.Effect<MapResponse, AiError.AiError, HttpClient.HttpClient> =>
|
|
290
|
+
Effect.map(postMap(cfg, buildMapBody(request)), toMapResponse)
|
|
291
|
+
|
|
181
292
|
// ---------------------------------------------------------------------------
|
|
182
293
|
// Constructors
|
|
183
294
|
// ---------------------------------------------------------------------------
|
|
@@ -189,9 +300,11 @@ const readImpl =
|
|
|
189
300
|
export const make = (
|
|
190
301
|
cfg: Config,
|
|
191
302
|
): Effect.Effect<FirecrawlReadService, never, HttpClient.HttpClient> =>
|
|
192
|
-
Effect.map(HttpClient.HttpClient
|
|
303
|
+
Effect.map(HttpClient.HttpClient, (client) => ({
|
|
193
304
|
read: (request) =>
|
|
194
305
|
readImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
306
|
+
map: (request) =>
|
|
307
|
+
mapImpl(cfg)(request).pipe(Effect.provideService(HttpClient.HttpClient, client)),
|
|
195
308
|
}))
|
|
196
309
|
|
|
197
310
|
/**
|