exa-ai 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4458cc74fb6924d5e7e57300ba28ae1db399d0d89bf68495b1203f5fe29d73d2
4
- data.tar.gz: 00b7f59be62aad198438660acdec2e1a2e3c86539bd1a462c4ea03f45c08a87b
3
+ metadata.gz: 1a5fb5324a2ae6dfb4380d91bce7d8d41a3f3b14e471e43bf254a5e763ef0113
4
+ data.tar.gz: 539a8486ec5e639c4f43fd3e81071ff0f1e5951b3e42c31bc1915d8e00d1df36
5
5
  SHA512:
6
- metadata.gz: b45e4d6a35d33910e9ce4b154b1b432efba13732b9c237ab94da5962944fae65bf3562a426dc9fa9c18af8c820e8db5d1a4ae3417605d171a136de2d01f149db
7
- data.tar.gz: 98f082e62eeef3cf7851abcda1cdd61ee5edd548c2cd0d5a65b7645510248ebd0a2274b743e83b7ff354e1ebb4016a92ab83e8a4f93300c255d298fdb9a2683b
6
+ metadata.gz: dd87558e4a7428b56d9e513478e9d02f88cc845bf7d14fa9c0822e4042dd73d100e686ce3c151bc755d680cc283017311ba2b5109eedfcffef03bd99d4457700
7
+ data.tar.gz: 49eac46680edc76e6bfa9a2032b9871841095e052cc6e21eac174675f4060149ddc3a690cdd5b6506f39dbaf621f124d7a288f227f99d1637979729e23d63042
data/README.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  Ruby client for the Exa.ai API. Search and analyze web content using neural search, question answering, code discovery, and research automation.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Requirements](#requirements)
8
+ - [Installation](#installation)
9
+ - [Configuration](#configuration)
10
+ - [Quick Start](#quick-start)
11
+ - [Features](#features)
12
+ - [Error Handling](#error-handling)
13
+ - [Documentation](#documentation)
14
+ - [Development](#development)
15
+ - [Testing](#testing)
16
+ - [Support](#support)
17
+ - [License](#license)
18
+
19
+ ## Requirements
20
+
21
+ - **Ruby 3.0.0 or higher**
22
+
23
+ ### Installing Ruby on macOS
24
+
25
+ If you're setting up on a fresh macOS laptop, the easiest way to get Ruby 3.x is through Homebrew:
26
+
27
+ **1. Install Homebrew** (if not already installed):
28
+
29
+ ```bash
30
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
31
+ ```
32
+
33
+ **2. Install Ruby:**
34
+
35
+ ```bash
36
+ brew install ruby
37
+ ```
38
+
39
+ **3. Add Homebrew's Ruby to your PATH** (follow the instructions Homebrew prints, usually adding to `~/.zshrc`):
40
+
41
+ ```bash
42
+ echo 'export PATH="/opt/homebrew/opt/ruby/bin:$PATH"' >> ~/.zshrc
43
+ source ~/.zshrc
44
+ ```
45
+
46
+ **4. Verify installation:**
47
+
48
+ ```bash
49
+ ruby -v # Should show Ruby 3.x
50
+ ```
51
+
52
+ **Alternative: Using a version manager**
53
+
54
+ For managing multiple Ruby versions, consider [rbenv](https://github.com/rbenv/rbenv) or [asdf](https://asdf-vm.com/).
55
+
5
56
  ## Installation
6
57
 
7
58
  Add to your Gemfile:
@@ -32,6 +83,20 @@ Get your API key from [dashboard.exa.ai](https://dashboard.exa.ai).
32
83
  export EXA_API_KEY="your-api-key-here"
33
84
  ```
34
85
 
86
+ **Using .env file (local development)**
87
+
88
+ Create a `.env` file in your project root:
89
+
90
+ ```bash
91
+ # Copy the example file
92
+ cp .env.example .env
93
+
94
+ # Edit .env and add your API key
95
+ EXA_API_KEY=your-api-key-here
96
+ ```
97
+
98
+ The gem automatically loads `.env` files in development when the `dotenv` gem is installed.
99
+
35
100
  **Ruby Code**
36
101
 
37
102
  ```ruby
@@ -193,6 +258,46 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for:
193
258
  - Code conventions
194
259
  - Building and releasing
195
260
 
261
+ ## Testing
262
+
263
+ ### Running Tests
264
+
265
+ ```bash
266
+ # Run unit tests (integration tests skip by default)
267
+ bundle exec rake test
268
+
269
+ # Run integration tests (VCR-based, no real API calls)
270
+ RUN_INTEGRATION_TESTS=true bundle exec rake test
271
+
272
+ # Run CLI integration tests (real API calls, requires explicit opt-in)
273
+ RUN_CLI_INTEGRATION_TESTS=true bundle exec rake test
274
+ ```
275
+
276
+ ### Integration Tests
277
+
278
+ **Integration tests are skipped by default** to prevent accidental API calls.
279
+
280
+ **VCR-based integration tests (`RUN_INTEGRATION_TESTS`):**
281
+ - Use recorded HTTP interactions (VCR cassettes)
282
+ - No real API calls when replaying cassettes
283
+ - Set `RUN_INTEGRATION_TESTS=true` to run them
284
+ - Safe to run during development
285
+
286
+ **CLI integration tests (`RUN_CLI_INTEGRATION_TESTS`):**
287
+ - Make real API calls through shell commands
288
+ - Consume Exa's concurrent search quota
289
+ - Set `RUN_CLI_INTEGRATION_TESTS=true` AND `EXA_API_KEY` to run them
290
+ - **Warning:** Can exhaust API quota and trigger rate limits lasting 1-2 days
291
+
292
+ **When to run integration tests:**
293
+ - VCR tests: Anytime (safe, no real API calls)
294
+ - CLI tests: Only before releases or when testing CLI-specific functionality
295
+
296
+ **Test Coverage:**
297
+ - **Unit tests** - Fast, no API calls, always run
298
+ - **VCR integration tests** - Replay cassettes, skipped by default
299
+ - **CLI integration tests** - Real API calls via shell, skipped by default
300
+
196
301
  ## Support
197
302
 
198
303
  - **Documentation**: https://docs.exa.ai
data/exe/exa-ai-answer CHANGED
@@ -9,7 +9,8 @@ def parse_args(argv)
9
9
  output_format: "json",
10
10
  api_key: nil,
11
11
  text: false,
12
- stream: false
12
+ stream: false,
13
+ skip_citations: false
13
14
  }
14
15
 
15
16
  # Extract query (first non-flag argument)
@@ -24,6 +25,9 @@ def parse_args(argv)
24
25
  when "--stream"
25
26
  args[:stream] = true
26
27
  i += 1
28
+ when "--skip-citations", "--no-citations"
29
+ args[:skip_citations] = true
30
+ i += 1
27
31
  when "--output-schema"
28
32
  args[:output_schema] = argv[i + 1]
29
33
  i += 2
@@ -48,6 +52,8 @@ def parse_args(argv)
48
52
  Options:
49
53
  --stream Stream answer chunks in real-time
50
54
  --text Include full text content from sources
55
+ --skip-citations Remove citations from output (saves tokens)
56
+ --no-citations Alias for --skip-citations
51
57
  --output-schema JSON JSON schema for structured output
52
58
  --system-prompt TEXT System prompt to guide answer generation
53
59
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
@@ -123,7 +129,7 @@ begin
123
129
  else
124
130
  # Non-streaming mode - collect full response and format
125
131
  result = client.answer(args[:query], **answer_params)
126
- output = Exa::CLI::Formatters::AnswerFormatter.format(result, output_format)
132
+ output = Exa::CLI::Formatters::AnswerFormatter.format(result, output_format, skip_citations: args[:skip_citations])
127
133
  puts output
128
134
  $stdout.flush
129
135
  end
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "exa-ai"
5
5
 
6
- VALID_FORMATS = %w[text url options].freeze
6
+ VALID_FORMATS = Exa::Constants::Websets::ENRICHMENT_FORMATS
7
7
 
8
8
  # Recursively convert hash keys from strings to symbols
9
9
  def deep_symbolize_keys(obj)
@@ -51,9 +51,7 @@ def parse_args(argv)
51
51
  --format TYPE One of: #{VALID_FORMATS.join(', ')}
52
52
 
53
53
  Options:
54
- --title TEXT Display title
55
54
  --options JSON Array of {label: "..."} (required if format=options, supports @file.json)
56
- --instructions TEXT Additional instructions
57
55
  --metadata JSON Custom metadata (supports @file.json)
58
56
  --wait Wait for enrichment to complete
59
57
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
@@ -91,15 +89,9 @@ def parse_args(argv)
91
89
  when "--format"
92
90
  args[:format] = argv[i + 1]
93
91
  i += 2
94
- when "--title"
95
- args[:title] = argv[i + 1]
96
- i += 2
97
92
  when "--options"
98
93
  args[:options] = parse_json_or_file(argv[i + 1])
99
94
  i += 2
100
- when "--instructions"
101
- args[:instructions] = argv[i + 1]
102
- i += 2
103
95
  when "--metadata"
104
96
  args[:metadata] = parse_json_or_file(argv[i + 1])
105
97
  i += 2
@@ -170,9 +162,7 @@ begin
170
162
  description: args[:description],
171
163
  format: args[:format]
172
164
  }
173
- enrichment_params[:title] = args[:title] if args[:title]
174
165
  enrichment_params[:options] = args[:options] if args[:options]
175
- enrichment_params[:instructions] = args[:instructions] if args[:instructions]
176
166
  enrichment_params[:metadata] = args[:metadata] if args[:metadata]
177
167
 
178
168
  # Create enrichment
@@ -56,6 +56,7 @@ def parse_args(argv)
56
56
  --entity-type TYPE Entity type (options: #{VALID_ENTITY_TYPES.join(', ')})
57
57
 
58
58
  Options:
59
+ --entity-description TXT Description for custom entity type (required with --entity-type custom)
59
60
  --csv-identifier N CSV column identifier (0-indexed)
60
61
  --metadata JSON Custom metadata (supports @file.json)
61
62
  --quiet Suppress normal output (only show errors)
@@ -102,6 +103,9 @@ def parse_args(argv)
102
103
  when "--entity-type"
103
104
  args[:entity_type] = argv[i + 1]
104
105
  i += 2
106
+ when "--entity-description"
107
+ args[:entity_description] = argv[i + 1]
108
+ i += 2
105
109
  when "--csv-identifier"
106
110
  args[:csv_identifier] = argv[i + 1].to_i
107
111
  i += 2
@@ -161,6 +165,17 @@ begin
161
165
  exit 1
162
166
  end
163
167
 
168
+ # Validate entity-description for custom entity type
169
+ if args[:entity_type] == "custom"
170
+ unless args[:entity_description]
171
+ $stderr.puts "Error: --entity-description is required when --entity-type is 'custom'"
172
+ $stderr.puts "Run 'exa-ai import-create --help' for usage information"
173
+ exit 1
174
+ end
175
+ elsif args[:entity_description]
176
+ $stderr.puts "Warning: --entity-description is only used with --entity-type custom (ignoring)"
177
+ end
178
+
164
179
  # Validate file exists
165
180
  unless File.exist?(args[:file_path])
166
181
  $stderr.puts "Error: File not found: #{args[:file_path]}"
@@ -177,12 +192,15 @@ begin
177
192
  client = Exa::CLI::Base.build_client(api_key)
178
193
 
179
194
  # Prepare import parameters
195
+ entity = { type: args[:entity_type] }
196
+ entity[:description] = args[:entity_description] if args[:entity_description]
197
+
180
198
  import_params = {
181
199
  file_path: args[:file_path],
182
200
  count: args[:count],
183
201
  title: args[:title],
184
202
  format: args[:format],
185
- entity: { type: args[:entity_type] }
203
+ entity: entity
186
204
  }
187
205
  import_params[:metadata] = args[:metadata] if args[:metadata]
188
206
 
data/exe/exa-ai-search CHANGED
@@ -2,189 +2,61 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "exa-ai"
5
-
6
- # Parse command-line arguments
7
- def parse_args(argv)
8
- args = {
9
- output_format: "json",
10
- api_key: nil
11
- }
12
-
13
- # Extract query (first non-flag argument)
14
- query_parts = []
15
- i = 0
16
- while i < argv.length
17
- arg = argv[i]
18
- case arg
19
- when "--num-results"
20
- args[:num_results] = argv[i + 1].to_i
21
- i += 2
22
- when "--type"
23
- search_type = argv[i + 1]
24
- valid_types = ["fast", "deep", "keyword", "auto"]
25
- unless valid_types.include?(search_type)
26
- $stderr.puts "Error: Search type must be one of: #{valid_types.join(', ')}"
27
- exit 1
28
- end
29
- args[:type] = search_type
30
- i += 2
31
- when "--category"
32
- category = argv[i + 1]
33
- valid_categories = ["company", "research paper", "news", "pdf", "github", "tweet", "personal site", "linkedin profile", "financial report"]
34
- unless valid_categories.include?(category)
35
- $stderr.puts "Error: Category must be one of: #{valid_categories.map { |c| "\"#{c}\"" }.join(', ')}"
36
- exit 1
37
- end
38
- args[:category] = category
39
- i += 2
40
- when "--include-domains"
41
- args[:include_domains] = argv[i + 1].split(",").map(&:strip)
42
- i += 2
43
- when "--exclude-domains"
44
- args[:exclude_domains] = argv[i + 1].split(",").map(&:strip)
45
- i += 2
46
- when "--api-key"
47
- args[:api_key] = argv[i + 1]
48
- i += 2
49
- when "--output-format"
50
- args[:output_format] = argv[i + 1]
51
- i += 2
52
- when "--linkedin"
53
- linkedin_type = argv[i + 1]
54
- valid_types = ["company", "person", "all"]
55
- unless valid_types.include?(linkedin_type)
56
- $stderr.puts "Error: LinkedIn type must be one of: #{valid_types.join(', ')}"
57
- exit 1
58
- end
59
- args[:linkedin] = linkedin_type
60
- i += 2
61
- when "--start-published-date"
62
- args[:start_published_date] = argv[i + 1]
63
- i += 2
64
- when "--end-published-date"
65
- args[:end_published_date] = argv[i + 1]
66
- i += 2
67
- when "--start-crawl-date"
68
- args[:start_crawl_date] = argv[i + 1]
69
- i += 2
70
- when "--end-crawl-date"
71
- args[:end_crawl_date] = argv[i + 1]
72
- i += 2
73
- when "--include-text"
74
- args[:include_text] ||= []
75
- args[:include_text] << argv[i + 1]
76
- i += 2
77
- when "--exclude-text"
78
- args[:exclude_text] ||= []
79
- args[:exclude_text] << argv[i + 1]
80
- i += 2
81
- when "--text"
82
- args[:text] = true
83
- i += 1
84
- when "--text-max-characters"
85
- args[:text_max_characters] = argv[i + 1].to_i
86
- i += 2
87
- when "--include-html-tags"
88
- args[:include_html_tags] = true
89
- i += 1
90
- when "--summary"
91
- args[:summary] = true
92
- i += 1
93
- when "--summary-query"
94
- args[:summary_query] = argv[i + 1]
95
- i += 2
96
- when "--summary-schema"
97
- schema_arg = argv[i + 1]
98
- args[:summary_schema] = if schema_arg.start_with?("@")
99
- JSON.parse(File.read(schema_arg[1..]))
100
- else
101
- JSON.parse(schema_arg)
102
- end
103
- i += 2
104
- when "--context"
105
- args[:context] = true
106
- i += 1
107
- when "--context-max-characters"
108
- args[:context_max_characters] = argv[i + 1].to_i
109
- i += 2
110
- when "--subpages"
111
- args[:subpages] = argv[i + 1].to_i
112
- i += 2
113
- when "--subpage-target"
114
- args[:subpage_target] ||= []
115
- args[:subpage_target] << argv[i + 1]
116
- i += 2
117
- when "--links"
118
- args[:links] = argv[i + 1].to_i
119
- i += 2
120
- when "--image-links"
121
- args[:image_links] = argv[i + 1].to_i
122
- i += 2
123
- when "--help", "-h"
124
- puts <<~HELP
125
- Usage: exa-ai search QUERY [OPTIONS]
126
-
127
- Search the web using Exa AI
128
-
129
- Arguments:
130
- QUERY Search query (required)
131
-
132
- Options:
133
- --num-results N Number of results to return (default: 10)
134
- --type TYPE Search type: fast, deep, keyword, or auto (default: fast)
135
- --category CAT Focus on specific data category
136
- Options: "company", "research paper", "news", "pdf",
137
- "github", "tweet", "personal site", "linkedin profile",
138
- "financial report"
139
- --include-domains D Comma-separated list of domains to include
140
- --exclude-domains D Comma-separated list of domains to exclude
141
- --start-published-date DATE Filter by published date (ISO 8601 format)
142
- --end-published-date DATE Filter by published date (ISO 8601 format)
143
- --start-crawl-date DATE Filter by crawl date (ISO 8601 format)
144
- --end-crawl-date DATE Filter by crawl date (ISO 8601 format)
145
- --include-text PHRASE Include results with exact phrase (repeatable)
146
- --exclude-text PHRASE Exclude results with exact phrase (repeatable)
147
-
148
- Content Extraction:
149
- --text Include full webpage text
150
- --text-max-characters N Max characters for webpage text
151
- --include-html-tags Include HTML tags in text extraction
152
- --summary Include AI-generated summary
153
- --summary-query PROMPT Custom prompt for summary generation
154
- --summary-schema FILE JSON schema for summary structure (@file syntax)
155
- --context Format results as context for LLM RAG
156
- --context-max-characters N Max characters for context string
157
- --subpages N Number of subpages to crawl
158
- --subpage-target PHRASE Subpage target phrases (repeatable)
159
- --links N Number of links to extract per result
160
- --image-links N Number of image links to extract
161
-
162
- General Options:
163
- --linkedin TYPE Search LinkedIn: company, person, or all
164
- --api-key KEY Exa API key (or set EXA_API_KEY env var)
165
- --output-format FMT Output format: json, pretty, or text (default: json)
166
- --help, -h Show this help message
167
-
168
- Examples:
169
- exa-ai search "ruby programming"
170
- exa-ai search "machine learning" --num-results 5 --type deep
171
- exa-ai search "Latest LLM research" --category "research paper"
172
- exa-ai search "AI startups" --category company
173
- exa-ai search "Anthropic" --linkedin company
174
- exa-ai search "Dario Amodei" --linkedin person
175
- exa-ai search "AI" --linkedin all
176
- exa-ai search "AI research" --include-domains arxiv.org,scholar.google.com
177
- exa-ai search "tutorials" --output-format pretty
178
- HELP
179
- exit 0
180
- else
181
- query_parts << arg
182
- i += 1
183
- end
184
- end
185
-
186
- args[:query] = query_parts.join(" ")
187
- args
5
+ require_relative "../lib/exa/cli/search_parser"
6
+
7
+ def print_help
8
+ puts <<~HELP
9
+ Usage: exa-ai search QUERY [OPTIONS]
10
+
11
+ Search the web using Exa AI
12
+
13
+ Arguments:
14
+ QUERY Search query (required)
15
+
16
+ Options:
17
+ --num-results N Number of results to return (default: 10)
18
+ --type TYPE Search type: fast, deep, keyword, or auto (default: fast)
19
+ --category CAT Focus on specific data category
20
+ Options: "company", "research paper", "news", "pdf",
21
+ "github", "tweet", "personal site", "financial report",
22
+ "people"
23
+ --include-domains D Comma-separated list of domains to include
24
+ --exclude-domains D Comma-separated list of domains to exclude
25
+ --start-published-date DATE Filter by published date (ISO 8601 format)
26
+ --end-published-date DATE Filter by published date (ISO 8601 format)
27
+ --start-crawl-date DATE Filter by crawl date (ISO 8601 format)
28
+ --end-crawl-date DATE Filter by crawl date (ISO 8601 format)
29
+ --include-text PHRASE Include results with exact phrase (repeatable)
30
+ --exclude-text PHRASE Exclude results with exact phrase (repeatable)
31
+
32
+ Content Extraction:
33
+ --text Include full webpage text
34
+ --text-max-characters N Max characters for webpage text
35
+ --include-html-tags Include HTML tags in text extraction
36
+ --summary Include AI-generated summary
37
+ --summary-query PROMPT Custom prompt for summary generation
38
+ --summary-schema FILE JSON schema for summary structure (@file syntax)
39
+ --context Format results as context for LLM RAG
40
+ --context-max-characters N Max characters for context string
41
+ --subpages N Number of subpages to crawl
42
+ --subpage-target PHRASE Subpage target phrases (repeatable)
43
+ --links N Number of links to extract per result
44
+ --image-links N Number of image links to extract
45
+
46
+ General Options:
47
+ --api-key KEY Exa API key (or set EXA_API_KEY env var)
48
+ --output-format FMT Output format: json, pretty, or text (default: json)
49
+ --help, -h Show this help message
50
+
51
+ Examples:
52
+ exa-ai search "ruby programming"
53
+ exa-ai search "machine learning" --num-results 5 --type deep
54
+ exa-ai search "Latest LLM research" --category "research paper"
55
+ exa-ai search "AI startups" --category company
56
+ exa-ai search "Dario Amodei" --category people
57
+ exa-ai search "AI research" --include-domains arxiv.org,scholar.google.com
58
+ exa-ai search "tutorials" --output-format pretty
59
+ HELP
188
60
  end
189
61
 
190
62
  # Build contents parameter from extracted flags
@@ -238,15 +110,15 @@ end
238
110
 
239
111
  # Main execution
240
112
  begin
241
- args = parse_args(ARGV)
242
-
243
- # Validate query
244
- if args[:query].nil? || args[:query].empty?
245
- $stderr.puts "Error: Query is required"
246
- $stderr.puts "Run 'exa-ai search --help' for usage information"
247
- exit 1
113
+ # Handle help flag
114
+ if ARGV.include?("--help") || ARGV.include?("-h")
115
+ print_help
116
+ exit 0
248
117
  end
249
118
 
119
+ # Parse command-line arguments
120
+ args = Exa::CLI::SearchParser.parse(ARGV)
121
+
250
122
  # Resolve API key
251
123
  api_key = Exa::CLI::Base.resolve_api_key(args[:api_key])
252
124
 
@@ -272,17 +144,8 @@ begin
272
144
  contents = build_contents(args)
273
145
  search_params.merge!(contents) if contents
274
146
 
275
- # Execute search based on LinkedIn type
276
- result = case args[:linkedin]
277
- when "company"
278
- client.linkedin_company(args[:query], **search_params)
279
- when "person"
280
- client.linkedin_person(args[:query], **search_params)
281
- when "all"
282
- client.search(args[:query], includeDomains: ["linkedin.com"], **search_params)
283
- else
284
- client.search(args[:query], **search_params)
285
- end
147
+ # Execute search
148
+ result = client.search(args[:query], **search_params)
286
149
 
287
150
  # Format and output result
288
151
  output = Exa::CLI::Formatters::SearchFormatter.format(result, output_format)
@@ -80,32 +80,51 @@ def parse_args(argv)
80
80
  Create a new webset from search criteria or an import
81
81
 
82
82
  Required (choose one):
83
- --search JSON Search configuration (supports @file.json)
84
- --import ID Import or webset ID to create webset from
85
- (accepts import_* or webset_* IDs)
83
+ --search JSON Search configuration as JSON (supports @file.json)
84
+ Format: {"query":"...","count":10,"scope":[...]}
85
+ The 'scope' field limits search to specific sources
86
+ --import ID Import/webset ID to attach data to this webset
87
+ (loads data but does NOT filter searches)
88
+ Format: import_abc123 or webset_xyz789
86
89
 
87
90
  Options:
88
91
  --enrichments JSON Array of enrichment configs (supports @file.json)
89
- --exclude JSON Array of exclude configs (supports @file.json)
92
+ Format: [{"description":"...","format":"text"}]
93
+ --exclude JSON Sources to exclude from searches (supports @file.json)
94
+ Format: [{"source":"import|webset","id":"..."}]
90
95
  --external-id ID External identifier for the webset
91
96
  --metadata JSON Custom metadata (supports @file.json)
97
+ Format: {"key":"value"}
92
98
  --wait Wait for webset to reach idle status
93
99
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
94
100
  --output-format FMT Output format: json, pretty, or text (default: json)
95
101
  --help, -h Show this help message
96
102
 
103
+ JSON Format Details:
104
+ search.scope Array of source references to limit search
105
+ Format: [{"source":"import|webset","id":"..."}]
106
+ With relationship (hop search):
107
+ [{"source":"webset","id":"ws_123",
108
+ "relationship":{"definition":"investors of","limit":3}}]
109
+
110
+ IMPORTANT: Cannot use the same import ID in both --import and search.scope
111
+ (this will return a 400 error from the API)
112
+
97
113
  Examples:
98
114
  # Create webset from search
99
115
  exa-ai webset-create --search '{"query":"AI startups","count":10}'
100
116
  exa-ai webset-create --search @search.json --enrichments @enrichments.json
101
117
  exa-ai webset-create --search @search.json --wait
102
118
 
119
+ # Create webset with scoped search (filter to specific import)
120
+ exa-ai webset-create --search '{"query":"CEOs","count":10,"scope":[{"source":"import","id":"import_abc"}]}'
121
+
103
122
  # Create webset from import
104
123
  exa-ai webset-create --import import_abc123
105
124
  exa-ai webset-create --import import_def456 --enrichments @enrichments.json
106
125
 
107
- # Create webset from existing webset
108
- exa-ai webset-create --import webset_xyz789
126
+ # Load import AND run search (search not scoped to import)
127
+ exa-ai webset-create --import import_abc123 --search '{"query":"investors","count":20}'
109
128
  HELP
110
129
  exit 0
111
130
  else