exaonruby 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +614 -0
  4. data/exaonruby.gemspec +37 -0
  5. data/exe/exa +7 -0
  6. data/lib/exa/cli.rb +458 -0
  7. data/lib/exa/client.rb +210 -0
  8. data/lib/exa/configuration.rb +81 -0
  9. data/lib/exa/endpoints/answer.rb +109 -0
  10. data/lib/exa/endpoints/contents.rb +141 -0
  11. data/lib/exa/endpoints/events.rb +71 -0
  12. data/lib/exa/endpoints/find_similar.rb +154 -0
  13. data/lib/exa/endpoints/imports.rb +145 -0
  14. data/lib/exa/endpoints/monitors.rb +193 -0
  15. data/lib/exa/endpoints/research.rb +158 -0
  16. data/lib/exa/endpoints/search.rb +195 -0
  17. data/lib/exa/endpoints/webhooks.rb +161 -0
  18. data/lib/exa/endpoints/webset_enrichments.rb +162 -0
  19. data/lib/exa/endpoints/webset_items.rb +90 -0
  20. data/lib/exa/endpoints/webset_searches.rb +137 -0
  21. data/lib/exa/endpoints/websets.rb +214 -0
  22. data/lib/exa/errors.rb +180 -0
  23. data/lib/exa/resources/answer_response.rb +101 -0
  24. data/lib/exa/resources/base.rb +56 -0
  25. data/lib/exa/resources/contents_response.rb +123 -0
  26. data/lib/exa/resources/event.rb +84 -0
  27. data/lib/exa/resources/import.rb +137 -0
  28. data/lib/exa/resources/monitor.rb +205 -0
  29. data/lib/exa/resources/paginated_response.rb +87 -0
  30. data/lib/exa/resources/research_task.rb +165 -0
  31. data/lib/exa/resources/search_response.rb +111 -0
  32. data/lib/exa/resources/search_result.rb +95 -0
  33. data/lib/exa/resources/webhook.rb +152 -0
  34. data/lib/exa/resources/webset.rb +491 -0
  35. data/lib/exa/resources/webset_item.rb +256 -0
  36. data/lib/exa/utils/parameter_converter.rb +159 -0
  37. data/lib/exa/utils/webhook_handler.rb +239 -0
  38. data/lib/exa/version.rb +7 -0
  39. data/lib/exa.rb +130 -0
  40. metadata +146 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2da95920bde51eb7724ef7c1cfa01d25906d4cb04cde2d2dc9d1055ec1267f9e
