gsearch-mcp-cli 0.0.1__py3-none-any.whl
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.
- gsearch_mcp_cli/__init__.py +6 -0
- gsearch_mcp_cli/cli/__init__.py +0 -0
- gsearch_mcp_cli/cli/ai_cmd.py +83 -0
- gsearch_mcp_cli/cli/ai_doc.py +88 -0
- gsearch_mcp_cli/cli/config_cmd.py +64 -0
- gsearch_mcp_cli/cli/doctor_cmd.py +77 -0
- gsearch_mcp_cli/cli/fetch_cmd.py +50 -0
- gsearch_mcp_cli/cli/main.py +64 -0
- gsearch_mcp_cli/cli/search_cmd.py +92 -0
- gsearch_mcp_cli/cli/setup_cmd.py +390 -0
- gsearch_mcp_cli/cli/skill_cmd.py +157 -0
- gsearch_mcp_cli/config.py +78 -0
- gsearch_mcp_cli/constants.py +143 -0
- gsearch_mcp_cli/core/__init__.py +0 -0
- gsearch_mcp_cli/core/browser.py +217 -0
- gsearch_mcp_cli/core/cdp.py +198 -0
- gsearch_mcp_cli/core/extractor.py +229 -0
- gsearch_mcp_cli/core/fetcher.py +187 -0
- gsearch_mcp_cli/core/rate_limiter.py +49 -0
- gsearch_mcp_cli/core/warmup.py +61 -0
- gsearch_mcp_cli/data/SKILL.md +202 -0
- gsearch_mcp_cli/data/references/commands.md +220 -0
- gsearch_mcp_cli/data/references/mcp-tools.md +44 -0
- gsearch_mcp_cli/exceptions.py +46 -0
- gsearch_mcp_cli/mcp/__init__.py +5 -0
- gsearch_mcp_cli/mcp/__main__.py +4 -0
- gsearch_mcp_cli/mcp/server.py +147 -0
- gsearch_mcp_cli/models.py +50 -0
- gsearch_mcp_cli/shared.py +289 -0
- gsearch_mcp_cli-0.0.1.dist-info/METADATA +131 -0
- gsearch_mcp_cli-0.0.1.dist-info/RECORD +33 -0
- gsearch_mcp_cli-0.0.1.dist-info/WHEEL +4 -0
- gsearch_mcp_cli-0.0.1.dist-info/entry_points.txt +3 -0
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
|
|
7
|
+
from gsearch_mcp_cli import shared
|
|
8
|
+
from gsearch_mcp_cli.exceptions import GSearchError
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TIME_CHOICES = click.Choice(["hour", "1h", "12h", "day", "24h", "week", "7d", "month", "year"], case_sensitive=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("ai")
|
|
17
|
+
@click.argument("query")
|
|
18
|
+
@click.option("--json", "json_output", is_flag=True, help="JSON output")
|
|
19
|
+
@click.option("--lang", default="en", help="Search language")
|
|
20
|
+
@click.option("--region", default=None, help="Search region (e.g., US, CA, GB)")
|
|
21
|
+
@click.option("--time", "time_filter", type=TIME_CHOICES, default=None,
|
|
22
|
+
help="Time filter: hour, day, week, month, year")
|
|
23
|
+
@click.option("--after", "date_from", default=None, help="Results after date (YYYY-MM-DD)")
|
|
24
|
+
@click.option("--before", "date_to", default=None, help="Results before date (YYYY-MM-DD)")
|
|
25
|
+
@click.option("--timeout", default=30, type=float, help="Timeout in seconds")
|
|
26
|
+
def ai_cmd(query, json_output, lang, region, time_filter, date_from, date_to, timeout):
|
|
27
|
+
"""Run a Google AI Mode search and return synthesized results."""
|
|
28
|
+
async def _run():
|
|
29
|
+
try:
|
|
30
|
+
result = await shared.ai_search(
|
|
31
|
+
query=query,
|
|
32
|
+
language=lang,
|
|
33
|
+
region=region,
|
|
34
|
+
time_filter=time_filter,
|
|
35
|
+
date_from=date_from,
|
|
36
|
+
date_to=date_to,
|
|
37
|
+
timeout=timeout,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if json_output:
|
|
41
|
+
click.echo(result.model_dump_json(indent=2))
|
|
42
|
+
else:
|
|
43
|
+
_print_ai_result(result)
|
|
44
|
+
|
|
45
|
+
except GSearchError as e:
|
|
46
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
finally:
|
|
49
|
+
await shared.shutdown()
|
|
50
|
+
|
|
51
|
+
asyncio.run(_run())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _print_ai_result(result):
|
|
55
|
+
"""Pretty-print AI Mode results with Rich panels."""
|
|
56
|
+
if result.synthesis:
|
|
57
|
+
console.print()
|
|
58
|
+
console.print(Panel(
|
|
59
|
+
result.synthesis,
|
|
60
|
+
title=f"AI Synthesis — {result.query}",
|
|
61
|
+
border_style="green",
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
if result.sections:
|
|
65
|
+
for section in result.sections:
|
|
66
|
+
body_parts = []
|
|
67
|
+
if section.content:
|
|
68
|
+
body_parts.append(section.content)
|
|
69
|
+
if section.items:
|
|
70
|
+
body_parts.extend(f" • {item}" for item in section.items)
|
|
71
|
+
console.print(Panel(
|
|
72
|
+
"\n".join(body_parts) if body_parts else "[dim]No content[/dim]",
|
|
73
|
+
title=section.heading,
|
|
74
|
+
border_style="cyan",
|
|
75
|
+
))
|
|
76
|
+
|
|
77
|
+
if result.sources:
|
|
78
|
+
console.print("\n[bold]Sources[/bold]")
|
|
79
|
+
console.print("─" * 60)
|
|
80
|
+
for src in result.sources:
|
|
81
|
+
console.print(f" [link={src.url}]{src.domain}[/link] — {src.title}")
|
|
82
|
+
|
|
83
|
+
console.print()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
AI_DOC = """gsearch-mcp-cli v{version} — Google Search CLI and MCP for AI Agents
|
|
2
|
+
|
|
3
|
+
SETUP
|
|
4
|
+
gsearch setup # One-time profile warmup (~8s)
|
|
5
|
+
gsearch setup --check # Verify profile works
|
|
6
|
+
gsearch setup --reset # Delete and recreate profile
|
|
7
|
+
gsearch setup add cursor # Configure MCP for Cursor
|
|
8
|
+
gsearch setup add claude-code # Configure MCP for Claude Code
|
|
9
|
+
gsearch setup list # Show all MCP setup targets
|
|
10
|
+
gsearch doctor # Diagnose browser, profile, connectivity
|
|
11
|
+
|
|
12
|
+
SEARCH (default command)
|
|
13
|
+
gsearch "query" # Regular Google search (default)
|
|
14
|
+
gsearch search "query" --json # JSON output
|
|
15
|
+
gsearch search "query" --no-ai # Skip AI Overview
|
|
16
|
+
gsearch search "query" -n 5 # Max 5 results
|
|
17
|
+
gsearch search "query" --lang fr # French results
|
|
18
|
+
gsearch search "query" --region CA # Canadian results
|
|
19
|
+
|
|
20
|
+
TIME-FILTERED SEARCH (use for news, recent events, time-sensitive queries)
|
|
21
|
+
gsearch "AI news" --time hour # Last hour
|
|
22
|
+
gsearch "AI news" --time 1h # Last hour (alias)
|
|
23
|
+
gsearch "news" --time 12h # Last 12 hours
|
|
24
|
+
gsearch "news" --time day # Last 24 hours
|
|
25
|
+
gsearch "news" --time 24h # Last 24 hours (alias)
|
|
26
|
+
gsearch "updates" --time week # Last 7 days
|
|
27
|
+
gsearch "updates" --time 7d # Last 7 days (alias)
|
|
28
|
+
gsearch "developments" --time month # Last month
|
|
29
|
+
gsearch "report" --time year # Last year
|
|
30
|
+
gsearch "events" --time 6h # Last 6 hours (arbitrary)
|
|
31
|
+
gsearch "earnings" --after 2026-01-01 --before 2026-03-31 # Custom date range
|
|
32
|
+
|
|
33
|
+
Time filter values: hour, 1h, 12h, day, 24h, week, 7d, month, 1m, year, 1y
|
|
34
|
+
Arbitrary hours: any "Nh" pattern (e.g., 3h, 6h, 18h)
|
|
35
|
+
Custom range: --after YYYY-MM-DD and/or --before YYYY-MM-DD
|
|
36
|
+
|
|
37
|
+
AI MODE (Google AI Mode / g.ai)
|
|
38
|
+
gsearch ai "query" # AI Mode synthesized response
|
|
39
|
+
gsearch ai "query" --json # JSON output
|
|
40
|
+
gsearch ai "query" --time day # With time filter
|
|
41
|
+
gsearch ai "query" --after 2026-01-01 # With date range
|
|
42
|
+
|
|
43
|
+
FETCH (URL to Markdown)
|
|
44
|
+
gsearch fetch "url" # Fetch and convert to Markdown
|
|
45
|
+
gsearch fetch "url" --raw # Plain text output
|
|
46
|
+
gsearch fetch "url" --json # JSON with metadata
|
|
47
|
+
gsearch fetch "url" --browser always # Force browser rendering
|
|
48
|
+
|
|
49
|
+
CONFIG
|
|
50
|
+
gsearch config show # Show all settings
|
|
51
|
+
gsearch config set browser brave # Set preferred browser
|
|
52
|
+
gsearch config set rate_limit 1.0 # Search throttle (seconds)
|
|
53
|
+
|
|
54
|
+
SKILL INSTALL
|
|
55
|
+
gsearch skill install cursor # Install for Cursor
|
|
56
|
+
gsearch skill install claude-code # Install for Claude Code
|
|
57
|
+
gsearch skill install cc-claw # Install for CC-Claw
|
|
58
|
+
gsearch skill list # Show all targets
|
|
59
|
+
|
|
60
|
+
MCP TOOLS (for AI agents via MCP protocol)
|
|
61
|
+
gsearch_search(query, max_results=10, include_ai=True, language="en",
|
|
62
|
+
time_filter=None, date_from=None, date_to=None, timeout_seconds=30)
|
|
63
|
+
gsearch_ai(query, language="en",
|
|
64
|
+
time_filter=None, date_from=None, date_to=None, timeout_seconds=30)
|
|
65
|
+
gsearch_fetch(url, format="md", use_browser="auto", timeout_seconds=30)
|
|
66
|
+
|
|
67
|
+
time_filter: "hour", "12h", "day", "week", "month", "year", or any "Nh" (e.g., "6h")
|
|
68
|
+
date_from / date_to: "YYYY-MM-DD" for custom date range
|
|
69
|
+
|
|
70
|
+
WHEN TO USE TIME FILTERS
|
|
71
|
+
"latest news about X" -> time_filter="day" or "12h"
|
|
72
|
+
"what happened today" -> time_filter="day"
|
|
73
|
+
"breaking news" -> time_filter="hour"
|
|
74
|
+
"recent developments" -> time_filter="week"
|
|
75
|
+
"this month's updates" -> time_filter="month"
|
|
76
|
+
"between Jan and March" -> date_from="2026-01-01", date_to="2026-03-31"
|
|
77
|
+
|
|
78
|
+
NOTES
|
|
79
|
+
- First run: gsearch setup runs automatically on first search
|
|
80
|
+
- No Google account needed: anonymous, pseudonymous sessions
|
|
81
|
+
- Rate limited: 1 search/2s default (configurable)
|
|
82
|
+
- Headless: no visible browser window
|
|
83
|
+
- SSRF protection: fetch validates URLs against private IP ranges
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_ai_doc(version: str) -> str:
|
|
88
|
+
return AI_DOC.format(version=version)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""gsearch config — view and modify configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from gsearch_mcp_cli.config import DEFAULTS, Config
|
|
10
|
+
from gsearch_mcp_cli.exceptions import ConfigError
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group("config")
|
|
16
|
+
def config_cmd() -> None:
|
|
17
|
+
"""View and modify gsearch configuration."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@config_cmd.command("show")
|
|
21
|
+
def config_show() -> None:
|
|
22
|
+
"""Print all configuration values."""
|
|
23
|
+
cfg = Config()
|
|
24
|
+
data = cfg.show()
|
|
25
|
+
|
|
26
|
+
table = Table(title="gsearch configuration")
|
|
27
|
+
table.add_column("Key", style="cyan", no_wrap=True)
|
|
28
|
+
table.add_column("Value", style="green")
|
|
29
|
+
table.add_column("Default", style="dim")
|
|
30
|
+
|
|
31
|
+
for key in sorted(data):
|
|
32
|
+
current = data[key]
|
|
33
|
+
default = DEFAULTS.get(key)
|
|
34
|
+
style = "" if current == default else "bold yellow"
|
|
35
|
+
table.add_row(key, str(current), str(default), style=style)
|
|
36
|
+
|
|
37
|
+
console.print(table)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@config_cmd.command("get")
|
|
41
|
+
@click.argument("key")
|
|
42
|
+
def config_get(key: str) -> None:
|
|
43
|
+
"""Get a single configuration value."""
|
|
44
|
+
cfg = Config()
|
|
45
|
+
try:
|
|
46
|
+
value = cfg.get(key)
|
|
47
|
+
console.print(f"[cyan]{key}[/] = [green]{value}[/]")
|
|
48
|
+
except ConfigError as exc:
|
|
49
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@config_cmd.command("set")
|
|
54
|
+
@click.argument("key")
|
|
55
|
+
@click.argument("value")
|
|
56
|
+
def config_set(key: str, value: str) -> None:
|
|
57
|
+
"""Set a configuration value."""
|
|
58
|
+
cfg = Config()
|
|
59
|
+
try:
|
|
60
|
+
cfg.set(key, value)
|
|
61
|
+
console.print(f"[green]✓[/] Set [cyan]{key}[/] = [green]{cfg.get(key)}[/]")
|
|
62
|
+
except ConfigError as exc:
|
|
63
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
|
64
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""gsearch doctor — run diagnostic checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from gsearch_mcp_cli.config import Config
|
|
9
|
+
from gsearch_mcp_cli.constants import CHROME_PROFILE_DIR
|
|
10
|
+
from gsearch_mcp_cli.core.browser import find_browser
|
|
11
|
+
from gsearch_mcp_cli.exceptions import BrowserNotFoundError
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
PASS = "[bold green]✓[/]"
|
|
16
|
+
FAIL = "[bold red]✗[/]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_browser() -> bool:
|
|
20
|
+
try:
|
|
21
|
+
path = find_browser()
|
|
22
|
+
console.print(f" {PASS} Browser found: {path}")
|
|
23
|
+
return True
|
|
24
|
+
except BrowserNotFoundError as exc:
|
|
25
|
+
console.print(f" {FAIL} Browser not found: {exc}")
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _check_profile_dir() -> bool:
|
|
30
|
+
if CHROME_PROFILE_DIR.exists():
|
|
31
|
+
console.print(f" {PASS} Profile directory exists: {CHROME_PROFILE_DIR}")
|
|
32
|
+
return True
|
|
33
|
+
console.print(f" {FAIL} Profile directory missing: {CHROME_PROFILE_DIR}")
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_warmed_up() -> bool:
|
|
38
|
+
marker = CHROME_PROFILE_DIR / ".warmed_up"
|
|
39
|
+
if marker.exists():
|
|
40
|
+
console.print(f" {PASS} Profile is warmed up")
|
|
41
|
+
return True
|
|
42
|
+
console.print(f" {FAIL} Profile not warmed up — run: gsearch setup")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _check_config() -> bool:
|
|
47
|
+
try:
|
|
48
|
+
cfg = Config()
|
|
49
|
+
cfg.show()
|
|
50
|
+
console.print(f" {PASS} Config file valid")
|
|
51
|
+
return True
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
console.print(f" {FAIL} Config error: {exc}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@click.command("doctor")
|
|
58
|
+
def doctor_cmd() -> None:
|
|
59
|
+
"""Run diagnostic checks on gsearch environment."""
|
|
60
|
+
console.print("[bold]Running diagnostics…[/]\n")
|
|
61
|
+
|
|
62
|
+
checks = [
|
|
63
|
+
("Browser", _check_browser),
|
|
64
|
+
("Profile directory", _check_profile_dir),
|
|
65
|
+
("Profile warmup", _check_warmed_up),
|
|
66
|
+
("Configuration", _check_config),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
passed = sum(1 for _, fn in checks if fn())
|
|
70
|
+
total = len(checks)
|
|
71
|
+
|
|
72
|
+
console.print()
|
|
73
|
+
if passed == total:
|
|
74
|
+
console.print(f"[bold green]All {total} checks passed.[/]")
|
|
75
|
+
else:
|
|
76
|
+
console.print(f"[bold yellow]{passed}/{total} checks passed.[/]")
|
|
77
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from gsearch_mcp_cli import shared
|
|
7
|
+
from gsearch_mcp_cli.exceptions import GSearchError
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command("fetch")
|
|
13
|
+
@click.argument("url")
|
|
14
|
+
@click.option("--raw", is_flag=True, help="Output plain text instead of markdown")
|
|
15
|
+
@click.option("--json", "json_output", is_flag=True, help="JSON output")
|
|
16
|
+
@click.option("--browser", type=click.Choice(["auto", "always", "never"]),
|
|
17
|
+
default="auto", help="Browser usage mode")
|
|
18
|
+
@click.option("--timeout", default=30, type=float, help="Timeout in seconds")
|
|
19
|
+
def fetch_cmd(url, raw, json_output, browser, timeout):
|
|
20
|
+
"""Fetch a URL and return its content."""
|
|
21
|
+
async def _run():
|
|
22
|
+
try:
|
|
23
|
+
fmt = "txt" if raw else "md"
|
|
24
|
+
result = await shared.fetch(
|
|
25
|
+
url=url,
|
|
26
|
+
format=fmt,
|
|
27
|
+
use_browser=browser,
|
|
28
|
+
timeout=timeout,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if json_output:
|
|
32
|
+
click.echo(result.model_dump_json(indent=2))
|
|
33
|
+
else:
|
|
34
|
+
_print_fetch_result(result, raw=raw)
|
|
35
|
+
|
|
36
|
+
except GSearchError as e:
|
|
37
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
finally:
|
|
40
|
+
await shared.shutdown()
|
|
41
|
+
|
|
42
|
+
asyncio.run(_run())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _print_fetch_result(result, *, raw=False):
|
|
46
|
+
"""Print fetched content."""
|
|
47
|
+
if not raw:
|
|
48
|
+
console.print(f"\n[bold]{result.title}[/bold]")
|
|
49
|
+
console.print(f"[dim]{result.url} • {result.word_count} words[/dim]\n")
|
|
50
|
+
click.echo(result.content)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from gsearch_mcp_cli import __version__
|
|
4
|
+
from gsearch_mcp_cli.cli.ai_cmd import ai_cmd
|
|
5
|
+
from gsearch_mcp_cli.cli.ai_doc import get_ai_doc
|
|
6
|
+
from gsearch_mcp_cli.cli.config_cmd import config_cmd
|
|
7
|
+
from gsearch_mcp_cli.cli.doctor_cmd import doctor_cmd
|
|
8
|
+
from gsearch_mcp_cli.cli.fetch_cmd import fetch_cmd
|
|
9
|
+
from gsearch_mcp_cli.cli.search_cmd import search_cmd
|
|
10
|
+
from gsearch_mcp_cli.cli.setup_cmd import setup_cmd
|
|
11
|
+
from gsearch_mcp_cli.cli.skill_cmd import skill_cmd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _print_docs(ctx, param, value):
|
|
15
|
+
if not value or ctx.resilient_parsing:
|
|
16
|
+
return
|
|
17
|
+
click.echo(get_ai_doc(__version__))
|
|
18
|
+
ctx.exit()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DefaultSearchGroup(click.Group):
|
|
22
|
+
"""Click group that falls back to 'search' for unknown commands."""
|
|
23
|
+
|
|
24
|
+
def parse_args(self, ctx, args):
|
|
25
|
+
if args and not args[0].startswith("-"):
|
|
26
|
+
if args[0] not in self.commands:
|
|
27
|
+
args = ["search"] + args
|
|
28
|
+
return super().parse_args(ctx, args)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group(cls=DefaultSearchGroup, invoke_without_command=True)
|
|
32
|
+
@click.version_option(__version__, prog_name="gsearch")
|
|
33
|
+
@click.option("--ai", is_flag=True, callback=_print_docs, expose_value=False,
|
|
34
|
+
is_eager=True, help="Print AI-optimized documentation")
|
|
35
|
+
@click.option("--verbose", is_flag=True, default=False, help="Enable debug logging")
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def cli(ctx, verbose):
|
|
38
|
+
"""gsearch — Google Search CLI for AI agents."""
|
|
39
|
+
ctx.ensure_object(dict)
|
|
40
|
+
ctx.obj["verbose"] = verbose
|
|
41
|
+
|
|
42
|
+
if verbose:
|
|
43
|
+
import logging
|
|
44
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
45
|
+
|
|
46
|
+
if ctx.invoked_subcommand is None:
|
|
47
|
+
click.echo(ctx.get_help())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
cli.add_command(search_cmd, "search")
|
|
51
|
+
cli.add_command(ai_cmd, "ai")
|
|
52
|
+
cli.add_command(fetch_cmd, "fetch")
|
|
53
|
+
cli.add_command(skill_cmd, "skill")
|
|
54
|
+
cli.add_command(setup_cmd, "setup")
|
|
55
|
+
cli.add_command(config_cmd, "config")
|
|
56
|
+
cli.add_command(doctor_cmd, "doctor")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
cli()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
|
|
7
|
+
from gsearch_mcp_cli import shared
|
|
8
|
+
from gsearch_mcp_cli.exceptions import GSearchError
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TIME_CHOICES = click.Choice(["hour", "1h", "12h", "day", "24h", "week", "7d", "month", "year"], case_sensitive=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("search")
|
|
17
|
+
@click.argument("query")
|
|
18
|
+
@click.option("--max-results", "-n", default=10, help="Max organic results")
|
|
19
|
+
@click.option("--json", "json_output", is_flag=True, help="JSON output")
|
|
20
|
+
@click.option("--no-ai", is_flag=True, help="Skip AI Overview extraction")
|
|
21
|
+
@click.option("--lang", default="en", help="Search language")
|
|
22
|
+
@click.option("--region", default=None, help="Search region (e.g., US, CA, GB)")
|
|
23
|
+
@click.option("--time", "time_filter", type=TIME_CHOICES, default=None,
|
|
24
|
+
help="Time filter: hour, day, week, month, year")
|
|
25
|
+
@click.option("--after", "date_from", default=None, help="Results after date (YYYY-MM-DD)")
|
|
26
|
+
@click.option("--before", "date_to", default=None, help="Results before date (YYYY-MM-DD)")
|
|
27
|
+
@click.option("--timeout", default=30, type=float, help="Timeout in seconds")
|
|
28
|
+
def search_cmd(query, max_results, json_output, no_ai, lang, region, time_filter, date_from, date_to, timeout):
|
|
29
|
+
"""Search Google and return results with AI Overview."""
|
|
30
|
+
async def _run():
|
|
31
|
+
try:
|
|
32
|
+
result = await shared.search(
|
|
33
|
+
query=query,
|
|
34
|
+
max_results=max_results,
|
|
35
|
+
include_ai=not no_ai,
|
|
36
|
+
language=lang,
|
|
37
|
+
region=region,
|
|
38
|
+
time_filter=time_filter,
|
|
39
|
+
date_from=date_from,
|
|
40
|
+
date_to=date_to,
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if json_output:
|
|
45
|
+
click.echo(result.model_dump_json(indent=2))
|
|
46
|
+
else:
|
|
47
|
+
_print_results(result)
|
|
48
|
+
|
|
49
|
+
except GSearchError as e:
|
|
50
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
51
|
+
raise SystemExit(1)
|
|
52
|
+
finally:
|
|
53
|
+
await shared.shutdown()
|
|
54
|
+
|
|
55
|
+
asyncio.run(_run())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _print_results(result):
|
|
59
|
+
"""Pretty-print search results with Rich."""
|
|
60
|
+
if result.ai_overview:
|
|
61
|
+
console.print()
|
|
62
|
+
console.print(Panel(
|
|
63
|
+
result.ai_overview.text[:1000],
|
|
64
|
+
title="AI Overview",
|
|
65
|
+
border_style="blue",
|
|
66
|
+
))
|
|
67
|
+
if result.ai_overview.sources:
|
|
68
|
+
sources = ", ".join(s.domain for s in result.ai_overview.sources)
|
|
69
|
+
console.print(f" [dim]Sources: {sources}[/dim]")
|
|
70
|
+
console.print()
|
|
71
|
+
|
|
72
|
+
if result.results:
|
|
73
|
+
console.print("[bold]Search Results[/bold]")
|
|
74
|
+
console.print("─" * 60)
|
|
75
|
+
for r in result.results:
|
|
76
|
+
console.print(f"\n[bold]{r.position}. {r.title}[/bold]")
|
|
77
|
+
console.print(f" [link={r.url}]{r.url}[/link]")
|
|
78
|
+
if r.snippet:
|
|
79
|
+
console.print(f" [dim]{r.snippet[:200]}[/dim]")
|
|
80
|
+
|
|
81
|
+
if result.people_also_ask:
|
|
82
|
+
console.print("\n[bold]People Also Ask[/bold]")
|
|
83
|
+
console.print("─" * 60)
|
|
84
|
+
for q in result.people_also_ask:
|
|
85
|
+
console.print(f" • {q}")
|
|
86
|
+
|
|
87
|
+
if result.related_searches:
|
|
88
|
+
console.print("\n[bold]Related Searches[/bold]")
|
|
89
|
+
for q in result.related_searches:
|
|
90
|
+
console.print(f" • {q}")
|
|
91
|
+
|
|
92
|
+
console.print()
|