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
8 changes: 8 additions & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ This prints all commands, flags, workflows, and tips. Read it fully before proce
| Load tables into a workspace | `kbagent workspace load --project PROJECT --workspace-id WORKSPACE-ID --tables TABLES` |
| Execute SQL query in a workspace via Query Service | `kbagent workspace query --project PROJECT --workspace-id WORKSPACE-ID` |
| Create a workspace from a transformation config | `kbagent workspace from-transformation --project PROJECT --component-id COMPONENT-ID --config-id CONFIG-ID` |
| Initialize a sync working directory for a Keboola project | `kbagent sync init --project PROJECT` |
| Download all configurations from a Keboola project to local files | `kbagent sync pull --project PROJECT` |
| Show which local configurations have been modified, added, or deleted | `kbagent sync status` |
| Show detailed diff between local and remote configurations | `kbagent sync diff --project PROJECT` |
| Push local configuration changes to a Keboola project | `kbagent sync push --project PROJECT` |
| Link the current git branch to a Keboola development branch | `kbagent sync branch-link --project PROJECT` |
| Remove the branch mapping for the current git branch | `kbagent sync branch-unlink` |
| Show the branch mapping status for the current git branch | `kbagent sync branch-status` |
<!-- END AUTO-GENERATED COMMANDS -->

## Response format
Expand Down
5 changes: 5 additions & 0 deletions src/keboola_agent_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .commands.org import org_app
from .commands.project import project_app
from .commands.repl import repl_command
from .commands.sync import sync_app
from .commands.tool import tool_app
from .commands.version import version_command
from .commands.workspace import workspace_app
Expand All @@ -32,6 +33,7 @@
from .services.mcp_service import McpService
from .services.org_service import OrgService
from .services.project_service import ProjectService
from .services.sync_service import SyncService
from .services.version_service import VersionService
from .services.workspace_service import WorkspaceService

