@ever-works/jina-plugin 1.0.0 → 1.0.3

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/index.js ADDED
@@ -0,0 +1,353 @@
1
+ import { isIP } from 'net';
2
+
3
+ // ../../plugin/dist/helpers/ssrf-guard.js
4
+ function isSafeWebhookUrl(rawUrl) {
5
+ let url;
6
+ try {
7
+ url = new URL(rawUrl);
8
+ } catch {
9
+ return false;
10
+ }
11
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
12
+ return false;
13
+ }
14
+ let host = url.hostname.toLowerCase();
15
+ if (host.startsWith("[") && host.endsWith("]")) {
16
+ host = host.slice(1, -1);
17
+ }
18
+ if (CLOUD_METADATA_HOSTNAMES.has(host)) {
19
+ return false;
20
+ }
21
+ const ipKind = isIP(host);
22
+ if (ipKind === 4 && isPrivateIPv4(host)) return false;
23
+ if (ipKind === 6 && isPrivateIPv6(host)) return false;
24
+ return true;
25
+ }
26
+ var CLOUD_METADATA_HOSTNAMES = /* @__PURE__ */ new Set([
27
+ // GCP
28
+ "metadata.google.internal",
29
+ "metadata.goog",
30
+ // AWS
31
+ "instance-data",
32
+ "instance-data.ec2.internal",
33
+ // Azure
34
+ "metadata.azure.internal"
35
+ ]);
36
+ function isPrivateIPv4(ip) {
37
+ const parts = ip.split(".").map((p) => Number(p));
38
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return false;
39
+ const [a, b] = parts;
40
+ if (a === 10) return true;
41
+ if (a === 127) return true;
42
+ if (a === 0) return true;
43
+ if (a === 169 && b === 254) return true;
44
+ if (a === 172 && b >= 16 && b <= 31) return true;
45
+ if (a === 192 && b === 168) return true;
46
+ if (a === 192 && b === 0 && parts[2] === 0) return true;
47
+ if (a === 100 && b >= 64 && b <= 127) return true;
48
+ if (a >= 224) return true;
49
+ return false;
50
+ }
51
+ function isPrivateIPv6(ip) {
52
+ const lower = ip.toLowerCase();
53
+ if (lower === "::1" || lower === "::") return true;
54
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
55
+ const firstHextet = lower.split(":", 1)[0];
56
+ if (/^[0-9a-f]{1,4}$/.test(firstHextet)) {
57
+ const group0 = parseInt(firstHextet, 16);
58
+ if ((group0 & 65472) === 65152) return true;
59
+ }
60
+ if (lower.startsWith("::ffff:")) {
61
+ const v4 = lower.slice("::ffff:".length);
62
+ if (isIP(v4) === 4) return isPrivateIPv4(v4);
63
+ }
64
+ return false;
65
+ }
66
+
67
+ // src/jina.plugin.ts
68
+ var JINA_READER_URL = "https://r.jina.ai/";
69
+ var JINA_SEARCH_URL = "https://s.jina.ai/";
70
+ var JinaReaderPlugin = class {
71
+ id = "jina";
72
+ name = "Jina AI";
73
+ version = "1.0.0";
74
+ category = "content-extractor";
75
+ capabilities = ["search", "content-extractor"];
76
+ providerName = "Jina";
77
+ settingsSchema = {
78
+ type: "object",
79
+ required: ["apiKey"],
80
+ properties: {
81
+ apiKey: {
82
+ type: "string",
83
+ title: "API Key",
84
+ description: "Your Jina API key. Get one at https://jina.ai",
85
+ "x-secret": true,
86
+ "x-envVar": "PLUGIN_JINA_API_KEY",
87
+ "x-scope": "user"
88
+ }
89
+ }
90
+ };
91
+ configurationMode = "hybrid";
92
+ context;
93
+ async search(options) {
94
+ const startTime = Date.now();
95
+ const apiKey = options.settings?.apiKey;
96
+ try {
97
+ const headers = {
98
+ Accept: "application/json",
99
+ "Content-Type": "application/json",
100
+ "X-Respond-With": "no-content",
101
+ Authorization: `Bearer ${apiKey}`
102
+ };
103
+ if (options.includeDomains && options.includeDomains.length > 0) {
104
+ headers["X-Site"] = options.includeDomains[0];
105
+ }
106
+ const body = {
107
+ q: options.query
108
+ };
109
+ if (options.limit) {
110
+ body.num = options.limit;
111
+ }
112
+ if (options.region) {
113
+ body.gl = options.region;
114
+ }
115
+ if (options.language) {
116
+ body.hl = options.language;
117
+ }
118
+ const response = await fetch(JINA_SEARCH_URL, {
119
+ method: "POST",
120
+ headers,
121
+ body: JSON.stringify(body),
122
+ signal: AbortSignal.timeout(3e4)
123
+ });
124
+ if (!response.ok) {
125
+ throw new Error(`Jina Search API returned ${response.status}: ${response.statusText}`);
126
+ }
127
+ const json = await response.json();
128
+ const results = (json.data || []).map((r, index) => {
129
+ let source;
130
+ try {
131
+ if (r.url) source = new URL(r.url).hostname;
132
+ } catch {
133
+ }
134
+ return {
135
+ title: r.title || "",
136
+ url: r.url || "",
137
+ snippet: r.description || "",
138
+ position: index + 1,
139
+ publishedDate: r.date || void 0,
140
+ source
141
+ };
142
+ });
143
+ return {
144
+ results,
145
+ query: options.query,
146
+ totalResults: results.length,
147
+ hasMore: false,
148
+ duration: Date.now() - startTime
149
+ };
150
+ } catch (error) {
151
+ this.context?.logger.error(`Jina search failed: ${error instanceof Error ? error.message : String(error)}`);
152
+ throw error;
153
+ }
154
+ }
155
+ async isAvailable() {
156
+ if (!this.context) return false;
157
+ const settings = await this.context.getSettings();
158
+ return Boolean(settings?.apiKey);
159
+ }
160
+ async validateConnection(settings) {
161
+ const apiKey = settings.apiKey;
162
+ if (!apiKey) {
163
+ return { success: false, message: "Jina AI API key is not configured." };
164
+ }
165
+ try {
166
+ const response = await fetch(JINA_SEARCH_URL, {
167
+ method: "POST",
168
+ headers: {
169
+ Accept: "application/json",
170
+ "Content-Type": "application/json",
171
+ "X-Respond-With": "no-content",
172
+ Authorization: `Bearer ${apiKey}`
173
+ },
174
+ body: JSON.stringify({ q: "test", num: 1 }),
175
+ signal: AbortSignal.timeout(15e3)
176
+ });
177
+ if (!response.ok) {
178
+ return {
179
+ success: false,
180
+ message: `Jina AI connection failed (${response.status}): ${response.statusText}`
181
+ };
182
+ }
183
+ return { success: true, message: "Jina AI connection verified." };
184
+ } catch (error) {
185
+ return {
186
+ success: false,
187
+ message: `Jina AI connection failed: ${error instanceof Error ? error.message : String(error)}`
188
+ };
189
+ }
190
+ }
191
+ async getRateLimitInfo() {
192
+ return {
193
+ remaining: -1,
194
+ limit: -1,
195
+ period: "minute"
196
+ };
197
+ }
198
+ async extract(options) {
199
+ const startTime = Date.now();
200
+ const { url, settings } = options;
201
+ const apiKey = settings?.apiKey;
202
+ if (!isSafeWebhookUrl(url)) {
203
+ return {
204
+ success: false,
205
+ url,
206
+ error: `URL host is not safe to fetch (SSRF guard blocked: ${url})`,
207
+ duration: Date.now() - startTime
208
+ };
209
+ }
210
+ try {
211
+ const headers = {
212
+ Accept: "application/json",
213
+ "X-Respond-With": "markdown",
214
+ Authorization: `Bearer ${apiKey}`
215
+ };
216
+ const response = await fetch(JINA_READER_URL, {
217
+ method: "POST",
218
+ headers: {
219
+ ...headers,
220
+ "Content-Type": "application/json"
221
+ },
222
+ body: JSON.stringify({ url }),
223
+ signal: AbortSignal.timeout(options.timeout || 3e4)
224
+ });
225
+ if (!response.ok) {
226
+ return {
227
+ success: false,
228
+ url,
229
+ error: `Jina API returned ${response.status}: ${response.statusText}`,
230
+ duration: Date.now() - startTime
231
+ };
232
+ }
233
+ const json = await response.json();
234
+ const data = json.data;
235
+ if (!data || !data.content) {
236
+ return {
237
+ success: false,
238
+ url,
239
+ error: "No content returned from Jina API",
240
+ duration: Date.now() - startTime
241
+ };
242
+ }
243
+ const wordCount = data.content.split(/\s+/).filter((w) => w.length > 0).length;
244
+ const images = data.images ? Object.entries(data.images).map(([alt, src]) => ({ src, alt })) : void 0;
245
+ const links = data.links ? Object.entries(data.links).map(([text, href]) => ({
246
+ href,
247
+ text,
248
+ isExternal: true
249
+ })) : void 0;
250
+ const metadata = {};
251
+ if (data.description) metadata.description = data.description;
252
+ if (data.publishedTime) metadata.publishedDate = data.publishedTime;
253
+ if (data.metadata?.lang) metadata.language = data.metadata.lang;
254
+ return {
255
+ success: true,
256
+ url,
257
+ finalUrl: data.url !== url ? data.url : void 0,
258
+ title: data.title,
259
+ content: data.content,
260
+ markdown: data.content,
261
+ images: options.includeImages !== false ? images : void 0,
262
+ links: options.includeLinks !== false ? links : void 0,
263
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
264
+ duration: Date.now() - startTime,
265
+ wordCount,
266
+ readingTime: Math.ceil(wordCount / 200)
267
+ };
268
+ } catch (error) {
269
+ return {
270
+ success: false,
271
+ url,
272
+ error: error instanceof Error ? error.message : String(error),
273
+ duration: Date.now() - startTime
274
+ };
275
+ }
276
+ }
277
+ async extractBatch(urls, options) {
278
+ const batchSize = 5;
279
+ const results = [];
280
+ for (let i = 0; i < urls.length; i += batchSize) {
281
+ const batch = urls.slice(i, i + batchSize);
282
+ const batchResults = await Promise.all(batch.map((url) => this.extract({ url, ...options })));
283
+ results.push(...batchResults);
284
+ if (i + batchSize < urls.length) {
285
+ await new Promise((resolve) => setTimeout(resolve, 100));
286
+ }
287
+ }
288
+ return results;
289
+ }
290
+ async canExtract(url) {
291
+ return isSafeWebhookUrl(url);
292
+ }
293
+ getSupportedFormats() {
294
+ return ["text", "markdown"];
295
+ }
296
+ async onLoad(context) {
297
+ this.context = context;
298
+ context.logger.log("Jina AI Reader Plugin loaded");
299
+ }
300
+ async onUnload() {
301
+ this.context = void 0;
302
+ }
303
+ async healthCheck() {
304
+ return {
305
+ status: "healthy",
306
+ message: "Jina AI Reader plugin is ready",
307
+ checkedAt: Date.now()
308
+ };
309
+ }
310
+ getManifest() {
311
+ return {
312
+ id: this.id,
313
+ name: this.name,
314
+ version: this.version,
315
+ description: "Web search and content extraction using the Jina AI APIs",
316
+ category: this.category,
317
+ capabilities: [...this.capabilities],
318
+ author: { name: "Ever Works Team" },
319
+ license: "AGPL-3.0",
320
+ builtIn: true,
321
+ systemPlugin: false,
322
+ autoEnable: false,
323
+ homepage: "https://jina.ai",
324
+ icon: {
325
+ type: "url",
326
+ value: "https://jina.ai/icons/favicon-128x128.png"
327
+ },
328
+ readme: [
329
+ "## What does Jina AI do?",
330
+ "",
331
+ "Jina AI provides web search with LLM-optimized results and content extraction that converts any web page into clean markdown.",
332
+ "",
333
+ "## Why use it?",
334
+ "",
335
+ "- **Web search** \u2014 search the web and get results with content already extracted",
336
+ "- **Content extraction** \u2014 converts pages to clean markdown, strips ads and navigation",
337
+ "- **Domain filtering** \u2014 restrict search to specific domains",
338
+ "",
339
+ "## How it works in Ever Works",
340
+ "",
341
+ "During work generation, Jina finds relevant information about each item via search and extracts clean content from web pages for enriching descriptions.",
342
+ "",
343
+ "## Getting started",
344
+ "",
345
+ "1. Get an API key at [jina.ai](https://jina.ai)",
346
+ "2. Enter the key in the **API Key** field below",
347
+ "3. Enable the plugin"
348
+ ].join("\n")
349
+ };
350
+ }
351
+ };
352
+
353
+ export { JinaReaderPlugin, JinaReaderPlugin as default };
package/package.json CHANGED
@@ -1,75 +1,88 @@
1
1
  {
2
- "name": "@ever-works/jina-plugin",
3
- "version": "1.0.0",
4
- "description": "Jina AI Plugin - Web search and content extraction using Jina AI APIs",
5
- "author": {
6
- "name": "Ever Co. LTD",
7
- "email": "ever@ever.co",
8
- "url": "https://ever.co"
9
- },
10
- "license": "AGPL-3.0",
11
- "private": false,
12
- "type": "module",
13
- "main": "./dist/index.cjs",
14
- "module": "./dist/index.js",
15
- "types": "./dist/index.d.ts",
16
- "exports": {
17
- ".": {
18
- "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js",
20
- "require": "./dist/index.cjs"
21
- }
22
- },
23
- "peerDependencies": {
24
- "@ever-works/plugin": "1.1.0"
25
- },
26
- "devDependencies": {
27
- "@types/node": "^22.0.0",
28
- "tsup": "^8.4.0",
29
- "typescript": "^5.7.3",
30
- "vitest": "^4.1.0",
31
- "@ever-works/plugin": "1.1.0"
32
- },
33
- "everworks": {
34
- "plugin": {
35
- "id": "jina",
36
- "name": "Jina AI",
37
- "version": "1.0.0",
38
- "category": "content-extractor",
39
- "capabilities": [
40
- "search",
41
- "content-extractor"
42
- ],
43
- "description": "Web search and content extraction using Jina AI APIs. Search via s.jina.ai and extract content via r.jina.ai, no SDK needed.",
44
- "author": {
45
- "name": "Ever Works Team"
46
- },
47
- "license": "AGPL-3.0",
48
- "autoEnable": false,
49
- "systemPlugin": false,
50
- "builtIn": true,
51
- "envVars": [
52
- {
53
- "name": "PLUGIN_JINA_API_KEY",
54
- "required": true,
55
- "secret": true,
56
- "description": "Jina API key"
57
- }
58
- ],
59
- "distribution": "registry"
60
- }
61
- },
62
- "publishConfig": {
63
- "access": "restricted",
64
- "registry": "https://registry.npmjs.org"
65
- },
66
- "scripts": {
67
- "build": "tsc --noEmit && tsup",
68
- "dev": "tsup --watch",
69
- "type-check": "tsc --noEmit",
70
- "clean": "rm -rf dist",
71
- "test": "vitest run --passWithNoTests",
72
- "test:watch": "vitest",
73
- "test:coverage": "vitest run --coverage"
74
- }
75
- }
2
+ "name": "@ever-works/jina-plugin",
3
+ "version": "1.0.3",
4
+ "description": "Jina AI Plugin - Web search and content extraction using Jina AI APIs",
5
+ "author": {
6
+ "name": "Ever Co. LTD",
7
+ "email": "ever@ever.co",
8
+ "url": "https://ever.co"
9
+ },
10
+ "license": "AGPL-3.0",
11
+ "private": false,
12
+ "type": "module",
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc --noEmit && tsup",
28
+ "dev": "tsup --watch",
29
+ "type-check": "tsc --noEmit",
30
+ "clean": "rm -rf dist",
31
+ "test": "vitest run --passWithNoTests",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage"
34
+ },
35
+ "peerDependencies": {
36
+ "@ever-works/plugin": "^1.2.5"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^26.6.4",
40
+ "@ever-works/plugin": "^1.2.5",
41
+ "tsup": "^8.4.0",
42
+ "typescript": "^5.7.3",
43
+ "vitest": "^5.0.5"
44
+ },
45
+ "everworks": {
46
+ "plugin": {
47
+ "id": "jina",
48
+ "name": "Jina AI",
49
+ "version": "1.0.0",
50
+ "category": "content-extractor",
51
+ "capabilities": [
52
+ "search",
53
+ "content-extractor"
54
+ ],
55
+ "description": "Web search and content extraction using Jina AI APIs. Search via s.jina.ai and extract content via r.jina.ai, no SDK needed.",
56
+ "author": {
57
+ "name": "Ever Works Team"
58
+ },
59
+ "license": "AGPL-3.0",
60
+ "autoEnable": false,
61
+ "systemPlugin": false,
62
+ "builtIn": true,
63
+ "envVars": [
64
+ {
65
+ "name": "PLUGIN_JINA_API_KEY",
66
+ "required": true,
67
+ "secret": true,
68
+ "description": "Jina API key"
69
+ }
70
+ ],
71
+ "distribution": "registry"
72
+ }
73
+ },
74
+ "repository": {
75
+ "type": "git",
76
+ "url": "git+https://github.com/ever-works/ever-works.git",
77
+ "directory": "packages/plugins/jina"
78
+ },
79
+ "homepage": "https://github.com/ever-works/ever-works/tree/main/packages/plugins/jina#readme",
80
+ "bugs": {
81
+ "url": "https://github.com/ever-works/ever-works/issues"
82
+ },
83
+ "everworksRelease": {
84
+ "contentHash": "sha256:38644b829f1b71573e5aca4b3bd20edb1809e2886bad28b2bf80f6315199e61d",
85
+ "commit": "19905c70cf05be3bc523db030597046067cd322a",
86
+ "source": "https://github.com/ever-works/ever-works/tree/19905c70cf05be3bc523db030597046067cd322a/packages/plugins/jina"
87
+ }
88
+ }