exa-ai 0.6.0 → 0.6.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: 4f55e9efe411da4b9eaf3018791aa7819fc8872c69e32369627014594eb23670
4
+ data.tar.gz: 0d074ce4bb6eaa2902b80fe5df239be717f182c3703ebfcd6c45944de7cd1245
5
5
  SHA512:
6
- metadata.gz: b45e4d6a35d33910e9ce4b154b1b432efba13732b9c237ab94da5962944fae65bf3562a426dc9fa9c18af8c820e8db5d1a4ae3417605d171a136de2d01f149db
7
- data.tar.gz: 98f082e62eeef3cf7851abcda1cdd61ee5edd548c2cd0d5a65b7645510248ebd0a2274b743e83b7ff354e1ebb4016a92ab83e8a4f93300c255d298fdb9a2683b
6
+ metadata.gz: 732d915bf1eadcabff77ae2dcf7c573d426021bea3dac1fe0b2013958dde5a53129c62675ff66279b5204233c970b988ea72ad29ef94bb4b5219cba55910145b
7
+ data.tar.gz: f4f73347c282e2ebb2209afd42a02e1fb253e9d682a5959ce1cd2ea27519f132c156fb897bbd3177d24f4b88bd06874afd76b81bf70ccb225dcb3ad64ec6249b
@@ -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
 
@@ -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
@@ -104,11 +104,20 @@ def parse_args(argv)
104
104
  --entity TYPE Entity type: person, company, article, research_paper, custom
105
105
  --entity-description TXT Description for custom entity type (required with --entity custom)
106
106
  --criteria JSON Search criteria array (supports @file.json)
107
+ Format: [{"description":"criterion 1"},{"description":"criterion 2"}]
107
108
  --exclude JSON Items to exclude from results (supports @file.json)
109
+ Format: [{"source":"import|webset","id":"..."}]
108
110
  --scope JSON Limit search to specific sources (supports @file.json)
111
+ Format: [{"source":"import|webset","id":"..."}]
112
+ Filters this search to only items from these sources
113
+ With relationship (hop search):
114
+ [{"source":"webset","id":"ws_123",
115
+ "relationship":{"definition":"investors of","limit":3}}]
109
116
  --recall Estimate total available results
110
- --behavior TYPE "override" or "append" (default: override)
117
+ --behavior TYPE "override" (replace items) or "append" (add items)
118
+ Default: override when scope is present, append otherwise
111
119
  --metadata JSON Custom metadata (supports @file.json)
120
+ Format: {"key":"value"}
112
121
  --api-key KEY Exa API key (or set EXA_API_KEY env var)
113
122
  --output-format FMT Output format: json, pretty, or text (default: json)
114
123
  --help, -h Show this help message
@@ -121,11 +130,15 @@ def parse_args(argv)
121
130
  exa-ai webset-search-create ws_123 --query "tech CEOs" --entity person
122
131
  exa-ai webset-search-create ws_123 --query "Silicon Valley firms" --entity company
123
132
 
124
- # Search with custom entity type
125
- exa-ai webset-search-create ws_123 --query "Ford Mustang" \\
126
- --entity custom --entity-description "vintage cars"
133
+ # Scoped search (filter to specific import)
134
+ exa-ai webset-search-create ws_123 --query "CTOs" \\
135
+ --scope '[{"source":"import","id":"import_abc"}]'
127
136
 
128
- # Other options
137
+ # Hop search (find investors of companies in webset)
138
+ exa-ai webset-search-create ws_123 --query "investors" \\
139
+ --scope '[{"source":"webset","id":"ws_companies","relationship":{"definition":"investors of","limit":5}}]'
140
+
141
+ # Search with criteria and behavior
129
142
  exa-ai webset-search-create ws_123 --query "machine learning" --count 50
130
143
  exa-ai webset-search-create ws_123 --query "research" --behavior append --recall
131
144
  HELP
@@ -203,7 +216,7 @@ begin
203
216
  search = client.create_webset_search(webset_id: args[:webset_id], **search_params)
204
217
 
205
218
  # Format and output result
206
- output = Exa::CLI::Formatters::SearchFormatter.format(search, output_format)
219
+ output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, output_format)
207
220
  puts output
208
221
  $stdout.flush
209
222
 