Expand All @@ -51,6 +53,7 @@
app.add_typer(explorer_app, name="explorer")
app.add_typer(llm_app, name="llm")
app.add_typer(workspace_app, name="workspace")
app.add_typer(sync_app, name="sync")
app.command("context")(context_command)
app.command("doctor")(doctor_command)
app.command("init")(init_command)
Expand Down Expand Up @@ -125,6 +128,7 @@ def main(
org_service = OrgService(config_store=config_store)
mcp_service = McpService(config_store=config_store)
branch_service = BranchService(config_store=config_store)
sync_service = SyncService(config_store=config_store)
workspace_service = WorkspaceService(config_store=config_store)
kbc_service = KbcService(config_store=config_store)
doctor_service = DoctorService(config_store=config_store, mcp_service=mcp_service)
Expand All @@ -149,6 +153,7 @@ def main(
ctx.obj["org_service"] = org_service
ctx.obj["mcp_service"] = mcp_service
ctx.obj["branch_service"] = branch_service
ctx.obj["sync_service"] = sync_service
ctx.obj["workspace_service"] = workspace_service
ctx.obj["kbc_service"] = kbc_service
ctx.obj["doctor_service"] = doctor_service
Expand Down
192 changes: 192 additions & 0 deletions src/keboola_agent_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,51 @@ def list_components(self, component_type: str | None = None) -> list[dict[str, A
response = self._request("GET", "/v2/storage/components", params=params)
return response.json()

def list_components_with_configs(self, branch_id: int | None = None) -> list[dict[str, Any]]:
"""List all components with full configuration bodies and rows.

Makes a single API call to fetch everything needed for sync pull.
Uses the include=configuration,rows parameter to get full config
bodies and config rows in one request.

Args:
branch_id: If set, target a specific dev branch.

Returns:
List of component dicts, each containing a 'configurations' list
with full config bodies and nested 'rows'.
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
resp = self._request(
"GET",
f"{prefix}/components",
params={"include": "configuration,rows"},
)
return resp.json()

def list_config_rows(
self,
component_id: str,
config_id: str,
branch_id: int | None = None,
) -> list[dict[str, Any]]:
"""List all rows for a specific configuration.

Args:
component_id: Component identifier (e.g. 'keboola.ex-http').
config_id: Configuration ID.
branch_id: If set, target a specific dev branch.

Returns:
List of config row dicts.
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
resp = self._request(
"GET",
f"{prefix}/components/{quote(component_id)}/configs/{quote(config_id)}/rows",
)
return resp.json()

def get_config_detail(self, component_id: str, config_id: str) -> dict[str, Any]:
"""Get detailed information about a specific configuration.

Expand All @@ -188,6 +233,153 @@ def get_config_detail(self, component_id: str, config_id: str) -> dict[str, Any]
)
return response.json()

def create_config(
self,
component_id: str,
name: str,
configuration: dict[str, Any],
description: str = "",
branch_id: int | None = None,
) -> dict[str, Any]:
"""Create a new configuration for a component.

POST /v2/storage/[branch/{id}/]components/{comp_id}/configs

Args:
component_id: Component identifier.
name: Configuration name.
configuration: Configuration body (parameters, storage, etc.).
description: Optional description.
branch_id: If set, target a specific dev branch.

Returns:
Created configuration dict including the assigned 'id'.
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
resp = self._request(
"POST",
f"{prefix}/components/{quote(component_id)}/configs",
data={
"name": name,
"description": description,
"configuration": json.dumps(configuration),
},
)
return resp.json()

def update_config(
self,
component_id: str,
config_id: str,
name: str | None = None,
configuration: dict[str, Any] | None = None,
description: str | None = None,
change_description: str = "",
branch_id: int | None = None,
) -> dict[str, Any]:
"""Update an existing configuration.

PUT /v2/storage/[branch/{id}/]components/{comp_id}/configs/{config_id}

Only provided (non-None) fields are sent in the request.

Returns:
Updated configuration dict.
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
data: dict[str, Any] = {}
if name is not None:
data["name"] = name
if description is not None:
data["description"] = description
if configuration is not None:
data["configuration"] = json.dumps(configuration)
if change_description:
data["changeDescription"] = change_description
resp = self._request(
"PUT",
f"{prefix}/components/{quote(component_id)}/configs/{quote(config_id)}",
data=data,
)
return resp.json()

def create_config_row(
self,
component_id: str,
config_id: str,
name: str,
configuration: dict[str, Any],
description: str = "",
branch_id: int | None = None,
) -> dict[str, Any]:
"""Create a new configuration row.

POST /v2/storage/[branch/{id}/]components/{comp_id}/configs/{config_id}/rows

Returns:
Created row dict including the assigned 'id'.
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
resp = self._request(
"POST",
f"{prefix}/components/{quote(component_id)}/configs/{quote(config_id)}/rows",
data={
"name": name,
"description": description,
"configuration": json.dumps(configuration),
},
)
return resp.json()

def update_config_row(
self,
component_id: str,
config_id: str,
row_id: str,
name: str | None = None,
configuration: dict[str, Any] | None = None,
description: str | None = None,
change_description: str = "",
branch_id: int | None = None,
) -> dict[str, Any]:
"""Update an existing configuration row.

PUT /v2/storage/[branch/{id}/]components/{comp_id}/configs/{config_id}/rows/{row_id}
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
data: dict[str, Any] = {}
if name is not None:
data["name"] = name
if description is not None:
data["description"] = description
if configuration is not None:
data["configuration"] = json.dumps(configuration)
if change_description:
data["changeDescription"] = change_description
resp = self._request(
"PUT",
f"{prefix}/components/{quote(component_id)}/configs/{quote(config_id)}/rows/{quote(row_id)}",
data=data,
)
return resp.json()

def delete_config_row(
self,
component_id: str,
config_id: str,
row_id: str,
branch_id: int | None = None,
) -> None:
"""Delete a configuration row.

DELETE /v2/storage/[branch/{id}/]components/{comp_id}/configs/{config_id}/rows/{row_id}
"""
prefix = f"/v2/storage/branch/{branch_id}" if branch_id else "/v2/storage"
self._request(
"DELETE",
f"{prefix}/components/{quote(component_id)}/configs/{quote(config_id)}/rows/{quote(row_id)}",
)

def _wait_for_storage_job(self, job: dict[str, Any]) -> dict[str, Any]:
"""Poll a Storage API job until it reaches a terminal state.

Expand Down
77 changes: 77 additions & 0 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,60 @@
Example:
kbagent --json workspace from-transformation --project prod --component-id keboola.snowflake-transformation --config-id 22777254

### Project Sync (GitOps Workflow)

kbagent sync init --project ALIAS [--directory DIR] [--git-branching]
Initialize a sync working directory for a Keboola project.
Creates .keboola/manifest.json with project metadata and naming conventions.
Use --git-branching to enable git-to-Keboola branch mapping.
Example:
mkdir my-project && cd my-project
kbagent --json sync init --project prod
kbagent --json sync init --project prod --git-branching

kbagent sync pull --project ALIAS [--directory DIR] [--force]
Download all configurations from Keboola to local files.
Creates a dev-friendly directory structure with _config.yml files.
SQL transformations are extracted into transform.sql with block markers.
Python code is extracted into transform.py/code.py + pyproject.toml.
Example:
kbagent --json sync pull --project prod

kbagent sync status [--directory DIR]
Show which local configs have been modified, added, or deleted since last pull.
Uses SHA256 hash comparison for reliable change detection.
Example:
kbagent --json sync status

kbagent sync diff --project ALIAS [--directory DIR]
Show detailed diff between local files and remote Keboola state.
Compares config content (ignoring encrypted value nonces).
Example:
kbagent --json sync diff --project prod

kbagent sync push --project ALIAS [--directory DIR] [--dry-run] [--force]
Push local changes to Keboola. Creates new configs, updates modified,
deletes removed (with --force). New configs get IDs from API automatically.
--dry-run shows what would change without applying.
Example:
kbagent --json sync push --project prod --dry-run
kbagent --json sync push --project prod

kbagent sync branch-link --project ALIAS [--branch-id ID] [--branch-name NAME]
Link current git branch to a Keboola development branch.
Auto-creates the Keboola branch if it doesn't exist with the same name.
Requires --git-branching mode enabled via sync init.
Example:
git checkout -b feature/new-etl
kbagent --json sync branch-link --project prod

kbagent sync branch-unlink [--directory DIR]
Remove the branch mapping for the current git branch.
Does NOT delete the Keboola branch itself.

kbagent sync branch-status [--directory DIR]
Show the current git branch mapping status.

### Utility Commands

kbagent init [--from-global]
Expand Down Expand Up @@ -497,6 +551,29 @@
KBC_MANAGE_API_TOKEN=xxx kbagent --json org setup --org-id 123 --url https://connection.keboola.com --yes
This creates Storage API tokens for ALL projects in the org and registers them automatically.

17. Sync workflow -- manage configs as local files with GitOps:
# Step 1: Initialize and pull
mkdir my-project && cd my-project
kbagent --json sync init --project prod
kbagent --json sync pull --project prod

# Step 2: Edit configs locally
# SQL is in transform.sql, Python in code.py, config in _config.yml
# Edit with any IDE, get git diffs, code review, etc.

# Step 3: Review and push
kbagent --json sync status # local changes
kbagent --json sync diff --project prod # vs remote
kbagent --json sync push --project prod --dry-run # preview
kbagent --json sync push --project prod # apply

# Git-branching mode (maps git branches to Keboola dev branches):
kbagent --json sync init --project prod --git-branching
git checkout -b feature/new-etl
kbagent --json sync branch-link --project prod # creates Keboola dev branch
kbagent --json sync pull --project prod
# ... edit, push, then merge via PR + Keboola UI

## Exit Codes

0 Success
Expand Down
Loading
Loading