@mayurajs/cloudflare-quick-action 0.0.0-stage → 1.1.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/tools.js ADDED
@@ -0,0 +1,197 @@
1
+ import { defineTool, media, MayuraError, withMedia } from 'mayura';
2
+ import { originPolicy } from 'mayura/browser';
3
+ function object(fields, maxString, check) {
4
+ return { '~standard': { version: 1, vendor: 'mayura-cloudflare', validate: (value) => {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value))
6
+ return { issues: [{ message: 'Expected an object.' }] };
7
+ const input = value;
8
+ for (const name of Object.keys(input))
9
+ if (!Object.hasOwn(fields, name))
10
+ return { issues: [{ message: `Unexpected field ${name}.` }] };
11
+ for (const [name, kind] of Object.entries(fields)) {
12
+ const item = input[name];
13
+ if (item === undefined) {
14
+ if (kind.endsWith('?'))
15
+ continue;
16
+ return { issues: [{ message: `${name} is required.` }] };
17
+ }
18
+ if (kind.startsWith('string') && kind !== 'strings' && (typeof item !== 'string' || item.length > maxString))
19
+ return { issues: [{ message: `${name} must be a string.` }] };
20
+ if (kind === 'strings' && (!Array.isArray(item) || item.length === 0 || item.length > 20 || item.some(entry => typeof entry !== 'string' || entry === '' || entry.length > 500)))
21
+ return { issues: [{ message: `${name} is 1 to 20 strings.` }] };
22
+ if (kind.startsWith('integer') && (typeof item !== 'number' || !Number.isSafeInteger(item) || item < 1))
23
+ return { issues: [{ message: `${name} must be a positive whole number.` }] };
24
+ if (kind.startsWith('boolean') && typeof item !== 'boolean')
25
+ return { issues: [{ message: `${name} must be true or false.` }] };
26
+ if (kind.startsWith('object') && (!item || typeof item !== 'object' || Array.isArray(item) || JSON.stringify(item).length > 20_000))
27
+ return { issues: [{ message: `${name} must be a JSON Schema object.` }] };
28
+ }
29
+ const message = check?.(input);
30
+ return message === undefined ? { value: input } : { issues: [{ message }] };
31
+ } } };
32
+ }
33
+ const anything = { '~standard': { version: 1, vendor: 'mayura-cloudflare', validate: (value) => ({ value }) } };
34
+ const schema = (required, properties) => ({ type: 'object', additionalProperties: false, ...(required.length ? { required: [...required] } : {}), properties });
35
+ const encoder = new TextEncoder();
36
+ const decoder = new TextDecoder();
37
+ function clip(text, max) {
38
+ const bytes = encoder.encode(text);
39
+ return bytes.byteLength <= max ? { text, truncated: false } : { text: decoder.decode(bytes.subarray(0, max)).replace(/�$/u, ''), truncated: true };
40
+ }
41
+ const correctable = new Set(['INVALID_INPUT', 'PERMISSION_DENIED', 'LIMIT_EXCEEDED']);
42
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\/]/gu, '\\$&');
43
+ /** Request patterns that let a page load only from the origins (already checked by originPolicy). */
44
+ export function originPatterns(origins) {
45
+ return origins.map(origin => {
46
+ const match = /^(https?):\/\/(\*\.)?([^/:]+|\[[^\]]+\])(?::(\d{1,5}))?\/?$/u.exec(origin.toLowerCase());
47
+ const port = match[4] ? `:${match[4]}` : `(:${match[1] === 'https' ? 443 : 80})?`;
48
+ return `^${match[1]}://${match[2] ? '([a-z0-9-]+\\.)+' : ''}${escape(match[3])}${port}(/|\\?|#|$)`;
49
+ });
50
+ }
51
+ /**
52
+ * Cloudflare Browser Run's quick actions as tools: `<name>.markdown`, `<name>.links` and `<name>.scrape` read pages
53
+ * (`web:<name>:read`); screenshots, JSON extraction and crawls are each off until enabled. Every URL must be within
54
+ * `origins`, and the page Cloudflare renders loads nothing from elsewhere. Page content comes back as the tools'
55
+ * results, for the model to read: treat it as untrusted text.
56
+ */
57
+ export function quickActionTools(actions, options) {
58
+ if (!actions || typeof actions.markdown !== 'function')
59
+ throw new MayuraError('INVALID_CONFIG', 'quickActionTools() needs quick actions from cloudflareQuickActions().');
60
+ const name = options?.name ?? 'cloudflare';
61
+ if (typeof name !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/u.test(name))
62
+ throw new MayuraError('INVALID_CONFIG', 'quickActionTools(): name is lowercase letters, digits, _ and -, starting with a letter.');
63
+ const policy = originPolicy(options.origins);
64
+ const allowRequestPattern = policy.all ? undefined : originPatterns(options.origins);
65
+ for (const flag of ['screenshot', 'json', 'crawl']) {
66
+ if (options[flag] !== undefined && typeof options[flag] !== 'boolean')
67
+ throw new MayuraError('INVALID_CONFIG', `quickActionTools(): ${flag} must be a boolean.`);
68
+ }
69
+ const bound = (value, label, fallback, min, max) => {
70
+ const result = value ?? fallback;
71
+ if (!Number.isSafeInteger(result) || result < min || result > max)
72
+ throw new MayuraError('INVALID_CONFIG', `quickActionTools(): ${label} is ${min} to ${max}.`);
73
+ return result;
74
+ };
75
+ const maxPageBytes = bound(options.maxPageBytes, 'maxPageBytes', 65_536, 1_024, 4_194_304);
76
+ const maxCrawlPages = bound(options.maxCrawlPages, 'maxCrawlPages', 10, 1, 100);
77
+ const crawlTimeoutMs = bound(options.crawlTimeoutMs, 'crawlTimeoutMs', 300_000, 10_000, 3_600_000);
78
+ const costs = options.costMicros ?? {};
79
+ for (const [tool, cost] of Object.entries(costs))
80
+ if (!Number.isSafeInteger(cost) || cost < 0)
81
+ throw new MayuraError('INVALID_CONFIG', `quickActionTools(): costMicros.${tool} must be a non-negative whole number.`);
82
+ /** The page request for a URL the tools may read, or a refusal the model can act on. */
83
+ const page = (url) => {
84
+ let parsed;
85
+ try {
86
+ parsed = new URL(String(url));
87
+ }
88
+ catch {
89
+ throw new MayuraError('INVALID_INPUT', 'url must be an http(s) URL.');
90
+ }
91
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
92
+ throw new MayuraError('INVALID_INPUT', 'url must be an http(s) URL.');
93
+ if (!policy.allows(parsed.href))
94
+ throw new MayuraError('PERMISSION_DENIED', `${parsed.origin} is outside the sites these tools may read.`);
95
+ return { url: parsed.href, ...(allowRequestPattern ? { allowRequestPattern } : {}) };
96
+ };
97
+ const tool = (id, permission, cost, description, input, inputJsonSchema, execute, extra = {}, timeoutMs = 120_000) => defineTool({ id: `${name}.${id}`, version: '1', effects: 'read', capabilities: [`web:${name}:${permission}`], timeoutMs, costMicros: cost ?? 0,
98
+ description, input, output: anything, inputJsonSchema, ...extra,
99
+ execute: async (request, context) => {
100
+ try {
101
+ return await execute(request, context.signal);
102
+ }
103
+ catch (error) {
104
+ // Mistakes the model can put right come back as the result, so it can try again; Cloudflare's failures are thrown.
105
+ if (error instanceof MayuraError && correctable.has(error.code))
106
+ return { error: error.code, message: error.message };
107
+ throw error;
108
+ }
109
+ } });
110
+ const urlField = { type: 'string', description: 'An http(s) URL.' };
111
+ const tools = [
112
+ tool('markdown', 'read', costs.markdown, `Read a web page, rendered by Cloudflare's browser, as markdown. Only some sites may be read. Up to ${maxPageBytes} bytes.`, object({ url: 'string' }, 8_192), schema(['url'], { url: urlField }), async (request, signal) => {
113
+ const target = page(request['url']);
114
+ const content = clip(await actions.markdown(target, { signal }), maxPageBytes);
115
+ return { url: target.url, markdown: content.text, ...(content.truncated ? { truncated: true } : {}) };
116
+ }),
117
+ tool('links', 'read', costs.links, 'List the links on a web page, rendered by Cloudflare\'s browser.', object({ url: 'string', visibleOnly: 'boolean?', excludeExternal: 'boolean?' }, 8_192), schema(['url'], { url: urlField, visibleOnly: { type: 'boolean' }, excludeExternal: { type: 'boolean' } }), async (request, signal) => {
118
+ const target = page(request['url']);
119
+ const links = await actions.links({ ...target, visibleLinksOnly: request['visibleOnly'] === true, excludeExternalLinks: request['excludeExternal'] === true }, { signal });
120
+ return { url: target.url, links: links.slice(0, 1_000) };
121
+ }),
122
+ tool('scrape', 'read', costs.scrape, 'Read the elements of a web page that match CSS selectors: their text and attributes.', object({ url: 'string', selectors: 'strings' }, 8_192), schema(['url', 'selectors'], { url: urlField, selectors: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 20, description: 'CSS selectors, such as h1 or a.product-link.' } }), async (request, signal) => {
123
+ const target = page(request['url']);
124
+ const found = await actions.scrape({ ...target, selectors: request['selectors'] }, { signal });
125
+ let budget = maxPageBytes;
126
+ let truncated = false;
127
+ const elements = found.map(group => ({ selector: group.selector, results: group.results.flatMap(element => {
128
+ const text = clip(element.text, Math.max(0, budget));
129
+ budget -= encoder.encode(text.text).byteLength;
130
+ if (text.truncated)
131
+ truncated = true;
132
+ return budget < 0 || (text.text === '' && element.text !== '') ? [] : [{ text: text.text, attributes: element.attributes.slice(0, 20) }];
133
+ }) }));
134
+ return { url: target.url, elements, ...(truncated ? { truncated: true } : {}) };
135
+ }),
136
+ ];
137
+ if (options.screenshot) {
138
+ tools.push(tool('screenshot', 'read', costs.screenshot, 'See a web page, rendered by Cloudflare\'s browser, as an image: the visible part, or (fullPage) all of it.', object({ url: 'string', fullPage: 'boolean?' }, 8_192), schema(['url'], { url: urlField, fullPage: { type: 'boolean' } }), async (request, signal) => {
139
+ const target = page(request['url']);
140
+ const image = await actions.screenshot({ ...target, fullPage: request['fullPage'] === true }, { signal });
141
+ if (image.byteLength > 16 * 1_048_576)
142
+ throw new MayuraError('LIMIT_EXCEEDED', 'The screenshot is larger than 16 MiB.');
143
+ return withMedia({ url: target.url, bytes: image.byteLength }, [media(image, 'image/png')]);
144
+ }, { media: { accept: ['image/png'], maxItems: 1, maxBytes: 16 * 1_048_576 } }));
145
+ }
146
+ if (options.json) {
147
+ tools.push(tool('json', 'extract', costs.json, 'Pull structured data out of a web page with Cloudflare\'s model: say what to extract in prompt, and optionally give a JSON Schema for its shape.', object({ url: 'string', prompt: 'string', schema: 'object?' }, 8_192, request => request.prompt.trim() === '' ? 'prompt is required.' : undefined), schema(['url', 'prompt'], { url: urlField, prompt: { type: 'string' }, schema: { type: 'object', description: 'A JSON Schema for the data.' } }), async (request, signal) => {
148
+ const target = page(request['url']);
149
+ const data = await actions.json({ ...target, prompt: request['prompt'], ...(request['schema'] ? { schema: request['schema'] } : {}) }, { signal });
150
+ if (encoder.encode(JSON.stringify(data ?? null)).byteLength > maxPageBytes)
151
+ throw new MayuraError('LIMIT_EXCEEDED', `The extracted data is larger than ${maxPageBytes} bytes.`);
152
+ return { url: target.url, data: (data ?? null) };
153
+ }));
154
+ }
155
+ if (options.crawl) {
156
+ tools.push(tool('crawl', 'crawl', costs.crawl, `Read up to ${maxCrawlPages} pages of a site, starting at a URL and following its links on the same site, as markdown, rendered by Cloudflare's browser.`, object({ url: 'string', limit: 'integer?', includePattern: 'string?' }, 8_192, request => (request.limit ?? 1) > maxCrawlPages ? `limit is at most ${maxCrawlPages}.` : undefined), schema(['url'], { url: urlField, limit: { type: 'integer', minimum: 1, maximum: maxCrawlPages }, includePattern: { type: 'string', description: 'A wildcard pattern the pages to read must match, such as https://example.com/docs/**.' } }), async (request, signal) => {
157
+ const target = page(request['url']);
158
+ const limit = request['limit'] ?? maxCrawlPages;
159
+ const id = await actions.crawl.start({ ...target, limit, formats: ['markdown'], ...(request['includePattern'] ? { includePatterns: [request['includePattern']] } : {}) }, { signal });
160
+ const deadline = Date.now() + crawlTimeoutMs;
161
+ let finished = false;
162
+ try {
163
+ for (;;) {
164
+ const status = await actions.crawl.status(id, { signal, limit });
165
+ if (status.status !== 'running') {
166
+ finished = true;
167
+ if (status.status !== 'completed')
168
+ throw new MayuraError('TOOL_FAILED', `Cloudflare's crawl ended without completing (${status.status}).`);
169
+ // Only pages read, and within the sites allowed.
170
+ const pages = status.records.filter(record => record.status === 'completed' && policy.allows(record.url)).slice(0, limit).map(record => {
171
+ const content = clip(record.markdown ?? '', maxPageBytes);
172
+ return { url: record.url, ...(record.title ? { title: record.title.slice(0, 500) } : {}), markdown: content.text, ...(content.truncated ? { truncated: true } : {}) };
173
+ });
174
+ return { url: target.url, pages };
175
+ }
176
+ if (Date.now() >= deadline)
177
+ throw new MayuraError('TIMEOUT', 'The crawl took too long, and was cancelled.');
178
+ await new Promise((resolve, reject) => {
179
+ const stop = () => { clearTimeout(timer); reject(new MayuraError('CANCELLED', 'The crawl was cancelled.')); };
180
+ const timer = setTimeout(() => { signal.removeEventListener('abort', stop); resolve(undefined); }, 2_000);
181
+ if (signal.aborted)
182
+ stop();
183
+ else
184
+ signal.addEventListener('abort', stop, { once: true });
185
+ });
186
+ }
187
+ }
188
+ finally {
189
+ // A crawl not finished is cancelled, so it stops spending.
190
+ if (!finished)
191
+ await actions.crawl.cancel(id, { signal: AbortSignal.timeout(30_000) }).catch(() => undefined);
192
+ }
193
+ }, {}, crawlTimeoutMs + 60_000));
194
+ }
195
+ return tools;
196
+ }
197
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineTool, media, MayuraError, withMedia, type AnyTool, type JsonObject, type Schema } from 'mayura';\nimport { originPolicy } from 'mayura/browser';\nimport type { QuickActions } from './client.js';\n\nexport interface QuickActionToolsOptions {\n /** Names the tools (`<name>.markdown`, ...) and their permissions (`web:<name>:read`, ...); `cloudflare` by default. */\n readonly name?: string;\n /**\n * The sites the tools may read, as `createBrowsers` takes them: `['https://example.com', 'https://*.example.com']`,\n * or `'all'`. Required: nothing is allowed by default. The page Cloudflare renders loads nothing from other sites.\n */\n readonly origins: 'all' | readonly string[];\n /** Make `<name>.screenshot`, an image of a page; permission `web:<name>:read`. Off by default. */\n readonly screenshot?: boolean;\n /** Make `<name>.json`, structured data from a page by Cloudflare's model (Workers AI); permission `web:<name>:extract`. Off by default. */\n readonly json?: boolean;\n /** Make `<name>.crawl`, which reads many pages of a site; permission `web:<name>:crawl`. Off by default. */\n readonly crawl?: boolean;\n /** The most bytes of a page's content returned to the model; 64 KiB by default. */\n readonly maxPageBytes?: number;\n /** The most pages one crawl reads; 10 by default (at most 100). */\n readonly maxCrawlPages?: number;\n /** The longest one crawl runs before it is cancelled; 5 minutes by default. */\n readonly crawlTimeoutMs?: number;\n /** What each call costs at most, in micro-units of your budget currency, by tool; 0 by default. */\n readonly costMicros?: { readonly markdown?: number; readonly links?: number; readonly scrape?: number; readonly screenshot?: number; readonly json?: number; readonly crawl?: number };\n}\n\ntype Kind = 'string' | 'integer?' | 'boolean?' | 'string?' | 'object?' | 'strings';\nfunction object<T>(fields: Readonly<Record<string, Kind>>, maxString: number, check?: (value: T) => string | undefined): Schema<T> {\n return { '~standard': { version: 1, vendor: 'mayura-cloudflare', validate: (value: unknown) => {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return { issues: [{ message: 'Expected an object.' }] };\n const input = value as Record<string, unknown>;\n for (const name of Object.keys(input)) if (!Object.hasOwn(fields, name)) return { issues: [{ message: `Unexpected field ${name}.` }] };\n for (const [name, kind] of Object.entries(fields)) {\n const item = input[name];\n if (item === undefined) { if (kind.endsWith('?')) continue; return { issues: [{ message: `${name} is required.` }] }; }\n if (kind.startsWith('string') && kind !== 'strings' && (typeof item !== 'string' || item.length > maxString)) return { issues: [{ message: `${name} must be a string.` }] };\n if (kind === 'strings' && (!Array.isArray(item) || item.length === 0 || item.length > 20 || item.some(entry => typeof entry !== 'string' || entry === '' || entry.length > 500))) return { issues: [{ message: `${name} is 1 to 20 strings.` }] };\n if (kind.startsWith('integer') && (typeof item !== 'number' || !Number.isSafeInteger(item) || item < 1)) return { issues: [{ message: `${name} must be a positive whole number.` }] };\n if (kind.startsWith('boolean') && typeof item !== 'boolean') return { issues: [{ message: `${name} must be true or false.` }] };\n if (kind.startsWith('object') && (!item || typeof item !== 'object' || Array.isArray(item) || JSON.stringify(item).length > 20_000)) return { issues: [{ message: `${name} must be a JSON Schema object.` }] };\n }\n const message = check?.(input as T);\n return message === undefined ? { value: input as T } : { issues: [{ message }] };\n } } } as Schema<T>;\n}\nconst anything: Schema<unknown> = { '~standard': { version: 1, vendor: 'mayura-cloudflare', validate: (value: unknown) => ({ value }) } } as Schema<unknown>;\nconst schema = (required: readonly string[], properties: Record<string, unknown>) => ({ type: 'object', additionalProperties: false, ...(required.length ? { required: [...required] } : {}), properties }) as unknown as JsonObject;\nconst encoder = new TextEncoder(); const decoder = new TextDecoder();\nfunction clip(text: string, max: number): { text: string; truncated: boolean } {\n const bytes = encoder.encode(text);\n return bytes.byteLength <= max ? { text, truncated: false } : { text: decoder.decode(bytes.subarray(0, max)).replace(/�$/u, ''), truncated: true };\n}\nconst correctable = new Set(['INVALID_INPUT', 'PERMISSION_DENIED', 'LIMIT_EXCEEDED']);\nconst escape = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\/]/gu, '\\\\$&');\n\n/** Request patterns that let a page load only from the origins (already checked by originPolicy). */\nexport function originPatterns(origins: readonly string[]): string[] {\n return origins.map(origin => {\n const match = /^(https?):\\/\\/(\\*\\.)?([^/:]+|\\[[^\\]]+\\])(?::(\\d{1,5}))?\\/?$/u.exec(origin.toLowerCase())!;\n const port = match[4] ? `:${match[4]}` : `(:${match[1] === 'https' ? 443 : 80})?`;\n return `^${match[1]}://${match[2] ? '([a-z0-9-]+\\\\.)+' : ''}${escape(match[3]!)}${port}(/|\\\\?|#|$)`;\n });\n}\n\n/**\n * Cloudflare Browser Run's quick actions as tools: `<name>.markdown`, `<name>.links` and `<name>.scrape` read pages\n * (`web:<name>:read`); screenshots, JSON extraction and crawls are each off until enabled. Every URL must be within\n * `origins`, and the page Cloudflare renders loads nothing from elsewhere. Page content comes back as the tools'\n * results, for the model to read: treat it as untrusted text.\n */\nexport function quickActionTools(actions: QuickActions, options: QuickActionToolsOptions): AnyTool[] {\n if (!actions || typeof actions.markdown !== 'function') throw new MayuraError('INVALID_CONFIG', 'quickActionTools() needs quick actions from cloudflareQuickActions().');\n const name = options?.name ?? 'cloudflare';\n if (typeof name !== 'string' || !/^[a-z][a-z0-9_-]{0,31}$/u.test(name)) throw new MayuraError('INVALID_CONFIG', 'quickActionTools(): name is lowercase letters, digits, _ and -, starting with a letter.');\n const policy = originPolicy(options.origins);\n const allowRequestPattern = policy.all ? undefined : originPatterns(options.origins as readonly string[]);\n for (const flag of ['screenshot', 'json', 'crawl'] as const) {\n if (options[flag] !== undefined && typeof options[flag] !== 'boolean') throw new MayuraError('INVALID_CONFIG', `quickActionTools(): ${flag} must be a boolean.`);\n }\n const bound = (value: number | undefined, label: string, fallback: number, min: number, max: number) => {\n const result = value ?? fallback;\n if (!Number.isSafeInteger(result) || result < min || result > max) throw new MayuraError('INVALID_CONFIG', `quickActionTools(): ${label} is ${min} to ${max}.`);\n return result;\n };\n const maxPageBytes = bound(options.maxPageBytes, 'maxPageBytes', 65_536, 1_024, 4_194_304);\n const maxCrawlPages = bound(options.maxCrawlPages, 'maxCrawlPages', 10, 1, 100);\n const crawlTimeoutMs = bound(options.crawlTimeoutMs, 'crawlTimeoutMs', 300_000, 10_000, 3_600_000);\n const costs = options.costMicros ?? {};\n for (const [tool, cost] of Object.entries(costs)) if (!Number.isSafeInteger(cost) || (cost as number) < 0) throw new MayuraError('INVALID_CONFIG', `quickActionTools(): costMicros.${tool} must be a non-negative whole number.`);\n\n /** The page request for a URL the tools may read, or a refusal the model can act on. */\n const page = (url: unknown) => {\n let parsed: URL;\n try { parsed = new URL(String(url)); } catch { throw new MayuraError('INVALID_INPUT', 'url must be an http(s) URL.'); }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new MayuraError('INVALID_INPUT', 'url must be an http(s) URL.');\n if (!policy.allows(parsed.href)) throw new MayuraError('PERMISSION_DENIED', `${parsed.origin} is outside the sites these tools may read.`);\n return { url: parsed.href, ...(allowRequestPattern ? { allowRequestPattern } : {}) };\n };\n const tool = (id: string, permission: string, cost: number | undefined, description: string, input: Schema<unknown>, inputJsonSchema: JsonObject,\n execute: (request: Record<string, unknown>, signal: AbortSignal) => Promise<JsonObject | ReturnType<typeof withMedia>>, extra: Record<string, unknown> = {}, timeoutMs = 120_000) =>\n defineTool({ id: `${name}.${id}`, version: '1', effects: 'read', capabilities: [`web:${name}:${permission}`], timeoutMs, costMicros: cost ?? 0,\n description, input, output: anything, inputJsonSchema, ...extra,\n execute: async (request: unknown, context: { readonly signal: AbortSignal }) => {\n try { return await execute(request as Record<string, unknown>, context.signal); }\n catch (error) {\n // Mistakes the model can put right come back as the result, so it can try again; Cloudflare's failures are thrown.\n if (error instanceof MayuraError && correctable.has(error.code)) return { error: error.code, message: error.message } as JsonObject;\n throw error;\n }\n } } as never) as unknown as AnyTool;\n const urlField = { type: 'string', description: 'An http(s) URL.' };\n\n const tools: AnyTool[] = [\n tool('markdown', 'read', costs.markdown, `Read a web page, rendered by Cloudflare's browser, as markdown. Only some sites may be read. Up to ${maxPageBytes} bytes.`,\n object<{ url: string }>({ url: 'string' }, 8_192), schema(['url'], { url: urlField }),\n async (request, signal) => {\n const target = page(request['url']); const content = clip(await actions.markdown(target, { signal }), maxPageBytes);\n return { url: target.url, markdown: content.text, ...(content.truncated ? { truncated: true } : {}) } as JsonObject;\n }),\n tool('links', 'read', costs.links, 'List the links on a web page, rendered by Cloudflare\\'s browser.',\n object<{ url: string; visibleOnly?: boolean; excludeExternal?: boolean }>({ url: 'string', visibleOnly: 'boolean?', excludeExternal: 'boolean?' }, 8_192),\n schema(['url'], { url: urlField, visibleOnly: { type: 'boolean' }, excludeExternal: { type: 'boolean' } }),\n async (request, signal) => {\n const target = page(request['url']);\n const links = await actions.links({ ...target, visibleLinksOnly: request['visibleOnly'] === true, excludeExternalLinks: request['excludeExternal'] === true }, { signal });\n return { url: target.url, links: links.slice(0, 1_000) } as unknown as JsonObject;\n }),\n tool('scrape', 'read', costs.scrape, 'Read the elements of a web page that match CSS selectors: their text and attributes.',\n object<{ url: string; selectors: string[] }>({ url: 'string', selectors: 'strings' }, 8_192),\n schema(['url', 'selectors'], { url: urlField, selectors: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 20, description: 'CSS selectors, such as h1 or a.product-link.' } }),\n async (request, signal) => {\n const target = page(request['url']);\n const found = await actions.scrape({ ...target, selectors: request['selectors'] as string[] }, { signal });\n let budget = maxPageBytes; let truncated = false;\n const elements = found.map(group => ({ selector: group.selector, results: group.results.flatMap(element => {\n const text = clip(element.text, Math.max(0, budget)); budget -= encoder.encode(text.text).byteLength;\n if (text.truncated) truncated = true;\n return budget < 0 || (text.text === '' && element.text !== '') ? [] : [{ text: text.text, attributes: element.attributes.slice(0, 20) }];\n }) }));\n return { url: target.url, elements, ...(truncated ? { truncated: true } : {}) } as unknown as JsonObject;\n }),\n ];\n if (options.screenshot) {\n tools.push(tool('screenshot', 'read', costs.screenshot, 'See a web page, rendered by Cloudflare\\'s browser, as an image: the visible part, or (fullPage) all of it.',\n object<{ url: string; fullPage?: boolean }>({ url: 'string', fullPage: 'boolean?' }, 8_192), schema(['url'], { url: urlField, fullPage: { type: 'boolean' } }),\n async (request, signal) => {\n const target = page(request['url']);\n const image = await actions.screenshot({ ...target, fullPage: request['fullPage'] === true }, { signal });\n if (image.byteLength > 16 * 1_048_576) throw new MayuraError('LIMIT_EXCEEDED', 'The screenshot is larger than 16 MiB.');\n return withMedia({ url: target.url, bytes: image.byteLength } as JsonObject, [media(image, 'image/png')]);\n }, { media: { accept: ['image/png'], maxItems: 1, maxBytes: 16 * 1_048_576 } }));\n }\n if (options.json) {\n tools.push(tool('json', 'extract', costs.json, 'Pull structured data out of a web page with Cloudflare\\'s model: say what to extract in prompt, and optionally give a JSON Schema for its shape.',\n object<{ url: string; prompt: string; schema?: Record<string, unknown> }>({ url: 'string', prompt: 'string', schema: 'object?' }, 8_192, request => request.prompt.trim() === '' ? 'prompt is required.' : undefined),\n schema(['url', 'prompt'], { url: urlField, prompt: { type: 'string' }, schema: { type: 'object', description: 'A JSON Schema for the data.' } }),\n async (request, signal) => {\n const target = page(request['url']);\n const data = await actions.json({ ...target, prompt: request['prompt'] as string, ...(request['schema'] ? { schema: request['schema'] as Record<string, unknown> } : {}) }, { signal });\n if (encoder.encode(JSON.stringify(data ?? null)).byteLength > maxPageBytes) throw new MayuraError('LIMIT_EXCEEDED', `The extracted data is larger than ${maxPageBytes} bytes.`);\n return { url: target.url, data: (data ?? null) as JsonObject } as JsonObject;\n }));\n }\n if (options.crawl) {\n tools.push(tool('crawl', 'crawl', costs.crawl, `Read up to ${maxCrawlPages} pages of a site, starting at a URL and following its links on the same site, as markdown, rendered by Cloudflare's browser.`,\n object<{ url: string; limit?: number; includePattern?: string }>({ url: 'string', limit: 'integer?', includePattern: 'string?' }, 8_192, request => (request.limit ?? 1) > maxCrawlPages ? `limit is at most ${maxCrawlPages}.` : undefined),\n schema(['url'], { url: urlField, limit: { type: 'integer', minimum: 1, maximum: maxCrawlPages }, includePattern: { type: 'string', description: 'A wildcard pattern the pages to read must match, such as https://example.com/docs/**.' } }),\n async (request, signal) => {\n const target = page(request['url']); const limit = (request['limit'] as number | undefined) ?? maxCrawlPages;\n const id = await actions.crawl.start({ ...target, limit, formats: ['markdown'], ...(request['includePattern'] ? { includePatterns: [request['includePattern'] as string] } : {}) }, { signal });\n const deadline = Date.now() + crawlTimeoutMs; let finished = false;\n try {\n for (;;) {\n const status = await actions.crawl.status(id, { signal, limit });\n if (status.status !== 'running') {\n finished = true;\n if (status.status !== 'completed') throw new MayuraError('TOOL_FAILED', `Cloudflare's crawl ended without completing (${status.status}).`);\n // Only pages read, and within the sites allowed.\n const pages = status.records.filter(record => record.status === 'completed' && policy.allows(record.url)).slice(0, limit).map(record => {\n const content = clip(record.markdown ?? '', maxPageBytes);\n return { url: record.url, ...(record.title ? { title: record.title.slice(0, 500) } : {}), markdown: content.text, ...(content.truncated ? { truncated: true } : {}) };\n });\n return { url: target.url, pages } as unknown as JsonObject;\n }\n if (Date.now() >= deadline) throw new MayuraError('TIMEOUT', 'The crawl took too long, and was cancelled.');\n await new Promise((resolve, reject) => {\n const stop = () => { clearTimeout(timer); reject(new MayuraError('CANCELLED', 'The crawl was cancelled.')); };\n const timer = setTimeout(() => { signal.removeEventListener('abort', stop); resolve(undefined); }, 2_000);\n if (signal.aborted) stop(); else signal.addEventListener('abort', stop, { once: true });\n });\n }\n } finally {\n // A crawl not finished is cancelled, so it stops spending.\n if (!finished) await actions.crawl.cancel(id, { signal: AbortSignal.timeout(30_000) }).catch(() => undefined);\n }\n }, {}, crawlTimeoutMs + 60_000));\n }\n return tools;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@mayurajs/cloudflare-quick-action",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "Cloudflare Browser Run quick actions (markdown, links, scrape, screenshots, PDFs, JSON, crawls) as a client and as tools.",
5
+ "author": {
6
+ "name": "Aryabh",
7
+ "email": "dev@rokad.co"
8
+ },
9
+ "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/KartikeyAI/mayura.git",
13
+ "directory": "extensions/cloudflare-quick-action"
14
+ },
15
+ "homepage": "https://github.com/KartikeyAI/mayura#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/KartikeyAI/mayura/issues"
18
+ },
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "engines": {
22
+ "node": ">=22.12.0 <23 || >=24.14.1 <25"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src/**/*.ts",
33
+ "README.md",
34
+ "LICENSE",
35
+ "NOTICE"
36
+ ],
37
+ "dependencies": {},
38
+ "peerDependencies": {
39
+ "mayura": "^1.1.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true
44
+ }
45
+ }
package/src/client.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { MayuraError } from 'mayura';
2
+
3
+ export interface QuickActionsOptions {
4
+ /** Your Cloudflare account id (32 hex characters). */
5
+ readonly accountId: string;
6
+ /** A Cloudflare API token with the "Browser Rendering - Edit" permission. Nothing is read from the environment. */
7
+ readonly apiToken: string;
8
+ /** For tests and proxies: the fetch to use. */
9
+ readonly fetch?: typeof fetch;
10
+ /** The API's address; `https://api.cloudflare.com` by default. */
11
+ readonly baseUrl?: string;
12
+ }
13
+
14
+ /** What every action renders: a page, and optionally only the requests it may make. */
15
+ export interface PageRequest {
16
+ readonly url: string;
17
+ /** Regular expressions: the page loads only what matches one. */
18
+ readonly allowRequestPattern?: readonly string[];
19
+ }
20
+ export interface CallOptions { readonly signal?: AbortSignal }
21
+ export interface ScrapedElement { readonly text: string; readonly html: string; readonly attributes: readonly { readonly name: string; readonly value: string }[] }
22
+ export interface CrawlStart {
23
+ readonly url: string;
24
+ /** Pages to read; 10 by default. */
25
+ readonly limit?: number;
26
+ readonly depth?: number;
27
+ readonly formats?: readonly ('markdown' | 'html')[];
28
+ readonly includePatterns?: readonly string[];
29
+ readonly excludePatterns?: readonly string[];
30
+ readonly allowRequestPattern?: readonly string[];
31
+ }
32
+ export type CrawlStatus = 'running' | 'completed' | 'errored' | 'cancelled_due_to_timeout' | 'cancelled_due_to_limits' | 'cancelled_by_user';
33
+ export interface CrawlRecord { readonly url: string; readonly status: string; readonly markdown?: string; readonly html?: string; readonly title?: string; readonly httpStatus?: number }
34
+ export interface CrawlResult { readonly id: string; readonly status: CrawlStatus; readonly total: number; readonly finished: number; readonly records: readonly CrawlRecord[]; readonly cursor?: string | number }
35
+
36
+ /** Cloudflare Browser Run's quick actions: one call renders a page and gives back what was asked for. */
37
+ export interface QuickActions {
38
+ markdown(request: PageRequest, options?: CallOptions): Promise<string>;
39
+ /** The page's HTML once rendered. */
40
+ content(request: PageRequest, options?: CallOptions): Promise<string>;
41
+ links(request: PageRequest & { readonly visibleLinksOnly?: boolean; readonly excludeExternalLinks?: boolean }, options?: CallOptions): Promise<readonly string[]>;
42
+ scrape(request: PageRequest & { readonly selectors: readonly string[] }, options?: CallOptions): Promise<readonly { readonly selector: string; readonly results: readonly ScrapedElement[] }[]>;
43
+ screenshot(request: PageRequest & { readonly fullPage?: boolean }, options?: CallOptions): Promise<Uint8Array>;
44
+ pdf(request: PageRequest, options?: CallOptions): Promise<Uint8Array>;
45
+ /** Structured data from the page by Cloudflare's model (Workers AI, billed as such): a prompt, a JSON Schema, or both. */
46
+ json(request: PageRequest & { readonly prompt?: string; readonly schema?: Record<string, unknown> }, options?: CallOptions): Promise<unknown>;
47
+ readonly crawl: {
48
+ start(request: CrawlStart, options?: CallOptions): Promise<string>;
49
+ status(id: string, options?: CallOptions & { readonly cursor?: string | number; readonly limit?: number }): Promise<CrawlResult>;
50
+ cancel(id: string, options?: CallOptions): Promise<void>;
51
+ };
52
+ }
53
+
54
+ const statuses: readonly CrawlStatus[] = ['running', 'completed', 'errored', 'cancelled_due_to_timeout', 'cancelled_due_to_limits', 'cancelled_by_user'];
55
+ const failure = (status: number) => new MayuraError('TOOL_FAILED',
56
+ status === 429 ? 'Cloudflare is rate limiting Browser Run requests (HTTP 429).' : `Cloudflare refused the request (HTTP ${status}).`);
57
+ const invalid = () => new MayuraError('TOOL_FAILED', 'Cloudflare returned a response that is not valid.');
58
+
59
+ /**
60
+ * A client for Cloudflare Browser Run's REST quick actions, from any runtime with no dependency. Nothing it reads of
61
+ * Cloudflare's errors reaches its own: a failure names only its HTTP status.
62
+ */
63
+ export function cloudflareQuickActions(options: QuickActionsOptions): QuickActions {
64
+ if (!options || typeof options.accountId !== 'string' || !/^[0-9a-f]{32}$/u.test(options.accountId)) throw new MayuraError('INVALID_CONFIG', 'cloudflareQuickActions(): accountId must be a Cloudflare account id.');
65
+ if (typeof options.apiToken !== 'string' || !/^[A-Za-z0-9_-]{20,512}$/u.test(options.apiToken)) throw new MayuraError('INVALID_CONFIG', 'cloudflareQuickActions(): apiToken must be a Cloudflare API token.');
66
+ if (options.fetch !== undefined && typeof options.fetch !== 'function') throw new MayuraError('INVALID_CONFIG', 'cloudflareQuickActions(): fetch must be a function.');
67
+ const base = (() => {
68
+ try { const url = new URL(options.baseUrl ?? 'https://api.cloudflare.com'); if (url.protocol !== 'https:' && url.hostname !== '127.0.0.1' && url.hostname !== 'localhost') throw new Error(); return url.origin; }
69
+ catch { throw new MayuraError('INVALID_CONFIG', 'cloudflareQuickActions(): baseUrl must be an https URL.'); }
70
+ })();
71
+ const root = `${base}/client/v4/accounts/${options.accountId}/browser-run`;
72
+ const fetcher = options.fetch ?? globalThis.fetch;
73
+ const headers = { authorization: `Bearer ${options.apiToken}`, 'content-type': 'application/json' };
74
+ const page = (request: PageRequest) => {
75
+ if (!request || typeof request.url !== 'string' || !/^https?:\/\//u.test(request.url)) throw new MayuraError('INVALID_INPUT', 'url must be an http(s) URL.');
76
+ return { url: request.url, ...(request.allowRequestPattern ? { allowRequestPattern: [...request.allowRequestPattern] } : {}) };
77
+ };
78
+ const send = async (method: 'GET' | 'POST' | 'DELETE', path: string, body: unknown, signal: AbortSignal | undefined) => {
79
+ const reply = await fetcher(`${root}${path}`, { method, headers, ...(signal ? { signal } : {}), ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
80
+ if (!reply.ok) { void reply.body?.cancel().catch(() => undefined); throw failure(reply.status); }
81
+ return reply;
82
+ };
83
+ /** The `result` of Cloudflare's JSON envelope. */
84
+ const result = async (reply: Response): Promise<unknown> => {
85
+ const data = await reply.json().catch(() => undefined) as { success?: unknown; result?: unknown } | undefined;
86
+ if (!data || data.success !== true) throw invalid();
87
+ return data.result;
88
+ };
89
+ const bytes = async (reply: Response): Promise<Uint8Array> => {
90
+ if (/json/u.test(reply.headers.get('content-type') ?? '')) { void reply.body?.cancel().catch(() => undefined); throw invalid(); }
91
+ return new Uint8Array(await reply.arrayBuffer());
92
+ };
93
+ const text = (value: unknown) => { if (typeof value !== 'string') throw invalid(); return value; };
94
+
95
+ const actions: QuickActions = {
96
+ markdown: async (request, call = {}) => text(await result(await send('POST', '/markdown', page(request), call.signal))),
97
+ content: async (request, call = {}) => {
98
+ const reply = await send('POST', '/content', page(request), call.signal);
99
+ // Cloudflare does not document whether HTML comes in its envelope or as itself: either is taken.
100
+ return /json/u.test(reply.headers.get('content-type') ?? '') ? text(await result(reply)) : reply.text();
101
+ },
102
+ links: async (request, call = {}) => {
103
+ const value = await result(await send('POST', '/links', { ...page(request), ...(request.visibleLinksOnly ? { visibleLinksOnly: true } : {}), ...(request.excludeExternalLinks ? { excludeExternalLinks: true } : {}) }, call.signal));
104
+ if (!Array.isArray(value)) throw invalid();
105
+ return value.filter((link): link is string => typeof link === 'string');
106
+ },
107
+ scrape: async (request, call = {}) => {
108
+ if (!Array.isArray(request.selectors) || request.selectors.length === 0 || request.selectors.length > 20 || request.selectors.some(item => typeof item !== 'string' || item === '' || item.length > 500)) {
109
+ throw new MayuraError('INVALID_INPUT', 'selectors are 1 to 20 CSS selectors.');
110
+ }
111
+ const value = await result(await send('POST', '/scrape', { ...page(request), elements: request.selectors.map(selector => ({ selector })) }, call.signal));
112
+ if (!Array.isArray(value)) throw invalid();
113
+ return value.map(item => {
114
+ const entry = item as { selector?: unknown; results?: unknown };
115
+ return { selector: typeof entry?.selector === 'string' ? entry.selector : '', results: (Array.isArray(entry?.results) ? entry.results : []).map(found => {
116
+ const element = found as { text?: unknown; html?: unknown; attributes?: unknown };
117
+ return { text: typeof element?.text === 'string' ? element.text : '', html: typeof element?.html === 'string' ? element.html : '',
118
+ attributes: (Array.isArray(element?.attributes) ? element.attributes : []).filter((attribute): attribute is { name: string; value: string } =>
119
+ typeof (attribute as { name?: unknown })?.name === 'string' && typeof (attribute as { value?: unknown })?.value === 'string').map(({ name, value }) => ({ name, value })) };
120
+ }) };
121
+ });
122
+ },
123
+ screenshot: async (request, call = {}) => bytes(await send('POST', '/screenshot', { ...page(request), screenshotOptions: { type: 'png', fullPage: request.fullPage === true } }, call.signal)),
124
+ pdf: async (request, call = {}) => bytes(await send('POST', '/pdf', page(request), call.signal)),
125
+ json: async (request, call = {}) => {
126
+ if (request.prompt === undefined && request.schema === undefined) throw new MayuraError('INVALID_INPUT', 'json needs a prompt, a schema, or both.');
127
+ return result(await send('POST', '/json', { ...page(request), ...(request.prompt ? { prompt: request.prompt } : {}),
128
+ ...(request.schema ? { response_format: { type: 'json_schema', json_schema: request.schema } } : {}) }, call.signal));
129
+ },
130
+ crawl: Object.freeze({
131
+ start: async (request: CrawlStart, call: CallOptions = {}) => {
132
+ const id = await result(await send('POST', '/crawl', { ...page(request), limit: request.limit ?? 10, ...(request.depth ? { depth: request.depth } : {}),
133
+ formats: [...(request.formats ?? ['markdown'])],
134
+ options: { includeSubdomains: false, includeExternalLinks: false,
135
+ ...(request.includePatterns ? { includePatterns: [...request.includePatterns] } : {}), ...(request.excludePatterns ? { excludePatterns: [...request.excludePatterns] } : {}) } }, call.signal));
136
+ if (typeof id !== 'string' || !/^[A-Za-z0-9-]{1,128}$/u.test(id)) throw invalid();
137
+ return id;
138
+ },
139
+ status: async (id: string, call: CallOptions & { readonly cursor?: string | number; readonly limit?: number } = {}) => {
140
+ if (typeof id !== 'string' || !/^[A-Za-z0-9-]{1,128}$/u.test(id)) throw new MayuraError('INVALID_INPUT', 'id must be a crawl job id.');
141
+ const query = new URLSearchParams({ limit: String(call.limit ?? 100), ...(call.cursor !== undefined ? { cursor: String(call.cursor) } : {}) });
142
+ const value = await result(await send('GET', `/crawl/${id}?${query}`, undefined, call.signal)) as { id?: unknown; status?: unknown; total?: unknown; finished?: unknown; records?: unknown; cursor?: unknown } | undefined;
143
+ if (!value || !statuses.includes(value.status as CrawlStatus)) throw invalid();
144
+ const records = (Array.isArray(value.records) ? value.records : []).flatMap(item => {
145
+ const record = item as { url?: unknown; status?: unknown; markdown?: unknown; html?: unknown; metadata?: { title?: unknown; status?: unknown } };
146
+ if (typeof record?.url !== 'string') return [];
147
+ return [{ url: record.url, status: typeof record.status === 'string' ? record.status : '', ...(typeof record.markdown === 'string' ? { markdown: record.markdown } : {}),
148
+ ...(typeof record.html === 'string' ? { html: record.html } : {}), ...(typeof record.metadata?.title === 'string' ? { title: record.metadata.title } : {}),
149
+ ...(typeof record.metadata?.status === 'number' ? { httpStatus: record.metadata.status } : {}) }];
150
+ });
151
+ return { id, status: value.status as CrawlStatus, total: Number(value.total) || 0, finished: Number(value.finished) || 0, records,
152
+ ...(typeof value.cursor === 'string' || typeof value.cursor === 'number' ? { cursor: value.cursor } : {}) };
153
+ },
154
+ cancel: async (id: string, call: CallOptions = {}) => {
155
+ if (typeof id !== 'string' || !/^[A-Za-z0-9-]{1,128}$/u.test(id)) throw new MayuraError('INVALID_INPUT', 'id must be a crawl job id.');
156
+ const reply = await send('DELETE', `/crawl/${id}`, undefined, call.signal);
157
+ void reply.body?.cancel().catch(() => undefined);
158
+ },
159
+ }),
160
+ };
161
+ return Object.freeze(actions);
162
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { cloudflareQuickActions } from './client.js';
2
+ export type { CallOptions, CrawlRecord, CrawlResult, CrawlStart, CrawlStatus, PageRequest, QuickActions, QuickActionsOptions, ScrapedElement } from './client.js';
3
+ export { originPatterns, quickActionTools } from './tools.js';
4
+ export type { QuickActionToolsOptions } from './tools.js';