@@ -78,7 +78,7 @@ begin
78
78
  search = client.get_webset_search(webset_id: webset_id, id: search_id)
79
79
 
80
80
  # Format and output
81
- output = Exa::CLI::Formatters::SearchFormatter.format(search, output_format)
81
+ output = Exa::CLI::Formatters::WebsetSearchFormatter.format(search, output_format)
82
82
  puts output
83
83
  $stdout.flush
84
84
 
@@ -9,7 +9,7 @@ module Exa
9
9
  when "json"
10
10
  JSON.generate(enrichment.to_h)
11
11
  when "pretty"
12
- JSON.pretty_generate(enrichment.to_h)
12
+ format_as_pretty(enrichment)
13
13
  when "text"
14
14
  format_as_text(enrichment)
15
15
  when "toon"
@@ -24,7 +24,7 @@ module Exa
24
24
  when "json"
25
25
  JSON.generate(collection.to_h)
26
26
  when "pretty"
27
- JSON.pretty_generate(collection.to_h)
27
+ format_collection_as_pretty(collection)
28
28
  when "text"
29
29
  format_collection_as_text(collection)
30
30
  when "toon"
@@ -34,6 +34,31 @@ module Exa
34
34
  end
35
35
  end
36
36
 
37
+ def self.format_as_pretty(enrichment)
38
+ lines = []
39
+ lines << "Enrichment ID: #{enrichment.id}"
40
+ lines << "Webset ID: #{enrichment.webset_id}" if enrichment.webset_id
41
+ lines << "Status: #{enrichment.status}"
42
+ lines << "Title: #{enrichment.title}" if enrichment.title
43
+ lines << "Description: #{enrichment.description}" if enrichment.description
44
+ lines << "Format: #{enrichment.format}" if enrichment.format
45
+
46
+ if enrichment.options && !enrichment.options.empty?
47
+ lines << ""
48
+ lines << "Options (#{enrichment.options.length}):"
49
+ enrichment.options.each do |option|
50
+ lines << " • #{option['label']}" if option['label']
51
+ end
52
+ end
53
+
54
+ lines << ""
55
+ lines << "Created: #{enrichment.created_at}" if enrichment.created_at
56
+ lines << "Updated: #{enrichment.updated_at}" if enrichment.updated_at
57
+
58
+ lines.join("\n")
59
+ end
60
+ private_class_method :format_as_pretty
61
+
37
62
  def self.format_as_text(enrichment)
38
63
  lines = []
39
64
  lines << "Enrichment: #{enrichment.id}"
@@ -57,6 +82,33 @@ module Exa
57
82
  end
58
83
  private_class_method :format_as_text
59
84
 
85
+ def self.format_collection_as_pretty(collection)
86
+ lines = []
87
+ lines << "Enrichments (#{collection.data.length} items)"
88
+ lines << ""
89
+
90
+ collection.data.each_with_index do |enr, idx|
91
+ lines << "" if idx > 0 # Blank line between enrichments
92
+
93
+ lines << "Enrichment ID: #{enr['id']}"
94
+ lines << "Webset ID: #{enr['websetId']}" if enr['websetId']
95
+ lines << "Status: #{enr['status']}"
96
+ lines << "Title: #{enr['title']}" if enr['title']
97
+ lines << "Description: #{enr['description']}" if enr['description']
98
+ lines << "Format: #{enr['format']}" if enr['format']
99
+ lines << "Created: #{enr['createdAt']}" if enr['createdAt']
100
+ lines << "Updated: #{enr['updatedAt']}" if enr['updatedAt']
101
+ end
102
+
103
+ if collection.has_more
104
+ lines << ""
105
+ lines << "Next Cursor: #{collection.next_cursor}"
106
+ end
107
+
108
+ lines.join("\n")
109
+ end
110
+ private_class_method :format_collection_as_pretty
111
+
60
112
  def self.format_collection_as_text(collection)
61
113
  lines = ["Enrichments (#{collection.data.length} items):"]
62
114
  collection.data.each do |enr|
@@ -9,7 +9,7 @@ module Exa
9
9
  when "json"
10
10
  JSON.generate(import.to_h)
11
11
  when "pretty"
12
- JSON.pretty_generate(import.to_h)
12
+ format_as_pretty(import)
13
13
  when "text"
14
14
  format_as_text(import)
15
15
  when "toon"
