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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.31.0",
"version": "0.32.0",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ kbagent storage create-table --project NAME --bucket-id ID --name NAME --column
kbagent storage upload-table --project NAME --table-id ID --file PATH [--incremental] [--branch ID]
kbagent storage download-table --project NAME --table-id ID [--output FILE] [--columns COL ...] [--limit N] [--branch ID]
kbagent storage delete-table --project NAME --table-id ID [--table-id ...] [--force] [--dry-run] [--yes] [--branch ID]
kbagent storage truncate-table --project NAME --table-id ID [--table-id ...] [--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 swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes]
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.31.0",
"version": "0.32.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
20 changes: 20 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ a critical failure.
`config row-delete`, `config oauth-url` need 0.30.0+,
`project edit --new-alias` (cascading rename across config.json +
nested sync dir; warns on lineage cache rebuild) needs 0.31.0+,
`storage truncate-table` needs 0.32.0+,
`storage retype` is a future composite), you
MUST refuse the task and return a handoff message to the parent:
`"Cannot proceed safely on kbagent <version>. Missing: <commands>.
Expand Down Expand Up @@ -112,6 +113,7 @@ a critical failure.
| Retype table columns | fetch types via `workspace query`, draft types YAML, write new transformation that produces typed output table, then `kbagent storage swap-tables` (0.28.0+) to flip the typed copy into the original name in a dev branch | `kbagent --hint client create_table_definition` if the future `storage retype` composite (§14.3) is not yet present | `POST /v2/storage/buckets/.../tables-definition` (REST) followed by manual config rewrites |
| Create typed table with native types | `kbagent storage create-table --column pk:VARCHAR(40) --column amount:NUMBER(18,2) --not-null pk --default amount=0` (0.25.0+) | `tool call create_table` (accepts the same `definition.length` shape via MCP) | re-creating via raw REST to `/v2/storage/...tables-definition` |
| Promote typed rebuild back into the original name | `kbagent storage swap-tables --project P --table-id in.c-foo.data --target-table-id in.c-foo.data_change_log --branch <ID> --yes` (0.28.0+) -- async storage job (`tableSwap`); client polls to completion before returning. Service refuses without a branch | -- | renaming or deleting + re-uploading (loses history; downstream configs need to be rewritten) |
| Re-seed a table without losing its schema / PK / dependents | `kbagent storage truncate-table --project P --table-id in.c-foo.data [--branch ID] [--dry-run] [--yes]` (0.32.0+) -- DELETE `/tables/{id}/rows?allowTruncate=1`; endpoint is uniformly async on every branch (returns a queued `tableRowsDelete` job; client polls via `_wait_for_storage_job`). Do NOT pass `async=true` -- the API rejects it. Batch via repeated `--table-id`. Returns `{truncated[], failed[], dry_run, project_alias}` with `truncated[]` entries carrying `{table_id, rows_before, rows_after, branch_id}`. Permission class: `destructive` | `tool call delete_table_rows` if the upstream MCP exposes it | drop + recreate the table (loses descriptions, PK, sharing edges, and breaks every downstream config reference); deleting rows via raw SQL in a workspace (bypasses the Storage API audit trail) |
| Debug a failed job | `kbagent job detail --project P --job-id J --json` + `kbagent job run ... --log-tail-lines 200` | `kbagent workspace from-transformation` for SQL repro | "I think the issue is..." without reading logs |
| Ad-hoc SQL / row-count / type audit | `kbagent workspace create` + `kbagent workspace load` + `kbagent workspace query --sql "..."` | `kbagent workspace from-transformation` for existing transform debugging | querying Keboola Storage directly via Snowflake credentials outside the workspace abstraction |
| Inspect dev branch | `kbagent branch list --project P`, `kbagent branch use --project P --branch ID` | `tool call get_branch` | acting on `main` when a dev branch exists |
Expand Down Expand Up @@ -232,6 +234,24 @@ success, not a failure.
verification payload but do not treat it as a failure signal.
Production writes never materialize anything.

- **`storage truncate-table` is row-only; schema and dependents are
preserved** (0.32.0+): the underlying call is
`DELETE /v2/storage/[branch/{id}/]tables/{id}/rows?allowTruncate=1`.
The endpoint is **uniformly async** on every branch -- it returns
HTTP 202 with a queued storage job (`operationName: tableRowsDelete`)
that the client polls to completion via `_wait_for_storage_job`,
same machinery as `delete_table`. Production branches finish the
job in under a second; dev branches may take longer. **Do not pass
`async=true`** -- the Storage API rejects it with HTTP 400
("async: This field was not expected.") for this endpoint, even
though sibling destructive endpoints (`delete_table`, `delete_bucket`)
require it. Aliases, sharing edges, primary keys, descriptions, and
downstream config references all survive -- only the rows are
removed. The Storage API requires the `allowTruncate=1` opt-in
whenever no row filter is sent; kbagent always passes it. Prefer
this over `delete-table` for any "re-seed" pattern; reach for
`delete-table` only when the table itself is being retired.

- **`project invite` "already invited / already member" is a no-op, not a failure** (0.29.0+):
Re-inviting a user the project already knows returns HTTP 400 from the
Manage API. kbagent normalises both "...already been invited..." and
Expand Down
1 change: 1 addition & 0 deletions plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Upload a CSV file into a storage table | `kbagent storage upload-table --project PROJECT --table-id TABLE-ID --file FILE` |
| Export a storage table to a local CSV file | `kbagent storage download-table --project PROJECT --table-id TABLE-ID` |
| Delete one or more storage tables | `kbagent storage delete-table --project PROJECT --table-id TABLE-ID` |
| Truncate (delete all rows from) one or more storage tables | `kbagent storage truncate-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` |
| Swap two storage tables in a development branch | `kbagent storage swap-tables --project PROJECT --table-id TABLE-ID --target-table-id TARGET-TABLE-ID` |
| Delete one or more storage buckets | `kbagent storage delete-bucket --project PROJECT --bucket-id BUCKET-ID` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ All seven commands authenticate via `KBC_MANAGE_API_TOKEN` (Manage API), not the
- `storage upload-table --project NAME --table-id ID --file PATH [--incremental] [--branch ID]` -- upload CSV (branch-aware)
- `storage download-table --project NAME --table-id ID [--output FILE] [--columns COL ...] [--limit N] [--branch ID]` -- export table to CSV (branch-aware)
- `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 truncate-table --project NAME --table-id ID [--table-id ...] [--dry-run] [--yes] [--branch ID]` (since v0.32.0) -- delete all rows while preserving table schema, primary key, descriptions, sharing edges, and downstream dependents. Batch via repeated `--table-id`. Endpoint is uniformly async-via-job on every branch (returns a queued `tableRowsDelete` job; client polls via `_wait_for_storage_job` before returning). Idempotent (truncating an empty table is a no-op). Use when re-seeding a table without losing the schema contract
- `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 swap-tables --project NAME --table-id ID --target-table-id ID --branch ID [--dry-run] [--yes]` (since v0.28.0) -- swap two storage tables in a dev branch (POST `/tables/{id}/swap`). Both tables exchange physical positions; aliases are NOT transferred (they keep pointing at the same physical position and therefore expose the OTHER table's data after the swap). Service refuses without a branch (active branch via `branch use` works too). Use to flip a typed rebuild ("data_change_log") into the original name ("data") without touching downstream config references
Expand Down
43 changes: 43 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,49 @@
swaps it back into the original name. After merging the branch the
original table now carries the typed schema with no downstream config
rewrite required.

## `storage truncate-table` preserves schema; endpoint is uniformly async-via-job (since v0.32.0)

- `kbagent storage truncate-table --project P --table-id T [--branch ID]
[--dry-run] [--yes]` calls
`DELETE /v2/storage/[branch/{id}/]tables/{id}/rows?allowTruncate=1`
on the Storage API. The `allowTruncate=1` flag is a safety opt-in
the API requires whenever no row filter is sent -- omitting it
returns HTTP 400. kbagent always passes it; do the same in any
`--hint client` script.
- **Do NOT pass `async=true` on this endpoint.** Sibling destructive
endpoints (`delete_table`, `delete_bucket`) require `async=true`,
but the row-delete endpoint **rejects** it with HTTP 400
(`"async: This field was not expected."` -- verified live
2026-05-11 on connection.europe-west3.gcp.keboola.com). The endpoint
is inherently async on every branch: it always returns HTTP 202
with a queued storage job (`operationName: tableRowsDelete`) that
the client polls via `_wait_for_storage_job` -- same machinery as
`delete_table`, just without the `async=true` query param.
- **Sub-second on production, longer on dev branches.** Same poll
loop in both cases; only wall-clock latency differs. From the
caller's perspective the call always blocks until rows_after=0
is authoritative on return.
- **Idempotent.** Truncating an empty table is a no-op success
(`rows_before=0`, `rows_after=0`, `failed=[]`). Safe to retry; safe
to run as a pre-load step that may or may not have data to clear.
- **What survives:** column definitions, types, primary key,
descriptions, sharing edges, and every downstream config reference
(aliases, input/output mappings, transformation refs). What does
not survive: the rows. Pick `truncate-table` whenever the schema
contract must hold; pick `delete-table` only when retiring the
table itself.
- **Propagation.** The Storage API removes the rows immediately on
the warehouse side -- consumers of an aliased / shared bucket see
zero rows on the next query, no quiesce window. A downstream
transformation that started reading the table *just before*
truncate may see partial state mid-job. Plan re-seed steps so the
truncate completes before any downstream job picks it up.
- **Permission classification.** `storage.truncate-table` is
`destructive` -- alongside `delete-table`, `delete-column`,
`delete-bucket`, `swap-tables`. Schema preservation does not
downgrade the row-data destruction.

## `data-app create --auth public` writes the canonical noneProxyAuthorization shape (since v0.29.0; fixes v0.27.0 silent HTTP 503)

- **What changed.** v0.27.0's `--auth public` wrote NO `authorization`
Expand Down
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.31.0"
version = "0.32.0"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
5 changes: 5 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.32.0": [
'New: `kbagent storage truncate-table --project NAME --table-id ID [--table-id ...] [--dry-run] [--yes] [--branch ID]` -- row-level truncation that drops every row from one or more storage tables while preserving the table definition (columns, types, primary key, descriptions, sharing edges, and every downstream config reference). Closes the only confirmed FIIA-migration gap (Coverage Matrix Row 25; R5 per-phase reload invariant). Wraps `DELETE /v2/storage/[branch/{id}/]tables/{id}/rows?allowTruncate=1`. Notable departure from sibling destructive endpoints: the row-delete endpoint is inherently async on every branch and rejects `async=true` as an unknown field (verified live 2026-05-11: HTTP 400 `"async: This field was not expected."`). The client therefore omits `async=true` and lets the endpoint return its natural HTTP 202 + queued `tableRowsDelete` job, which `_wait_for_storage_job` polls to completion -- same machinery as `delete_table`, just without the query-param dance. Multi-target: per-table errors accumulate without aborting the batch (one missing table does not block the rest). JSON envelope mirrors `delete-tables`\'s naming with a richer per-target receipt: `{truncated: [{table_id, rows_before, rows_after, branch_id}], failed: [{id, error}], dry_run, project_alias, would_truncate?}`. `--dry-run` captures `rows_before` via `get_table_detail` without truncating. Idempotent (truncating an empty table is a no-op success). Permission classification: `destructive` (gated behind `--allow-destructive` / `cli:destructive` policies) alongside `delete-table` / `delete-column` / `delete-bucket` / `swap-tables` -- schema preservation does not downgrade row-data deletion. Use this over `delete-table` whenever the schema contract must survive (sharing edges, aliases, dependent transformations, primary keys, column descriptions).',
"Tests: `tests/test_storage_truncate.py` adds 21 unit tests across three layers -- HTTP shape (5: URL+query-params with allowTruncate=1, branch_id URL prefix, async-poll roundtrip, URL encoding of dotted table IDs, 4xx propagation via `pytest_httpx`), service business logic (10: happy path, branch_id carried into truncated[] entries, non-numeric `rowsCount` defaults to 0, missing `rowsCount` defaults to 0, batch partial failure with NOT_FOUND on second target, dry-run skips `truncate_table`, branch_id propagation to both `get_table_detail` and `truncate_table`, unknown-project `ConfigError`, try/finally `close()` on API error, empty-list short-circuit), and CLI integration (6: JSON happy path with `--yes`, `--dry-run` JSON shape, `--branch` flag override, active-branch fallback, exit 1 on `failed[]`, exit 5 on `ConfigError`). E2E coverage in `tests/test_e2e.py::TestFullE2E` adds step 11.1 `_test_truncate_table_roundtrip`: snapshots schema (columns + primary key + identity) on the 8-row test table, dry-runs the truncate (verifies `would_truncate.rows_before` matches), applies it (verifies `rows_after=0`), re-verifies schema integrity (columns + PK + identity unchanged), then restores the 5+3 CSV pair so downstream hops see the original row count. Live-API smoke against project 1143 on `connection.europe-west3.gcp.keboola.com` 2026-05-11 confirmed the full flow including the async-only endpoint discovery.",
"Plugin docs: synced across all 7 silent-drift surfaces (CLAUDE.md #17). `commands/context.py` AGENT_CONTEXT gains the storage-Lifecycle entry. `CLAUDE.md` `## All CLI Commands` lists the new signature. `keboola-expert.md` Rule 6 VERSION GATE adds `storage truncate-table needs 0.32.0+`, the Tool Selection Matrix adds a `Re-seed a table without losing its schema / PK / dependents` row, and a new inline gotcha clarifies the uniformly-async behavior + the `async=true` rejection. `SKILL.md` auto-regenerated via `make skill-gen`. `commands-reference.md` adds the bullet between `delete-table` and `delete-column`. `gotchas.md` adds a `(since v0.32.0)` section covering the `allowTruncate=1` opt-in, the live-API discovery that `async=true` is rejected, the uniform async-via-job behavior, idempotence, propagation timing, and permission classification. Hint registry adds two-step entry under `storage.truncate-table` so `--hint client` and `--hint service` emit reusable snippets.",
],
"0.31.0": [
"New: `kbagent project edit --new-alias NEW [--dry-run]` -- rename the alias of an existing project connection without going through `project remove` + `project add` (which forces token re-entry). Cascades the rename through everything that persists the alias on disk: the `config.json` `projects` dict key (`pop(old)` + insert under `new`) AND the `default_project` field if it matched the old alias. When a nested-layout sync workspace is present at `<cwd>/<old-alias>/.keboola/manifest.json`, the directory itself is also renamed to `<cwd>/<new-alias>/` -- mirrors the `kbagent config rename` precedent (`-2`-suffix collision handling, git-mv with shutil.move fallback). Skips the disk step when no sync workspace is present. Combined with `--url` and/or `--token` in a single invocation those mutations target the NEW alias post-rename, so `kbagent project edit --project foo --new-alias bar --token NEW` is one atomic operation with the expected ordering. Backed by the new `ConfigStore.rename_project(old, new)` method (atomic dict-key swap + `default_project` update saved as one transaction) and a fail-closed `ProjectService._rename_project_alias()` helper that validates collision before touching any state. Validation: empty `new_alias`, whitespace-only `new_alias`, and `new_alias` that already exists are all rejected with `ConfigError` exit code 5.",
"New: `--dry-run` previews the rename (collision detection, planned disk-rename method `git_mv` vs `shutil_move`, lineage-cache warning) without mutating any state. Validation errors (`..` path-traversal, collision, invalid format) raise the same `ConfigError` exit-5 codes as the live path -- callers can rely on `--dry-run` as a 1:1 pre-flight. Token re-verification is also skipped in dry-run mode (no API hit). Result dict carries `dry_run: True` and a `planned` sub-dict. Backed by `_plan_project_alias_rename()` and `_plan_nested_sync_dir()` helpers in `services/project_service.py` -- pure read-only mirrors of the live `_rename_project_alias` / `_rename_nested_sync_dir`. Addresses PR #266 review NIT (UX consideration: even non-classically-destructive ops benefit from a dry-run pre-flight).",
Expand Down
Loading
Loading