@ever-works/valyu-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.
@@ -1,321 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import type { PluginContext, SearchOptions, ContentExtractionOptions } from '@ever-works/plugin';
3
-
4
- const { searchMock, contentsMock, ValyuCtorMock } = vi.hoisted(() => {
5
- const search = vi.fn();
6
- const contents = vi.fn();
7
- const ctor = vi.fn().mockImplementation(function () {
8
- return { search, contents };
9
- });
10
- return { searchMock: search, contentsMock: contents, ValyuCtorMock: ctor };
11
- });
12
-
13
- vi.mock('valyu-js', () => ({
14
- Valyu: ValyuCtorMock
15
- }));
16
-
17
- const { ValyuSearchPlugin } = await import('../valyu.plugin.js');
18
-
19
- const buildContext = (settings: Record<string, unknown> = {}): PluginContext =>
20
- ({
21
- pluginId: 'valyu',
22
- logger: {
23
- log: vi.fn(),
24
- debug: vi.fn(),
25
- warn: vi.fn(),
26
- error: vi.fn()
27
- },
28
- getSettings: vi.fn().mockResolvedValue(settings)
29
- }) as unknown as PluginContext;
30
-
31
- describe('ValyuSearchPlugin', () => {
32
- let plugin: ValyuSearchPlugin;
33
-
34
- beforeEach(() => {
35
- vi.clearAllMocks();
36
- plugin = new ValyuSearchPlugin();
37
- });
38
-
39
- describe('metadata', () => {
40
- it('exposes stable identity fields', () => {
41
- expect(plugin.id).toBe('valyu');
42
- expect(plugin.name).toBe('Valyu');
43
- expect(plugin.version).toBe('1.0.0');
44
- expect(plugin.category).toBe('search');
45
- expect(plugin.providerName).toBe('Valyu');
46
- });
47
-
48
- it('declares both search and content-extractor capabilities', () => {
49
- expect(plugin.capabilities).toEqual(['search', 'content-extractor']);
50
- });
51
-
52
- it('uses hybrid configuration mode', () => {
53
- expect(plugin.configurationMode).toBe('hybrid');
54
- });
55
- });
56
-
57
- describe('settingsSchema', () => {
58
- it('requires apiKey and exposes a responseLength enum', () => {
59
- expect(plugin.settingsSchema.required).toContain('apiKey');
60
- const props = plugin.settingsSchema.properties as Record<string, Record<string, unknown>>;
61
- expect(props.apiKey['x-secret']).toBe(true);
62
- expect(props.apiKey['x-envVar']).toBe('PLUGIN_VALYU_API_KEY');
63
- expect(props.responseLength.default).toBe('medium');
64
- expect(props.responseLength.enum).toEqual(['short', 'medium', 'large', 'max']);
65
- });
66
- });
67
-
68
- describe('search', () => {
69
- const opts = (overrides: Partial<SearchOptions> = {}): SearchOptions => ({
70
- query: 'hello',
71
- settings: { apiKey: 'k' },
72
- ...overrides
73
- });
74
-
75
- it('throws when apiKey is missing', async () => {
76
- await expect(plugin.search({ query: 'hello', settings: {} })).rejects.toThrow(/API key not configured/i);
77
- expect(ValyuCtorMock).not.toHaveBeenCalled();
78
- });
79
-
80
- it('maps SDK results to SearchResult shape', async () => {
81
- searchMock.mockResolvedValueOnce({
82
- results: [
83
- {
84
- title: 'Example',
85
- url: 'https://example.com',
86
- content: 'snippet text',
87
- relevance_score: 0.7,
88
- source: 'web',
89
- description: 'desc',
90
- publication_date: '2026-01-01'
91
- }
92
- ]
93
- });
94
- const r = await plugin.search(opts());
95
-
96
- expect(ValyuCtorMock).toHaveBeenCalledWith('k');
97
- expect(r.results).toHaveLength(1);
98
- expect(r.results[0]).toMatchObject({
99
- title: 'Example',
100
- url: 'https://example.com',
101
- snippet: 'snippet text',
102
- position: 1,
103
- publishedDate: '2026-01-01'
104
- });
105
- expect(r.results[0].metadata).toMatchObject({
106
- relevanceScore: 0.7,
107
- source: 'web',
108
- description: 'desc'
109
- });
110
- expect(r.totalResults).toBe(1);
111
- expect(r.hasMore).toBe(false);
112
- });
113
-
114
- it('coerces non-string content to string', async () => {
115
- searchMock.mockResolvedValueOnce({
116
- results: [{ title: 't', url: 'https://e', content: { foo: 'bar' } }]
117
- });
118
- const r = await plugin.search(opts());
119
- expect(typeof r.results[0].snippet).toBe('string');
120
- });
121
-
122
- it('forwards limit, responseLength, and country code', async () => {
123
- searchMock.mockResolvedValueOnce({ results: [] });
124
- await plugin.search(
125
- opts({
126
- limit: 7,
127
- region: 'us',
128
- settings: { apiKey: 'k', responseLength: 'large' }
129
- })
130
- );
131
- expect(searchMock).toHaveBeenCalledWith(
132
- 'hello',
133
- expect.objectContaining({
134
- searchType: 'all',
135
- maxNumResults: 7,
136
- responseLength: 'large',
137
- countryCode: 'US'
138
- })
139
- );
140
- });
141
-
142
- it('uses includedSources when includeDomains is set', async () => {
143
- searchMock.mockResolvedValueOnce({ results: [] });
144
- await plugin.search(opts({ includeDomains: ['a.com'] }));
145
- expect(searchMock.mock.calls[0][1]).toMatchObject({ includedSources: ['a.com'] });
146
- });
147
-
148
- it('uses excludeSources when only excludeDomains is set', async () => {
149
- searchMock.mockResolvedValueOnce({ results: [] });
150
- await plugin.search(opts({ excludeDomains: ['b.com'] }));
151
- expect(searchMock.mock.calls[0][1]).toMatchObject({ excludeSources: ['b.com'] });
152
- });
153
-
154
- it('translates timeRange into start/end ISO date strings', async () => {
155
- searchMock.mockResolvedValueOnce({ results: [] });
156
- await plugin.search(opts({ timeRange: 'week' }));
157
- const call = searchMock.mock.calls[0][1];
158
- expect(call.startDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
159
- expect(call.endDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
160
- });
161
-
162
- it('skips date filters when timeRange is "all"', async () => {
163
- searchMock.mockResolvedValueOnce({ results: [] });
164
- await plugin.search(opts({ timeRange: 'all' }));
165
- const call = searchMock.mock.calls[0][1];
166
- expect(call.startDate).toBeUndefined();
167
- expect(call.endDate).toBeUndefined();
168
- });
169
-
170
- it('logs and rethrows on SDK failure', async () => {
171
- const ctx = buildContext();
172
- await plugin.onLoad(ctx);
173
- searchMock.mockRejectedValueOnce(new Error('boom'));
174
- await expect(plugin.search(opts())).rejects.toThrow(/boom/);
175
- expect(ctx.logger.error).toHaveBeenCalled();
176
- });
177
- });
178
-
179
- describe('isAvailable', () => {
180
- it('returns false without context', async () => {
181
- expect(await plugin.isAvailable()).toBe(false);
182
- });
183
-
184
- it('reflects whether settings has an apiKey', async () => {
185
- await plugin.onLoad(buildContext({ apiKey: 'k' }));
186
- expect(await plugin.isAvailable()).toBe(true);
187
-
188
- await plugin.onLoad(buildContext({}));
189
- expect(await plugin.isAvailable()).toBe(false);
190
- });
191
- });
192
-
193
- describe('validateConnection', () => {
194
- it('fails fast without apiKey', async () => {
195
- const r = await plugin.validateConnection({});
196
- expect(r.success).toBe(false);
197
- expect(r.message).toMatch(/not configured/i);
198
- });
199
-
200
- it('returns success when SDK search works', async () => {
201
- searchMock.mockResolvedValueOnce({ results: [] });
202
- const r = await plugin.validateConnection({ apiKey: 'k' });
203
- expect(r.success).toBe(true);
204
- });
205
-
206
- it('returns failure when SDK search rejects', async () => {
207
- searchMock.mockRejectedValueOnce(new Error('bad key'));
208
- const r = await plugin.validateConnection({ apiKey: 'k' });
209
- expect(r.success).toBe(false);
210
- expect(r.message).toMatch(/bad key/);
211
- });
212
- });
213
-
214
- describe('extract', () => {
215
- const opts = (overrides: Partial<ContentExtractionOptions> = {}): ContentExtractionOptions => ({
216
- url: 'https://example.com/post',
217
- settings: { apiKey: 'k' },
218
- ...overrides
219
- });
220
-
221
- it('returns success with markdown and word count', async () => {
222
- contentsMock.mockResolvedValueOnce({
223
- results: [
224
- {
225
- url: 'https://example.com/post',
226
- title: 'Title',
227
- content: 'one two three'
228
- }
229
- ]
230
- });
231
- const r = await plugin.extract(opts());
232
- expect(r.success).toBe(true);
233
- expect(r.title).toBe('Title');
234
- expect(r.markdown).toBe('one two three');
235
- expect(r.wordCount).toBe(3);
236
- });
237
-
238
- it('returns failure when no results are produced', async () => {
239
- contentsMock.mockResolvedValueOnce({ results: [] });
240
- const r = await plugin.extract(opts());
241
- expect(r.success).toBe(false);
242
- expect(r.error).toMatch(/No content extracted/);
243
- });
244
-
245
- it('returns failure when SDK throws', async () => {
246
- contentsMock.mockRejectedValueOnce(new Error('extract failed'));
247
- const r = await plugin.extract(opts());
248
- expect(r.success).toBe(false);
249
- expect(r.error).toMatch(/extract failed/);
250
- });
251
- });
252
-
253
- describe('extractBatch', () => {
254
- it('chunks URLs into batches of 10 and aggregates results', async () => {
255
- const urls = Array.from({ length: 12 }, (_, i) => `https://example.com/${i}`);
256
- contentsMock
257
- .mockResolvedValueOnce({
258
- results: urls.slice(0, 10).map((u) => ({ url: u, title: 't', content: 'a b' }))
259
- })
260
- .mockResolvedValueOnce({
261
- results: urls.slice(10).map((u) => ({ url: u, title: 't', content: 'c d e' }))
262
- });
263
-
264
- const r = await plugin.extractBatch(urls, { settings: { apiKey: 'k' } });
265
- expect(contentsMock).toHaveBeenCalledTimes(2);
266
- expect(r).toHaveLength(12);
267
- expect(r[0]).toMatchObject({ success: true, wordCount: 2 });
268
- expect(r[10]).toMatchObject({ success: true, wordCount: 3 });
269
- });
270
-
271
- it('returns per-URL failures when the batch call throws', async () => {
272
- contentsMock.mockRejectedValueOnce(new Error('batch failed'));
273
- const r = await plugin.extractBatch(['https://a', 'https://b'], { settings: { apiKey: 'k' } });
274
- expect(r).toHaveLength(2);
275
- expect(r.every((x) => x.success === false)).toBe(true);
276
- });
277
- });
278
-
279
- describe('canExtract / getSupportedFormats', () => {
280
- it('accepts http and https URLs', async () => {
281
- expect(await plugin.canExtract('https://example.com')).toBe(true);
282
- expect(await plugin.canExtract('http://example.com')).toBe(true);
283
- });
284
-
285
- it('rejects non-http schemes and invalid URLs', async () => {
286
- expect(await plugin.canExtract('ftp://example.com')).toBe(false);
287
- expect(await plugin.canExtract('garbage')).toBe(false);
288
- });
289
-
290
- it('exposes text + markdown formats', () => {
291
- expect(plugin.getSupportedFormats()).toEqual(['text', 'markdown']);
292
- });
293
- });
294
-
295
- describe('lifecycle', () => {
296
- it('logs on load and clears context on unload', async () => {
297
- const ctx = buildContext();
298
- await plugin.onLoad(ctx);
299
- expect(ctx.logger.log).toHaveBeenCalledWith('Valyu Plugin loaded');
300
- await plugin.onUnload();
301
- expect(await plugin.isAvailable()).toBe(false);
302
- });
303
- });
304
-
305
- describe('healthCheck + manifest', () => {
306
- it('reports healthy', async () => {
307
- const h = await plugin.healthCheck();
308
- expect(h.status).toBe('healthy');
309
- });
310
-
311
- it('returns a manifest aligned with plugin metadata', () => {
312
- const m = plugin.getManifest();
313
- expect(m.id).toBe('valyu');
314
- expect(m.category).toBe('search');
315
- expect(m.capabilities).toEqual(['search', 'content-extractor']);
316
- expect(m.builtIn).toBe(true);
317
- expect(m.systemPlugin).toBe(false);
318
- expect(m.autoEnable).toBe(false);
319
- });
320
- });
321
- });
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { ValyuSearchPlugin } from './valyu.plugin.js';
2
- export { ValyuSearchPlugin as default } from './valyu.plugin.js';
@@ -1,365 +0,0 @@
1
- import type {
2
- IPlugin,
3
- ISearchPlugin,
4
- IContentExtractorPlugin,
5
- PluginContext,
6
- PluginCategory,
7
- PluginManifest,
8
- PluginHealthCheck,
9
- JsonSchema,
10
- PluginSettings,
11
- SearchOptions,
12
- SearchResponse,
13
- SearchResult,
14
- RateLimitInfo,
15
- ContentExtractionOptions,
16
- ContentExtractionResult,
17
- ConnectionValidationResult
18
- } from '@ever-works/plugin';
19
-
20
- import { Valyu, type SearchOptions as ValyuSearchOptions } from 'valyu-js';
21
- import { isSafeWebhookUrl } from '@ever-works/plugin/helpers/ssrf-guard';
22
-
23
- export class ValyuSearchPlugin implements IPlugin, ISearchPlugin, IContentExtractorPlugin {
24
- readonly id = 'valyu';
25
- readonly name = 'Valyu';
26
- readonly version = '1.0.0';
27
- readonly category: PluginCategory = 'search';
28
- readonly capabilities: readonly string[] = ['search', 'content-extractor'];
29
- readonly providerName = 'Valyu';
30
-
31
- readonly settingsSchema: JsonSchema = {
32
- type: 'object',
33
- properties: {
34
- apiKey: {
35
- type: 'string',
36
- title: 'API Key',
37
- description: 'Your Valyu API key. Get one at https://valyu.ai',
38
- 'x-secret': true,
39
- 'x-envVar': 'PLUGIN_VALYU_API_KEY',
40
- 'x-scope': 'user'
41
- },
42
- responseLength: {
43
- type: 'string',
44
- title: 'Response Length',
45
- description:
46
- 'Content volume per result: short (~25k chars), medium (~50k), large (~100k), or max (unlimited)',
47
- enum: ['short', 'medium', 'large', 'max'],
48
- default: 'medium'
49
- }
50
- },
51
- required: ['apiKey']
52
- };
53
-
54
- readonly configurationMode: 'admin-only' | 'user-required' | 'hybrid' = 'hybrid';
55
-
56
- private context?: PluginContext;
57
-
58
- async search(options: SearchOptions): Promise<SearchResponse> {
59
- const client = this.getClient(options.settings);
60
- const startTime = Date.now();
61
-
62
- const maxNumResults = options.limit || 20;
63
- const responseLength = (options.settings?.responseLength as ValyuSearchOptions['responseLength']) || 'medium';
64
-
65
- try {
66
- const searchOptions: ValyuSearchOptions = {
67
- searchType: 'all',
68
- maxNumResults,
69
- responseLength
70
- };
71
-
72
- if (options.includeDomains && options.includeDomains.length > 0) {
73
- searchOptions.includedSources = options.includeDomains as string[];
74
- } else if (options.excludeDomains && options.excludeDomains.length > 0) {
75
- searchOptions.excludeSources = options.excludeDomains as string[];
76
- }
77
-
78
- if (options.region) {
79
- searchOptions.countryCode = options.region.toUpperCase() as ValyuSearchOptions['countryCode'];
80
- }
81
-
82
- if (options.timeRange && options.timeRange !== 'all') {
83
- const now = new Date();
84
- const start = new Date(now);
85
- if (options.timeRange === 'day') start.setDate(now.getDate() - 1);
86
- else if (options.timeRange === 'week') start.setDate(now.getDate() - 7);
87
- else if (options.timeRange === 'month') start.setMonth(now.getMonth() - 1);
88
- else if (options.timeRange === 'year') start.setFullYear(now.getFullYear() - 1);
89
- searchOptions.startDate = start.toISOString().split('T')[0];
90
- searchOptions.endDate = now.toISOString().split('T')[0];
91
- }
92
-
93
- const response = await client.search(options.query, searchOptions);
94
-
95
- const results: SearchResult[] = (response.results || []).map((r, index) => ({
96
- title: r.title,
97
- url: r.url,
98
- snippet: typeof r.content === 'string' ? r.content : String(r.content),
99
- position: index + 1,
100
- publishedDate: r.publication_date,
101
- metadata: {
102
- relevanceScore: r.relevance_score,
103
- source: r.source,
104
- description: r.description
105
- }
106
- }));
107
-
108
- return {
109
- results,
110
- query: options.query,
111
- totalResults: results.length,
112
- hasMore: false,
113
- duration: Date.now() - startTime
114
- };
115
- } catch (error) {
116
- this.context?.logger.error(
117
- `Valyu search failed: ${error instanceof Error ? error.message : String(error)}`
118
- );
119
- throw error;
120
- }
121
- }
122
-
123
- async isAvailable(): Promise<boolean> {
124
- if (!this.context) return false;
125
- const settings = await this.context.getSettings();
126
- return Boolean(settings?.apiKey);
127
- }
128
-
129
- async validateConnection(settings: Record<string, unknown>): Promise<ConnectionValidationResult> {
130
- const apiKey = settings.apiKey as string | undefined;
131
- if (!apiKey) {
132
- return { success: false, message: 'Valyu API key is not configured.' };
133
- }
134
-
135
- try {
136
- const client = this.getClient(settings);
137
- await client.search('test', { maxNumResults: 1, searchType: 'all' });
138
- return { success: true, message: 'Valyu connection verified.' };
139
- } catch (error) {
140
- return {
141
- success: false,
142
- message: `Valyu connection failed: ${error instanceof Error ? error.message : String(error)}`
143
- };
144
- }
145
- }
146
-
147
- async getRateLimitInfo(): Promise<RateLimitInfo> {
148
- return {
149
- remaining: -1,
150
- limit: -1,
151
- period: 'month'
152
- };
153
- }
154
-
155
- async extract(options: ContentExtractionOptions): Promise<ContentExtractionResult> {
156
- const client = this.getClient(options.settings);
157
- const startTime = Date.now();
158
-
159
- // Security (SSRF): refuse to hand private/loopback/link-local/cloud-metadata
160
- // URLs to the Valyu SDK, which would fetch them server-side and return the
161
- // rendered content. `canExtract()` is advisory and may be bypassed by a
162
- // direct call, so enforce the guard here. The Valyu SDK performs the fetch
163
- // internally, so redirect targets can't be re-checked — this is the lexical
164
- // first line of defense for the initial URL.
165
- if (!isSafeWebhookUrl(options.url)) {
166
- return {
167
- success: false,
168
- url: options.url,
169
- error: `URL host is not safe to fetch (SSRF guard blocked: ${options.url})`,
170
- duration: Date.now() - startTime
171
- };
172
- }
173
-
174
- try {
175
- const response = await client.contents([options.url]);
176
-
177
- if (!response.results || response.results.length === 0) {
178
- return {
179
- success: false,
180
- url: options.url,
181
- error: 'No content extracted',
182
- duration: Date.now() - startTime
183
- };
184
- }
185
-
186
- const result = response.results[0];
187
- const content = typeof result.content === 'string' ? result.content : String(result.content || '');
188
-
189
- return {
190
- success: true,
191
- url: options.url,
192
- finalUrl: result.url !== options.url ? result.url : undefined,
193
- title: result.title,
194
- content,
195
- markdown: content,
196
- duration: Date.now() - startTime,
197
- wordCount: content ? content.split(/\s+/).length : 0
198
- };
199
- } catch (error) {
200
- return {
201
- success: false,
202
- url: options.url,
203
- error: error instanceof Error ? error.message : String(error),
204
- duration: Date.now() - startTime
205
- };
206
- }
207
- }
208
-
209
- async extractBatch(
210
- urls: readonly string[],
211
- options?: Partial<ContentExtractionOptions>
212
- ): Promise<readonly ContentExtractionResult[]> {
213
- const client = this.getClient(options?.settings);
214
- const startTime = Date.now();
215
-
216
- // Security (SSRF): drop private/loopback/link-local/cloud-metadata URLs
217
- // before they reach the Valyu SDK (which would fetch them server-side),
218
- // emitting a failed result for each. This also stops a single batch call
219
- // from being used to probe many internal hosts/ports at once. Legitimate
220
- // public URLs are unaffected.
221
- const safeUrls: string[] = [];
222
- const blockedResults: ContentExtractionResult[] = [];
223
- for (const url of urls) {
224
- if (isSafeWebhookUrl(url)) {
225
- safeUrls.push(url);
226
- } else {
227
- blockedResults.push({
228
- success: false,
229
- url,
230
- error: `URL host is not safe to fetch (SSRF guard blocked: ${url})`,
231
- duration: Date.now() - startTime
232
- });
233
- }
234
- }
235
-
236
- try {
237
- const batchSize = 10;
238
- const allResults: ContentExtractionResult[] = [];
239
-
240
- for (let i = 0; i < safeUrls.length; i += batchSize) {
241
- const batch = safeUrls.slice(i, i + batchSize);
242
- const response = await client.contents(batch as string[]);
243
-
244
- const results = (response.results || []).map((result, index) => {
245
- const content = typeof result.content === 'string' ? result.content : String(result.content || '');
246
- const requestedUrl = batch[index] || result.url;
247
- return {
248
- success: true,
249
- url: requestedUrl,
250
- finalUrl: result.url !== requestedUrl ? result.url : undefined,
251
- title: result.title,
252
- content,
253
- markdown: content,
254
- duration: Date.now() - startTime,
255
- wordCount: content ? content.split(/\s+/).length : 0
256
- };
257
- });
258
-
259
- allResults.push(...results);
260
- }
261
-
262
- return [...blockedResults, ...allResults];
263
- } catch (error) {
264
- // Only the safe URLs were sent to the SDK, so surface the fetch error
265
- // for those; blocked URLs already carry their SSRF-guard reason.
266
- const failed = safeUrls.map((url) => ({
267
- success: false,
268
- url,
269
- error: error instanceof Error ? error.message : String(error),
270
- duration: Date.now() - startTime
271
- }));
272
- return [...blockedResults, ...failed];
273
- }
274
- }
275
-
276
- async canExtract(url: string): Promise<boolean> {
277
- // Security (SSRF): `isSafeWebhookUrl` enforces http(s) AND rejects
278
- // private/loopback/link-local/cloud-metadata hosts, so callers that gate
279
- // on canExtract() won't even surface an internal URL as extractable.
280
- // (extract()/extractBatch() re-check independently in case this is bypassed.)
281
- return isSafeWebhookUrl(url);
282
- }
283
-
284
- getSupportedFormats(): readonly ('text' | 'html' | 'markdown')[] {
285
- return ['text', 'markdown'];
286
- }
287
-
288
- private getClient(settings?: PluginSettings): Valyu {
289
- const apiKey = settings?.apiKey as string;
290
-
291
- if (!apiKey) {
292
- throw new Error(
293
- 'Valyu API key not configured. ' +
294
- 'Set it in plugin settings or via PLUGIN_VALYU_API_KEY environment variable.'
295
- );
296
- }
297
-
298
- return new Valyu(apiKey);
299
- }
300
-
301
- async onLoad(context: PluginContext): Promise<void> {
302
- this.context = context;
303
- context.logger.log('Valyu Plugin loaded');
304
- }
305
-
306
- async onUnload(): Promise<void> {
307
- this.context = undefined;
308
- }
309
-
310
- async healthCheck(): Promise<PluginHealthCheck> {
311
- return {
312
- status: 'healthy',
313
- message: 'Valyu plugin is ready (API key required for operations)',
314
- checkedAt: Date.now()
315
- };
316
- }
317
-
318
- getManifest(): PluginManifest {
319
- return {
320
- id: this.id,
321
- name: this.name,
322
- version: this.version,
323
- description: 'AI-native search and content extraction across web and proprietary data sources',
324
- category: this.category,
325
- capabilities: [...this.capabilities],
326
- author: { name: 'Ever Works Team' },
327
- license: 'AGPL-3.0',
328
- builtIn: true,
329
- systemPlugin: false,
330
- autoEnable: false,
331
- homepage: 'https://valyu.ai',
332
- icon: {
333
- type: 'svg',
334
- value: `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"><g fill="#1e1d1d" fill-opacity="1"><g transform="translate(-3, 88)"><path d="M 48.3125 1.734375 L 59.78125 1.734375 L 91.5625 -71.453125 L 91.5625 -77.5 L 16.328125 -77.5 L 16.328125 -71.453125 Z M 64.421875 -32.640625 L 54.15625 -7.140625 L 53.609375 -7.140625 L 43.234375 -32.640625 L 27.78125 -68.96875 L 79.984375 -68.96875 Z M 64.421875 -32.640625 "/></g></g></svg>`,
335
- darkValue: `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet"><g fill="#ebebeb" fill-opacity="1"><g transform="translate(-3, 88)"><path d="M 48.3125 1.734375 L 59.78125 1.734375 L 91.5625 -71.453125 L 91.5625 -77.5 L 16.328125 -77.5 L 16.328125 -71.453125 Z M 64.421875 -32.640625 L 54.15625 -7.140625 L 53.609375 -7.140625 L 43.234375 -32.640625 L 27.78125 -68.96875 L 79.984375 -68.96875 Z M 64.421875 -32.640625 "/></g></g></svg>`
336
- },
337
- readme: [
338
- '## What does Valyu do?',
339
- '',
340
- 'Valyu is an AI-native search and content extraction service that searches across web and proprietary data sources. It returns results optimized for AI applications and RAG pipelines, providing the source material that Ever Works uses to generate accurate work items.',
341
- '',
342
- '## Why use it?',
343
- '',
344
- '- **Multi-source search** — search across web, proprietary datasets (arXiv, PubMed, financial data), and news',
345
- '- **AI-optimized results** — returns content formatted for AI processing with relevance scoring',
346
- '- **Content extraction** — pulls clean text and markdown from web pages',
347
- '- **Domain filtering** — include or exclude specific websites from search results',
348
- '- **Date filtering** — restrict results to specific time periods',
349
- '',
350
- '## How it works in Ever Works',
351
- '',
352
- 'During work generation, the search facade uses Valyu to find information about each item, discover relevant source URLs, and extract content from web pages. This powers automatic descriptions, source URL resolution, and content enrichment across the generation pipeline.',
353
- '',
354
- '## Getting started',
355
- '',
356
- '1. Create an account at [valyu.ai](https://valyu.ai)',
357
- '2. Copy your API key from the Valyu dashboard',
358
- '3. Enter the key in the **API Key** field below',
359
- '4. Valyu will be used during work generation when selected as the search provider'
360
- ].join('\n')
361
- };
362
- }
363
- }
364
-
365
- export default ValyuSearchPlugin;