Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ src/keboola_agent_cli/
lineage.py # LAYER 1: CLI commands for cross-project data lineage
org.py # LAYER 1: CLI commands for organization bulk onboarding
tool.py # LAYER 1: CLI commands for MCP tool list/call
explorer.py # LAYER 1: CLI commands for KBC Explorer dashboard generation
context.py # LAYER 1: Agent usage instructions
doctor.py # LAYER 1: Health check command
services/
Expand All @@ -75,6 +76,7 @@ src/keboola_agent_cli/
lineage_service.py # LAYER 2: Cross-project lineage via bucket sharing
org_service.py # LAYER 2: Organization setup orchestration
mcp_service.py # LAYER 2: MCP tool integration (keboola-mcp-server wrapper)
explorer_service.py # LAYER 2: KBC Explorer catalog/orchestration generation
doctor_service.py # LAYER 2: Health check business logic

tests/
Expand All @@ -92,6 +94,7 @@ tests/
test_lineage_service.py # Lineage service tests
test_mcp_service.py # MCP service tests
test_org_service.py # Org service tests (slugify, setup, idempotency)
test_explorer_service.py # Explorer service tests (tier assignment, job stats, generation)
test_doctor_service.py # Doctor service tests
test_http_base.py # BaseHttpClient tests
test_helpers.py # Command helpers tests
Expand Down Expand Up @@ -145,7 +148,7 @@ Both inherit from `BaseHttpClient` (`http_base.py`) which provides shared retry/

9. **Tests**: use `typer.testing.CliRunner` for CLI tests, `unittest.mock` for mocking services and clients, `pytest` fixtures from `conftest.py`.

10. **Dependencies**: typer, rich, httpx, pydantic, platformdirs, mcp. Dev: pytest, pytest-httpx, pytest-asyncio, ruff.
10. **Dependencies**: typer, rich, httpx, pydantic, platformdirs, mcp, jsonschema, pyyaml. Dev: pytest, pytest-httpx, pytest-asyncio, ruff.

11. **Error accumulation**: multi-project operations collect per-project errors without stopping. One project failing doesn't block others (see `lineage_service.py`, `org_service.py`).

Expand Down Expand Up @@ -177,6 +180,8 @@ kbagent org setup --org-id ID --url URL [--dry-run] [--yes] [--token-description
kbagent tool list [--project NAME]
kbagent tool call TOOL_NAME [--project NAME] [--input JSON]

kbagent explorer [--project NAME] [--output-dir DIR] [--job-limit N] [--tiers FILE] [--no-open]

kbagent context
kbagent doctor
```
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ dependencies = [
"pydantic>=2.5",
"platformdirs>=4",
"mcp>=1.0.0,<2.0.0",
"jsonschema>=4.20",
"pyyaml>=6",
]

[project.scripts]
Expand Down
86 changes: 86 additions & 0 deletions src/keboola_agent_cli/commands/explorer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Explorer command - generate data files and open the kbc-explorer dashboard.

Thin CLI layer: parses arguments, calls ExplorerService, formats output.
No business logic belongs here.
"""

from pathlib import Path
from typing import Optional

import typer

from ..errors import ConfigError
from ._helpers import emit_project_warnings, get_formatter, get_service

explorer_app = typer.Typer(help="Generate and open the KBC Explorer dashboard")


@explorer_app.callback(invoke_without_command=True)
def explorer(
ctx: typer.Context,
project: Optional[list[str]] = typer.Option(
None,
"--project",
help="Project alias(es) to include (repeatable, default: all)",
),
output_dir: Optional[Path] = typer.Option(
None,
"--output-dir",
help="Directory to write catalog/orchestration files (default: kbc-explorer/)",
),
job_limit: int = typer.Option(
500,
"--job-limit",
help="Max jobs per project for statistics (default: 500)",
),
tiers: Optional[Path] = typer.Option(
None,
"--tiers",
help="Path to YAML tier config file for project tier assignments",
),
no_open: bool = typer.Option(
False,
"--no-open",
help="Generate files but don't open the browser",
),
) -> None:
"""Generate explorer data from connected projects and open the dashboard."""
formatter = get_formatter(ctx)
service = get_service(ctx, "explorer_service")

aliases = project if project else None

try:
result = service.generate(
aliases=aliases,
output_dir=output_dir,
job_limit=job_limit,
open_browser=not no_open,
tiers_config=tiers,
)
except ConfigError as exc:
formatter.error(message=exc.message, error_code="CONFIG_ERROR")
raise typer.Exit(code=5) from None

if formatter.json_mode:
formatter.output(result)
else:
projects_count = result["projects_count"]
configs_count = result["configs_count"]
jobs_sampled = result["jobs_sampled"]
lineage_edges = result["lineage_edges"]
orch_count = result["orchestrations_count"]
out_dir = result["output_dir"]

formatter.console.print(
f"[bold green]Explorer generated![/bold green] "
f"{projects_count} projects, {configs_count} configs, "
f"{jobs_sampled} jobs, {lineage_edges} lineage edges, "
f"{orch_count} orchestrations"
)
formatter.console.print(f"Output: {out_dir}")

if not no_open:
formatter.console.print("Opening dashboard in browser...")

emit_project_warnings(formatter, result)
Loading