@@ -24,7 +24,7 @@ module Exa
24
24
  when "json"
25
25
  JSON.generate(collection.to_h)
26
26
  when "pretty"
27
- JSON.pretty_generate(collection.to_h)
27
+ format_collection_as_pretty(collection)
28
28
  when "text"
29
29
  format_collection_as_text(collection)
30
30
  when "toon"
@@ -34,6 +34,43 @@ module Exa
34
34
  end
35
35
  end
36
36
 
37
+ def self.format_as_pretty(import)
38
+ lines = []
39
+ lines << "Import ID: #{import.id}"
40
+ lines << "Status: #{import.status}"
41
+ lines << "Title: #{import.title}" if import.title
42
+ lines << "Format: #{import.format}" if import.format
43
+
44
+ if import.entity
45
+ entity_type = import.entity['type'] || import.entity[:type]
46
+ lines << "Entity Type: #{entity_type}" if entity_type
47
+ end
48
+
49
+ lines << "Count: #{import.count}" if import.count
50
+
51
+ if import.failed?
52
+ lines << ""
53
+ lines << "Failure:"
54
+ lines << " Reason: #{import.failed_reason}" if import.failed_reason
55
+ lines << " Message: #{import.failed_message}" if import.failed_message
56
+ lines << " Failed At: #{import.failed_at}" if import.failed_at
57
+ end
58
+
59
+ if import.upload_url
60
+ lines << ""
61
+ lines << "Upload:"
62
+ lines << " URL: #{import.upload_url}"
63
+ lines << " Valid Until: #{import.upload_valid_until}" if import.upload_valid_until
64
+ end
65
+
66
+ lines << ""
67
+ lines << "Created: #{import.created_at}" if import.created_at
68
+ lines << "Updated: #{import.updated_at}" if import.updated_at
69
+
70
+ lines.join("\n")
71
+ end
72
+ private_class_method :format_as_pretty
73
+
37
74
  def self.format_as_text(import)
38
75
  lines = []
39
76
  lines << "Import: #{import.id}"
@@ -68,6 +105,37 @@ module Exa
68
105
  end
69
106
  private_class_method :format_as_text
70
107
 
108
+ def self.format_collection_as_pretty(collection)
109
+ lines = []
110
+ lines << "Imports (#{collection.data.length} items)"
111
+ lines << ""
112
+
113
+ collection.data.each_with_index do |imp, idx|
114
+ lines << "" if idx > 0 # Blank line between imports
115
+
116
+ lines << "Import ID: #{imp.id}"
117
+ lines << "Status: #{imp.status}"
118
+ lines << "Title: #{imp.title}" if imp.title
119
+ lines << "Format: #{imp.format}" if imp.format
120
+ lines << "Entity Type: #{imp.entity['type']}" if imp.entity && imp.entity['type']
121
+ lines << "Count: #{imp.count}" if imp.count
122
+ lines << "Created: #{imp.created_at}" if imp.created_at
123
+ lines << "Updated: #{imp.updated_at}" if imp.updated_at
124
+
125
+ if imp.status == 'failed'
126
+ lines << "Failed Reason: #{imp.failed_reason}" if imp.failed_reason
127
+ end
128
+ end
129
+
130
+ if collection.has_more
131
+ lines << ""
132
+ lines << "Next Cursor: #{collection.next_cursor}"
133
+ end
134
+
135
+ lines.join("\n")
136
+ end
137
+ private_class_method :format_collection_as_pretty
138
+
71
139
  def self.format_collection_as_text(collection)
72
140
  lines = ["Imports (#{collection.data.length} items):"]
73
141
  collection.data.each do |imp|
@@ -9,7 +9,7 @@ module Exa
9
9
  when "json"
10
10
  JSON.generate(monitor.to_h)
11
11
  when "pretty"
12
- JSON.pretty_generate(monitor.to_h)
12
+ format_as_pretty(monitor)
13
13
  when "text"
14
14
  format_as_text(monitor)
15
15
  when "toon"
@@ -24,7 +24,7 @@ module Exa
24
24
  when "json"
25
25
  JSON.generate(collection.to_h)
26
26
  when "pretty"
27
- JSON.pretty_generate(collection.to_h)
27
+ format_collection_as_pretty(collection)
28
28
  when "text"
