querit-cli 0.1.0__tar.gz

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.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: querit-cli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for the Querit API — search from the command line.
5
+ License-Expression: MIT
6
+ Keywords: agents,ai,cli,querit,web-search
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: click>=8.1.0
9
+ Requires-Dist: querit>=0.1.5
10
+ Requires-Dist: rich>=13.0.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
13
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "querit-cli"
7
+ version = "0.1.0"
8
+ description = "CLI tool for the Querit API — search from the command line."
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ keywords = ["ai", "querit", "web-search", "cli", "agents"]
12
+
13
+ dependencies = [
14
+ "querit>=0.1.5",
15
+ "click>=8.1.0",
16
+ "rich>=13.0.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ querit = "querit_cli.cli:main"
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "pytest>=8.0.0",
25
+ "ruff>=0.4.0",
26
+ ]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["querit_cli"]
30
+
31
+ [tool.ruff]
32
+ line-length = 120
33
+ target-version = "py310"
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "W", "F", "I", "B", "UP"]
37
+ ignore = ["E501"]
@@ -0,0 +1,3 @@
1
+ """Querit CLI — search the web from the command line."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,85 @@
1
+ """Main CLI entry point — wires all commands into the `querit` group."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from querit_cli import __version__
8
+ from querit_cli.commands.auth import auth_status, login, logout
9
+ from querit_cli.commands.search import search
10
+
11
+
12
+ @click.group(invoke_without_command=True)
13
+ @click.option("--version", is_flag=True, default=False, help="Show version and exit.")
14
+ @click.option("--status", "show_status", is_flag=True, default=False, help="Show version and auth status.")
15
+ @click.option("--json", "json_output", is_flag=True, default=False, help="Output as JSON (for agents and scripts).")
16
+ @click.pass_context
17
+ def cli(ctx: click.Context, version: bool, show_status: bool, json_output: bool) -> None:
18
+ """Querit CLI — search the web from the command line.
19
+
20
+ Authenticate with: querit login --api-key YOUR_KEY
21
+ Or set the QUERIT_API_KEY environment variable.
22
+ """
23
+ ctx.ensure_object(dict)
24
+ ctx.obj["json_output"] = json_output
25
+
26
+ if version:
27
+ if json_output:
28
+ import json
29
+ click.echo(json.dumps({"version": __version__}))
30
+ else:
31
+ click.echo(f"querit-cli {__version__}")
32
+ ctx.exit(0)
33
+ return
34
+
35
+ if show_status:
36
+ _print_status(json_output)
37
+ ctx.exit(0)
38
+ return
39
+
40
+ if ctx.invoked_subcommand is None:
41
+ from querit_cli.repl import run_repl
42
+ run_repl()
43
+ ctx.exit(0)
44
+
45
+
46
+ def _print_status(json_output: bool) -> None:
47
+ """Show version + auth status."""
48
+ import json as _json
49
+ import os
50
+
51
+ from querit_cli.config import get_api_key
52
+
53
+ key = get_api_key()
54
+ authenticated = key is not None
55
+
56
+ if json_output:
57
+ click.echo(_json.dumps({
58
+ "version": __version__,
59
+ "authenticated": authenticated,
60
+ }))
61
+ else:
62
+ from rich.console import Console
63
+ console = Console()
64
+ console.print(f" [bold #00C2C2]querit[/bold #00C2C2] v{__version__}")
65
+ console.print()
66
+ if authenticated:
67
+ source = "QUERIT_API_KEY env var" if os.environ.get("QUERIT_API_KEY") else "~/.querit/config.json"
68
+ console.print(f" [#9BC0AE]>[/#9BC0AE] Authenticated via {source}")
69
+ else:
70
+ console.print(" [#FAA2FB]>[/#FAA2FB] Not authenticated")
71
+ console.print(" Run: querit login --api-key YOUR_KEY")
72
+
73
+
74
+ cli.add_command(login)
75
+ cli.add_command(logout)
76
+ cli.add_command(auth_status)
77
+ cli.add_command(search)
78
+
79
+
80
+ def main() -> None:
81
+ cli()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,79 @@
1
+ """Querit API client — wraps the official querit Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from querit import QueritClient as _SDKClient
8
+ from querit.models.request import SearchRequest, SearchFilters, SiteFilter, GeoFilter
9
+ from querit.errors import QueritError
10
+
11
+ from querit_cli.common import QueritAPIError
12
+
13
+
14
+ class QueritClient:
15
+ """Thin wrapper around the official Querit Python SDK."""
16
+
17
+ def __init__(self, api_key: str, timeout: int = 30) -> None:
18
+ # SDK internally sets: Authorization: Bearer {api_key}
19
+ # so pass the raw key only — do NOT prepend "Bearer " here
20
+ self._sdk = _SDKClient(api_key=api_key, timeout=timeout)
21
+
22
+ def search(
23
+ self,
24
+ query: str,
25
+ *,
26
+ count: int | None = None,
27
+ sites_include: list[str] | None = None,
28
+ sites_exclude: list[str] | None = None,
29
+ time_range: str | None = None,
30
+ countries: list[str] | None = None,
31
+ languages: list[str] | None = None,
32
+ ) -> dict[str, Any]:
33
+ """Call the Querit search API via the official SDK."""
34
+ search_filters = None
35
+ if any([sites_include, sites_exclude, time_range, countries, languages]):
36
+ search_filters = SearchFilters(
37
+ sites=SiteFilter(
38
+ include=sites_include or [],
39
+ exclude=sites_exclude or [],
40
+ ) if (sites_include or sites_exclude) else None,
41
+ geo=GeoFilter(
42
+ countries=countries,
43
+ ) if countries else None,
44
+ time_range=time_range,
45
+ languages=languages,
46
+ )
47
+
48
+ request = SearchRequest(
49
+ query=query,
50
+ **({} if count is None else {"count": count}),
51
+ **({"filters": search_filters} if search_filters else {}),
52
+ )
53
+
54
+ try:
55
+ response = self._sdk.search(request)
56
+ except QueritError as e:
57
+ raise QueritAPIError(str(e)) from e
58
+ except Exception as e:
59
+ raise QueritAPIError(f"Unexpected error: {e}") from e
60
+
61
+ # Normalise SDK response object → dict so output.py stays unchanged
62
+ results = [
63
+ {
64
+ "url": getattr(item, "url", ""),
65
+ "title": getattr(item, "title", ""),
66
+ "snippet": getattr(item, "snippet", "") or getattr(item, "description", ""),
67
+ "site_name": getattr(item, "site_name", ""),
68
+ "page_age": getattr(item, "page_age", ""),
69
+ "site_icon": getattr(item, "site_icon", ""),
70
+ }
71
+ for item in (response.results or [])
72
+ ]
73
+
74
+ return {
75
+ "took": getattr(response, "took", ""),
76
+ "search_id": getattr(response, "search_id", None),
77
+ "query_context": {"query": query},
78
+ "results": {"result": results},
79
+ }
File without changes
@@ -0,0 +1,54 @@
1
+ """querit login / logout / auth — credential management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.option("--api-key", default=None, help="Your Querit API key.")
10
+ def login(api_key: str | None) -> None:
11
+ """Save your Querit API key locally."""
12
+ from rich.console import Console
13
+ from querit_cli.config import save_api_key
14
+
15
+ console = Console(stderr=True)
16
+ if not api_key:
17
+ api_key = click.prompt(" Enter your Querit API key", hide_input=True)
18
+
19
+ save_api_key(api_key.strip())
20
+ console.print(" [#9BC0AE]>[/#9BC0AE] API key saved.")
21
+ console.print(" [dim]Stored in ~/.querit/config.json[/dim]")
22
+
23
+
24
+ @click.command()
25
+ def logout() -> None:
26
+ """Remove your saved Querit API key."""
27
+ from rich.console import Console
28
+ from querit_cli.config import clear_credentials
29
+
30
+ console = Console(stderr=True)
31
+ clear_credentials()
32
+ console.print(" [#9BC0AE]>[/#9BC0AE] Credentials cleared.")
33
+
34
+
35
+ @click.command("auth")
36
+ def auth_status() -> None:
37
+ """Show current authentication status."""
38
+ import os
39
+ from rich.console import Console
40
+ from querit_cli.config import get_api_key
41
+
42
+ console = Console(stderr=True)
43
+ key = get_api_key()
44
+
45
+ console.print()
46
+ if key:
47
+ masked = key[:8] + "..." if len(key) > 8 else key
48
+ source = "QUERIT_API_KEY env var" if os.environ.get("QUERIT_API_KEY") else "~/.querit/config.json"
49
+ console.print(f" [#9BC0AE]>[/#9BC0AE] Authenticated via {source}")
50
+ console.print(f" [dim]Key: {masked}[/dim]")
51
+ else:
52
+ console.print(" [#FAA2FB]>[/#FAA2FB] Not authenticated")
53
+ console.print(" Run: [#00C2C2]querit login --api-key YOUR_KEY[/#00C2C2]")
54
+ console.print()
@@ -0,0 +1,86 @@
1
+ """querit search — web search via the Querit API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from querit_cli.common import handle_api_error
8
+
9
+
10
+ @click.command()
11
+ @click.argument("query", required=False)
12
+ @click.option("--count", type=int, default=None,
13
+ help="Maximum number of results to return.")
14
+ @click.option("--sites-include", multiple=True, metavar="SITE",
15
+ help="Only include results from this site (repeatable). E.g. --sites-include bbc.com")
16
+ @click.option("--sites-exclude", multiple=True, metavar="SITE",
17
+ help="Exclude results from this site (repeatable). E.g. --sites-exclude reddit.com")
18
+ @click.option("--time-range", default=None, metavar="RANGE",
19
+ help=(
20
+ "Time range filter. Examples: "
21
+ "d7 (past 7 days), w2 (past 2 weeks), m3 (past 3 months), "
22
+ "y1 (past 1 year), 2024-01-01to2024-06-30 (date range)."
23
+ ))
24
+ @click.option("--countries", multiple=True, metavar="CC",
25
+ help="Limit results to these countries (repeatable). E.g. --countries france --countries germany")
26
+ @click.option("--languages", multiple=True, metavar="LANG",
27
+ help="Limit results to these languages (repeatable). E.g. --languages en --languages zh")
28
+ @click.option("--output", "-o", "output_file", default=None,
29
+ help="Save full JSON response to this file.")
30
+ @click.option("--json", "json_flag", is_flag=True, default=False,
31
+ help="Output as JSON (for scripts and agents).")
32
+ @click.pass_context
33
+ def search(
34
+ ctx: click.Context,
35
+ query: str | None,
36
+ count: int | None,
37
+ sites_include: tuple[str, ...],
38
+ sites_exclude: tuple[str, ...],
39
+ time_range: str | None,
40
+ countries: tuple[str, ...],
41
+ languages: tuple[str, ...],
42
+ output_file: str | None,
43
+ json_flag: bool,
44
+ ) -> None:
45
+ """Search the web using the Querit API.
46
+
47
+ QUERY is your search term.
48
+
49
+ Examples:
50
+
51
+ querit search "climate change"
52
+
53
+ querit search "python tutorial" --count 5
54
+
55
+ querit search "news" --time-range d7 --countries US
56
+
57
+ querit search "site news" --sites-include bbc.com --sites-include reuters.com
58
+ """
59
+ from querit_cli.config import get_client
60
+ from querit_cli.output import print_search_results
61
+ from querit_cli.theme import spinner
62
+
63
+ # Inherit --json from parent context (the global `querit --json` flag)
64
+ json_mode = json_flag or bool(ctx.obj and ctx.obj.get("json_output"))
65
+
66
+ if not query:
67
+ raise click.UsageError('QUERY is required. Example: querit search "your query"')
68
+
69
+ client = get_client()
70
+
71
+ try:
72
+ with spinner("Searching...", json_mode=json_mode):
73
+ response = client.search(
74
+ query,
75
+ count=count,
76
+ sites_include=list(sites_include) or None,
77
+ sites_exclude=list(sites_exclude) or None,
78
+ time_range=time_range,
79
+ countries=list(countries) or None,
80
+ languages=list(languages) or None,
81
+ )
82
+ except Exception as e:
83
+ handle_api_error(e, json_mode)
84
+ return
85
+
86
+ print_search_results(response, json_mode=json_mode, output_file=output_file)
@@ -0,0 +1,30 @@
1
+ """Shared utilities: custom exception, error handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import click
8
+
9
+ from querit_cli.theme import err_console
10
+
11
+
12
+ class QueritAPIError(Exception):
13
+ """Structured error from the Querit API."""
14
+
15
+ def __init__(self, message: str, *, status: int | None = None) -> None:
16
+ super().__init__(message)
17
+ self.status = status
18
+
19
+
20
+ def handle_api_error(e: Exception, json_mode: bool) -> None:
21
+ """Print a formatted error message and exit."""
22
+ if json_mode:
23
+ click.echo(json.dumps({"error": str(e)}))
24
+ raise SystemExit(4)
25
+ if isinstance(e, QueritAPIError) and e.status == 429:
26
+ err_console.print(f" [#FFC769]>[/#FFC769] Rate limit exceeded: {e}")
27
+ err_console.print(" [dim]Check your plan at[/dim] [#00C2C2 link=https://www.querit.ai]querit.ai[/#00C2C2 link=https://www.querit.ai]")
28
+ raise SystemExit(3)
29
+ err_console.print(f" [#FAA2FB]> Error:[/#FAA2FB] {e}")
30
+ raise SystemExit(4)
@@ -0,0 +1,77 @@
1
+ """API key storage and retrieval for the Querit CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ CONFIG_DIR = Path.home() / ".querit"
10
+ CONFIG_FILE = CONFIG_DIR / "config.json"
11
+
12
+ # Override base URL via environment variable if needed
13
+ BASE_URL = os.environ.get("QUERIT_BASE_URL", "https://api.querit.ai")
14
+
15
+
16
+ def _read_config() -> dict:
17
+ if CONFIG_FILE.exists():
18
+ try:
19
+ return json.loads(CONFIG_FILE.read_text())
20
+ except (json.JSONDecodeError, OSError):
21
+ return {}
22
+ return {}
23
+
24
+
25
+ def _write_config(data: dict) -> None:
26
+ old_umask = os.umask(0o077)
27
+ try:
28
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
29
+ CONFIG_DIR.chmod(0o700)
30
+ CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n")
31
+ CONFIG_FILE.chmod(0o600)
32
+ finally:
33
+ os.umask(old_umask)
34
+
35
+
36
+ def save_api_key(api_key: str) -> None:
37
+ config = _read_config()
38
+ config["api_key"] = api_key
39
+ _write_config(config)
40
+
41
+
42
+ def clear_credentials() -> None:
43
+ if CONFIG_FILE.exists():
44
+ CONFIG_FILE.unlink()
45
+
46
+
47
+ def get_api_key() -> str | None:
48
+ """Resolve the API key with precedence: env var > config file."""
49
+ key = os.environ.get("QUERIT_API_KEY")
50
+ if key:
51
+ return key
52
+ return _read_config().get("api_key")
53
+
54
+
55
+ def get_api_key_or_exit() -> str:
56
+ """Get the API key or print an error and exit."""
57
+ import sys
58
+
59
+ key = get_api_key()
60
+ if not key:
61
+ from rich.console import Console
62
+ console = Console(stderr=True)
63
+ console.print(" [#FAA2FB]> Error:[/#FAA2FB] No Querit API key found.")
64
+ console.print()
65
+ console.print(" Set your API key using one of:")
66
+ console.print(" [#00C2C2]querit login --api-key YOUR_KEY[/#00C2C2]")
67
+ console.print(" [dim]export QUERIT_API_KEY=YOUR_KEY[/dim]")
68
+ console.print()
69
+ console.print(" Get a key at [link=https://www.querit.ai]querit.ai[/link]")
70
+ sys.exit(3)
71
+ return key
72
+
73
+
74
+ def get_client():
75
+ """Return an authenticated QueritClient."""
76
+ from querit_cli.client import QueritClient
77
+ return QueritClient(api_key=get_api_key_or_exit())
@@ -0,0 +1,94 @@
1
+ """Formatted output for Querit CLI search results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.rule import Rule
11
+ from rich.text import Text
12
+ from rich.theme import Theme
13
+
14
+ _theme = Theme({
15
+ "markdown.h1": "bold #00C2C2",
16
+ "markdown.h2": "bold #00C2C2",
17
+ "markdown.h3": "bold #00C2C2",
18
+ })
19
+
20
+ console = Console(theme=_theme)
21
+ err_console = Console(stderr=True)
22
+
23
+
24
+ def emit(data: Any, *, json_mode: bool, output_file: str | None = None) -> None:
25
+ """Serialise data as JSON to stdout or a file."""
26
+ text = json.dumps(data, indent=2, ensure_ascii=False)
27
+ if output_file:
28
+ with open(output_file, "w", encoding="utf-8") as f:
29
+ f.write(text + "\n")
30
+ else:
31
+ click.echo(text)
32
+
33
+
34
+ def print_search_results(
35
+ response: dict,
36
+ *,
37
+ json_mode: bool,
38
+ output_file: str | None = None,
39
+ ) -> None:
40
+ """Render Querit search results in human-readable or JSON format."""
41
+ if json_mode:
42
+ emit(response, json_mode=True, output_file=output_file)
43
+ return
44
+
45
+ results = response.get("results", {}).get("result", [])
46
+ took = response.get("took", "")
47
+
48
+ if not results:
49
+ console.print()
50
+ console.print(" [dim]No results found.[/dim]")
51
+ console.print()
52
+ return
53
+
54
+ console.print()
55
+ for i, r in enumerate(results, 1):
56
+ title = r.get("title") or r.get("url", "Untitled")
57
+ url = r.get("url", "")
58
+ snippet = r.get("snippet", "")
59
+ site = r.get("site_name", "")
60
+ page_age = r.get("page_age", "")
61
+
62
+ # Title
63
+ title_text = Text()
64
+ title_text.append(f" {i}. ", style="dim")
65
+ title_text.append(title, style="bold #00C2C2")
66
+ console.print(title_text)
67
+
68
+ # Meta: site name + age + url
69
+ meta = Text(" ")
70
+ if site:
71
+ meta.append(site, style="#9BC0AE")
72
+ if page_age:
73
+ meta.append(f" {page_age}", style="dim")
74
+ if url:
75
+ meta.append(f" {url}", style="dim underline")
76
+ console.print(meta)
77
+
78
+ # Snippet
79
+ if snippet:
80
+ short = snippet[:300] + ("…" if len(snippet) > 300 else "")
81
+ console.print(f" [dim]{short}[/dim]")
82
+
83
+ console.print()
84
+
85
+ # Footer: result count + server response time
86
+ footer_parts = [f"{len(results)} results"]
87
+ if took:
88
+ footer_parts.append(took)
89
+ console.print(Rule(f"[dim]{' | '.join(footer_parts)}[/dim]", style="dim"))
90
+ console.print()
91
+
92
+ if output_file:
93
+ emit(response, json_mode=True, output_file=output_file)
94
+ err_console.print(f" [dim]Saved to {output_file}[/dim]")
@@ -0,0 +1,143 @@
1
+ """Interactive REPL — gives querit a clean, chat-like shell."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import readline # noqa: F401 — enables arrow-key history in input()
6
+ import shlex
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.rule import Rule
11
+ from rich.text import Text
12
+
13
+ from querit_cli import __version__
14
+ from querit_cli.config import get_api_key
15
+ from querit_cli.theme import LOGO
16
+
17
+ err_console = Console(stderr=True)
18
+
19
+ _REPL_COMMANDS = {"search", "login", "logout", "auth"}
20
+
21
+
22
+ def _print_banner() -> None:
23
+ """Print the welcome banner inside the REPL."""
24
+ key = get_api_key()
25
+
26
+ err_console.print(LOGO)
27
+ err_console.print(f" [dim]v{__version__}[/dim]")
28
+ err_console.print()
29
+
30
+ if key:
31
+ err_console.print(" [#9BC0AE]>[/#9BC0AE] Authenticated")
32
+ else:
33
+ err_console.print(" [#FAA2FB]>[/#FAA2FB] Not authenticated")
34
+ err_console.print(" Type [#00C2C2]login[/#00C2C2] to set your API key.")
35
+
36
+ err_console.print()
37
+
38
+ tips = Text()
39
+ tips.append(" Tips: ", style="bold")
40
+ tips.append("search ", style="#00C2C2")
41
+ tips.append('"query"', style="dim")
42
+ tips.append(" | ", style="dim")
43
+ tips.append("search ", style="#00C2C2")
44
+ tips.append('"query" --count 5', style="dim")
45
+ tips.append(" | ", style="dim")
46
+ tips.append("help", style="#00C2C2")
47
+ tips.append(" | ", style="dim")
48
+ tips.append("exit", style="#00C2C2")
49
+ err_console.print(tips)
50
+ err_console.print()
51
+
52
+
53
+ def _print_help() -> None:
54
+ """Print REPL help."""
55
+ err_console.print()
56
+ cmds = Text()
57
+ cmds.append(" Commands\n\n", style="bold")
58
+ cmds.append(' search "your query"', style="#00C2C2")
59
+ cmds.append(" Web search\n")
60
+ cmds.append(' search "query" --count N', style="#00C2C2")
61
+ cmds.append(" Limit result count\n")
62
+ cmds.append(' search "query" --time-range d7', style="#00C2C2")
63
+ cmds.append(" Past 7 days\n")
64
+ cmds.append(' search "query" --sites-include bbc.com', style="#00C2C2")
65
+ cmds.append(" Site filter\n")
66
+ cmds.append(' search "query" --countries US --languages en', style="#00C2C2")
67
+ cmds.append(" Geo/lang filter\n")
68
+ cmds.append(" login", style="#00C2C2")
69
+ cmds.append(" Authenticate\n")
70
+ cmds.append(" logout", style="#00C2C2")
71
+ cmds.append(" Clear credentials\n")
72
+ cmds.append(" auth", style="#00C2C2")
73
+ cmds.append(" Auth status\n")
74
+ cmds.append(" exit / quit / Ctrl+C", style="#00C2C2")
75
+ cmds.append(" Leave\n")
76
+ err_console.print(cmds)
77
+
78
+
79
+ def _prompt() -> str:
80
+ """Print the separator + prompt and read a line of input."""
81
+ err_console.print(Rule(style="dim"))
82
+ try:
83
+ return input("\001\033[38;2;0;194;194m\002\u276f\001\033[0m\002 ")
84
+ except EOFError:
85
+ return "exit"
86
+
87
+
88
+ def run_repl() -> None:
89
+ """Enter the interactive REPL loop."""
90
+ err_console.print()
91
+ _print_banner()
92
+
93
+ while True:
94
+ try:
95
+ line = _prompt()
96
+ except KeyboardInterrupt:
97
+ err_console.print()
98
+ err_console.print(" [dim]Farewell, hope to see you again![/dim]")
99
+ err_console.print()
100
+ break
101
+
102
+ line = line.strip()
103
+ if not line:
104
+ continue
105
+
106
+ if line in ("exit", "quit", "q"):
107
+ err_console.print()
108
+ err_console.print(" [dim]Farewell, hope to see you again![/dim]")
109
+ err_console.print()
110
+ break
111
+
112
+ if line in ("help", "?"):
113
+ _print_help()
114
+ continue
115
+
116
+ try:
117
+ args = shlex.split(line)
118
+ except ValueError as e:
119
+ err_console.print(f" [#FAA2FB]Parse error:[/#FAA2FB] {e}")
120
+ continue
121
+
122
+ # Allow "querit search ..." as well as plain "search ..."
123
+ if args and args[0] == "querit":
124
+ args = args[1:]
125
+
126
+ if not args:
127
+ continue
128
+
129
+ from querit_cli.cli import cli
130
+ err_console.print()
131
+ try:
132
+ cli(args, standalone_mode=False)
133
+ except SystemExit:
134
+ pass
135
+ except KeyboardInterrupt:
136
+ err_console.print()
137
+ err_console.print(" [dim]Cancelled.[/dim]")
138
+ except click.exceptions.UsageError as e:
139
+ err_console.print(f" [#FAA2FB]>[/#FAA2FB] {e.format_message()}")
140
+ except Exception as e:
141
+ err_console.print(f" [#FAA2FB]> Error:[/#FAA2FB] {e}")
142
+
143
+ err_console.print()
@@ -0,0 +1,43 @@
1
+ """Brand colours, Rich console instances, and the spinner helper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import contextmanager
6
+ from typing import Generator
7
+
8
+ from rich.console import Console
9
+
10
+ # Brand colours
11
+ TEAL = "#00C2C2"
12
+ PINK = "#FAA2FB"
13
+ YELLOW = "#FFC769"
14
+ BLUE = "#5B8AF9"
15
+ GREEN = "#9BC0AE"
16
+
17
+ BRAND = TEAL
18
+ ACCENT = BLUE
19
+ SUCCESS = GREEN
20
+ WARN = YELLOW
21
+ ERROR = PINK
22
+
23
+ console = Console()
24
+ err_console = Console(stderr=True)
25
+
26
+ LOGO = """\
27
+ [#00C2C2] ___ _ _ ___ ____ _ _____ [/#00C2C2]
28
+ [#5B8AF9] / _ \\| | | |/ _ \\| _ \\| ||_ _|[/#5B8AF9]
29
+ [#9BC0AE] | | | | | | | | | | |_) | | | | [/#9BC0AE]
30
+ [#FFC769] | |_| | |_| | |_| | _ <| |__| | [/#FFC769]
31
+ [#FAA2FB] \\__\\_\\\\___/ \\___/|_| \\_\\_____|_| [/#FAA2FB]"""
32
+
33
+ LOGO_COMPACT = "[#00C2C2 bold]querit[/#00C2C2 bold]"
34
+
35
+
36
+ @contextmanager
37
+ def spinner(message: str, *, json_mode: bool = False) -> Generator[None, None, None]:
38
+ """Show a live spinner on stderr; silent in JSON mode."""
39
+ if json_mode:
40
+ yield
41
+ return
42
+ with err_console.status(f"[{TEAL}]{message}[/{TEAL}]", spinner="dots"):
43
+ yield