Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ kbagent storage download-table --project NAME --table-id ID [--output FILE] [--c
kbagent storage delete-table --project NAME --table-id ID [--table-id ...] [--force] [--dry-run] [--yes] [--branch ID]
kbagent storage delete-column --project NAME --table-id ID --column COL [--column ...] [--force] [--dry-run] [--yes] [--branch ID]
kbagent storage delete-bucket --project NAME --bucket-id ID [--bucket-id ...] [--force] [--dry-run] [--yes] [--branch ID]
kbagent storage describe-bucket --project NAME --bucket-id ID [--text STR | --file PATH | --stdin] [--branch ID]
kbagent storage describe-table --project NAME --table-id ID [--text STR | --file PATH | --stdin] [--branch ID]
kbagent storage describe-column --project NAME --table-id ID --column NAME=DESC [--column ...] [--branch ID]
kbagent storage describe-batch --project NAME --from-file YAML [--branch ID]
kbagent storage files --project NAME [--tag TAG ...] [--limit N] [--offset N] [--query Q] [--branch ID]
kbagent storage file-upload --project NAME --file PATH [--name NAME] [--tag TAG ...] [--permanent] [--branch ID]
kbagent storage file-download --project NAME [--file-id ID | --tag TAG ...] [--output FILE]
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.21.1",
"version": "0.22.0",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
4 changes: 4 additions & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Delete one or more storage tables | `kbagent storage delete-table --project PROJECT --table-id TABLE-ID` |
| Delete one or more columns from a storage table | `kbagent storage delete-column --project PROJECT --table-id TABLE-ID --column COLUMN` |
| Delete one or more storage buckets | `kbagent storage delete-bucket --project PROJECT --bucket-id BUCKET-ID` |
| Set the description on a storage bucket | `kbagent storage describe-bucket --project PROJECT --bucket-id BUCKET-ID` |
| Set the description on a storage table | `kbagent storage describe-table --project PROJECT --table-id TABLE-ID` |
| Set descriptions on one or more columns of a storage table | `kbagent storage describe-column --project PROJECT --table-id TABLE-ID --column COLUMN` |
| Apply descriptions to buckets, tables, and columns from a YAML file | `kbagent storage describe-batch --project PROJECT --from-file FROM-FILE` |
| List Storage Files with optional tag filtering | `kbagent storage files --project PROJECT` |
| Show Storage File metadata (without downloading) | `kbagent storage file-detail --project PROJECT --file-id FILE-ID` |
| Upload a local file to Storage Files | `kbagent storage file-upload --project PROJECT --file FILE` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ All commands support `--json` for structured output. Multi-project flags (`--pro
- `storage delete-table --project NAME --table-id ID [--table-id ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete tables, --force cascade-deletes aliased tables (branch-aware)
- `storage delete-column --project NAME --table-id ID --column COL [--column ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete columns from a table (branch-aware)
- `storage delete-bucket --project NAME --bucket-id ID [--bucket-id ...] [--force] [--dry-run] [--yes] [--branch ID]` -- delete buckets (branch-aware)
- `storage describe-bucket --project NAME --bucket-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a bucket description (stored as `KBC.description` in bucket metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage bucket-detail`
- `storage describe-table --project NAME --table-id ID [--text STR | --file PATH | --stdin] [--branch ID]` -- set a table description (stored as `KBC.description` in table metadata, upsert). Provide exactly one of `--text`, `--file`, `--stdin`. Read back via `storage table-detail`
- `storage describe-column --project NAME --table-id ID --column NAME=DESCRIPTION [--column ...] [--branch ID]` -- set one or more column descriptions. Stored as `KBC.column.{name}.description` keys in the table's metadata (Keboola has no user-writable column-metadata endpoint). Read back in `storage table-detail` under `column_details[].description`
- `storage describe-batch --project NAME --from-file PATH [--branch ID]` -- apply bucket/table/column descriptions from a YAML file (top-level `buckets`, `tables`, `columns` sections, all optional). Partial-failure tolerant: per-item errors are collected and reported, the batch does not abort. Non-zero exit only when at least one item failed

## Storage Files
- `storage files --project NAME [--tag TAG ...] [--limit N] [--offset N] [--query Q] [--branch ID]` -- list Storage Files, optionally filtered by tag/query
Expand Down
29 changes: 29 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,35 @@ They live at different endpoints in the Storage API
(`/v2/storage/branch/{id}/metadata` vs. `/v2/storage/dev-branches/{id}`),
so setting a branch's description will **not** update the dashboard.

## Storage descriptions: key convention + precedence + partial failures

`kbagent storage describe-bucket / describe-table / describe-column / describe-batch`
write descriptive metadata onto storage objects. Three behaviors are easy to miss:

- **Column descriptions use a metadata-key convention, not a column endpoint.**
The Keboola Storage API has no user-writable column-level metadata endpoint,
so `describe-column` stores each description as a `KBC.column.{name}.description`
entry on the **table's** metadata (upsert). `storage table-detail` reads them
back via the same key and surfaces them under `column_details[].description`.
Renaming or deleting a column does NOT automatically clean these entries up
(they remain on the table's metadata under the old name). Same convention for
table and bucket descriptions: stored as `KBC.description` (provider=user) on
the object's metadata.
- **`describe-batch` is partial-failure-tolerant.** Item-level errors are
collected into `result.errors[]` but the batch keeps processing the remaining
items. The CLI exits non-zero only if `error_count > 0`, so in scripts always
inspect `errors[]` (or at least `error_count`) rather than relying solely on
the exit code — and when consuming `--json` output, never trust a zero-exit
as "everything applied."
- **Description-field precedence: metadata wins.** When both the native Storage
API `description` field and a user-provided `KBC.description` (provider=user)
metadata entry are present, `storage bucket-detail` / `storage table-detail`
surface the **metadata value**. The native field is only settable at object
creation time via the Storage API; all user updates flow through the metadata
endpoint, so the metadata entry is the authoritative source. `KBC.description`
entries whose provider is not `user` (e.g. `system`) are ignored during
read-back and the native field is used as fallback.

## `job terminate` quirks

Queue API's kill endpoint (`POST /jobs/{id}/kill`) has a few non-obvious behaviors the
Expand Down
258 changes: 258 additions & 0 deletions plugins/kbagent/skills/kbagent/references/storage-describe-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
# Storage Describe Workflow

`kbagent storage describe-*` attaches human-readable descriptions to storage
buckets, tables, and columns so that downstream consumers (dashboards, the
MCP `get_buckets`/`get_tables` tools, AI agents) can surface meaningful
documentation rather than raw IDs. Descriptions are stored as metadata on
the storage object and round-trip via `storage bucket-detail` / `storage
table-detail`.

## Quick reference

| Command | Purpose |
|---------|---------|
| `storage describe-bucket` | Set a bucket description |
| `storage describe-table` | Set a table description |
| `storage describe-column` | Set descriptions on one or more columns |
| `storage describe-batch` | Apply bucket/table/column descriptions from a YAML file |
| `storage bucket-detail` | Read back the bucket description |
| `storage table-detail` | Read back the table description and `column_details[].description` |

## When to use

- Onboarding a new project: document every source bucket, output table, and
business-critical column so new engineers (or Kai) can self-serve.
- After a schema migration: refresh column descriptions so SQL reviews can
spot intent mismatches.
- Before sharing a bucket cross-project: the description is visible in the
receiving project's dashboard.
- From CI: write a batch YAML alongside the repo and call `describe-batch`
after every `sync push` to keep documentation in lockstep with config.

## Storage model (what actually gets written)

Descriptions are stored as metadata entries on the object:

- **Bucket description** -- `KBC.description` (provider=user) on bucket metadata
- **Table description** -- `KBC.description` (provider=user) on table metadata
- **Column description** -- `KBC.column.{column_name}.description` on the
**table's** metadata. Keboola has no user-writable column-metadata endpoint,
so this key convention is the storage layer for column descriptions. Read
them back via `storage table-detail` (`column_details[].description`).

Descriptions are `upsert`: calling `describe-*` with a new text replaces
whatever was there before. There is no append mode.

## Single-item: bucket

```bash
# Inline text
kbagent --json storage describe-bucket \
--project ALIAS \
--bucket-id in.c-sales \
--text "Daily sales fact data, partitioned by region"

# From a file (markdown supported)
kbagent --json storage describe-bucket \
--project ALIAS \
--bucket-id in.c-sales \
--file ./docs/sales-bucket.md

# From stdin (useful in pipelines)
echo "Generated description" | kbagent --json storage describe-bucket \
--project ALIAS \
--bucket-id in.c-sales \
--stdin
```

Exactly one of `--text`, `--file`, `--stdin` must be provided.

Read back:

```bash
kbagent --json storage bucket-detail --project ALIAS --bucket-id in.c-sales \
| jq '.data.description, .data.metadata'
```

## Single-item: table

Identical shape to `describe-bucket`:

```bash
kbagent --json storage describe-table \
--project ALIAS \
--table-id in.c-sales.orders \
--text "All sales orders, one row per line item"
```

Read back:

```bash
kbagent --json storage table-detail --project ALIAS --table-id in.c-sales.orders \
| jq '.data.description, .data.column_details'
```

## Single-item: columns

`describe-column` takes **one or more** `--column NAME=DESCRIPTION` flags in
a single call. All entries are applied in one API roundtrip:

```bash
kbagent --json storage describe-column \
--project ALIAS \
--table-id in.c-sales.orders \
--column "order_id=Unique order identifier" \
--column "total=Order total in USD (gross)" \
--column "created_at=Server-side creation timestamp (UTC)"
```

Column descriptions live under `KBC.column.{name}.description` on the
**table's** metadata -- they are NOT attached to the column record itself.
If you rename or delete a column, the old key lingers until you manually
clean it up (there is no `--delete-column-description` command today).

Read back via `storage table-detail`:

```json
{
"data": {
"table_id": "in.c-sales.orders",
"description": "All sales orders, one row per line item",
"column_details": [
{"name": "order_id", "type": "INTEGER", "description": "Unique order identifier"},
{"name": "total", "type": "NUMERIC", "description": "Order total in USD (gross)"}
]
}
}
```

Columns without a matching metadata entry simply omit `description`.

## Batch: YAML schema

For more than a handful of items, hand-maintain a YAML file and apply it
with `storage describe-batch`. The schema has three top-level sections,
all optional:

```yaml
# descriptions.yaml
buckets:
in.c-sales: |
Sales fact and dimension tables.
Refreshed nightly from the production OLTP via Keboola ex-db-postgres.
in.c-marketing: Marketing funnel events

tables:
in.c-sales.orders: All sales orders (one row per line item)
in.c-sales.customers: Customer master list, PII-scrubbed
in.c-marketing.events: Raw funnel events

columns:
in.c-sales.orders:
order_id: Unique order identifier
total: Order total in USD (gross)
created_at: Server-side creation timestamp (UTC)
in.c-sales.customers:
customer_id: Primary key
email_hash: SHA-256 of the customer email (PII-scrubbed)
```

Apply it:

```bash
kbagent --json storage describe-batch \
--project ALIAS \
--from-file ./descriptions.yaml
```

Response shape:

```json
{
"status": "ok",
"data": {
"project_alias": "ALIAS",
"applied": [
{"type": "bucket", "id": "in.c-sales", "description": "Sales fact..."},
{"type": "table", "id": "in.c-sales.orders", "description": "All sales orders..."},
{"type": "columns", "id": "in.c-sales.orders", "columns": {"order_id": "...", "total": "..."}}
],
"errors": [],
"applied_count": 3,
"error_count": 0
}
}
```

In human mode, a Rich progress spinner shows per-item progress ("Describing
bucket in.c-sales", "Describing table in.c-sales.orders", ...) so large
batches do not look frozen. The spinner is suppressed under `--json` so
structured output is the only thing on stdout.

## Partial-failure semantics

`describe-batch` does **not** abort on the first error. Each item is
attempted independently; failures are collected into `errors[]` and the
batch continues:

```json
{
"data": {
"applied": [{"type": "bucket", "id": "in.c-good", ...}],
"errors": [
{"type": "bucket", "id": "in.c-typo", "error": "Bucket in.c-typo not found"},
{"type": "table", "id": "in.c-x.missing", "error": "Table not found"}
],
"applied_count": 1,
"error_count": 2
}
}
```

The CLI exits **1** when `error_count > 0`. In scripts, always inspect the
`errors[]` list -- a zero exit alone does not mean the whole batch went in
without issues (it means there were no partial failures). A non-zero exit
means *some* items failed; the successful items still landed.

## End-to-end example: onboarding a new bucket

```bash
# 1. Create the bucket and tables (or sync them from another project)
kbagent storage create-bucket --project ALIAS --stage in --name c-sales
kbagent storage create-table --project ALIAS --bucket-id in.c-sales --name orders \
--column order_id:INTEGER --column total:NUMERIC --primary-key order_id

# 2. Apply all descriptions from a tracked YAML file
kbagent --json storage describe-batch \
--project ALIAS \
--from-file ./docs/keboola/descriptions.yaml

# 3. Verify by reading back
kbagent --json storage table-detail --project ALIAS --table-id in.c-sales.orders \
| jq '{description: .data.description, columns: .data.column_details}'
```

## Precedence vs the native description field

The Storage API has a native `description` field on buckets and tables, but
it is only settable at creation time. Anything you set with `describe-*`
lives on the metadata endpoint. When both are present, `storage bucket-detail`
/ `storage table-detail` surface the metadata value (the one you wrote with
`describe-*`). The native field is the fallback for legacy objects where
no metadata entry exists. System-provided `KBC.description` entries (e.g.
those auto-stamped by components) are filtered out on read-back -- only
entries with `provider="user"` are considered the canonical description.

## Key behaviors

- `describe-*` is **upsert** -- no append mode; re-running replaces the value.
- Column descriptions piggy-back on table metadata via the
`KBC.column.{name}.description` key convention.
- `describe-batch` is **partial-failure-tolerant** -- check `errors[]` even
on exit code 0.
- All commands support `--branch ID` to target a dev branch.
- Read back via `storage bucket-detail` / `storage table-detail` -- the
`metadata` field on those responses contains the raw metadata array if
you need to inspect timestamps or providers.
- Non-user (`system`) `KBC.description` entries are ignored on read-back;
they do not override the native `description` field.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-agent-cli"
version = "0.21.1"
version = "0.22.0"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
8 changes: 8 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.22.0": [
"New: storage describe-bucket -- set KBC.description on a bucket via metadata POST (upsert-by-key, provider=user)",
"New: storage describe-table -- set KBC.description on a table via metadata POST; description surfaces in table-detail",
"New: storage describe-column -- set per-column descriptions using KBC.column.{name}.description convention in table metadata; readable via table-detail column_details[].description",
"New: storage describe-batch --from-file -- apply bucket/table/column descriptions from a YAML file in one shot; failures collected, remaining items continue",
"Fix: storage table-detail now returns 'description' and 'metadata' fields (extracted from table metadata array)",
"Fix: storage bucket-detail now returns 'description' and 'metadata' fields (KBC.description in metadata takes precedence over native creation-time description field)",
],
"0.21.1": [
"Fix: sync pull on a newly created dev branch now writes config rows (#193) -- idempotent skip guard for rows was missing a file-existence check, causing rows to be silently skipped when the branch directory was new (hash matched main because the branch is a clone)",
],
Expand Down
Loading
Loading