29
29
  format_collection_as_text(collection)
30
30
  when "toon"
@@ -34,6 +34,35 @@ module Exa
34
34
  end
35
35
  end
36
36
 
37
+ def self.format_as_pretty(monitor)
38
+ lines = []
39
+ lines << "Monitor ID: #{monitor.id}"
40
+ lines << "Webset ID: #{monitor.webset_id}" if monitor.webset_id
41
+ lines << "Status: #{monitor.status}"
42
+
43
+ if monitor.cadence
44
+ lines << ""
45
+ lines << "Cadence:"
46
+ lines << " Cron: #{monitor.cadence['cron']}" if monitor.cadence['cron']
47
+ lines << " Timezone: #{monitor.cadence['timezone']}" if monitor.cadence['timezone']
48
+ end
49
+
50
+ if monitor.behavior
51
+ lines << ""
52
+ lines << "Behavior:"
53
+ lines << " Type: #{monitor.behavior['type']}" if monitor.behavior['type']
54
+ lines << " Query: #{monitor.behavior['query']}" if monitor.behavior['query']
55
+ lines << " Count: #{monitor.behavior['count']}" if monitor.behavior['count']
56
+ end
57
+
58
+ lines << ""
59
+ lines << "Created: #{monitor.created_at}" if monitor.created_at
60
+ lines << "Updated: #{monitor.updated_at}" if monitor.updated_at
61
+
62
+ lines.join("\n")
63
+ end
64
+ private_class_method :format_as_pretty
65
+
37
66
  def self.format_as_text(monitor)
38
67
  lines = []
39
68
  lines << "Monitor: #{monitor.id}"
@@ -60,6 +89,40 @@ module Exa
60
89
  end
61
90
  private_class_method :format_as_text
62
91
 
92
+ def self.format_collection_as_pretty(collection)
93
+ lines = []
94
+ lines << "Monitors (#{collection.data.length} items)"
95
+ lines << ""
96
+
97
+ collection.data.each_with_index do |mon, idx|
98
+ lines << "" if idx > 0 # Blank line between monitors
99
+
100
+ lines << "Monitor ID: #{mon['id']}"
101
+ lines << "Webset ID: #{mon['websetId']}" if mon['websetId']
102
+ lines << "Status: #{mon['status']}"
103
+
104
+ if mon['cadence']
105
+ lines << "Cron: #{mon['cadence']['cron']}" if mon['cadence']['cron']
106
+ lines << "Timezone: #{mon['cadence']['timezone']}" if mon['cadence']['timezone']
107
+ end
108
+
109
+ if mon['behavior']
110
+ lines << "Query: #{mon['behavior']['query']}" if mon['behavior']['query']
111
+ end
112
+
113
+ lines << "Created: #{mon['createdAt']}" if mon['createdAt']
114
+ lines << "Updated: #{mon['updatedAt']}" if mon['updatedAt']
115
+ end
116
+
117
+ if collection.has_more
118
+ lines << ""
119
+ lines << "Next Cursor: #{collection.next_cursor}"
120
+ end
121
+
122
+ lines.join("\n")
123
+ end
124
+ private_class_method :format_collection_as_pretty
125
+
63
126
  def self.format_collection_as_text(collection)
64
127
  lines = ["Monitors (#{collection.data.length} items):"]
65
128
  collection.data.each do |mon|
@@ -9,7 +9,7 @@ module Exa
9
9
  when "json"
10
10
  JSON.generate(monitor_run.to_h)
11
11
  when "pretty"
12
- JSON.pretty_generate(monitor_run.to_h)
12
+ format_as_pretty(monitor_run)
13
13
  when "text"
14
14
  format_as_text(monitor_run)
15
15
  when "toon"
@@ -24,7 +24,7 @@ module Exa
24
24
  when "json"
25
25
  JSON.generate(collection.to_h)
26
26
  when "pretty"
27
- JSON.pretty_generate(collection.to_h)
27
+ format_collection_as_pretty(collection)
28
28
  when "text"
29
29
  format_collection_as_text(collection)
30
30
  when "toon"
@@ -34,6 +34,27 @@ module Exa
34
34
  end
35
35
  end
36
36
 
