web-search-plus-plugin 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +4 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/index.ts +129 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +22 -0
- package/scripts/search.py +2526 -0
- package/scripts/setup.py +453 -0
package/scripts/setup.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Web Search Plus - Interactive Setup Wizard
|
|
4
|
+
==========================================
|
|
5
|
+
|
|
6
|
+
Runs on first use (when no config.json exists) to configure providers and API keys.
|
|
7
|
+
Creates config.json with your settings. API keys are stored locally only.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/setup.py # Interactive setup
|
|
11
|
+
python3 scripts/setup.py --reset # Reset and reconfigure
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# ANSI colors for terminal output
|
|
20
|
+
class Colors:
|
|
21
|
+
HEADER = '\033[95m'
|
|
22
|
+
BLUE = '\033[94m'
|
|
23
|
+
CYAN = '\033[96m'
|
|
24
|
+
GREEN = '\033[92m'
|
|
25
|
+
YELLOW = '\033[93m'
|
|
26
|
+
RED = '\033[91m'
|
|
27
|
+
BOLD = '\033[1m'
|
|
28
|
+
DIM = '\033[2m'
|
|
29
|
+
RESET = '\033[0m'
|
|
30
|
+
|
|
31
|
+
def color(text: str, c: str) -> str:
|
|
32
|
+
"""Wrap text in color codes."""
|
|
33
|
+
return f"{c}{text}{Colors.RESET}"
|
|
34
|
+
|
|
35
|
+
def print_header():
|
|
36
|
+
"""Print the setup wizard header."""
|
|
37
|
+
print()
|
|
38
|
+
print(color("╔════════════════════════════════════════════════════════════╗", Colors.CYAN))
|
|
39
|
+
print(color("║ 🔍 Web Search Plus - Setup Wizard ║", Colors.CYAN))
|
|
40
|
+
print(color("╚════════════════════════════════════════════════════════════╝", Colors.CYAN))
|
|
41
|
+
print()
|
|
42
|
+
print(color("This wizard will help you configure your search providers.", Colors.DIM))
|
|
43
|
+
print(color("API keys are stored locally in config.json (gitignored).", Colors.DIM))
|
|
44
|
+
print()
|
|
45
|
+
|
|
46
|
+
def print_provider_info():
|
|
47
|
+
"""Print information about each provider."""
|
|
48
|
+
print(color("📚 Available Providers:", Colors.BOLD))
|
|
49
|
+
print()
|
|
50
|
+
|
|
51
|
+
providers = [
|
|
52
|
+
{
|
|
53
|
+
"name": "Serper",
|
|
54
|
+
"emoji": "🔎",
|
|
55
|
+
"best_for": "Google results, shopping, local businesses, news",
|
|
56
|
+
"free_tier": "2,500 queries/month",
|
|
57
|
+
"signup": "https://serper.dev",
|
|
58
|
+
"strengths": ["Fastest response times", "Product prices & specs", "Knowledge Graph", "Local business data"]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "Tavily",
|
|
62
|
+
"emoji": "📖",
|
|
63
|
+
"best_for": "Research, explanations, in-depth analysis",
|
|
64
|
+
"free_tier": "1,000 queries/month",
|
|
65
|
+
"signup": "https://tavily.com",
|
|
66
|
+
"strengths": ["AI-synthesized answers", "Full page content", "Domain filtering", "Academic research"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "Exa",
|
|
70
|
+
"emoji": "🧠",
|
|
71
|
+
"best_for": "Semantic search, finding similar content, discovery",
|
|
72
|
+
"free_tier": "1,000 queries/month",
|
|
73
|
+
"signup": "https://exa.ai",
|
|
74
|
+
"strengths": ["Neural/semantic understanding", "Similar page discovery", "Startup/company finder", "Date filtering"]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "You.com",
|
|
78
|
+
"emoji": "🤖",
|
|
79
|
+
"best_for": "RAG applications, real-time info, LLM-ready snippets",
|
|
80
|
+
"free_tier": "Limited free tier",
|
|
81
|
+
"signup": "https://api.you.com",
|
|
82
|
+
"strengths": ["LLM-ready snippets", "Combined web + news", "Live page crawling", "Real-time information"]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "SearXNG",
|
|
86
|
+
"emoji": "🔒",
|
|
87
|
+
"best_for": "Privacy-first search, multi-source aggregation, $0 API cost",
|
|
88
|
+
"free_tier": "FREE (self-hosted)",
|
|
89
|
+
"signup": "https://docs.searxng.org/admin/installation.html",
|
|
90
|
+
"strengths": ["Privacy-preserving (no tracking)", "70+ search engines", "Self-hosted = $0 API cost", "Diverse results"]
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for p in providers:
|
|
95
|
+
print(f" {p['emoji']} {color(p['name'], Colors.BOLD)}")
|
|
96
|
+
print(f" Best for: {color(p['best_for'], Colors.GREEN)}")
|
|
97
|
+
print(f" Free tier: {p['free_tier']}")
|
|
98
|
+
print(f" Sign up: {color(p['signup'], Colors.BLUE)}")
|
|
99
|
+
print()
|
|
100
|
+
|
|
101
|
+
def ask_yes_no(prompt: str, default: bool = True) -> bool:
|
|
102
|
+
"""Ask a yes/no question."""
|
|
103
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
104
|
+
while True:
|
|
105
|
+
response = input(f"{prompt} {color(suffix, Colors.DIM)}: ").strip().lower()
|
|
106
|
+
if response == "":
|
|
107
|
+
return default
|
|
108
|
+
if response in ("y", "yes"):
|
|
109
|
+
return True
|
|
110
|
+
if response in ("n", "no"):
|
|
111
|
+
return False
|
|
112
|
+
print(color(" Please enter 'y' or 'n'", Colors.YELLOW))
|
|
113
|
+
|
|
114
|
+
def ask_choice(prompt: str, options: list, default: str = None) -> str:
|
|
115
|
+
"""Ask user to choose from a list of options."""
|
|
116
|
+
print(f"\n{prompt}")
|
|
117
|
+
for i, opt in enumerate(options, 1):
|
|
118
|
+
marker = color("→", Colors.GREEN) if opt == default else " "
|
|
119
|
+
print(f" {marker} {i}. {opt}")
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
hint = f" [default: {default}]" if default else ""
|
|
123
|
+
response = input(f"Enter number (1-{len(options)}){color(hint, Colors.DIM)}: ").strip()
|
|
124
|
+
|
|
125
|
+
if response == "" and default:
|
|
126
|
+
return default
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
idx = int(response)
|
|
130
|
+
if 1 <= idx <= len(options):
|
|
131
|
+
return options[idx - 1]
|
|
132
|
+
except ValueError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
print(color(f" Please enter a number between 1 and {len(options)}", Colors.YELLOW))
|
|
136
|
+
|
|
137
|
+
def ask_api_key(provider: str, signup_url: str) -> str:
|
|
138
|
+
"""Ask for an API key with validation."""
|
|
139
|
+
print()
|
|
140
|
+
print(f" {color(f'Get your {provider} API key:', Colors.DIM)} {color(signup_url, Colors.BLUE)}")
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
key = input(f" Enter your {provider} API key: ").strip()
|
|
144
|
+
|
|
145
|
+
if not key:
|
|
146
|
+
print(color(" ⚠️ No key entered. This provider will be disabled.", Colors.YELLOW))
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Basic validation
|
|
150
|
+
if len(key) < 10:
|
|
151
|
+
print(color(" ⚠️ Key seems too short. Please check and try again.", Colors.YELLOW))
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Mask key for confirmation
|
|
155
|
+
masked = key[:4] + "..." + key[-4:] if len(key) > 12 else key[:2] + "..."
|
|
156
|
+
print(color(f" ✓ Key saved: {masked}", Colors.GREEN))
|
|
157
|
+
return key
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def ask_searxng_instance(docs_url: str) -> str:
|
|
161
|
+
"""Ask for SearXNG instance URL with connection test."""
|
|
162
|
+
print()
|
|
163
|
+
print(f" {color('SearXNG is self-hosted. You need your own instance.', Colors.DIM)}")
|
|
164
|
+
print(f" {color('Setup guide:', Colors.DIM)} {color(docs_url, Colors.BLUE)}")
|
|
165
|
+
print()
|
|
166
|
+
print(f" {color('Example URLs:', Colors.DIM)}")
|
|
167
|
+
print(f" • http://localhost:8080 (local Docker)")
|
|
168
|
+
print(f" • https://searx.your-domain.com (self-hosted)")
|
|
169
|
+
print()
|
|
170
|
+
|
|
171
|
+
while True:
|
|
172
|
+
url = input(f" Enter your SearXNG instance URL: ").strip()
|
|
173
|
+
|
|
174
|
+
if not url:
|
|
175
|
+
print(color(" ⚠️ No URL entered. SearXNG will be disabled.", Colors.YELLOW))
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Basic URL validation
|
|
179
|
+
if not url.startswith(("http://", "https://")):
|
|
180
|
+
print(color(" ⚠️ URL must start with http:// or https://", Colors.YELLOW))
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# SSRF protection: validate URL before connecting
|
|
184
|
+
try:
|
|
185
|
+
import ipaddress
|
|
186
|
+
import socket
|
|
187
|
+
from urllib.parse import urlparse as _urlparse
|
|
188
|
+
_parsed = _urlparse(url)
|
|
189
|
+
_hostname = _parsed.hostname or ""
|
|
190
|
+
_blocked = {"169.254.169.254", "metadata.google.internal", "metadata.internal"}
|
|
191
|
+
if _hostname in _blocked:
|
|
192
|
+
print(color(f" ❌ Blocked: {_hostname} is a cloud metadata endpoint.", Colors.RED))
|
|
193
|
+
continue
|
|
194
|
+
if not os.environ.get("SEARXNG_ALLOW_PRIVATE", "").strip() == "1":
|
|
195
|
+
_resolved = socket.getaddrinfo(_hostname, _parsed.port or 80, proto=socket.IPPROTO_TCP)
|
|
196
|
+
for _fam, _t, _p, _cn, _sa in _resolved:
|
|
197
|
+
_ip = ipaddress.ip_address(_sa[0])
|
|
198
|
+
if _ip.is_loopback or _ip.is_private or _ip.is_link_local or _ip.is_reserved:
|
|
199
|
+
print(color(f" ❌ Blocked: {_hostname} resolves to private IP {_ip}.", Colors.RED))
|
|
200
|
+
print(color(f" Set SEARXNG_ALLOW_PRIVATE=1 if intentional.", Colors.DIM))
|
|
201
|
+
raise ValueError("private_ip")
|
|
202
|
+
except ValueError as _ve:
|
|
203
|
+
if str(_ve) == "private_ip":
|
|
204
|
+
continue
|
|
205
|
+
raise
|
|
206
|
+
except socket.gaierror:
|
|
207
|
+
print(color(f" ❌ Cannot resolve hostname: {_hostname}", Colors.RED))
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# Test connection
|
|
211
|
+
print(color(f" Testing connection to {url}...", Colors.DIM))
|
|
212
|
+
try:
|
|
213
|
+
import urllib.request
|
|
214
|
+
import urllib.error
|
|
215
|
+
|
|
216
|
+
test_url = f"{url.rstrip('/')}/search?q=test&format=json"
|
|
217
|
+
req = urllib.request.Request(
|
|
218
|
+
test_url,
|
|
219
|
+
headers={"User-Agent": "ClawdBot-WebSearchPlus/2.5", "Accept": "application/json"}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
223
|
+
data = response.read().decode("utf-8")
|
|
224
|
+
import json
|
|
225
|
+
result = json.loads(data)
|
|
226
|
+
|
|
227
|
+
# Check if it looks like SearXNG JSON response
|
|
228
|
+
if "results" in result or "query" in result:
|
|
229
|
+
print(color(f" ✓ Connection successful! SearXNG instance is working.", Colors.GREEN))
|
|
230
|
+
return url.rstrip("/")
|
|
231
|
+
else:
|
|
232
|
+
print(color(f" ⚠️ Connected but response doesn't look like SearXNG JSON.", Colors.YELLOW))
|
|
233
|
+
if ask_yes_no(" Use this URL anyway?", default=False):
|
|
234
|
+
return url.rstrip("/")
|
|
235
|
+
|
|
236
|
+
except urllib.error.HTTPError as e:
|
|
237
|
+
if e.code == 403:
|
|
238
|
+
print(color(f" ⚠️ JSON API is disabled (403 Forbidden).", Colors.YELLOW))
|
|
239
|
+
print(color(f" Enable JSON in settings.yml: search.formats: [html, json]", Colors.DIM))
|
|
240
|
+
else:
|
|
241
|
+
print(color(f" ⚠️ HTTP error: {e.code} {e.reason}", Colors.YELLOW))
|
|
242
|
+
|
|
243
|
+
if ask_yes_no(" Try a different URL?", default=True):
|
|
244
|
+
continue
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
except urllib.error.URLError as e:
|
|
248
|
+
print(color(f" ⚠️ Cannot reach instance: {e.reason}", Colors.YELLOW))
|
|
249
|
+
if ask_yes_no(" Try a different URL?", default=True):
|
|
250
|
+
continue
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print(color(f" ⚠️ Error: {e}", Colors.YELLOW))
|
|
255
|
+
if ask_yes_no(" Try a different URL?", default=True):
|
|
256
|
+
continue
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def ask_result_count() -> int:
|
|
260
|
+
"""Ask for default result count."""
|
|
261
|
+
options = ["3 (fast, minimal)", "5 (balanced - recommended)", "10 (comprehensive)"]
|
|
262
|
+
choice = ask_choice("Default number of results per search?", options, "5 (balanced - recommended)")
|
|
263
|
+
|
|
264
|
+
if "3" in choice:
|
|
265
|
+
return 3
|
|
266
|
+
elif "10" in choice:
|
|
267
|
+
return 10
|
|
268
|
+
return 5
|
|
269
|
+
|
|
270
|
+
def run_setup(skill_dir: Path, force_reset: bool = False):
|
|
271
|
+
"""Run the interactive setup wizard."""
|
|
272
|
+
config_path = skill_dir / "config.json"
|
|
273
|
+
example_path = skill_dir / "config.example.json"
|
|
274
|
+
|
|
275
|
+
# Check if config already exists
|
|
276
|
+
if config_path.exists() and not force_reset:
|
|
277
|
+
print(color("✓ config.json already exists!", Colors.GREEN))
|
|
278
|
+
print()
|
|
279
|
+
if not ask_yes_no("Do you want to reconfigure?", default=False):
|
|
280
|
+
print(color("Setup cancelled. Your existing config is unchanged.", Colors.DIM))
|
|
281
|
+
return False
|
|
282
|
+
print()
|
|
283
|
+
|
|
284
|
+
print_header()
|
|
285
|
+
print_provider_info()
|
|
286
|
+
|
|
287
|
+
# Load example config as base
|
|
288
|
+
if example_path.exists():
|
|
289
|
+
with open(example_path) as f:
|
|
290
|
+
config = json.load(f)
|
|
291
|
+
else:
|
|
292
|
+
config = {
|
|
293
|
+
"defaults": {"provider": "serper", "max_results": 5},
|
|
294
|
+
"auto_routing": {"enabled": True, "fallback_provider": "serper"},
|
|
295
|
+
"serper": {},
|
|
296
|
+
"tavily": {},
|
|
297
|
+
"exa": {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Remove any existing API keys from example
|
|
301
|
+
for provider in ["serper", "tavily", "exa"]:
|
|
302
|
+
if provider in config:
|
|
303
|
+
config[provider].pop("api_key", None)
|
|
304
|
+
|
|
305
|
+
enabled_providers = []
|
|
306
|
+
|
|
307
|
+
# ===== Question 1: Which providers to enable =====
|
|
308
|
+
print(color("─" * 60, Colors.DIM))
|
|
309
|
+
print(color("\n📋 Step 1: Choose Your Providers\n", Colors.BOLD))
|
|
310
|
+
print("Select which search providers you want to enable.")
|
|
311
|
+
print(color("(You need at least one API key to use this skill)", Colors.DIM))
|
|
312
|
+
print()
|
|
313
|
+
|
|
314
|
+
providers_info = {
|
|
315
|
+
"serper": ("Serper", "https://serper.dev", "Google results, shopping, local"),
|
|
316
|
+
"tavily": ("Tavily", "https://tavily.com", "Research, explanations, analysis"),
|
|
317
|
+
"exa": ("Exa", "https://exa.ai", "Semantic search, similar content"),
|
|
318
|
+
"you": ("You.com", "https://api.you.com", "RAG applications, real-time info"),
|
|
319
|
+
"searxng": ("SearXNG", "https://docs.searxng.org/admin/installation.html", "Privacy-first, self-hosted, $0 cost")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for provider, (name, url, desc) in providers_info.items():
|
|
323
|
+
print(f" {color(name, Colors.BOLD)}: {desc}")
|
|
324
|
+
|
|
325
|
+
# Special handling for SearXNG
|
|
326
|
+
if provider == "searxng":
|
|
327
|
+
print(color(" Note: SearXNG requires a self-hosted instance (no API key needed)", Colors.DIM))
|
|
328
|
+
if ask_yes_no(f" Do you have a SearXNG instance?", default=False):
|
|
329
|
+
instance_url = ask_searxng_instance(url)
|
|
330
|
+
if instance_url:
|
|
331
|
+
if "searxng" not in config:
|
|
332
|
+
config["searxng"] = {}
|
|
333
|
+
config["searxng"]["instance_url"] = instance_url
|
|
334
|
+
enabled_providers.append(provider)
|
|
335
|
+
else:
|
|
336
|
+
print(color(f" → {name} disabled (no instance URL)", Colors.DIM))
|
|
337
|
+
else:
|
|
338
|
+
print(color(f" → {name} skipped (no instance)", Colors.DIM))
|
|
339
|
+
else:
|
|
340
|
+
if ask_yes_no(f" Enable {name}?", default=True):
|
|
341
|
+
# ===== Question 2: API key for each enabled provider =====
|
|
342
|
+
api_key = ask_api_key(name, url)
|
|
343
|
+
if api_key:
|
|
344
|
+
config[provider]["api_key"] = api_key
|
|
345
|
+
enabled_providers.append(provider)
|
|
346
|
+
else:
|
|
347
|
+
print(color(f" → {name} disabled (no API key)", Colors.DIM))
|
|
348
|
+
else:
|
|
349
|
+
print(color(f" → {name} disabled", Colors.DIM))
|
|
350
|
+
print()
|
|
351
|
+
|
|
352
|
+
if not enabled_providers:
|
|
353
|
+
print()
|
|
354
|
+
print(color("⚠️ No providers enabled!", Colors.RED))
|
|
355
|
+
print("You need at least one API key to use web-search-plus.")
|
|
356
|
+
print("Run this setup again when you have an API key.")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
# ===== Question 3: Default provider =====
|
|
360
|
+
print(color("─" * 60, Colors.DIM))
|
|
361
|
+
print(color("\n⚙️ Step 2: Default Settings\n", Colors.BOLD))
|
|
362
|
+
|
|
363
|
+
if len(enabled_providers) > 1:
|
|
364
|
+
default_provider = ask_choice(
|
|
365
|
+
"Which provider should be the default for general queries?",
|
|
366
|
+
enabled_providers,
|
|
367
|
+
enabled_providers[0]
|
|
368
|
+
)
|
|
369
|
+
else:
|
|
370
|
+
default_provider = enabled_providers[0]
|
|
371
|
+
print(f"Default provider: {color(default_provider, Colors.GREEN)} (only one enabled)")
|
|
372
|
+
|
|
373
|
+
config["defaults"]["provider"] = default_provider
|
|
374
|
+
config["auto_routing"]["fallback_provider"] = default_provider
|
|
375
|
+
|
|
376
|
+
# ===== Question 4: Auto-routing =====
|
|
377
|
+
print()
|
|
378
|
+
print(color("Auto-routing", Colors.BOLD) + " automatically picks the best provider for each query:")
|
|
379
|
+
print(color(" • 'iPhone price' → Serper (shopping intent)", Colors.DIM))
|
|
380
|
+
print(color(" • 'how does TCP work' → Tavily (research intent)", Colors.DIM))
|
|
381
|
+
print(color(" • 'companies like Stripe' → Exa (discovery intent)", Colors.DIM))
|
|
382
|
+
print()
|
|
383
|
+
|
|
384
|
+
auto_routing = ask_yes_no("Enable auto-routing?", default=True)
|
|
385
|
+
config["auto_routing"]["enabled"] = auto_routing
|
|
386
|
+
|
|
387
|
+
if not auto_routing:
|
|
388
|
+
print(color(f" → All queries will use {default_provider}", Colors.DIM))
|
|
389
|
+
|
|
390
|
+
# ===== Question 5: Result count =====
|
|
391
|
+
print()
|
|
392
|
+
max_results = ask_result_count()
|
|
393
|
+
config["defaults"]["max_results"] = max_results
|
|
394
|
+
|
|
395
|
+
# Set disabled providers
|
|
396
|
+
all_providers = ["serper", "tavily", "exa", "you", "searxng"]
|
|
397
|
+
disabled = [p for p in all_providers if p not in enabled_providers]
|
|
398
|
+
config["auto_routing"]["disabled_providers"] = disabled
|
|
399
|
+
|
|
400
|
+
# ===== Save config =====
|
|
401
|
+
print()
|
|
402
|
+
print(color("─" * 60, Colors.DIM))
|
|
403
|
+
print(color("\n💾 Saving Configuration\n", Colors.BOLD))
|
|
404
|
+
|
|
405
|
+
with open(config_path, 'w') as f:
|
|
406
|
+
json.dump(config, f, indent=2)
|
|
407
|
+
|
|
408
|
+
print(color(f"✓ Configuration saved to: {config_path}", Colors.GREEN))
|
|
409
|
+
print()
|
|
410
|
+
|
|
411
|
+
# ===== Summary =====
|
|
412
|
+
print(color("📋 Configuration Summary:", Colors.BOLD))
|
|
413
|
+
print(f" Enabled providers: {', '.join(enabled_providers)}")
|
|
414
|
+
print(f" Default provider: {default_provider}")
|
|
415
|
+
print(f" Auto-routing: {'enabled' if auto_routing else 'disabled'}")
|
|
416
|
+
print(f" Results per search: {max_results}")
|
|
417
|
+
print()
|
|
418
|
+
|
|
419
|
+
# ===== Test suggestion =====
|
|
420
|
+
print(color("🚀 Ready to search! Try:", Colors.BOLD))
|
|
421
|
+
print(color(f" python3 scripts/search.py -q \"your query here\"", Colors.CYAN))
|
|
422
|
+
print()
|
|
423
|
+
|
|
424
|
+
return True
|
|
425
|
+
|
|
426
|
+
def check_first_run(skill_dir: Path) -> bool:
|
|
427
|
+
"""Check if this is the first run (no config.json)."""
|
|
428
|
+
config_path = skill_dir / "config.json"
|
|
429
|
+
return not config_path.exists()
|
|
430
|
+
|
|
431
|
+
def main():
|
|
432
|
+
# Determine skill directory
|
|
433
|
+
script_path = Path(__file__).resolve()
|
|
434
|
+
skill_dir = script_path.parent.parent
|
|
435
|
+
|
|
436
|
+
# Check for --reset flag
|
|
437
|
+
force_reset = "--reset" in sys.argv
|
|
438
|
+
|
|
439
|
+
# Check for --check flag (just check if setup needed)
|
|
440
|
+
if "--check" in sys.argv:
|
|
441
|
+
if check_first_run(skill_dir):
|
|
442
|
+
print("Setup required: config.json not found")
|
|
443
|
+
sys.exit(1)
|
|
444
|
+
else:
|
|
445
|
+
print("Setup complete: config.json exists")
|
|
446
|
+
sys.exit(0)
|
|
447
|
+
|
|
448
|
+
# Run setup
|
|
449
|
+
success = run_setup(skill_dir, force_reset)
|
|
450
|
+
sys.exit(0 if success else 1)
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
main()
|