@cloro-dev/seo-provider 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cloro
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 all
13
+ 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 THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # cloro provider for seo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Adds [cloro](https://cloro.dev/?utm_source=seo-provider&utm_medium=byok) to
4
+ [`seo`](https://github.com/iannuttall/seo), the SEO command line tool and MCP
5
+ server. With your own cloro API key, `seo` gets:
6
+
7
+ - live Google results for `seo research serp`,
8
+ - Google organic results with the pages the AI Overview cites,
9
+ - answers from ChatGPT, Google AI Mode, Gemini and Bing AI Mode, with a check
10
+ of whether each answer cites your site, mentions it, or neither.
11
+
12
+ The package runs only on your machine, in the `seo` CLI and the local MCP
13
+ server (`seo mcp serve`). It has no runtime dependencies and sends every
14
+ request through `seo`, which keeps the key and limits the requests.
15
+
16
+ ## Requirements
17
+
18
+ - Node.js 22.19 or newer
19
+ - `seo` 0.17.3 or newer: `npm install --global seo`
20
+ - A cloro API key. The Free plan has 500 credits a month:
21
+ [sign up](https://dashboard.cloro.dev/api-keys?utm_source=seo-provider&utm_medium=byok).
22
+
23
+ ## Install and connect
24
+
25
+ ```sh
26
+ seo providers install @cloro-dev/seo-provider
27
+ seo providers connect cloro
28
+ ```
29
+
30
+ `seo providers connect` asks for the API key and stores it. You can also set
31
+ `SEO_CLORO_API_KEY` in the environment. The connection check reads your credit
32
+ balance and costs no credits.
33
+
34
+ ## Live Google results
35
+
36
+ ```sh
37
+ seo research serp --provider cloro --keywords "crm software,best crm"
38
+ ```
39
+
40
+ This uses the `serp-snapshot` capability: one Google search per keyword, by
41
+ country, language and device (desktop or mobile), up to 100 results. It
42
+ queues the search on cloro's async API and reads it until it finishes, up to
43
+ about a minute.
44
+
45
+ ## Actions
46
+
47
+ Run an action with `seo providers run cloro <action> --params '<json>'`.
48
+ Agents run them through `seo_describe_provider` and `seo_run_provider` in the
49
+ local MCP server.
50
+
51
+ ### serp
52
+
53
+ Google organic results and the pages the AI Overview cites, for up to 5
54
+ keywords.
55
+
56
+ ```sh
57
+ seo providers run cloro serp --params '{"keywords":["best crm for startups"],"country":"US"}'
58
+ ```
59
+
60
+ Each row has `keyword`, `type` (`organic` or `ai_overview`), `position`,
61
+ `url`, `domain` and `title`.
62
+
63
+ ### ai-answers
64
+
65
+ Asks up to 5 questions on each engine and reports, for each answer, whether
66
+ it cites the site (`cited`), names it (`mentioned`), or `neither`.
67
+
68
+ ```sh
69
+ seo providers run cloro ai-answers --params '{"domain":"example.com","questions":["what is the best crm for startups"]}'
70
+ ```
71
+
72
+ | Parameter | What it does |
73
+ | --- | --- |
74
+ | `domain` | The site to look for. Required. |
75
+ | `questions` | 1 to 5 questions. |
76
+ | `engines` | Any of `chatgpt`, `google_ai_mode`, `gemini`, `bing_ai_mode`. Default all four. |
77
+ | `brand` | Names that count as a mention. Default the first part of the domain. |
78
+ | `country` | Two-letter country code. Default `US`. |
79
+ | `waitSeconds` | Seconds to wait before reading the answers. Default 45 (20 for `serp`). |
80
+ | `tasks` | Task ids to read, instead of asking again. |
81
+
82
+ Each row has the `question`, the `engine`, the `result`, the cited URL, up to
83
+ 8 other cited sites, the date of the answer and the credits it cost.
84
+
85
+ ### Answers that are not ready
86
+
87
+ `serp` and `ai-answers` use cloro's async API. An AI answer can take one to
88
+ three minutes, and `seo` allows 8 requests for each run. An answer that is not
89
+ ready, or not read in this run, comes back with result `pending`, and its task
90
+ id is in `pendingTasks`. Read it later at no cost:
91
+
92
+ ```sh
93
+ seo providers run cloro ai-answers --params '{"domain":"example.com","tasks":["<task id>"]}'
94
+ ```
95
+
96
+ cloro keeps a task for 24 hours. A run sent again on the same day with the
97
+ same question gets the same tasks back and is not charged again. `credits`
98
+ in the result is what cloro charged for the tasks in that result.
99
+
100
+ ## Credits
101
+
102
+ | Request | Credits |
103
+ | --- | --- |
104
+ | Google search (`serp-snapshot`) | 3, plus 2 for each page after the first |
105
+ | Google search with AI Overview (`serp`) | 5 |
106
+ | ChatGPT answer | 5 |
107
+ | Google AI Mode, Gemini or Bing AI Mode answer | 4 |
108
+
109
+ cloro charges a request only when it succeeds. Plans and prices:
110
+ [cloro.dev/pricing](https://cloro.dev/pricing/?utm_source=seo-provider&utm_medium=byok).
111
+ The cost estimate that `seo` shows uses the highest paid rate per credit
112
+ (Lite plan), so larger plans pay less than the estimate.
113
+
114
+ ## Development
115
+
116
+ ```sh
117
+ npm install
118
+ npm run build
119
+ npm run typecheck
120
+ npm test
121
+ ```
122
+
123
+ The tests use payloads trimmed from live cloro responses.
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,14 @@
1
+ import { SeoProviderActivate } from "seo/provider-sdk";
2
+ type Item = Record<string, unknown>;
3
+ type Verdict = 'cited' | 'mentioned' | 'neither' | 'no_answer';
4
+ export declare function classify(answer: Item, site: {
5
+ domain: string;
6
+ names: string[];
7
+ }): {
8
+ result: Verdict;
9
+ citedUrl: string | null;
10
+ sources: string[];
11
+ model: string | null;
12
+ };
13
+ declare const activate: SeoProviderActivate;
14
+ export { activate as default };
package/dist/index.js ADDED
@@ -0,0 +1,789 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ // src/index.ts
3
+ const API = "https://api.cloro.dev";
4
+ const RESULTS_PER_PAGE = 10;
5
+ const MAX_DEPTH = 100;
6
+ const MICROS_PER_CREDIT = 800;
7
+ const MAX_KEYWORDS = 5;
8
+ const MAX_QUESTIONS = 5;
9
+ const MAX_SOURCES = 8;
10
+ const BING_PROMPT_LIMIT = 3500;
11
+ const ENGINES = {
12
+ chatgpt: {
13
+ type: "CHATGPT",
14
+ credits: 5
15
+ },
16
+ google_ai_mode: {
17
+ type: "AIMODE",
18
+ credits: 4
19
+ },
20
+ gemini: {
21
+ type: "GEMINI",
22
+ credits: 4
23
+ },
24
+ bing_ai_mode: {
25
+ type: "BING_AIMODE",
26
+ credits: 4
27
+ }
28
+ };
29
+ const ENGINE_NAMES = Object.keys(ENGINES);
30
+ function failure(code, message) {
31
+ return Object.assign(new Error(message), { code });
32
+ }
33
+ function apiKey(connection) {
34
+ const value = connection.credentials.apiKey?.trim() ?? "";
35
+ if (!value || value.length > 4096) throw failure("authentication", "cloro needs a valid API key.");
36
+ return value;
37
+ }
38
+ function headers(connection) {
39
+ return {
40
+ authorization: `Bearer ${apiKey(connection)}`,
41
+ "content-type": "application/json"
42
+ };
43
+ }
44
+ const items = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "object" && !!item) : [];
45
+ const text = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
46
+ function country(value) {
47
+ const code = typeof value === "string" ? value.trim().toUpperCase() : "US";
48
+ if (!/^[A-Z]{2}$/u.test(code)) throw failure("configuration", "Use a two-letter country code, such as US.");
49
+ return code;
50
+ }
51
+ function safeUrl(value) {
52
+ if (typeof value !== "string") return null;
53
+ try {
54
+ const parsed = new URL(value);
55
+ if (!["http:", "https:"].includes(parsed.protocol)) return null;
56
+ if (/(^|\.)google\.[a-z.]+$/u.test(parsed.hostname) && parsed.pathname.startsWith("/goto")) return null;
57
+ parsed.username = "";
58
+ parsed.password = "";
59
+ return parsed.toString();
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ const hostOf = (url) => {
65
+ if (!url) return null;
66
+ try {
67
+ return new URL(url).hostname.toLowerCase();
68
+ } catch {
69
+ return null;
70
+ }
71
+ };
72
+ async function verify(connection, runtime) {
73
+ if (typeof (await runtime.requestJson({
74
+ operation: "verify",
75
+ url: `${API}/v1/credits`,
76
+ headers: headers(connection)
77
+ }))?.remaining !== "number") throw failure("invalid-response", "cloro returned invalid credit data.");
78
+ }
79
+ function snapshotMarket(value) {
80
+ if (value.searchEngine !== "google") throw failure("configuration", "cloro SERP snapshots support Google.");
81
+ if (value.location) throw failure("configuration", "cloro SERP snapshots take country-level markets. Omit the location.");
82
+ if (!/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/iu.test(value.languageCode)) throw failure("configuration", "Use a valid language code, such as en.");
83
+ return {
84
+ countryCode: country(value.countryCode),
85
+ languageCode: value.languageCode,
86
+ device: value.device ?? "desktop"
87
+ };
88
+ }
89
+ function pagesFor(depth) {
90
+ if (!Number.isSafeInteger(depth) || depth < 1 || depth > MAX_DEPTH) throw failure("configuration", `cloro SERP depth must be from 1 to ${MAX_DEPTH}.`);
91
+ return Math.ceil(depth / RESULTS_PER_PAGE);
92
+ }
93
+ const snapshotCredits = (pages) => 3 + 2 * (pages - 1);
94
+ const SNAPSHOT_REQUESTS = 20;
95
+ const POLL_MS = 3e3;
96
+ const snakeCase = (key) => key.replace(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
97
+ function present(value) {
98
+ if (value === void 0 || value === null) return false;
99
+ if (Array.isArray(value)) return value.length > 0;
100
+ if (typeof value === "string") return value.trim().length > 0;
101
+ if (typeof value === "object") return Object.keys(value).length > 0;
102
+ return true;
103
+ }
104
+ async function serpSnapshot(input, runtime) {
105
+ const keyword = input.keyword.trim().replace(/\s+/gu, " ");
106
+ if (!keyword || keyword.length > 1e3) throw failure("configuration", "Use a keyword of 1 to 1,000 characters.");
107
+ const market = snapshotMarket(input.market);
108
+ const pages = pagesFor(input.depth);
109
+ const [queued] = await queue(input, runtime, "serp-snapshot:queue", [{
110
+ taskType: "GOOGLE",
111
+ payload: {
112
+ query: keyword,
113
+ country: market.countryCode,
114
+ hl: market.languageCode,
115
+ device: market.device,
116
+ ...pages > 1 ? { pages } : {}
117
+ }
118
+ }]);
119
+ if (!queued || "error" in queued) throw failure("remote-error", queued?.error ?? "cloro queued no task.");
120
+ let task;
121
+ for (let read = 1; read < SNAPSHOT_REQUESTS; read += 1) {
122
+ await setTimeout(POLL_MS);
123
+ [task] = await readTasks(input, runtime, "serp-snapshot:read", [queued.id]);
124
+ if (task?.state !== "pending") break;
125
+ }
126
+ if (task?.state === "failed") throw failure("remote-error", task.error);
127
+ if (task?.state !== "done") throw failure("timeout", `cloro did not finish the search in time. Task ${queued.id} keeps the result for 24 hours.`);
128
+ const result = task.response;
129
+ const observedAt = runtime.now();
130
+ const raw = items(result.organicResults);
131
+ const mapped = raw.flatMap((item) => {
132
+ const url = safeUrl(item.link);
133
+ const rank = item.position;
134
+ if (!url || typeof rank !== "number" || !Number.isSafeInteger(rank)) return [];
135
+ if (rank < 1) return [];
136
+ return [{
137
+ rankGroup: rank,
138
+ rankAbsolute: rank,
139
+ page: typeof item.page === "number" ? item.page : Math.ceil(rank / RESULTS_PER_PAGE),
140
+ domain: hostOf(url) ?? "",
141
+ url,
142
+ title: text(item.title),
143
+ description: text(item.snippet),
144
+ isFeaturedSnippet: null
145
+ }];
146
+ });
147
+ mapped.sort((left, right) => left.rankAbsolute - right.rankAbsolute || (left.url < right.url ? -1 : left.url > right.url ? 1 : 0));
148
+ const organicResults = mapped.slice(0, input.depth);
149
+ const invalidRows = raw.length - mapped.length;
150
+ const credits = snapshotCredits(pages);
151
+ const qualityWarnings = [{
152
+ code: "estimated-monetary-cost",
153
+ field: "cost.actualMicros",
154
+ message: `cloro charged ${task.credits} credits. Their money value depends on the plan, so the estimate uses the highest paid rate per credit.`
155
+ }];
156
+ if (invalidRows > 0) qualityWarnings.push({
157
+ code: "invalid-organic-results",
158
+ field: "organicResults",
159
+ message: `cloro returned ${invalidRows} organic result${invalidRows === 1 ? "" : "s"} without a valid rank or URL.`
160
+ });
161
+ return {
162
+ observedAt,
163
+ effectiveKeyword: keyword,
164
+ searchEngineDomain: null,
165
+ checkUrl: null,
166
+ resultCount: null,
167
+ pagesCount: Math.max(0, ...mapped.map((row) => row.page)) || null,
168
+ features: [...new Set(Object.entries(result).filter(([key, value]) => key !== "html" && present(value)).map(([key]) => key === "organicResults" ? "organic" : snakeCase(key)))].sort(),
169
+ organicResults,
170
+ localPack: {
171
+ present: false,
172
+ returnedRows: 0,
173
+ retainedRows: 0,
174
+ invalidRows: 0,
175
+ results: []
176
+ },
177
+ coverage: {
178
+ returnedRows: raw.length,
179
+ retainedRows: organicResults.length,
180
+ invalidRows,
181
+ providerTotalRows: null,
182
+ completeness: invalidRows > 0 ? "partial" : mapped.length > input.depth ? "capped" : "complete",
183
+ nextCursor: null
184
+ },
185
+ request: {
186
+ endpoint: "v1/async/task (GOOGLE)",
187
+ filters: {
188
+ countryCode: market.countryCode,
189
+ languageCode: market.languageCode,
190
+ device: market.device,
191
+ pages
192
+ },
193
+ sort: ["rankAbsolute:ascending", "url:codepoint-ascending"]
194
+ },
195
+ cost: {
196
+ estimatedMicros: credits * MICROS_PER_CREDIT,
197
+ actualMicros: null,
198
+ taskIds: [queued.id],
199
+ native: {
200
+ unit: "credit",
201
+ estimatedUnits: credits,
202
+ actualUnits: task.credits,
203
+ remainingBefore: null
204
+ }
205
+ },
206
+ qualityWarnings
207
+ };
208
+ }
209
+ async function queue(connection, runtime, operation, tasks) {
210
+ const data = await runtime.requestJson({
211
+ operation,
212
+ url: `${API}/v1/async/task/batch`,
213
+ method: "POST",
214
+ headers: headers(connection),
215
+ body: JSON.stringify(tasks)
216
+ });
217
+ const results = items(data?.results);
218
+ return tasks.map((_, index) => {
219
+ const result = results.find((item) => item.index === index);
220
+ const task = result?.task;
221
+ if (result?.success === true && typeof task?.id === "string") return { id: task.id };
222
+ const error = result?.error ?? {};
223
+ const details = error.details ?? {};
224
+ if (error.code === "RESOURCE_ALREADY_EXISTS" && typeof details.taskId === "string") return { id: details.taskId };
225
+ return { error: `cloro: ${text(error.code) ?? "no task"}${text(error.message) ? `, ${text(error.message)}` : ""}` };
226
+ });
227
+ }
228
+ const LIMIT_REACHED = /exceeded its request limit/u;
229
+ async function readTasks(connection, runtime, operation, ids) {
230
+ const out = [];
231
+ let stopped = false;
232
+ for (const id of ids) {
233
+ if (stopped) {
234
+ out.push({
235
+ state: "pending",
236
+ id,
237
+ type: null,
238
+ key: null
239
+ });
240
+ continue;
241
+ }
242
+ let data;
243
+ try {
244
+ data = await runtime.requestJson({
245
+ operation,
246
+ url: `${API}/v1/async/task/${encodeURIComponent(id)}`,
247
+ headers: headers(connection)
248
+ });
249
+ } catch (error) {
250
+ if (error instanceof Error && LIMIT_REACHED.test(error.message)) {
251
+ stopped = true;
252
+ out.push({
253
+ state: "pending",
254
+ id,
255
+ type: null,
256
+ key: null
257
+ });
258
+ continue;
259
+ }
260
+ const status = error.status;
261
+ out.push({
262
+ state: "failed",
263
+ id,
264
+ type: null,
265
+ key: null,
266
+ error: status === 404 ? "cloro: the task is gone. cloro keeps a task for 24 hours." : `cloro: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200)
267
+ });
268
+ continue;
269
+ }
270
+ const task = data?.task ?? {};
271
+ const status = text(task.status);
272
+ const type = text(task.taskType);
273
+ const key = text(task.idempotencyKey);
274
+ const response = data?.response;
275
+ if (status === "COMPLETED" && response && typeof response === "object") {
276
+ const credits = data?.credits ?? {};
277
+ out.push({
278
+ state: "done",
279
+ id,
280
+ type,
281
+ key,
282
+ createdAt: text(task.createdAt),
283
+ credits: typeof credits.creditsCharged === "number" ? credits.creditsCharged : 0,
284
+ response
285
+ });
286
+ } else if (status === "FAILED") {
287
+ const error = response?.error ?? {};
288
+ out.push({
289
+ state: "failed",
290
+ id,
291
+ type,
292
+ key,
293
+ error: `cloro: ${text(error.code) ?? "FAILED"}${text(error.message) ? `, ${text(error.message)}` : ""}`.slice(0, 200)
294
+ });
295
+ } else out.push({
296
+ state: "pending",
297
+ id,
298
+ type,
299
+ key
300
+ });
301
+ }
302
+ return out;
303
+ }
304
+ const today = (runtime) => runtime.now().slice(0, 10);
305
+ function stringList(value, name, max) {
306
+ if (value === void 0) return [];
307
+ if (!Array.isArray(value)) throw failure("configuration", `${name} must be a list.`);
308
+ const list = [...new Set(value.map((item) => typeof item === "string" ? item.trim() : ""))].filter(Boolean);
309
+ if (list.length > max) throw failure("configuration", `${name} takes up to ${max} values.`);
310
+ return list;
311
+ }
312
+ function waitSeconds(value, fallback) {
313
+ if (value === void 0) return fallback;
314
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 120) throw failure("configuration", "waitSeconds must be from 0 to 120.");
315
+ return value;
316
+ }
317
+ const serpKey = (code, day, keyword) => `seo-serp:${code}:${day}:${keyword}`;
318
+ const keywordOf = (key) => key?.startsWith("seo-serp:") ? key.split(":").slice(3).join(":") : null;
319
+ function serpRows(keyword, result) {
320
+ const organic = items(result.organicResults).map((item) => {
321
+ const url = safeUrl(item.link);
322
+ const site = /^(?:https?:\/\/)?([^/\s›]+)/u.exec(String(item.displayedLink ?? ""));
323
+ return {
324
+ keyword,
325
+ type: "organic",
326
+ position: typeof item.position === "number" ? item.position : null,
327
+ url,
328
+ domain: url ? hostOf(url) : site?.[1] ?? null,
329
+ title: text(item.title)
330
+ };
331
+ });
332
+ const overview = result.aioverview ?? {};
333
+ const cited = items(overview.sources).map((source) => {
334
+ const url = safeUrl(source.url);
335
+ return {
336
+ keyword,
337
+ type: "ai_overview",
338
+ position: typeof source.position === "number" ? source.position : null,
339
+ url,
340
+ domain: hostOf(url),
341
+ title: text(source.label)
342
+ };
343
+ });
344
+ return [...organic, ...cited];
345
+ }
346
+ async function serpAction(input, runtime) {
347
+ const { params } = input;
348
+ const given = stringList(params.tasks, "tasks", 50);
349
+ const errors = [];
350
+ let ids;
351
+ let wait = 0;
352
+ if (given.length) ids = given;
353
+ else {
354
+ const keywords = stringList(params.keywords, "keywords", MAX_KEYWORDS);
355
+ if (!keywords.length) throw failure("configuration", "Give 1 to 5 keywords, or tasks to read.");
356
+ const code = country(params.country);
357
+ const day = today(runtime);
358
+ const queued = await queue(input, runtime, "serp:queue", keywords.map((keyword) => ({
359
+ taskType: "GOOGLE",
360
+ idempotencyKey: serpKey(code, day, keyword),
361
+ payload: {
362
+ query: keyword,
363
+ country: code,
364
+ include: { aioverview: true }
365
+ }
366
+ })));
367
+ ids = [];
368
+ queued.forEach((entry, index) => {
369
+ if ("id" in entry) ids.push(entry.id);
370
+ else errors.push({
371
+ keyword: keywords[index] ?? null,
372
+ task: null,
373
+ error: entry.error
374
+ });
375
+ });
376
+ wait = waitSeconds(params.waitSeconds, 20);
377
+ }
378
+ if (wait && ids.length) await setTimeout(wait * 1e3);
379
+ const read = await readTasks(input, runtime, "serp:read", ids);
380
+ const rows = [];
381
+ const pendingTasks = [];
382
+ let credits = 0;
383
+ for (const task of read) if (task.state === "pending") pendingTasks.push(task.id);
384
+ else if (task.state === "failed") errors.push({
385
+ keyword: keywordOf(task.key),
386
+ task: task.id,
387
+ error: task.error
388
+ });
389
+ else {
390
+ credits += task.credits;
391
+ rows.push(...serpRows(keywordOf(task.key), task.response));
392
+ }
393
+ return {
394
+ rows,
395
+ credits,
396
+ pendingTasks,
397
+ errors
398
+ };
399
+ }
400
+ const answerKey = (engine, code, day, question) => `seo-ai-answers:${engine}:${code}:${day}:${question}`;
401
+ const questionOf = (key) => key?.startsWith("seo-ai-answers:") ? key.split(":").slice(4).join(":") : null;
402
+ const engineOf = (type) => ENGINE_NAMES.find((name) => ENGINES[name].type === type) ?? null;
403
+ function siteDomain(value) {
404
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
405
+ const domain = hostOf(/^https?:\/\//u.test(raw) ? raw : `https://${raw}`)?.replace(/^www\./u, "") ?? "";
406
+ if (!domain.includes(".")) throw failure("configuration", "Give the site as a domain, such as example.com.");
407
+ return domain;
408
+ }
409
+ function onSite(url, domain) {
410
+ const host = hostOf(url);
411
+ return !!host && (host === domain || host.endsWith(`.${domain}`));
412
+ }
413
+ const NOT_SOURCES = [
414
+ "images.openai.com",
415
+ "gstatic.com",
416
+ "googleusercontent.com",
417
+ "ggpht.com",
418
+ "example.com",
419
+ "example.org",
420
+ "example.net"
421
+ ];
422
+ const IMAGE = /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/iu;
423
+ function isSource(url) {
424
+ const host = hostOf(url)?.replace(/^www\./u, "");
425
+ if (!host || IMAGE.test(url)) return false;
426
+ if (/(^|\.)google\.[a-z.]+$/u.test(host) && new URL(url).pathname === "/search") return false;
427
+ return !NOT_SOURCES.some((name) => host === name || host.endsWith(`.${name}`));
428
+ }
429
+ function prose(markdown) {
430
+ return markdown.replace(/\[([^\]]*)\]\(https?:\/\/[^)]*\)/gu, (_, label) => /^\s*(\d+|[\w-]+(\.[\w-]+)+)\s*$/u.test(label) ? " " : label).replace(/https?:\/\/\S+/gu, " ");
431
+ }
432
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
433
+ function classify(answer, site) {
434
+ const markdown = typeof answer.text === "string" ? answer.text : "";
435
+ const given = items(answer.sources).map((source) => safeUrl(source.url)).filter((url) => !!url);
436
+ const model = text(answer.model);
437
+ if (!markdown && !given.length) return {
438
+ result: "no_answer",
439
+ citedUrl: null,
440
+ sources: [],
441
+ model
442
+ };
443
+ const linked = markdown.match(/https?:\/\/[^\s)\]]+/gu) ?? [];
444
+ const all = [...given, ...linked];
445
+ const citedUrl = all.find((url) => onSite(url, site.domain)) ?? null;
446
+ const sources = [...new Set(all.filter((url) => !onSite(url, site.domain) && isSource(url)).map((url) => hostOf(url)?.replace(/^www\./u, "")).filter((host) => !!host))].slice(0, MAX_SOURCES);
447
+ if (citedUrl) return {
448
+ result: "cited",
449
+ citedUrl,
450
+ sources,
451
+ model
452
+ };
453
+ const words = prose(markdown).toLowerCase();
454
+ return {
455
+ result: words.includes(site.domain) || site.names.some((name) => new RegExp(`(^|[^\\p{L}\\p{N}])${escape(name.toLowerCase())}($|[^\\p{L}\\p{N}])`, "u").test(words)) ? "mentioned" : "neither",
456
+ citedUrl: null,
457
+ sources,
458
+ model
459
+ };
460
+ }
461
+ const bingPromptLength = (prompt) => encodeURIComponent(prompt.trim()).replace(/%20/gu, "+").length;
462
+ async function aiAnswersAction(input, runtime) {
463
+ const { params } = input;
464
+ const domain = siteDomain(params.domain);
465
+ const brand = stringList(params.brand, "brand", 5);
466
+ const site = {
467
+ domain,
468
+ names: brand.length ? brand : [domain.split(".")[0] ?? domain]
469
+ };
470
+ const given = stringList(params.tasks, "tasks", 100);
471
+ const blank = {
472
+ citedUrl: null,
473
+ sources: [],
474
+ model: null,
475
+ answeredAt: null,
476
+ credits: 0,
477
+ task: null,
478
+ error: null
479
+ };
480
+ const rows = [];
481
+ const ids = [];
482
+ const asked = /* @__PURE__ */ new Map();
483
+ let wait = 0;
484
+ if (given.length) ids.push(...given);
485
+ else {
486
+ const questions = stringList(params.questions, "questions", MAX_QUESTIONS);
487
+ if (!questions.length) throw failure("configuration", "Give 1 to 5 questions, or tasks to read.");
488
+ const engines = params.engines === void 0 ? ENGINE_NAMES : stringList(params.engines, "engines", ENGINE_NAMES.length).map((name) => {
489
+ if (!(name in ENGINES)) throw failure("configuration", `Unknown engine ${name}. Use ${ENGINE_NAMES.join(", ")}.`);
490
+ return name;
491
+ });
492
+ const code = country(params.country);
493
+ const day = today(runtime);
494
+ const asks = [];
495
+ for (const question of questions) for (const engine of engines) {
496
+ if (engine === "bing_ai_mode" && bingPromptLength(question) > BING_PROMPT_LIMIT) {
497
+ rows.push({
498
+ question,
499
+ engine,
500
+ result: "unavailable",
501
+ ...blank,
502
+ error: `Bing AI Mode takes up to ${BING_PROMPT_LIMIT} characters after URL encoding.`
503
+ });
504
+ continue;
505
+ }
506
+ asks.push({
507
+ question,
508
+ engine
509
+ });
510
+ }
511
+ (asks.length ? await queue(input, runtime, "ai-answers:queue", asks.map(({ question, engine }) => ({
512
+ taskType: ENGINES[engine].type,
513
+ idempotencyKey: answerKey(engine, code, day, question),
514
+ payload: {
515
+ prompt: question,
516
+ country: code
517
+ }
518
+ }))) : []).forEach((entry, index) => {
519
+ const ask = asks[index];
520
+ if (!ask) return;
521
+ if ("id" in entry) {
522
+ ids.push(entry.id);
523
+ asked.set(entry.id, ask);
524
+ } else rows.push({
525
+ ...ask,
526
+ result: "unavailable",
527
+ ...blank,
528
+ error: entry.error
529
+ });
530
+ });
531
+ wait = waitSeconds(params.waitSeconds, 45);
532
+ }
533
+ if (wait && ids.length) await setTimeout(wait * 1e3);
534
+ for (const task of await readTasks(input, runtime, "ai-answers:read", ids)) if (task.state === "pending") rows.push({
535
+ question: questionOf(task.key) ?? asked.get(task.id)?.question ?? null,
536
+ engine: engineOf(task.type) ?? asked.get(task.id)?.engine ?? null,
537
+ result: "pending",
538
+ ...blank,
539
+ task: task.id
540
+ });
541
+ else if (task.state === "failed") rows.push({
542
+ question: questionOf(task.key),
543
+ engine: engineOf(task.type),
544
+ result: "unavailable",
545
+ ...blank,
546
+ task: task.id,
547
+ error: task.error
548
+ });
549
+ else rows.push({
550
+ question: questionOf(task.key),
551
+ engine: engineOf(task.type),
552
+ ...blank,
553
+ ...classify(task.response, site),
554
+ answeredAt: task.createdAt?.slice(0, 10) ?? null,
555
+ credits: task.credits,
556
+ task: task.id
557
+ });
558
+ return {
559
+ domain,
560
+ rows,
561
+ credits: rows.reduce((total, row) => total + row.credits, 0),
562
+ pendingTasks: rows.flatMap((row) => row.result === "pending" && row.task ? [row.task] : [])
563
+ };
564
+ }
565
+ const nullable = (type) => ({ type: [type, "null"] });
566
+ const strings = (maxItems, description) => ({
567
+ type: "array",
568
+ items: {
569
+ type: "string",
570
+ minLength: 1,
571
+ maxLength: 2e3
572
+ },
573
+ minItems: 1,
574
+ maxItems,
575
+ description
576
+ });
577
+ const commonParams = {
578
+ country: {
579
+ type: "string",
580
+ pattern: "^[A-Za-z]{2}$",
581
+ description: "Two-letter country code. Default US."
582
+ },
583
+ waitSeconds: {
584
+ type: "integer",
585
+ minimum: 0,
586
+ maximum: 120,
587
+ description: "Seconds to wait for queued tasks before reading them."
588
+ }
589
+ };
590
+ const serp = {
591
+ id: "serp",
592
+ description: "Google organic results and the pages the AI Overview cites, for up to 5 keywords. Results not ready come back as pendingTasks: run again with tasks to read them.",
593
+ inputSchema: {
594
+ type: "object",
595
+ properties: {
596
+ keywords: strings(MAX_KEYWORDS, "Keywords to search, 1 to 5."),
597
+ tasks: strings(50, "Task ids from pendingTasks, to read instead of searching."),
598
+ ...commonParams
599
+ },
600
+ additionalProperties: false
601
+ },
602
+ outputSchema: {
603
+ type: "object",
604
+ properties: {
605
+ rows: {
606
+ type: "array",
607
+ items: {
608
+ type: "object",
609
+ properties: {
610
+ keyword: nullable("string"),
611
+ type: {
612
+ type: "string",
613
+ enum: ["organic", "ai_overview"]
614
+ },
615
+ position: nullable("number"),
616
+ url: nullable("string"),
617
+ domain: nullable("string"),
618
+ title: nullable("string")
619
+ },
620
+ required: [
621
+ "keyword",
622
+ "type",
623
+ "position",
624
+ "url",
625
+ "domain",
626
+ "title"
627
+ ]
628
+ }
629
+ },
630
+ credits: { type: "number" },
631
+ pendingTasks: {
632
+ type: "array",
633
+ items: { type: "string" }
634
+ },
635
+ errors: {
636
+ type: "array",
637
+ items: {
638
+ type: "object",
639
+ properties: {
640
+ keyword: nullable("string"),
641
+ task: nullable("string"),
642
+ error: { type: "string" }
643
+ },
644
+ required: [
645
+ "keyword",
646
+ "task",
647
+ "error"
648
+ ]
649
+ }
650
+ }
651
+ },
652
+ required: [
653
+ "rows",
654
+ "credits",
655
+ "pendingTasks",
656
+ "errors"
657
+ ]
658
+ },
659
+ run: serpAction
660
+ };
661
+ const aiAnswers = {
662
+ id: "ai-answers",
663
+ description: "Ask ChatGPT, Google AI Mode, Gemini and Bing AI Mode up to 5 questions and report if each answer cites the site, mentions it, or neither. Answers not ready come back as pendingTasks.",
664
+ inputSchema: {
665
+ type: "object",
666
+ properties: {
667
+ domain: {
668
+ type: "string",
669
+ minLength: 3,
670
+ maxLength: 253,
671
+ description: "The site to look for, such as example.com."
672
+ },
673
+ questions: strings(MAX_QUESTIONS, "Questions to ask, 1 to 5."),
674
+ engines: {
675
+ type: "array",
676
+ items: {
677
+ type: "string",
678
+ enum: ENGINE_NAMES
679
+ },
680
+ minItems: 1,
681
+ maxItems: ENGINE_NAMES.length,
682
+ description: "Engines to ask. Default all four."
683
+ },
684
+ brand: strings(5, "Names that count as a mention. Default the first label of the domain."),
685
+ tasks: strings(100, "Task ids from pendingTasks, to read instead of asking."),
686
+ ...commonParams
687
+ },
688
+ required: ["domain"],
689
+ additionalProperties: false
690
+ },
691
+ outputSchema: {
692
+ type: "object",
693
+ properties: {
694
+ domain: { type: "string" },
695
+ rows: {
696
+ type: "array",
697
+ items: {
698
+ type: "object",
699
+ properties: {
700
+ question: nullable("string"),
701
+ engine: nullable("string"),
702
+ result: {
703
+ type: "string",
704
+ enum: [
705
+ "cited",
706
+ "mentioned",
707
+ "neither",
708
+ "no_answer",
709
+ "pending",
710
+ "unavailable"
711
+ ]
712
+ },
713
+ citedUrl: nullable("string"),
714
+ sources: {
715
+ type: "array",
716
+ items: { type: "string" }
717
+ },
718
+ model: nullable("string"),
719
+ answeredAt: nullable("string"),
720
+ credits: { type: "number" },
721
+ task: nullable("string"),
722
+ error: nullable("string")
723
+ },
724
+ required: [
725
+ "question",
726
+ "engine",
727
+ "result",
728
+ "citedUrl",
729
+ "sources",
730
+ "model",
731
+ "answeredAt",
732
+ "credits",
733
+ "task",
734
+ "error"
735
+ ]
736
+ }
737
+ },
738
+ credits: { type: "number" },
739
+ pendingTasks: {
740
+ type: "array",
741
+ items: { type: "string" }
742
+ }
743
+ },
744
+ required: [
745
+ "domain",
746
+ "rows",
747
+ "credits",
748
+ "pendingTasks"
749
+ ]
750
+ },
751
+ run: aiAnswersAction
752
+ };
753
+ const activate = (host) => {
754
+ host.registerProvider({
755
+ id: "cloro",
756
+ displayName: "cloro",
757
+ description: "Live Google results, AI Overview sources and answers from ChatGPT, Google AI Mode, Gemini and Bing AI Mode, from cloro.",
758
+ kinds: ["search-results", "ai-data"],
759
+ connection: {
760
+ fields: [{
761
+ id: "apiKey",
762
+ label: "API key",
763
+ kind: "secret",
764
+ required: true,
765
+ envVar: "SEO_CLORO_API_KEY"
766
+ }],
767
+ verify,
768
+ verificationNotice: "The connection check reads the credit balance. It costs no credits."
769
+ },
770
+ capabilities: [{
771
+ id: "serp-snapshot",
772
+ defaultDepth: 10,
773
+ maxDepth: MAX_DEPTH,
774
+ maxRequests: SNAPSHOT_REQUESTS,
775
+ markets: [{
776
+ searchEngines: ["google"],
777
+ devices: ["desktop", "mobile"],
778
+ location: "country-only"
779
+ }],
780
+ estimateCostMicros: ({ depth }) => snapshotCredits(pagesFor(depth)) * MICROS_PER_CREDIT,
781
+ estimateRequests: () => SNAPSHOT_REQUESTS,
782
+ run: serpSnapshot
783
+ }],
784
+ actions: [serp, aiAnswers]
785
+ });
786
+ };
787
+ export { classify, activate as default };
788
+
789
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["sleep"],"sources":["../src/index.ts"],"sourcesContent":["import { setTimeout as sleep } from 'node:timers/promises'\nimport type {\n SeoProviderActionAdapter,\n SeoProviderActivate,\n SeoProviderConnection,\n SeoProviderJson,\n SeoProviderRuntime,\n SeoSearchMarket,\n SeoSerpOrganicResult,\n SeoSerpSnapshotResult,\n} from 'seo/provider-sdk'\n\nconst API = 'https://api.cloro.dev'\nconst RESULTS_PER_PAGE = 10\nconst MAX_DEPTH = 100\n// The Lite plan rate ($30 for 37,500 credits), the highest per-credit rate\n// of the paid plans. Larger plans pay less, so the estimate is an upper\n// bound.\nconst MICROS_PER_CREDIT = 800\nconst MAX_KEYWORDS = 5\nconst MAX_QUESTIONS = 5\nconst MAX_SOURCES = 8\n// Bing AI Mode refuses a prompt longer than this after URL encoding.\nconst BING_PROMPT_LIMIT = 3_500\n\n// cloro's AI engines, by the taskType of its async API, with the credits\n// one answer costs.\nconst ENGINES = {\n chatgpt: { type: 'CHATGPT', credits: 5 },\n google_ai_mode: { type: 'AIMODE', credits: 4 },\n gemini: { type: 'GEMINI', credits: 4 },\n bing_ai_mode: { type: 'BING_AIMODE', credits: 4 },\n} as const\ntype EngineName = keyof typeof ENGINES\nconst ENGINE_NAMES = Object.keys(ENGINES) as EngineName[]\n\ntype Item = Record<string, unknown>\n\nfunction failure(code: string, message: string): Error {\n return Object.assign(new Error(message), { code })\n}\n\nfunction apiKey(connection: SeoProviderConnection): string {\n const value = connection.credentials.apiKey?.trim() ?? ''\n if (!value || value.length > 4_096) {\n throw failure('authentication', 'cloro needs a valid API key.')\n }\n return value\n}\n\nfunction headers(connection: SeoProviderConnection): Record<string, string> {\n return {\n authorization: `Bearer ${apiKey(connection)}`,\n 'content-type': 'application/json',\n }\n}\n\nconst items = (value: unknown): Item[] =>\n Array.isArray(value)\n ? value.filter((item): item is Item => typeof item === 'object' && !!item)\n : []\nconst text = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim() : null\n\nfunction country(value: unknown): string {\n const code = typeof value === 'string' ? value.trim().toUpperCase() : 'US'\n if (!/^[A-Z]{2}$/u.test(code)) {\n throw failure('configuration', 'Use a two-letter country code, such as US.')\n }\n return code\n}\n\n// An http(s) URL, or null. A link cloro could not resolve comes back empty\n// or as a Google /goto redirect.\nfunction safeUrl(value: unknown): string | null {\n if (typeof value !== 'string') return null\n try {\n const parsed = new URL(value)\n if (!['http:', 'https:'].includes(parsed.protocol)) return null\n if (\n /(^|\\.)google\\.[a-z.]+$/u.test(parsed.hostname) &&\n parsed.pathname.startsWith('/goto')\n ) {\n return null\n }\n parsed.username = ''\n parsed.password = ''\n return parsed.toString()\n } catch {\n return null\n }\n}\n\nconst hostOf = (url: string | null): string | null => {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase()\n } catch {\n return null\n }\n}\n\n// --- Connection ------------------------------------------------------------\n\nasync function verify(\n connection: SeoProviderConnection,\n runtime: SeoProviderRuntime,\n): Promise<void> {\n const credits = (await runtime.requestJson({\n operation: 'verify',\n url: `${API}/v1/credits`,\n headers: headers(connection),\n })) as Item | null\n if (typeof credits?.remaining !== 'number') {\n throw failure('invalid-response', 'cloro returned invalid credit data.')\n }\n}\n\n// --- SERP snapshot ------------------------------------------------------------\n// One async Google task, read until it finishes. A sync request can take\n// longer than the host's 15-second request timeout when cloro retries, and\n// the async API costs 2 credits less.\n\nfunction snapshotMarket(value: SeoSearchMarket) {\n if (value.searchEngine !== 'google') {\n throw failure('configuration', 'cloro SERP snapshots support Google.')\n }\n if (value.location) {\n throw failure(\n 'configuration',\n 'cloro SERP snapshots take country-level markets. Omit the location.',\n )\n }\n if (!/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/iu.test(value.languageCode)) {\n throw failure('configuration', 'Use a valid language code, such as en.')\n }\n return {\n countryCode: country(value.countryCode),\n languageCode: value.languageCode,\n device: value.device ?? 'desktop',\n }\n}\n\nfunction pagesFor(depth: number): number {\n if (!Number.isSafeInteger(depth) || depth < 1 || depth > MAX_DEPTH) {\n throw failure(\n 'configuration',\n `cloro SERP depth must be from 1 to ${MAX_DEPTH}.`,\n )\n }\n return Math.ceil(depth / RESULTS_PER_PAGE)\n}\n\n// An async Google search costs 3 credits, plus 2 for each page after the\n// first.\nconst snapshotCredits = (pages: number) => 3 + 2 * (pages - 1)\n// The capability's request budget: one to queue, the rest to read.\nconst SNAPSHOT_REQUESTS = 20\nconst POLL_MS = 3_000\n\nconst snakeCase = (key: string) =>\n key.replace(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`)\n\nfunction present(value: unknown): boolean {\n if (value === undefined || value === null) return false\n if (Array.isArray(value)) return value.length > 0\n if (typeof value === 'string') return value.trim().length > 0\n if (typeof value === 'object') return Object.keys(value).length > 0\n return true\n}\n\nasync function serpSnapshot(\n input: SeoProviderConnection & {\n keyword: string\n market: SeoSearchMarket\n depth: number\n },\n runtime: SeoProviderRuntime,\n): Promise<SeoSerpSnapshotResult> {\n const keyword = input.keyword.trim().replace(/\\s+/gu, ' ')\n if (!keyword || keyword.length > 1_000) {\n throw failure('configuration', 'Use a keyword of 1 to 1,000 characters.')\n }\n const market = snapshotMarket(input.market)\n const pages = pagesFor(input.depth)\n const [queued] = await queue(input, runtime, 'serp-snapshot:queue', [\n {\n taskType: 'GOOGLE',\n payload: {\n query: keyword,\n country: market.countryCode,\n hl: market.languageCode,\n device: market.device,\n ...(pages > 1 ? { pages } : {}),\n },\n },\n ])\n if (!queued || 'error' in queued) {\n throw failure('remote-error', queued?.error ?? 'cloro queued no task.')\n }\n let task: ReadTask | undefined\n for (let read = 1; read < SNAPSHOT_REQUESTS; read += 1) {\n await sleep(POLL_MS)\n ;[task] = await readTasks(input, runtime, 'serp-snapshot:read', [queued.id])\n if (task?.state !== 'pending') break\n }\n if (task?.state === 'failed') throw failure('remote-error', task.error)\n if (task?.state !== 'done') {\n throw failure(\n 'timeout',\n `cloro did not finish the search in time. Task ${queued.id} keeps the result for 24 hours.`,\n )\n }\n const result = task.response\n const observedAt = runtime.now()\n const raw = items(result.organicResults)\n const mapped = raw.flatMap((item): SeoSerpOrganicResult[] => {\n const url = safeUrl(item.link)\n const rank = item.position\n if (!url || typeof rank !== 'number' || !Number.isSafeInteger(rank)) {\n return []\n }\n if (rank < 1) return []\n return [\n {\n rankGroup: rank,\n rankAbsolute: rank,\n page:\n typeof item.page === 'number'\n ? item.page\n : Math.ceil(rank / RESULTS_PER_PAGE),\n domain: hostOf(url) ?? '',\n url,\n title: text(item.title),\n description: text(item.snippet),\n isFeaturedSnippet: null,\n },\n ]\n })\n mapped.sort(\n (left, right) =>\n left.rankAbsolute - right.rankAbsolute ||\n (left.url < right.url ? -1 : left.url > right.url ? 1 : 0),\n )\n const organicResults = mapped.slice(0, input.depth)\n const invalidRows = raw.length - mapped.length\n const credits = snapshotCredits(pages)\n const qualityWarnings: SeoSerpSnapshotResult['qualityWarnings'][number][] = [\n {\n code: 'estimated-monetary-cost',\n field: 'cost.actualMicros',\n message: `cloro charged ${task.credits} credits. Their money value depends on the plan, so the estimate uses the highest paid rate per credit.`,\n },\n ]\n if (invalidRows > 0) {\n qualityWarnings.push({\n code: 'invalid-organic-results',\n field: 'organicResults',\n message: `cloro returned ${invalidRows} organic result${invalidRows === 1 ? '' : 's'} without a valid rank or URL.`,\n })\n }\n return {\n observedAt,\n effectiveKeyword: keyword,\n searchEngineDomain: null,\n checkUrl: null,\n resultCount: null,\n pagesCount: Math.max(0, ...mapped.map((row) => row.page)) || null,\n features: [\n ...new Set(\n Object.entries(result)\n .filter(([key, value]) => key !== 'html' && present(value))\n .map(([key]) =>\n key === 'organicResults' ? 'organic' : snakeCase(key),\n ),\n ),\n ].sort(),\n organicResults,\n localPack: {\n present: false,\n returnedRows: 0,\n retainedRows: 0,\n invalidRows: 0,\n results: [],\n },\n coverage: {\n returnedRows: raw.length,\n retainedRows: organicResults.length,\n invalidRows,\n providerTotalRows: null,\n completeness:\n invalidRows > 0\n ? 'partial'\n : mapped.length > input.depth\n ? 'capped'\n : 'complete',\n nextCursor: null,\n },\n request: {\n endpoint: 'v1/async/task (GOOGLE)',\n filters: {\n countryCode: market.countryCode,\n languageCode: market.languageCode,\n device: market.device,\n pages,\n },\n sort: ['rankAbsolute:ascending', 'url:codepoint-ascending'],\n },\n cost: {\n estimatedMicros: credits * MICROS_PER_CREDIT,\n actualMicros: null,\n taskIds: [queued.id],\n native: {\n unit: 'credit',\n estimatedUnits: credits,\n actualUnits: task.credits,\n remainingBefore: null,\n },\n },\n qualityWarnings,\n }\n}\n\n// --- Async tasks (serp and ai-answers actions) -----------------------------\n\ntype QueuedTask = {\n taskType: string\n idempotencyKey?: string\n payload: Record<string, unknown>\n}\n\n// Queues up to 500 tasks in one request. Each entry is the task id, or why\n// cloro refused the task. A task whose idempotency key was used before comes\n// back as that earlier task, so a run sent again on the same day does not\n// queue or charge twice. cloro charges a task only when it finishes.\nasync function queue(\n connection: SeoProviderConnection,\n runtime: SeoProviderRuntime,\n operation: string,\n tasks: QueuedTask[],\n): Promise<({ id: string } | { error: string })[]> {\n const data = (await runtime.requestJson({\n operation,\n url: `${API}/v1/async/task/batch`,\n method: 'POST',\n headers: headers(connection),\n body: JSON.stringify(tasks),\n })) as Item | null\n const results = items(data?.results)\n return tasks.map((_, index) => {\n const result = results.find((item) => item.index === index)\n const task = result?.task as Item | undefined\n if (result?.success === true && typeof task?.id === 'string') {\n return { id: task.id }\n }\n const error = (result?.error ?? {}) as Item\n const details = (error.details ?? {}) as Item\n if (\n error.code === 'RESOURCE_ALREADY_EXISTS' &&\n typeof details.taskId === 'string'\n ) {\n return { id: details.taskId }\n }\n return {\n error: `cloro: ${text(error.code) ?? 'no task'}${text(error.message) ? `, ${text(error.message)}` : ''}`,\n }\n })\n}\n\ntype ReadTask =\n | {\n state: 'done'\n id: string\n type: string | null\n key: string | null\n createdAt: string | null\n credits: number\n response: Item\n }\n | {\n state: 'failed'\n id: string\n type: string | null\n key: string | null\n error: string\n }\n | { state: 'pending'; id: string; type: string | null; key: string | null }\n\nconst LIMIT_REACHED = /exceeded its request limit/u\n\n// Reads queued tasks one request each, until the host's request limit for\n// this run. A task that is not read, or not finished, stays pending.\n// Reading a task costs no credits. cloro deletes a task 24 hours after it\n// was created.\nasync function readTasks(\n connection: SeoProviderConnection,\n runtime: SeoProviderRuntime,\n operation: string,\n ids: string[],\n): Promise<ReadTask[]> {\n const out: ReadTask[] = []\n let stopped = false\n for (const id of ids) {\n if (stopped) {\n out.push({ state: 'pending', id, type: null, key: null })\n continue\n }\n let data: Item | null\n try {\n data = (await runtime.requestJson({\n operation,\n url: `${API}/v1/async/task/${encodeURIComponent(id)}`,\n headers: headers(connection),\n })) as Item | null\n } catch (error) {\n if (error instanceof Error && LIMIT_REACHED.test(error.message)) {\n stopped = true\n out.push({ state: 'pending', id, type: null, key: null })\n continue\n }\n const status = (error as { status?: unknown }).status\n out.push({\n state: 'failed',\n id,\n type: null,\n key: null,\n error:\n status === 404\n ? 'cloro: the task is gone. cloro keeps a task for 24 hours.'\n : `cloro: ${error instanceof Error ? error.message : String(error)}`.slice(\n 0,\n 200,\n ),\n })\n continue\n }\n const task = (data?.task ?? {}) as Item\n const status = text(task.status)\n const type = text(task.taskType)\n const key = text(task.idempotencyKey)\n const response = data?.response as Item | undefined\n if (status === 'COMPLETED' && response && typeof response === 'object') {\n const credits = (data?.credits ?? {}) as Item\n out.push({\n state: 'done',\n id,\n type,\n key,\n createdAt: text(task.createdAt),\n credits:\n typeof credits.creditsCharged === 'number'\n ? credits.creditsCharged\n : 0,\n response,\n })\n } else if (status === 'FAILED') {\n const error = (response?.error ?? {}) as Item\n out.push({\n state: 'failed',\n id,\n type,\n key,\n error:\n `cloro: ${text(error.code) ?? 'FAILED'}${text(error.message) ? `, ${text(error.message)}` : ''}`.slice(\n 0,\n 200,\n ),\n })\n } else {\n out.push({ state: 'pending', id, type, key })\n }\n }\n return out\n}\n\nconst today = (runtime: SeoProviderRuntime) => runtime.now().slice(0, 10)\n\nfunction stringList(\n value: SeoProviderJson | undefined,\n name: string,\n max: number,\n): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value)) throw failure('configuration', `${name} must be a list.`)\n const list = [\n ...new Set(\n value.map((item) => (typeof item === 'string' ? item.trim() : '')),\n ),\n ].filter(Boolean)\n if (list.length > max) {\n throw failure('configuration', `${name} takes up to ${max} values.`)\n }\n return list\n}\n\nfunction waitSeconds(value: SeoProviderJson | undefined, fallback: number) {\n if (value === undefined) return fallback\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < 0 ||\n value > 120\n ) {\n throw failure('configuration', 'waitSeconds must be from 0 to 120.')\n }\n return value\n}\n\n// --- serp action -------------------------------------------------------------\n\n// The idempotency key carries the keyword, because cloro does not return a\n// task's payload: answers read back later still know their keyword.\nconst serpKey = (code: string, day: string, keyword: string) =>\n `seo-serp:${code}:${day}:${keyword}`\nconst keywordOf = (key: string | null) =>\n key?.startsWith('seo-serp:') ? key.split(':').slice(3).join(':') : null\n\ntype SerpRow = {\n keyword: string | null\n type: 'organic' | 'ai_overview'\n position: number | null\n url: string | null\n domain: string | null\n title: string | null\n}\n\nfunction serpRows(keyword: string | null, result: Item): SerpRow[] {\n const organic = items(result.organicResults).map((item): SerpRow => {\n const url = safeUrl(item.link)\n // Without the page, the displayed link still shows the site.\n const site = /^(?:https?:\\/\\/)?([^/\\s›]+)/u.exec(\n String(item.displayedLink ?? ''),\n )\n return {\n keyword,\n type: 'organic',\n position: typeof item.position === 'number' ? item.position : null,\n url,\n domain: url ? hostOf(url) : (site?.[1] ?? null),\n title: text(item.title),\n }\n })\n const overview = (result.aioverview ?? {}) as Item\n const cited = items(overview.sources).map((source): SerpRow => {\n const url = safeUrl(source.url)\n return {\n keyword,\n type: 'ai_overview',\n position: typeof source.position === 'number' ? source.position : null,\n url,\n domain: hostOf(url),\n title: text(source.label),\n }\n })\n return [...organic, ...cited]\n}\n\nasync function serpAction(\n input: SeoProviderConnection & {\n params: Readonly<Record<string, SeoProviderJson>>\n },\n runtime: SeoProviderRuntime,\n) {\n const { params } = input\n const given = stringList(params.tasks, 'tasks', 50)\n const errors: { keyword: string | null; task: string | null; error: string }[] =\n []\n let ids: string[]\n let wait = 0\n if (given.length) {\n ids = given\n } else {\n const keywords = stringList(params.keywords, 'keywords', MAX_KEYWORDS)\n if (!keywords.length) {\n throw failure('configuration', 'Give 1 to 5 keywords, or tasks to read.')\n }\n const code = country(params.country)\n const day = today(runtime)\n const queued = await queue(\n input,\n runtime,\n 'serp:queue',\n keywords.map((keyword) => ({\n taskType: 'GOOGLE',\n idempotencyKey: serpKey(code, day, keyword),\n payload: { query: keyword, country: code, include: { aioverview: true } },\n })),\n )\n ids = []\n queued.forEach((entry, index) => {\n if ('id' in entry) ids.push(entry.id)\n else errors.push({ keyword: keywords[index] ?? null, task: null, error: entry.error })\n })\n wait = waitSeconds(params.waitSeconds, 20)\n }\n if (wait && ids.length) await sleep(wait * 1_000)\n const read = await readTasks(input, runtime, 'serp:read', ids)\n const rows: SerpRow[] = []\n const pendingTasks: string[] = []\n let credits = 0\n for (const task of read) {\n if (task.state === 'pending') pendingTasks.push(task.id)\n else if (task.state === 'failed') {\n errors.push({ keyword: keywordOf(task.key), task: task.id, error: task.error })\n } else {\n credits += task.credits\n rows.push(...serpRows(keywordOf(task.key), task.response))\n }\n }\n return { rows, credits, pendingTasks, errors }\n}\n\n// --- ai-answers action -------------------------------------------------------\n\nconst answerKey = (\n engine: string,\n code: string,\n day: string,\n question: string,\n) => `seo-ai-answers:${engine}:${code}:${day}:${question}`\nconst questionOf = (key: string | null) =>\n key?.startsWith('seo-ai-answers:') ? key.split(':').slice(4).join(':') : null\nconst engineOf = (type: string | null): EngineName | null =>\n ENGINE_NAMES.find((name) => ENGINES[name].type === type) ?? null\n\n// The registrable part of a site: \"https://www.example.com/x\" -> example.com.\nfunction siteDomain(value: unknown): string {\n const raw = typeof value === 'string' ? value.trim().toLowerCase() : ''\n const host = hostOf(/^https?:\\/\\//u.test(raw) ? raw : `https://${raw}`)\n const domain = host?.replace(/^www\\./u, '') ?? ''\n if (!domain.includes('.')) {\n throw failure('configuration', 'Give the site as a domain, such as example.com.')\n }\n return domain\n}\n\nfunction onSite(url: string, domain: string): boolean {\n const host = hostOf(url)\n return !!host && (host === domain || host.endsWith(`.${domain}`))\n}\n\n// Hosts that show up in answers without being a source: image hosts, CDNs\n// and placeholder domains written into examples.\nconst NOT_SOURCES = [\n 'images.openai.com',\n 'gstatic.com',\n 'googleusercontent.com',\n 'ggpht.com',\n 'example.com',\n 'example.org',\n 'example.net',\n]\nconst IMAGE = /\\.(png|jpe?g|gif|webp|avif|svg)(\\?|#|$)/iu\n\nfunction isSource(url: string): boolean {\n const host = hostOf(url)?.replace(/^www\\./u, '')\n if (!host || IMAGE.test(url)) return false\n // AI Mode can cite a Google shopping search. It is not a source.\n if (/(^|\\.)google\\.[a-z.]+$/u.test(host) && new URL(url).pathname === '/search') {\n return false\n }\n return !NOT_SOURCES.some((name) => host === name || host.endsWith(`.${name}`))\n}\n\n// The answer's own words: citation pills such as [1](url) or\n// [example.com](url) and bare links go, so a citation is never also counted\n// as a mention by name.\nfunction prose(markdown: string): string {\n return markdown\n .replace(/\\[([^\\]]*)\\]\\(https?:\\/\\/[^)]*\\)/gu, (_, label: string) =>\n /^\\s*(\\d+|[\\w-]+(\\.[\\w-]+)+)\\s*$/u.test(label) ? ' ' : label,\n )\n .replace(/https?:\\/\\/\\S+/gu, ' ')\n}\n\nconst escape = (value: string) => value.replace(/[.*+?^${}()|[\\]\\\\]/gu, '\\\\$&')\n\ntype Verdict = 'cited' | 'mentioned' | 'neither' | 'no_answer'\n\n// What one answer says about the site: cited (a source or a link in the\n// text is on the site), mentioned (the text names the site or the brand),\n// neither, or no_answer.\nexport function classify(\n answer: Item,\n site: { domain: string; names: string[] },\n): {\n result: Verdict\n citedUrl: string | null\n sources: string[]\n model: string | null\n} {\n const markdown = typeof answer.text === 'string' ? answer.text : ''\n // Every source counts. ChatGPT sources carry a footnote flag, but links\n // written into the text came back with it too.\n const given = items(answer.sources)\n .map((source) => safeUrl(source.url))\n .filter((url): url is string => !!url)\n const model = text(answer.model)\n if (!markdown && !given.length) {\n return { result: 'no_answer', citedUrl: null, sources: [], model }\n }\n const linked = markdown.match(/https?:\\/\\/[^\\s)\\]]+/gu) ?? []\n const all = [...given, ...linked]\n const citedUrl = all.find((url) => onSite(url, site.domain)) ?? null\n const sources = [\n ...new Set(\n all\n .filter((url) => !onSite(url, site.domain) && isSource(url))\n .map((url) => hostOf(url)?.replace(/^www\\./u, ''))\n .filter((host): host is string => !!host),\n ),\n ].slice(0, MAX_SOURCES)\n if (citedUrl) return { result: 'cited', citedUrl, sources, model }\n const words = prose(markdown).toLowerCase()\n const mentioned =\n words.includes(site.domain) ||\n site.names.some((name) =>\n new RegExp(`(^|[^\\\\p{L}\\\\p{N}])${escape(name.toLowerCase())}($|[^\\\\p{L}\\\\p{N}])`, 'u').test(words),\n )\n return { result: mentioned ? 'mentioned' : 'neither', citedUrl: null, sources, model }\n}\n\n// Bing AI Mode counts the prompt after URL encoding, with a space as \"+\".\nconst bingPromptLength = (prompt: string) =>\n encodeURIComponent(prompt.trim()).replace(/%20/gu, '+').length\n\ntype AnswerRow = {\n question: string | null\n engine: EngineName | null\n result: Verdict | 'pending' | 'unavailable'\n citedUrl: string | null\n sources: string[]\n model: string | null\n answeredAt: string | null\n credits: number\n task: string | null\n error: string | null\n}\n\nasync function aiAnswersAction(\n input: SeoProviderConnection & {\n params: Readonly<Record<string, SeoProviderJson>>\n },\n runtime: SeoProviderRuntime,\n) {\n const { params } = input\n const domain = siteDomain(params.domain)\n const brand = stringList(params.brand, 'brand', 5)\n const site = { domain, names: brand.length ? brand : [domain.split('.')[0] ?? domain] }\n const given = stringList(params.tasks, 'tasks', 100)\n const blank = {\n citedUrl: null,\n sources: [],\n model: null,\n answeredAt: null,\n credits: 0,\n task: null,\n error: null,\n }\n const rows: AnswerRow[] = []\n const ids: string[] = []\n // What each task queued in this run asks, for tasks not read yet.\n const asked = new Map<string, { question: string; engine: EngineName }>()\n let wait = 0\n if (given.length) {\n ids.push(...given)\n } else {\n const questions = stringList(params.questions, 'questions', MAX_QUESTIONS)\n if (!questions.length) {\n throw failure('configuration', 'Give 1 to 5 questions, or tasks to read.')\n }\n const engines = params.engines === undefined\n ? ENGINE_NAMES\n : stringList(params.engines, 'engines', ENGINE_NAMES.length).map((name) => {\n if (!(name in ENGINES)) {\n throw failure(\n 'configuration',\n `Unknown engine ${name}. Use ${ENGINE_NAMES.join(', ')}.`,\n )\n }\n return name as EngineName\n })\n const code = country(params.country)\n const day = today(runtime)\n const asks: { question: string; engine: EngineName }[] = []\n for (const question of questions) {\n for (const engine of engines) {\n if (engine === 'bing_ai_mode' && bingPromptLength(question) > BING_PROMPT_LIMIT) {\n rows.push({\n question,\n engine,\n result: 'unavailable',\n ...blank,\n error: `Bing AI Mode takes up to ${BING_PROMPT_LIMIT} characters after URL encoding.`,\n })\n continue\n }\n asks.push({ question, engine })\n }\n }\n const queued = asks.length\n ? await queue(\n input,\n runtime,\n 'ai-answers:queue',\n asks.map(({ question, engine }) => ({\n taskType: ENGINES[engine].type,\n idempotencyKey: answerKey(engine, code, day, question),\n payload: { prompt: question, country: code },\n })),\n )\n : []\n queued.forEach((entry, index) => {\n const ask = asks[index]\n if (!ask) return\n if ('id' in entry) {\n ids.push(entry.id)\n asked.set(entry.id, ask)\n } else rows.push({ ...ask, result: 'unavailable', ...blank, error: entry.error })\n })\n wait = waitSeconds(params.waitSeconds, 45)\n }\n if (wait && ids.length) await sleep(wait * 1_000)\n for (const task of await readTasks(input, runtime, 'ai-answers:read', ids)) {\n if (task.state === 'pending') {\n rows.push({\n question: questionOf(task.key) ?? asked.get(task.id)?.question ?? null,\n engine: engineOf(task.type) ?? asked.get(task.id)?.engine ?? null,\n result: 'pending',\n ...blank,\n task: task.id,\n })\n } else if (task.state === 'failed') {\n rows.push({\n question: questionOf(task.key),\n engine: engineOf(task.type),\n result: 'unavailable',\n ...blank,\n task: task.id,\n error: task.error,\n })\n } else {\n rows.push({\n question: questionOf(task.key),\n engine: engineOf(task.type),\n ...blank,\n ...classify(task.response, site),\n answeredAt: task.createdAt?.slice(0, 10) ?? null,\n credits: task.credits,\n task: task.id,\n })\n }\n }\n return {\n domain,\n rows,\n credits: rows.reduce((total, row) => total + row.credits, 0),\n pendingTasks: rows.flatMap((row) =>\n row.result === 'pending' && row.task ? [row.task] : [],\n ),\n }\n}\n\n// --- Schemas -----------------------------------------------------------------\n\nconst nullable = (type: string) => ({ type: [type, 'null'] })\nconst strings = (maxItems: number, description: string) => ({\n type: 'array',\n items: { type: 'string', minLength: 1, maxLength: 2_000 },\n minItems: 1,\n maxItems,\n description,\n})\nconst commonParams = {\n country: {\n type: 'string',\n pattern: '^[A-Za-z]{2}$',\n description: 'Two-letter country code. Default US.',\n },\n waitSeconds: {\n type: 'integer',\n minimum: 0,\n maximum: 120,\n description: 'Seconds to wait for queued tasks before reading them.',\n },\n}\n\nconst serp: SeoProviderActionAdapter = {\n id: 'serp',\n description:\n 'Google organic results and the pages the AI Overview cites, for up to 5 keywords. Results not ready come back as pendingTasks: run again with tasks to read them.',\n inputSchema: {\n type: 'object',\n properties: {\n keywords: strings(MAX_KEYWORDS, 'Keywords to search, 1 to 5.'),\n tasks: strings(50, 'Task ids from pendingTasks, to read instead of searching.'),\n ...commonParams,\n },\n additionalProperties: false,\n },\n outputSchema: {\n type: 'object',\n properties: {\n rows: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n keyword: nullable('string'),\n type: { type: 'string', enum: ['organic', 'ai_overview'] },\n position: nullable('number'),\n url: nullable('string'),\n domain: nullable('string'),\n title: nullable('string'),\n },\n required: ['keyword', 'type', 'position', 'url', 'domain', 'title'],\n },\n },\n credits: { type: 'number' },\n pendingTasks: { type: 'array', items: { type: 'string' } },\n errors: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n keyword: nullable('string'),\n task: nullable('string'),\n error: { type: 'string' },\n },\n required: ['keyword', 'task', 'error'],\n },\n },\n },\n required: ['rows', 'credits', 'pendingTasks', 'errors'],\n },\n run: serpAction,\n}\n\nconst aiAnswers: SeoProviderActionAdapter = {\n id: 'ai-answers',\n description:\n 'Ask ChatGPT, Google AI Mode, Gemini and Bing AI Mode up to 5 questions and report if each answer cites the site, mentions it, or neither. Answers not ready come back as pendingTasks.',\n inputSchema: {\n type: 'object',\n properties: {\n domain: {\n type: 'string',\n minLength: 3,\n maxLength: 253,\n description: 'The site to look for, such as example.com.',\n },\n questions: strings(MAX_QUESTIONS, 'Questions to ask, 1 to 5.'),\n engines: {\n type: 'array',\n items: { type: 'string', enum: ENGINE_NAMES },\n minItems: 1,\n maxItems: ENGINE_NAMES.length,\n description: 'Engines to ask. Default all four.',\n },\n brand: strings(\n 5,\n 'Names that count as a mention. Default the first label of the domain.',\n ),\n tasks: strings(100, 'Task ids from pendingTasks, to read instead of asking.'),\n ...commonParams,\n },\n required: ['domain'],\n additionalProperties: false,\n },\n outputSchema: {\n type: 'object',\n properties: {\n domain: { type: 'string' },\n rows: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n question: nullable('string'),\n engine: nullable('string'),\n result: {\n type: 'string',\n enum: ['cited', 'mentioned', 'neither', 'no_answer', 'pending', 'unavailable'],\n },\n citedUrl: nullable('string'),\n sources: { type: 'array', items: { type: 'string' } },\n model: nullable('string'),\n answeredAt: nullable('string'),\n credits: { type: 'number' },\n task: nullable('string'),\n error: nullable('string'),\n },\n required: [\n 'question',\n 'engine',\n 'result',\n 'citedUrl',\n 'sources',\n 'model',\n 'answeredAt',\n 'credits',\n 'task',\n 'error',\n ],\n },\n },\n credits: { type: 'number' },\n pendingTasks: { type: 'array', items: { type: 'string' } },\n },\n required: ['domain', 'rows', 'credits', 'pendingTasks'],\n },\n run: aiAnswersAction,\n}\n\n// --- Registration -----------------------------------------------------------\n\nconst activate: SeoProviderActivate = (host) => {\n host.registerProvider({\n id: 'cloro',\n displayName: 'cloro',\n description:\n 'Live Google results, AI Overview sources and answers from ChatGPT, Google AI Mode, Gemini and Bing AI Mode, from cloro.',\n kinds: ['search-results', 'ai-data'],\n connection: {\n fields: [\n {\n id: 'apiKey',\n label: 'API key',\n kind: 'secret',\n required: true,\n envVar: 'SEO_CLORO_API_KEY',\n },\n ],\n verify,\n verificationNotice:\n 'The connection check reads the credit balance. It costs no credits.',\n },\n capabilities: [\n {\n id: 'serp-snapshot',\n defaultDepth: 10,\n maxDepth: MAX_DEPTH,\n maxRequests: SNAPSHOT_REQUESTS,\n markets: [\n {\n searchEngines: ['google'],\n devices: ['desktop', 'mobile'],\n location: 'country-only',\n },\n ],\n estimateCostMicros: ({ depth }) =>\n snapshotCredits(pagesFor(depth)) * MICROS_PER_CREDIT,\n // One to queue and, at most, the rest to read.\n estimateRequests: () => SNAPSHOT_REQUESTS,\n run: serpSnapshot,\n },\n ],\n actions: [serp, aiAnswers],\n })\n}\n\nexport default activate\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "@cloro-dev/seo-provider",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "cloro provider for the seo package: live Google results, AI Overview sources and AI answers from ChatGPT, Google AI Mode, Gemini and Bing AI Mode.",
5
+ "author": "cloro",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/cloro-dev/seo-provider#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/cloro-dev/seo-provider.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/cloro-dev/seo-provider/issues"
15
+ },
16
+ "keywords": [
17
+ "seo",
18
+ "cloro",
19
+ "serp",
20
+ "ai-overview",
21
+ "chatgpt",
22
+ "gemini",
23
+ "ai-mode",
24
+ "provider"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist/index.d.ts",
37
+ "dist/index.js",
38
+ "dist/index.js.map"
39
+ ],
40
+ "seo": {
41
+ "apiVersion": 1,
42
+ "providers": [
43
+ "./dist/index.js"
44
+ ]
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "engines": {
51
+ "node": ">=22.19.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown --config tsdown.config.ts",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "test": "node --test dist/index.test.js",
57
+ "package:check": "node scripts/package.test.mjs",
58
+ "prepack": "npm run build && npm test && npm run package:check"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^22.20.5",
62
+ "seo": "^0.17.3",
63
+ "tsdown": "^0.23.7",
64
+ "typescript": "^6.0.3",
65
+ "zod": "^4.6.7"
66
+ },
67
+ "peerDependencies": {
68
+ "seo": ">=0.17.3"
69
+ }
70
+ }