37
+ def self.format_as_pretty(monitor_run)
38
+ lines = []
39
+ lines << "Monitor Run ID: #{monitor_run.id}"
40
+ lines << "Monitor ID: #{monitor_run.monitor_id}" if monitor_run.monitor_id
41
+ lines << "Status: #{monitor_run.status}"
42
+
43
+ lines << ""
44
+ lines << "Created: #{monitor_run.created_at}" if monitor_run.created_at
45
+ lines << "Updated: #{monitor_run.updated_at}" if monitor_run.updated_at
46
+ lines << "Completed: #{monitor_run.completed_at}" if monitor_run.completed_at
47
+
48
+ if monitor_run.failed?
49
+ lines << ""
50
+ lines << "Failed: #{monitor_run.failed_at}" if monitor_run.failed_at
51
+ lines << "Reason: #{monitor_run.failed_reason}" if monitor_run.failed_reason
52
+ end
53
+
54
+ lines.join("\n")
55
+ end
56
+ private_class_method :format_as_pretty
57
+
37
58
  def self.format_as_text(monitor_run)
38
59
  lines = []
39
60
  lines << "Monitor Run: #{monitor_run.id}"
@@ -53,6 +74,36 @@ module Exa
53
74
  end
54
75
  private_class_method :format_as_text
55
76
 
77
+ def self.format_collection_as_pretty(collection)
78
+ lines = []
79
+ lines << "Monitor Runs (#{collection.data.length} items)"
80
+ lines << ""
81
+
82
+ collection.data.each_with_index do |run, idx|
83
+ lines << "" if idx > 0 # Blank line between runs
84
+
85
+ lines << "Monitor Run ID: #{run['id']}"
86
+ lines << "Monitor ID: #{run['monitorId']}" if run['monitorId']
87
+ lines << "Status: #{run['status']}"
88
+ lines << "Created: #{run['createdAt']}" if run['createdAt']
89
+ lines << "Updated: #{run['updatedAt']}" if run['updatedAt']
90
+ lines << "Completed: #{run['completedAt']}" if run['completedAt']
91
+
92
+ if run['status'] == 'failed'
93
+ lines << "Failed: #{run['failedAt']}" if run['failedAt']
94
+ lines << "Reason: #{run['failedReason']}" if run['failedReason']
95
+ end
96
+ end
97
+
98
+ if collection.has_more
99
+ lines << ""
100
+ lines << "Next Cursor: #{collection.next_cursor}"
101
+ end
102
+
103
+ lines.join("\n")
104
+ end
105
+ private_class_method :format_collection_as_pretty
106
+
56
107
  def self.format_collection_as_text(collection)
57
108
  lines = ["Monitor Runs (#{collection.data.length} items):"]
58
109
  collection.data.each do |run|
@@ -24,7 +24,7 @@ module Exa
24
24
  when "json"
25
25
  JSON.generate(collection.to_h)
26
26
  when "pretty"
27
- JSON.pretty_generate(collection.to_h)
27
+ format_collection_as_pretty(collection)
28
28
  when "text"
29
29
  format_collection_as_text(collection)
30
30
  when "toon"
@@ -66,6 +66,66 @@ module Exa
66
66
  lines.join("\n")
67
67
  end
68
68
  private_class_method :format_collection_as_text