4
+ data.tar.gz: 190c39f5437c045f60067856033ad54330ce05425dd0edea79cb0fa74fed989f
5
+ SHA512:
6
+ metadata.gz: dea6072b3c1b149c869a4d76854cd36a3aa1a18241cbdea6a105f1331e2fb297723def00c59a30e13c4c044d93095b03b1bd7594f9dc9815e8c20b3bc59c3546
7
+ data.tar.gz: 12db1ecb3e8b3528bcaf74c03ea9a259136d794a9c68125562d69fe1d031808582f140cb2e50eb06b2faf3b9ec476c2eefc56809fe013ad3439d63fba35a7776
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Exa Labs
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,614 @@
1
+ # Exa Ruby
2
+
3
+ A production-ready Ruby gem wrapper for the [Exa.ai](https://exa.ai) API, providing intelligent web search, content extraction, and structured data collection capabilities.
4
+
5
+ ## Features
6
+
7
+ - **Search API**: Neural search, deep search, and content extraction
8
+ - **Contents API**: Fetch full page contents with livecrawl support
9
+ - **Find Similar**: Discover semantically similar pages
10
+ - **Answer API**: LLM-powered question answering with citations
11
+ - **Research API**: Async research tasks with structured output
12
+ - **Websets API**: Build and manage structured web data collections
13
+ - **Monitors**: Automated scheduled searches and content refresh
14
+ - **Imports**: Upload CSV data into Websets
15
+ - **Webhooks & Events**: Real-time notifications for Websets activity
16
+ - **Beautiful CLI**: Colorful command-line interface
17
+ - **n8n/Zapier Integration**: Webhook signature verification utilities
18
+ - **Automatic Retries**: Built-in retry logic for transient failures
19
+ - **Rate Limit Handling**: Proper error handling with retry information
20
+ - **Type Documentation**: Comprehensive YARD documentation
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ ```ruby
27
+ gem 'exa'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ Or install it yourself as:
37
+
38
+ ```bash
39
+ gem install exa
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ### Configuration
45
+
46
+ Configure the client with your API key:
47
+
48
+ ```ruby
49
+ require 'exa'
50
+
51
+ # Option 1: Environment variable (recommended)
52
+ # Set EXA_API_KEY environment variable
53
+
54
+ # Option 2: Global configuration
55
+ Exa.configure do |config|
56
+ config.api_key = 'your-api-key'
57
+ config.timeout = 60 # Request timeout in seconds
58
+ config.max_retries = 3 # Retry attempts for transient failures
59
+ end
60
+
61
+ # Option 3: Direct client initialization
62
+ client = Exa::Client.new(api_key: 'your-api-key')
63
+ ```
64
+
65
+ ### Basic Search
66
+
67
+ ```ruby
68
+ # Simple search
69
+ results = Exa.search("Latest developments in LLMs")
70
+
71
+ # Access results
72
+ results.each do |result|
73
+ puts "#{result.title}: #{result.url}"
74
+ end
75
+
76
+ # Search with content extraction
77
+ results = Exa.search(
78
+ "AI research papers",
79
+ text: true,
80
+ num_results: 20
81
+ )
82
+
83
+ results.each do |result|
84
+ puts result.title
85
+ puts result.text[0..500] if result.text
86
+ end
87
+ ```
88
+
89
+ ### Deep Search
90
+
91
+ For comprehensive results with query expansion:
92
+
93
+ ```ruby
94
+ results = client.search(
95
+ "Machine learning startups",
96
+ type: :deep,
97
+ additional_queries: ["AI companies", "ML ventures"],
98
+ category: :company,
99
+ include_domains: ["linkedin.com", "crunchbase.com"],
100
+ start_published_date: "2024-01-01T00:00:00.000Z",
101
+ num_results: 50
102
+ )
103
+ ```
104
+
105
+ ### Get Contents
106
+
107
+ Fetch full page contents from URLs:
108
+
109
+ ```ruby
110
+ contents = client.get_contents(
111
+ ["https://arxiv.org/abs/2307.06435"],
112
+ text: true,
113
+ summary: true,
114
+ livecrawl: :preferred
115
+ )
116
+
117
+ contents.results.each do |page|
118
+ puts page.title
119
+ puts page.summary
120
+ end
121
+
122
+ # Check for failures
123
+ if !contents.all_success?
124
+ contents.failed_statuses.each do |status|
125
+ puts "Failed to fetch #{status.id}: #{status.error_tag}"
126
+ end
127
+ end
128
+ ```
129
+
130
+ ### Find Similar Links
131
+
132
+ Discover pages similar to a given URL:
133
+
134
+ ```ruby
135
+ similar = client.find_similar(
136
+ "https://arxiv.org/abs/2307.06435",
137
+ num_results: 20,
138
+ include_domains: ["arxiv.org", "paperswithcode.com"],
139
+ text: true
140
+ )
141
+
142
+ similar.each do |result|
143
+ puts "#{result.title}: #{result.url}"
144
+ end
145
+ ```
146
+
147
+ ## Answer API
148
+
149
+ Get LLM-powered answers to questions with citations from web sources:
150
+
151
+ ```ruby
152
+ # Simple question
153
+ response = client.answer("What is the latest valuation of SpaceX?")
154
+ puts response.answer # => "$350 billion."
155
+
156
+ # Access citations
157
+ response.citations.each do |citation|
158
+ puts "Source: #{citation.title}"
159
+ puts "URL: #{citation.url}"
160
+ end
161
+
162
+ # With search options
163
+ response = client.answer(
164
+ "What are the latest AI safety developments?",
165
+ text: true,
166
+ num_results: 10,
167
+ start_published_date: "2024-01-01T00:00:00.000Z"
168
+ )
169
+ ```
170
+
171
+ ## Research API
172
+
173
+ Create async research tasks for in-depth web research:
174
+
175
+ ```ruby
176
+ # Create a research task
177
+ task = client.create_research(
178
+ instructions: "Summarize the latest developments in AI safety research",
179
+ model: "exa-research" # or "exa-research-fast", "exa-research-pro"
180
+ )
181
+
182
+ puts "Task ID: #{task.research_id}"
183
+ puts "Status: #{task.status}" # => "pending" or "running"
184
+
185
+ # Poll for results
186
+ loop do
187
+ task = client.get_research(task.research_id)
188
+
189
+ case task.status
190
+ when "completed"
191
+ puts task.output
192
+ break
193
+ when "running"
194
+ puts "Progress: #{task.operations_completed}/#{task.operations_total}"
195
+ sleep 5
196
+ when "failed"
197
+ puts "Error: #{task.error_message}"
198
+ break
199
+ end
200
+ end
201
+
202
+ # With structured output schema
203
+ schema = {
204
+ type: "object",
205
+ properties: {
206
+ companies: { type: "array", items: { type: "string" } },
207
+ summary: { type: "string" }
208
+ }
209
+ }
210
+
211
+ task = client.create_research(
212
+ instructions: "Find the top 5 AI startups in 2024",
213
+ model: "exa-research-pro",
214
+ output_schema: schema
215
+ )
216
+
217
+ # List all research tasks
218
+ response = client.list_research(limit: 10)
219
+ response.data.each { |t| puts "#{t.research_id}: #{t.status}" }
220
+
221
+ # Cancel a running task
222
+ client.cancel_research(task.research_id)
223
+ ```
224
+
225
+ ## Websets API
226
+
227
+ Websets allow you to build structured collections of web data with automated search, verification, and enrichment.
228
+
229
+ ### Create a Webset
230
+
231
+ ```ruby
232
+ webset = client.create_webset(
233
+ search: {
234
+ query: "AI startups founded in 2024",
235
+ count: 100,
236
+ entity: { type: "company" },
237
+ criteria: [
238
+ { description: "Company must be focused on artificial intelligence" },
239
+ { description: "Founded in 2024" }
240
+ ]
241
+ },
242
+ enrichments: [
243
+ { description: "Company's total funding amount", format: "number" },
244
+ { description: "Number of employees", format: "number" },
245
+ {
246
+ description: "Primary industry vertical",
247
+ format: "enum",
248
+ options: [
249
+ { label: "Healthcare" },
250
+ { label: "Finance" },
251
+ { label: "Enterprise" },
252
+ { label: "Consumer" },
253
+ { label: "Other" }
254
+ ]
255
+ }
256
+ ],
257
+ external_id: "my-ai-startups-2024"
258
+ )
259
+
260
+ puts "Created Webset: #{webset.id}"
261
+ puts "Status: #{webset.status}"
262
+ ```
263
+
264
+ ### Monitor Webset Progress
265
+
266
+ ```ruby
267
+ webset = client.get_webset(webset.id)
268
+
269
+ webset.searches.each do |search|
270
+ puts "Search: #{search.query}"
271
+ puts "Found: #{search.found_count}"
272
+ puts "Completion: #{search.completion_percentage}%"
273
+ end
274
+ ```
275
+
276
+ ### List Webset Items
277
+
278
+ ```ruby
279
+ response = client.list_webset_items(webset.id, limit: 50)
280
+
281
+ response.data.each do |item|
282
+ puts "Item: #{item.url}"
283
+ puts "Type: #{item.type}"
284
+
285
+ # Check criteria evaluations
286
+ item.evaluations.each do |eval|
287
+ status = eval.satisfied? ? "✓" : "✗"
288
+ puts " #{status} #{eval.criterion}"
289
+ end
290
+
291
+ # Access enrichment results
292
+ item.enrichments.each do |enrichment|
293
+ puts " #{enrichment.format}: #{enrichment.result}"
294
+ end
295
+ end
296
+
297
+ # Paginate through all items
298
+ while response.has_more?
299
+ response = client.list_webset_items(webset.id, cursor: response.next_cursor)
300
+ # Process items...
301
+ end
302
+ ```
303
+
304
+ ### Add Searches to Existing Webset
305
+
306
+ ```ruby
307
+ search = client.create_webset_search(
308
+ webset.id,
309
+ query: "AI healthcare startups",
310
+ count: 50,
311
+ entity: { type: "company" },
312
+ criteria: [{ description: "Must be in healthcare industry" }]
313
+ )
314
+
315
+ puts "Search ID: #{search.id}"
316
+ ```
317
+
318
+ ### Add Enrichments
319
+
320
+ ```ruby
321
+ enrichment = client.create_webset_enrichment(
322
+ webset.id,
323
+ description: "CEO or founder name",
324
+ format: "text"
325
+ )
326
+ ```
327
+
328
+ ### Delete Resources
329
+
330
+ ```ruby
331
+ # Delete an item
332
+ client.delete_webset_item(webset.id, item.id)
333
+
334
+ # Delete an enrichment
335
+ client.delete_webset_enrichment(webset.id, enrichment.id)
336
+
337
+ # Cancel an in-progress search
338
+ client.cancel_webset_search(webset.id, search.id)
339
+
340
+ # Delete entire webset
341
+ client.delete_webset(webset.id)
342
+ ```
343
+
344
+ ## Search Options Reference
345
+
346
+ | Option | Type | Description |
347
+ |--------|------|-------------|
348
+ | `type` | Symbol | `:neural`, `:auto`, `:fast`, `:deep` |
349
+ | `category` | Symbol | `:people`, `:company`, `:research_paper`, `:news`, `:pdf`, `:github`, `:tweet`, `:personal_site`, `:financial_report` |
350
+ | `num_results` | Integer | Number of results (max 100) |
351
+ | `include_domains` | Array | Domains to include |
352
+ | `exclude_domains` | Array | Domains to exclude |
353
+ | `start_crawl_date` | String/Time | Results crawled after this date |
354
+ | `end_crawl_date` | String/Time | Results crawled before this date |
355
+ | `start_published_date` | String/Time | Results published after this date |
356
+ | `end_published_date` | String/Time | Results published before this date |
357
+ | `include_text` | Array | Keywords that must be present |
358
+ | `exclude_text` | Array | Keywords to exclude |
359
+ | `country` | String | Two-letter ISO country code |
360
+ | `text` | Boolean/Hash | Return text content |
361
+ | `highlights` | Boolean/Hash | Return highlights |
362
+ | `summary` | Boolean/Hash | Return AI summary |
363
+ | `context` | Boolean/Integer | Return context string for LLM |
364
+ | `moderation` | Boolean | Filter unsafe content |
365
+ | `livecrawl` | Symbol | `:never`, `:fallback`, `:preferred`, `:always` |
366
+
367
+ ## Error Handling
368
+
369
+ The gem provides specific error classes for different failure modes:
370
+
371
+ ```ruby
372
+ begin
373
+ results = client.search("query")
374
+ rescue Exa::AuthenticationError => e
375
+ puts "Invalid API key: #{e.message}"
376
+ rescue Exa::RateLimitError => e
377
+ puts "Rate limited. Retry after: #{e.retry_after} seconds"
378
+ rescue Exa::InvalidRequestError => e
379
+ puts "Invalid request: #{e.message}"
380
+ puts "Validation errors: #{e.validation_errors}" if e.validation_errors
381
+ rescue Exa::NotFoundError => e
382
+ puts "Resource not found: #{e.message}"
383
+ rescue Exa::ServerError => e
384
+ puts "Server error: #{e.message}"
385
+ rescue Exa::TimeoutError => e
386
+ puts "Request timed out: #{e.message}"
387
+ rescue Exa::Error => e
388
+ puts "General error: #{e.message}"
389
+ end
390
+ ```
391
+
392
+ ## Advanced Configuration
393
+
394
+ ```ruby
395
+ Exa.configure do |config|
396
+ config.api_key = ENV['EXA_API_KEY']
397
+ config.base_url = 'https://api.exa.ai'
398
+ config.websets_base_url = 'https://api.exa.ai/websets/v0'
399
+ config.timeout = 120
400
+ config.max_retries = 5
401
+ config.retry_delay = 1.0
402
+ config.max_retry_delay = 60.0
403
+ config.retry_statuses = [429, 500, 502, 503, 504]
404
+ end
405
+
406
+ # Enable logging
407
+ Exa.logger = Logger.new(STDOUT)
408
+ ```
409
+
410
+ ## Response Objects
411
+
412
+ ### SearchResponse
413
+
414
+ ```ruby
415
+ response = client.search("query")
416
+
417
+ response.request_id # Unique request ID
418
+ response.results # Array of SearchResult
419
+ response.context # Combined context string
420
+ response.cost # CostInfo object
421
+ response.total_cost # Cost in dollars
422
+
423
+ # Enumerable
424
+ response.each { |r| puts r.title }
425
+ response.first
426
+ response.count
427
+ ```
428
+
429
+ ### SearchResult
430
+
431
+ ```ruby
432
+ result.id # Unique identifier
433
+ result.title # Page title
434
+ result.url # Page URL
435
+ result.published_date # Publication date (Time)
436
+ result.author # Author name
437
+ result.text # Full text content
438
+ result.highlights # Highlighted snippets
439
+ result.summary # AI-generated summary
440
+ result.subpages # Related subpages
441
+ ```
442
+
443
+ ### Webset
444
+
445
+ ```ruby
446
+ webset.id # Unique identifier
447
+ webset.status # idle, pending, running, paused
448
+ webset.title # Webset title
449
+ webset.searches # Array of WebsetSearch
450
+ webset.items # Array of WebsetItem
451
+ webset.enrichments # Array of WebsetEnrichment
452
+ webset.created_at # Creation timestamp
453
+ ```
454
+
455
+ ## Monitors
456
+
457
+ Create automated schedules to keep Websets updated:
458
+
459
+ ```ruby
460
+ # Create a monitor to search daily
461
+ monitor = client.create_monitor(
462
+ webset_id: "webset_abc123",
463
+ cadence: { cron: "0 9 * * *", timezone: "America/New_York" },
464
+ behavior: {
465
+ type: "search",
466
+ config: {
467
+ count: 50,
468
+ query: "AI news today",
469
+ entity: { type: "article" },
470
+ behavior: "append"
471
+ }
472
+ }
473
+ )
474
+
475
+ # List monitors
476
+ client.list_monitors(webset_id: "webset_abc123")
477
+
478
+ # Update monitor
479
+ client.update_monitor(monitor.id, status: "disabled")
480
+
481
+ # Delete monitor
482
+ client.delete_monitor(monitor.id)
483
+ ```
484
+
485
+ ## Imports
486
+
487
+ Upload CSV data into Websets:
488
+
489
+ ```ruby
490
+ # Create an import
491
+ import = client.create_import(
492
+ size: 1024,
493
+ count: 100,
494
+ format: "csv",
495
+ entity: { type: "company" },
496
+ title: "Q4 Leads",
497
+ csv: { identifier: 1 }
498
+ )
499
+
500
+ # Upload file to the returned URL
501
+ puts "Upload to: #{import.upload_url}"
502
+ puts "Valid until: #{import.upload_valid_until}"
503
+
504
+ # Check import status
505
+ import = client.get_import(import.id)
506
+ puts "Status: #{import.status}"
507
+ ```
508
+
509
+ ## Webhooks & Events
510
+
511
+ Subscribe to real-time notifications:
512
+
513
+ ```ruby
514
+ # Create a webhook
515
+ webhook = client.create_webhook(
516
+ url: "https://example.com/webhooks/exa",
517
+ events: ["webset.item.created", "webset.item.enriched"]
518
+ )
519
+ puts "Secret (save this!): #{webhook.secret}"
520
+
521
+ # List events
522
+ events = client.list_events(
523
+ types: ["webset.item.created"],
524
+ limit: 50
525
+ )
526
+
527
+ events.data.each { |e| puts "#{e.type} at #{e.created_at}" }
528
+ ```
529
+
530
+ ## n8n & Zapier Integration
531
+
532
+ Verify webhook signatures in your integrations:
533
+
534
+ ```ruby
535
+ # In your webhook receiver (Rails, Sinatra, etc.)
536
+ raw_body = request.raw_post
537
+ signature = request.headers["X-Exa-Signature"]
538
+
539
+ if Exa::Utils::WebhookHandler.verify_signature(
540
+ raw_body,
541
+ signature,
542
+ secret: ENV["EXA_WEBHOOK_SECRET"]
543
+ )
544
+ event = Exa::Utils::WebhookHandler.parse_event(raw_body)
545
+
546
+ case event.type
547
+ when "webset.item.created"
548
+ # Handle new item
549
+ when "webset.item.enriched"
550
+ # Handle enriched item
551
+ end
552
+
553
+ head :ok
554
+ else
555
+ head :unauthorized
556
+ end
557
+
558
+ # One-liner with automatic error handling
559
+ event = Exa::Utils::WebhookHandler.construct_event(
560
+ raw_body,
561
+ request.headers,
562
+ secret: ENV["EXA_WEBHOOK_SECRET"]
563
+ )
564
+ ```
565
+
566
+ ## Command-Line Interface
567
+
568
+ The gem includes a beautiful CLI:
569
+
570
+ ```bash
571
+ # Search
572
+ exa search "latest AI research"
573
+ exa search "ML startups" -n 20 --type deep
574
+
575
+ # Answer questions
576
+ exa answer "What is the valuation of SpaceX?"
577
+
578
+ # Find similar pages
579
+ exa similar "https://arxiv.org/abs/2307.06435"
580
+
581
+ # Research
582
+ exa research "Summarize AI safety developments in 2024" --wait
583
+
584
+ # Manage Websets
585
+ exa websets list
586
+ exa websets get ws_abc123
587
+ exa websets items ws_abc123
588
+
589
+ # Output as JSON
590
+ exa search "AI news" --json
591
+
592
+ # Show version
593
+ exa version
594
+ ```
595
+
596
+ ## Requirements
597
+
598
+ - Ruby >= 3.1
599
+ - faraday >= 2.0
600
+ - faraday-retry >= 2.0
601
+ - thor >= 1.0
602
+
603
+ ## Development
604
+
605
+ After checking out the repo, run `bundle install` to install dependencies.
606
+
607
+ ## License
608
+
609
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
610
+
611
+ ## Contributing
612
+
613
+ Bug reports and pull requests are welcome on GitHub at https://github.com/exa-labs/exa-ruby.
614
+
data/exaonruby.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/exa/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "exaonruby"
7
+ spec.version = Exa::VERSION
8
+ spec.authors = ["tigel-agm"]
9
+ spec.email = []
10
+
11
+ spec.summary = "Complete Ruby client for the Exa.ai API with beautiful CLI"
12
+ spec.description = "A production-ready Ruby gem wrapper for the Exa.ai Search and Websets APIs. " \
13
+ "Features neural search, LLM-powered answers, async research tasks, " \
14
+ "Websets management (monitors, imports, webhooks), and a beautiful CLI. " \
15
+ "Includes n8n/Zapier webhook signature verification utilities."
16
+ spec.homepage = "https://github.com/tigel-agm/exaonruby"
17
+ spec.license = "MIT"
18
+ spec.required_ruby_version = ">= 3.1"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "https://github.com/tigel-agm/exaonruby"
22
+ spec.metadata["changelog_uri"] = "https://github.com/tigel-agm/exaonruby/blob/main/CHANGELOG.md"
23
+ spec.metadata["documentation_uri"] = "https://github.com/tigel-agm/exaonruby#readme"
24
+ spec.metadata["rubygems_mfa_required"] = "true"
25
+
26
+ # Include all lib files explicitly since we may not have git
27
+ spec.files = Dir.glob("{lib,exe}/**/*") + %w[LICENSE.txt README.md]
28
+ spec.files += Dir.glob("*.gemspec")
29
+
30
+ spec.bindir = "exe"
31
+ spec.executables = ["exa"]
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_dependency "faraday", ">= 2.0", "< 3.0"
35
+ spec.add_dependency "faraday-retry", ">= 2.0", "< 3.0"
36
+ spec.add_dependency "thor", ">= 1.0", "< 3.0"
37
+ end
data/exe/exa ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/exa"
5
+ require_relative "../lib/exa/cli"
6
+
7
+ Exa::CLI.start(ARGV)