terrascout 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,48 @@
1
+ # Environment
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.so
11
+ venv/
12
+ .venv/
13
+ *.egg-info/
14
+ dist/
15
+ build/
16
+
17
+ # IDE
18
+ .vscode/
19
+ .idea/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Node (frontend)
29
+ node_modules/
30
+ frontend/dist/
31
+
32
+ # Docker
33
+ *.log
34
+
35
+ # Testing
36
+ .pytest_cache/
37
+ .coverage
38
+ htmlcov/
39
+
40
+ # Playwright
41
+ frontend/e2e/screenshots/
42
+ frontend/playwright-report/
43
+ frontend/test-results/
44
+ docs/screenshots/
45
+
46
+ # Distribution
47
+ *.egg
48
+ *.whl
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: terrascout
3
+ Version: 0.1.0
4
+ Summary: Terrascout CLI — geospatial data scoping from the terminal
5
+ Project-URL: Homepage, http://130.162.194.71
6
+ Project-URL: Documentation, http://130.162.194.71/integrations
7
+ Project-URL: Release Notes, http://130.162.194.71/integrations
8
+ Project-URL: Support, http://130.162.194.71/integrations
9
+ Author-email: Mazdak Ghasemi Tootkaboni <maz@terrascout.dev>
10
+ License: Proprietary
11
+ Keywords: cli,earth-observation,geospatial,remote-sensing,satellite,terrascout
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: Other/Proprietary License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: rich>=13.0
27
+ Requires-Dist: typer>=0.12
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Terrascout CLI
31
+
32
+ Geospatial data scoping from the terminal. Find the best satellite imagery and spatial datasets for your project — ranked, scored, and assessed for feasibility.
33
+
34
+ Powered by [Terrascout](http://130.162.194.71).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install terrascout
40
+ ```
41
+
42
+ Requires Python 3.11+. No API key needed for free-tier features.
43
+
44
+ ## Quick Start
45
+
46
+ ```bash
47
+ # Check connectivity to Terrascout API
48
+ terrascout health
49
+
50
+ # Create a scope from a natural language brief
51
+ terrascout scope new "Monitor vegetation on a 500ha mine site near Emerald QLD, monthly for 12 months"
52
+
53
+ # View results for a scope
54
+ terrascout scope view <scope_id>
55
+
56
+ # Download a PDF compliance report
57
+ terrascout scope report <scope_id> --output report.pdf
58
+
59
+ # Compare recommendations side-by-side
60
+ terrascout scope compare <scope_id>
61
+ ```
62
+
63
+ ## Search
64
+
65
+ ```bash
66
+ # Browse free satellite data providers
67
+ terrascout search providers --free-only
68
+
69
+ # Filter by resolution
70
+ terrascout search providers --resolution 10
71
+
72
+ # Search STAC catalogs for recent scenes
73
+ terrascout search stac --collection sentinel-2-l2a --bbox 144,-38,145,-37
74
+ ```
75
+
76
+ ## Projects
77
+
78
+ ```bash
79
+ # List your project workspaces
80
+ terrascout project list
81
+
82
+ # Create a new project
83
+ terrascout project create "Mine Rehabilitation Monitoring"
84
+ ```
85
+
86
+ ## AI Agents
87
+
88
+ ```bash
89
+ # List available AI agents
90
+ terrascout agents list
91
+
92
+ # Run an agent on a scope
93
+ terrascout agents run methodology_writer --scope <scope_id>
94
+ ```
95
+
96
+ ## Configuration
97
+
98
+ Config is stored at `~/.terrascout/config.yaml`:
99
+
100
+ ```bash
101
+ # View current config
102
+ terrascout config show
103
+
104
+ # Point to a different API (e.g., local dev server)
105
+ terrascout config set api_url http://localhost:8000
106
+
107
+ # Point to production (default)
108
+ terrascout config set api_url http://130.162.194.71
109
+ ```
110
+
111
+ ## Authentication
112
+
113
+ ```bash
114
+ terrascout auth login --email user@example.com
115
+ terrascout auth status
116
+ terrascout auth logout
117
+ ```
118
+
119
+ Authentication is optional for core scoping features. Required for projects, agents, and alert subscriptions.
120
+
121
+ ## Also Available
122
+
123
+ - **Web app**: [http://130.162.194.71](http://130.162.194.71)
124
+ - **MCP Server**: Connect Terrascout to Claude, Cursor, Windsurf, and other AI assistants
125
+ - **REST API**: Full OpenAPI docs at [/docs](http://130.162.194.71/docs)
@@ -0,0 +1,96 @@
1
+ # Terrascout CLI
2
+
3
+ Geospatial data scoping from the terminal. Find the best satellite imagery and spatial datasets for your project — ranked, scored, and assessed for feasibility.
4
+
5
+ Powered by [Terrascout](http://130.162.194.71).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install terrascout
11
+ ```
12
+
13
+ Requires Python 3.11+. No API key needed for free-tier features.
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Check connectivity to Terrascout API
19
+ terrascout health
20
+
21
+ # Create a scope from a natural language brief
22
+ terrascout scope new "Monitor vegetation on a 500ha mine site near Emerald QLD, monthly for 12 months"
23
+
24
+ # View results for a scope
25
+ terrascout scope view <scope_id>
26
+
27
+ # Download a PDF compliance report
28
+ terrascout scope report <scope_id> --output report.pdf
29
+
30
+ # Compare recommendations side-by-side
31
+ terrascout scope compare <scope_id>
32
+ ```
33
+
34
+ ## Search
35
+
36
+ ```bash
37
+ # Browse free satellite data providers
38
+ terrascout search providers --free-only
39
+
40
+ # Filter by resolution
41
+ terrascout search providers --resolution 10
42
+
43
+ # Search STAC catalogs for recent scenes
44
+ terrascout search stac --collection sentinel-2-l2a --bbox 144,-38,145,-37
45
+ ```
46
+
47
+ ## Projects
48
+
49
+ ```bash
50
+ # List your project workspaces
51
+ terrascout project list
52
+
53
+ # Create a new project
54
+ terrascout project create "Mine Rehabilitation Monitoring"
55
+ ```
56
+
57
+ ## AI Agents
58
+
59
+ ```bash
60
+ # List available AI agents
61
+ terrascout agents list
62
+
63
+ # Run an agent on a scope
64
+ terrascout agents run methodology_writer --scope <scope_id>
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Config is stored at `~/.terrascout/config.yaml`:
70
+
71
+ ```bash
72
+ # View current config
73
+ terrascout config show
74
+
75
+ # Point to a different API (e.g., local dev server)
76
+ terrascout config set api_url http://localhost:8000
77
+
78
+ # Point to production (default)
79
+ terrascout config set api_url http://130.162.194.71
80
+ ```
81
+
82
+ ## Authentication
83
+
84
+ ```bash
85
+ terrascout auth login --email user@example.com
86
+ terrascout auth status
87
+ terrascout auth logout
88
+ ```
89
+
90
+ Authentication is optional for core scoping features. Required for projects, agents, and alert subscriptions.
91
+
92
+ ## Also Available
93
+
94
+ - **Web app**: [http://130.162.194.71](http://130.162.194.71)
95
+ - **MCP Server**: Connect Terrascout to Claude, Cursor, Windsurf, and other AI assistants
96
+ - **REST API**: Full OpenAPI docs at [/docs](http://130.162.194.71/docs)
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "terrascout"
7
+ version = "0.1.0"
8
+ description = "Terrascout CLI — geospatial data scoping from the terminal"
9
+ readme = "README.md"
10
+ license = {text = "Proprietary"}
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Mazdak Ghasemi Tootkaboni", email = "maz@terrascout.dev" },
14
+ ]
15
+ keywords = ["geospatial", "satellite", "remote-sensing", "cli", "terrascout", "earth-observation"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Science/Research",
20
+ "Intended Audience :: Developers",
21
+ "Topic :: Scientific/Engineering :: GIS",
22
+ "License :: Other/Proprietary License",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "typer>=0.12",
31
+ "rich>=13.0",
32
+ "httpx>=0.27",
33
+ "pyyaml>=6.0",
34
+ ]
35
+
36
+ [project.scripts]
37
+ terrascout = "terrascout_cli.main:app"
38
+
39
+ [project.urls]
40
+ Homepage = "http://130.162.194.71"
41
+ Documentation = "http://130.162.194.71/integrations"
42
+ "Release Notes" = "http://130.162.194.71/integrations"
43
+ "Support" = "http://130.162.194.71/integrations"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["terrascout_cli"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Terrascout CLI — geospatial data scoping from the terminal."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,52 @@
1
+ """HTTP API client — all backend communication goes through here."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from terrascout_cli.config import get_api_url, get_token
8
+
9
+ TIMEOUT = 30.0
10
+
11
+
12
+ def _headers() -> dict[str, str]:
13
+ headers: dict[str, str] = {"Accept": "application/json"}
14
+ token = get_token()
15
+ if token:
16
+ headers["Authorization"] = f"Bearer {token}"
17
+ return headers
18
+
19
+
20
+ def _base() -> str:
21
+ return get_api_url().rstrip("/")
22
+
23
+
24
+ def _url(path: str) -> str:
25
+ return f"{_base()}/api/v1{path}"
26
+
27
+
28
+ def get(path: str, params: dict[str, Any] | None = None) -> httpx.Response:
29
+ """GET request to the Terrascout API."""
30
+ return httpx.get(_url(path), params=params, headers=_headers(), timeout=TIMEOUT)
31
+
32
+
33
+ def post(path: str, json: dict[str, Any] | None = None) -> httpx.Response:
34
+ """POST request to the Terrascout API."""
35
+ return httpx.post(_url(path), json=json, headers=_headers(), timeout=TIMEOUT)
36
+
37
+
38
+ def get_raw(path: str, params: dict[str, Any] | None = None) -> httpx.Response:
39
+ """GET request returning raw bytes (for PDF downloads etc.)."""
40
+ return httpx.get(
41
+ _url(path),
42
+ params=params,
43
+ headers={**_headers(), "Accept": "application/pdf"},
44
+ timeout=60.0,
45
+ )
46
+
47
+
48
+ def health() -> dict[str, Any]:
49
+ """Check API health."""
50
+ resp = get("/health")
51
+ resp.raise_for_status()
52
+ return resp.json()
File without changes
@@ -0,0 +1,72 @@
1
+ """Agents commands — list, run."""
2
+
3
+ import typer
4
+ from rich.panel import Panel
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+
7
+ from terrascout_cli import client, output
8
+
9
+ app = typer.Typer(no_args_is_help=True)
10
+
11
+
12
+ @app.command("list")
13
+ def agents_list() -> None:
14
+ """List available AI agents."""
15
+ with Progress(
16
+ SpinnerColumn(),
17
+ TextColumn("[bold cyan]Loading agents..."),
18
+ transient=True,
19
+ console=output.console,
20
+ ):
21
+ resp = client.get("/agents")
22
+
23
+ if resp.status_code != 200:
24
+ output.error(f"API error {resp.status_code}: {resp.text[:200]}")
25
+ raise typer.Exit(1)
26
+
27
+ data = resp.json()
28
+ agents = data if isinstance(data, list) else data.get("agents", [])
29
+
30
+ if agents:
31
+ output.console.print(output.agents_table(agents))
32
+ else:
33
+ output.warning("No agents available.")
34
+
35
+
36
+ @app.command("run")
37
+ def agents_run(
38
+ slug: str = typer.Argument(help="Agent slug (e.g. methodology_writer)"),
39
+ scope_id: str = typer.Option(..., "--scope", "-s", help="Scope UUID for context"),
40
+ prompt: str = typer.Option("", "--prompt", "-p", help="Additional prompt/instructions"),
41
+ ) -> None:
42
+ """Execute an AI agent with scope context."""
43
+ body: dict = {"scope_id": scope_id}
44
+ if prompt:
45
+ body["prompt"] = prompt
46
+
47
+ with Progress(
48
+ SpinnerColumn(),
49
+ TextColumn(f"[bold cyan]Running agent '{slug}'..."),
50
+ transient=True,
51
+ console=output.console,
52
+ ):
53
+ resp = client.post(f"/agents/{slug}/run", json=body)
54
+
55
+ if resp.status_code == 401:
56
+ output.error("Authentication required. Run [bold]terrascout auth login[/].")
57
+ raise typer.Exit(1)
58
+ if resp.status_code == 403:
59
+ output.error("Tier upgrade required for this agent.")
60
+ raise typer.Exit(1)
61
+ if resp.status_code != 200:
62
+ output.error(f"API error {resp.status_code}: {resp.text[:200]}")
63
+ raise typer.Exit(1)
64
+
65
+ data = resp.json()
66
+ agent_output = data.get("output", data.get("result", ""))
67
+
68
+ output.console.print(Panel(
69
+ str(agent_output),
70
+ title=f"Agent: {slug}",
71
+ border_style="green",
72
+ ))
@@ -0,0 +1,91 @@
1
+ """Auth commands — login, logout, status."""
2
+
3
+ import typer
4
+ from rich.progress import Progress, SpinnerColumn, TextColumn
5
+
6
+ from terrascout_cli import client, output
7
+ from terrascout_cli.config import get_token, set_token
8
+
9
+ app = typer.Typer(no_args_is_help=True)
10
+
11
+
12
+ @app.command("login")
13
+ def auth_login(
14
+ email: str = typer.Option(..., "--email", "-e", prompt=True, help="Account email"),
15
+ password: str = typer.Option(..., "--password", "-p", prompt=True, hide_input=True, help="Password"),
16
+ ) -> None:
17
+ """Log in to Terrascout and store JWT token."""
18
+ import httpx
19
+
20
+ from terrascout_cli.config import get_api_url
21
+
22
+ with Progress(
23
+ SpinnerColumn(),
24
+ TextColumn("[bold cyan]Authenticating..."),
25
+ transient=True,
26
+ console=output.console,
27
+ ):
28
+ # Call Supabase-compatible auth endpoint
29
+ # The frontend uses Supabase JS SDK, but the CLI can use the REST API
30
+ try:
31
+ base = get_api_url().rstrip("/")
32
+ resp = httpx.post(
33
+ f"{base}/api/v1/auth/login",
34
+ json={"email": email, "password": password},
35
+ timeout=15.0,
36
+ )
37
+ except httpx.RequestError as exc:
38
+ output.error(f"Connection failed: {exc}")
39
+ raise typer.Exit(1)
40
+
41
+ if resp.status_code == 200:
42
+ data = resp.json()
43
+ token = data.get("access_token") or data.get("token")
44
+ if token:
45
+ set_token(token)
46
+ output.success(f"Logged in as [bold]{email}[/]")
47
+ else:
48
+ output.error("Login succeeded but no token returned.")
49
+ raise typer.Exit(1)
50
+ elif resp.status_code == 401:
51
+ output.error("Invalid email or password.")
52
+ raise typer.Exit(1)
53
+ else:
54
+ output.error(f"Login failed ({resp.status_code}): {resp.text[:200]}")
55
+ raise typer.Exit(1)
56
+
57
+
58
+ @app.command("logout")
59
+ def auth_logout() -> None:
60
+ """Clear stored authentication token."""
61
+ set_token(None)
62
+ output.success("Logged out.")
63
+
64
+
65
+ @app.command("status")
66
+ def auth_status() -> None:
67
+ """Show current authentication status."""
68
+ token = get_token()
69
+ if not token:
70
+ output.warning("Not logged in. Use [bold]terrascout auth login[/] to authenticate.")
71
+ return
72
+
73
+ # Try fetching profile to verify token is valid
74
+ with Progress(
75
+ SpinnerColumn(),
76
+ TextColumn("[bold cyan]Checking token..."),
77
+ transient=True,
78
+ console=output.console,
79
+ ):
80
+ resp = client.get("/profile")
81
+
82
+ if resp.status_code == 200:
83
+ profile = resp.json()
84
+ email = profile.get("email", "?")
85
+ tier = profile.get("tier", "free")
86
+ output.success(f"Authenticated as [bold]{email}[/] (tier: {tier})")
87
+ elif resp.status_code == 401:
88
+ output.error("Token expired or invalid. Run [bold]terrascout auth login[/] again.")
89
+ set_token(None)
90
+ else:
91
+ output.warning(f"Could not verify token (status {resp.status_code})")
@@ -0,0 +1,45 @@
1
+ """Config commands — show, set."""
2
+
3
+ import typer
4
+ from rich.table import Table
5
+
6
+ from terrascout_cli import output
7
+ from terrascout_cli.config import CONFIG_FILE, load_config, save_config
8
+
9
+ app = typer.Typer(no_args_is_help=True)
10
+
11
+
12
+ @app.command("show")
13
+ def config_show() -> None:
14
+ """Display current configuration."""
15
+ cfg = load_config()
16
+
17
+ table = Table(title=f"Config ({CONFIG_FILE})", border_style="cyan")
18
+ table.add_column("Key", style="bold white")
19
+ table.add_column("Value", style="cyan")
20
+
21
+ for key, value in cfg.items():
22
+ display = str(value) if value is not None else "[dim]not set[/]"
23
+ if key == "token" and value:
24
+ display = f"{value[:20]}..." if len(value) > 20 else value
25
+ table.add_row(key, display)
26
+
27
+ output.console.print(table)
28
+
29
+
30
+ @app.command("set")
31
+ def config_set(
32
+ key: str = typer.Argument(help="Config key (e.g. api_url)"),
33
+ value: str = typer.Argument(help="New value"),
34
+ ) -> None:
35
+ """Set a configuration value."""
36
+ cfg = load_config()
37
+
38
+ valid_keys = {"api_url", "token"}
39
+ if key not in valid_keys:
40
+ output.error(f"Unknown key '{key}'. Valid keys: {', '.join(sorted(valid_keys))}")
41
+ raise typer.Exit(1)
42
+
43
+ cfg[key] = value
44
+ save_config(cfg)
45
+ output.success(f"Set [bold]{key}[/] = {value}")
@@ -0,0 +1,109 @@
1
+ """Project commands — list, create, add-scope."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.progress import Progress, SpinnerColumn, TextColumn
7
+ from rich.table import Table
8
+
9
+ from terrascout_cli import client, output
10
+
11
+ app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ @app.command("list")
15
+ def project_list() -> None:
16
+ """List your projects."""
17
+ with Progress(
18
+ SpinnerColumn(),
19
+ TextColumn("[bold cyan]Loading projects..."),
20
+ transient=True,
21
+ console=output.console,
22
+ ):
23
+ resp = client.get("/projects")
24
+
25
+ if resp.status_code == 401:
26
+ output.error("Authentication required. Run [bold]terrascout auth login[/].")
27
+ raise typer.Exit(1)
28
+ if resp.status_code != 200:
29
+ output.error(f"API error {resp.status_code}: {resp.text[:200]}")
30
+ raise typer.Exit(1)
31
+
32
+ projects = resp.json()
33
+ if isinstance(projects, dict):
34
+ projects = projects.get("projects", [])
35
+
36
+ if not projects:
37
+ output.warning("No projects yet. Use [bold]terrascout project create[/] to start one.")
38
+ return
39
+
40
+ table = Table(title="Projects", border_style="cyan", show_lines=True)
41
+ table.add_column("ID", style="dim", max_width=36)
42
+ table.add_column("Name", style="bold white")
43
+ table.add_column("Status", style="cyan")
44
+ table.add_column("Scopes", justify="right")
45
+ table.add_column("Created", style="dim")
46
+
47
+ for p in projects:
48
+ table.add_row(
49
+ str(p.get("id", ""))[:36],
50
+ p.get("name", ""),
51
+ p.get("status", "active"),
52
+ str(p.get("scope_count", 0)),
53
+ str(p.get("created_at", ""))[:10],
54
+ )
55
+
56
+ output.console.print(table)
57
+
58
+
59
+ @app.command("create")
60
+ def project_create(
61
+ name: str = typer.Argument(help="Project name"),
62
+ description: Optional[str] = typer.Option(None, "--desc", "-d", help="Project description"),
63
+ ) -> None:
64
+ """Create a new project workspace."""
65
+ body: dict = {"name": name}
66
+ if description:
67
+ body["description"] = description
68
+
69
+ with Progress(
70
+ SpinnerColumn(),
71
+ TextColumn("[bold cyan]Creating project..."),
72
+ transient=True,
73
+ console=output.console,
74
+ ):
75
+ resp = client.post("/projects", json=body)
76
+
77
+ if resp.status_code == 401:
78
+ output.error("Authentication required. Run [bold]terrascout auth login[/].")
79
+ raise typer.Exit(1)
80
+ if resp.status_code not in (200, 201):
81
+ output.error(f"API error {resp.status_code}: {resp.text[:200]}")
82
+ raise typer.Exit(1)
83
+
84
+ data = resp.json()
85
+ output.success(f"Project created: [bold]{data.get('name', name)}[/] ({data.get('id', '?')})")
86
+
87
+
88
+ @app.command("add-scope")
89
+ def project_add_scope(
90
+ project_id: str = typer.Argument(help="Project UUID"),
91
+ scope_id: str = typer.Argument(help="Scope UUID to add"),
92
+ ) -> None:
93
+ """Add a scope to an existing project."""
94
+ with Progress(
95
+ SpinnerColumn(),
96
+ TextColumn("[bold cyan]Adding scope to project..."),
97
+ transient=True,
98
+ console=output.console,
99
+ ):
100
+ resp = client.post(f"/projects/{project_id}/scopes", json={"scope_id": scope_id})
101
+
102
+ if resp.status_code == 401:
103
+ output.error("Authentication required.")
104
+ raise typer.Exit(1)
105
+ if resp.status_code not in (200, 201):
106
+ output.error(f"API error {resp.status_code}: {resp.text[:200]}")
107
+ raise typer.Exit(1)
108
+
109
+ output.success(f"Scope [bold]{scope_id}[/] added to project [bold]{project_id}[/]")