69
+
70
+ def self.format_collection_as_pretty(collection)
71
+ lines = []
72
+
73
+ # Header with count and pagination info
74
+ header = "Websets (#{collection.data.length} items)"
75
+ header += " - Page #{collection.has_more ? '1 of many' : '1 of 1'}" if collection.data.any?
76
+ lines << header
77
+
78
+ if collection.has_more
79
+ lines << "Next Cursor: #{collection.next_cursor}"
80
+ end
81
+
82
+ lines << ""
83
+
84
+ # Format each webset
85
+ collection.data.each_with_index do |ws, idx|
86
+ lines << "" if idx > 0 # Blank line between websets
87
+
88
+ lines << "Webset ID: #{ws['id']}"
89
+ lines << "Status: #{ws['status']}"
90
+ lines << "Title: #{ws['title']}" if ws['title']
91
+ lines << "External ID: #{ws['externalId']}" if ws['externalId']
92
+ lines << "Created: #{ws['createdAt']}" if ws['createdAt']
93
+ lines << "Updated: #{ws['updatedAt']}" if ws['updatedAt']
94
+
95
+ # Searches
96
+ if ws['searches'] && !ws['searches'].empty?
97
+ lines << ""
98
+ lines << "Searches (#{ws['searches'].length}):"
99
+ ws['searches'].each do |search|
100
+ status_indicator = case search['status']
101
+ when 'completed' then '✓'
102
+ when 'running' then '→'
103
+ when 'failed' then '✗'
104
+ else '•'
105
+ end
106
+ lines << " #{status_indicator} #{search['query']} (#{search['status']})" if search['query']
107
+ end
108
+ end
109
+
110
+ # Enrichments
111
+ if ws['enrichments'] && !ws['enrichments'].empty?
112
+ lines << "Enrichments: #{ws['enrichments'].length}"
113
+ end
114
+
115
+ # Monitors
116
+ if ws['monitors'] && !ws['monitors'].empty?
117
+ lines << "Monitors: #{ws['monitors'].length}"
118
+ end
119
+
120
+ # Imports
121
+ if ws['imports'] && !ws['imports'].empty?
122
+ lines << "Imports: #{ws['imports'].length}"
123
+ end
124
+ end
125
+
126
+ lines.join("\n")
127
+ end
128
+ private_class_method :format_collection_as_pretty
69
129
  end
70
130
  end
71
131
  end
@@ -9,7 +9,7 @@ module Exa
9
9
  when "json"
10
10
  JSON.generate(item)
11
11
  when "pretty"
12
- JSON.pretty_generate(item)
12
+ format_as_pretty(item)
13
13
  when "text"
14
14
  format_as_text(item)
15
15
  when "toon"
@@ -24,7 +24,7 @@ module Exa
24
24
  when "json"
25
25
  JSON.generate(items)
26
26
  when "pretty"
27
- JSON.pretty_generate(items)
27
+ format_collection_as_pretty(items)
28
28
  when "text"
29
29
  format_collection_as_text(items)
30
30
  when "toon"
@@ -34,6 +34,27 @@ module Exa
34
34
  end
35
35
  end
36
36
 
37
+ def self.format_as_pretty(item)
38
+ lines = []
39
+ lines << "Item ID: #{item['id']}"
40
+ lines << "URL: #{item['url']}" if item['url']
41
+ lines << "Title: #{item['title']}" if item['title']
42
+ lines << "Status: #{item['status']}" if item['status']
43
+ lines << "Created: #{item['createdAt']}" if item['createdAt']
44
+ lines << "Updated: #{item['updatedAt']}" if item['updatedAt']
45
+
46
+ if item['entity']
47
+ lines << ""
48
+ lines << "Entity:"
49
+ lines << " Type: #{item['entity']['type']}" if item['entity']['type']
50
+ lines << " Name: #{item['entity']['name']}" if item['entity']['name']
51
+ lines << " Description: #{item['entity']['description']}" if item['entity']['description']
52
+ end
53
+
54
+ lines.join("\n")
55
+ end
56
+ private_class_method :format_as_pretty
57
+
37
58
  def self.format_as_text(item)
38
59
  lines = []
39
60
  lines << "Item: #{item['id']}"
@@ -53,6 +74,33 @@ module Exa
53
74
  end
54
75
  private_class_method :format_as_text
55
76
 
77
+ def self.format_collection_as_pretty(items)
78
+ lines = []
79
+ lines << "Items (#{items.length})"
80
+ lines << ""
81
+
82
+ items.each_with_index do |item, idx|
83
+ lines << "" if idx > 0 # Blank line between items
84
+
85
+ lines << "Item ID: #{item['id']}"
86
+ lines << "URL: #{item['url']}" if item['url']
87
+ lines << "Title: #{item['title']}" if item['title']
88
+ lines << "Status: #{item['status']}" if item['status']
89
+ lines << "Created: #{item['createdAt']}" if item['createdAt']
90
+ lines << "Updated: #{item['updatedAt']}" if item['updatedAt']
91
+
92
+ if item['entity']
93
+ entity_name = item['entity']['name']
94
+ entity_type = item['entity']['type']
95
+ lines << "Entity: #{entity_name}" if entity_name
96
+ lines << "Entity Type: #{entity_type}" if entity_type && !entity_name
97
+ end
98
+ end
99
+
100
+ lines.join("\n")
101
+ end
102
+ private_class_method :format_collection_as_pretty
103
+
56
104
  def self.format_collection_as_text(items)
57
105
  lines = ["Items (#{items.length} total):"]
58
106
  items.each_with_index do |item, idx|
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module CLI
5
+ module Formatters
6
+ class WebsetSearchFormatter
7
+ def self.format(search, format)
8
+ case format
9
+ when "json"
10
+ JSON.pretty_generate(search.to_h)
11
+ when "pretty"
12
+ format_pretty(search)
13
+ when "text"
14
+ format_text(search)
15
+ when "toon"
16
+ Exa::CLI::Base.encode_as_toon(search.to_h)
17
+ else
18
+ JSON.pretty_generate(search.to_h)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def self.format_pretty(search)
25
+ output = []
26
+ output << "Search ID: #{search.id}"
27
+ output << "Status: #{search.status}"
28
+ output << "Query: #{search.query}"
29
+ output << "Entity Type: #{search.entity&.[]('type') || 'N/A'}" if search.entity
30
+ output << "Count: #{search.count}" if search.count
31
+ output << "Behavior: #{search.behavior}"
32
+ output << "Recall: #{search.recall}" if search.recall
33
+ output << "Created: #{search.created_at}"
34
+ output << "Updated: #{search.updated_at}"
35
+ output << "Progress: #{search.progress}" if search.progress
36
+ output << ""
37
+
38
+ if search.canceled?
39
+ output << "Canceled: #{search.canceled_at}"
40
+ output << "Cancel Reason: #{search.canceled_reason}" if search.canceled_reason
41
+ end
42
+
43
+ output.join("\n")
44
+ end
45
+
46
+ def self.format_text(search)
47
+ [
48
+ "ID: #{search.id}",
49
+ "Status: #{search.status}",
50
+ "Query: #{search.query}",
51
+ "Behavior: #{search.behavior}"
52
+ ].join("\n")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -20,6 +20,7 @@ module Exa
20
20
  validate_exclude!(params[:exclude]) if params[:exclude]
21
21
  validate_external_id!(params[:externalId]) if params[:externalId]
22
22
  validate_metadata!(params[:metadata]) if params[:metadata]
23
+ validate_no_duplicate_ids_in_import_and_scope!(params)
23
24
  end
24
25
 
25
26
  private
@@ -184,6 +185,20 @@ module Exa
184
185
  raise ArgumentError, "#{name} must be at least #{min} characters" if min && value.length < min
185
186
  raise ArgumentError, "#{name} cannot exceed #{max} characters" if max && value.length > max
186
187
  end
188
+
189
+ def validate_no_duplicate_ids_in_import_and_scope!(params)
190
+ return unless params[:import] && params[:search] && params[:search][:scope]
191
+
192
+ import_ids = params[:import].map { |item| item[:id] }
193
+ scope_ids = params[:search][:scope].map { |item| item[:id] }
194
+
195
+ duplicates = import_ids & scope_ids
196
+
197
+ return if duplicates.empty?
198
+
199
+ raise ArgumentError,
200
+ "Cannot use the same import/webset ID in both :import and search[:scope]: #{duplicates.join(', ')}"
201
+ end
187
202
  end
188
203
  end
189
204
  end
data/lib/exa/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exa
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.1"
5
5
  end
data/lib/exa.rb CHANGED
@@ -67,6 +67,7 @@ require_relative "exa/cli/base"
67
67
  require_relative "exa/cli/polling"
68
68
  require_relative "exa/cli/error_handler"
69
69
  require_relative "exa/cli/formatters/search_formatter"
70
+ require_relative "exa/cli/formatters/webset_search_formatter"
70
71
  require_relative "exa/cli/formatters/context_formatter"
71
72
  require_relative "exa/cli/formatters/contents_formatter"
72
73
  require_relative "exa/cli/formatters/research_formatter"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exa-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Jackson
@@ -206,6 +206,7 @@ files:
206
206
  - lib/exa/cli/formatters/search_formatter.rb
207
207
  - lib/exa/cli/formatters/webset_formatter.rb
208
208
  - lib/exa/cli/formatters/webset_item_formatter.rb
209
+ - lib/exa/cli/formatters/webset_search_formatter.rb
209
210
  - lib/exa/cli/polling.rb
210
211
  - lib/exa/client.rb
211
212
  - lib/exa/connection.rb