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
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.26.0",
"version": "0.26.1",
"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
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ kbagent project description-get --project NAME
kbagent project description-set --project NAME [--text STR | --file PATH | --stdin]
kbagent project use ALIAS
kbagent project current
kbagent project invite --project ALIAS --email EMAIL --role admin|guest|readOnly|share [--reason TEXT] [--dry-run]
kbagent project invite --from-csv FILE [--default-role ROLE] [--workers N] [--dry-run]
kbagent project member-list --project ALIAS [--include-pending]
kbagent project invitation-list --project ALIAS
kbagent project invitation-cancel --project ALIAS --email EMAIL [--invitation-id ID] [--yes]
kbagent project member-remove --project ALIAS --email EMAIL [--yes]
kbagent project member-set-role --project ALIAS --email EMAIL --role admin|guest|readOnly|share

kbagent config list [--project NAME] [--component-type TYPE] [--component-id ID] [--branch ID] [--include-rows]
kbagent config detail --project NAME [--project NAME ...] --component-id ID [--config-id ID] [--branch ID] [--with-state]
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.DEFAULT_GOAL := help

.PHONY: help install install-mcp sync test test-unit test-integration test-e2e test-file lint lint-fix format format-check skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes check clean hooks
.PHONY: help install install-mcp sync test test-unit test-integration test-e2e test-e2e-invite test-file lint lint-fix format format-check skill-check skill-gen version-sync version-check changelog changelog-check check-error-codes check clean hooks

help: ## Show this help message
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
Expand All @@ -26,6 +26,9 @@ test-integration: ## Run integration tests only
test-e2e: ## Run E2E tests (E2E_API_TOKEN and E2E_URL required)
uv run pytest tests/test_e2e.py -v -s --tb=long

test-e2e-invite: ## Run project invite E2E (E2E_MANAGE_TOKEN + E2E_INVITE_PROJECT_ID required)
uv run pytest tests/test_e2e.py -v -s --tb=long -m e2e_invite

test-file: ## Run a specific test file (FILE=tests/test_cli.py)
uv run pytest $(FILE) -v

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.26.0",
"version": "0.26.1",
"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
35 changes: 33 additions & 2 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ a critical failure.
`kbagent --json context` and inspect the version. If missing commands
needed for the current task (e.g. `flow update` needs 0.22.0+,
`schedule find` needs 0.23.0+, `config set-default-bucket` needs
0.26.0+, `storage retype` is a future composite), you MUST refuse the
task and return a handoff message to the parent: `"Cannot proceed
0.26.0+, `project invite` / `project member-*` / `project invitation-*`
need 0.26.1+, `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>. Ask user to run
kbagent update, then re-invoke me."` Do not attempt the task with
workarounds that use MCP strip-bug-prone tools.
Expand Down Expand Up @@ -96,6 +97,13 @@ a critical failure.
| 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 |
| Invite a user to a project (single) | `kbagent project invite --project P --email E --role admin\|guest\|readOnly\|share` (0.26.1+) | raw `requests.post(/manage/projects/{id}/invitations)` only if version-gated out | `kbagent project invite` without `KBC_MANAGE_API_TOKEN` set; passing manage token via CLI flag |
| Invite many users (bulk) | `kbagent project invite --from-csv FILE [--default-role guest] [--workers N] [--dry-run]` (0.26.1+) | `--hint client` to generate a parallel script using `ManageClient` | per-row shell loop calling the CLI -- defeats the parallelism + idempotency the service already does |
| List active project members | `kbagent project member-list --project P [--include-pending]` (0.26.1+) | `tool call run_sync_action` against the Manage API | reading `.kbagent/config.json` to infer membership (it only stores the local user's token) |
| List pending invitations | `kbagent project invitation-list --project P` (0.26.1+) | -- | -- |
| Cancel a pending invitation | `kbagent project invitation-cancel --project P --email E --yes` (0.26.1+) | `--invitation-id ID` if email lookup is ambiguous | DELETE via raw HTTP without going through the service layer |
| Remove an active member | `kbagent project member-remove --project P --email E --yes` (0.26.1+, **destructive**) | `--hint client` for a script that removes by user_id directly | calling `member-remove` without `--yes` in non-interactive contexts (it will prompt and hang) |
| Change a member's role | `kbagent project member-set-role --project P --email E --role admin\|guest\|readOnly\|share` (0.26.1+) | -- | `PUT /manage/projects/{id}/users/{userId}` -- the API rejects PUT with 404, the kbagent client correctly uses **PATCH** |

If the table does not cover the user's task, **ask clarifying
questions** instead of guessing. Returning a targeted question is a
Expand Down Expand Up @@ -155,6 +163,29 @@ success, not a failure.
verification payload but do not treat it as a failure signal.
Production writes never materialize anything.

- **`project invite` "already invited / already member" is a no-op, not a failure** (0.26.1+):
Re-inviting a user the project already knows returns HTTP 400 from the
Manage API. kbagent normalises both "...already been invited..." and
"...already a member..." to `status="noop"` with a `note` field, exit 0.
**Do not retry on 400 from these commands** -- the user is already
on the project (or already pending). For bulk runs, `noop` rows count
toward `noop`, not `failed`, in the summary; surface that distinction
to the user when reporting bulk results.

- **`project invite --from-csv` ordering is non-deterministic** (0.26.1+):
Bulk invitation parallelises via `ThreadPoolExecutor` (default 8 workers).
The `rows[]` array in the JSON result is in completion order, not CSV
order. When reporting per-row outcomes to the user, **match by `email`,
not by index**. Partial-success exits 0 with `failed > 0` reflected in
the JSON -- treat that as a soft failure that needs review, not a
catastrophe.

- **`project member-set-role` uses PATCH, not PUT** (0.26.1+): The Manage
API endpoint is `PATCH /manage/projects/{id}/users/{userId}` with
`{"role": "..."}`. PUT returns 404 even on real members. kbagent's
`ManageClient.update_project_member_role` emits PATCH; if you write a
`--hint client` script that hits the endpoint directly, do the same.

- **`legacy_branch_storage: true` on `--branch` writes** (0.25.2+):
Projects without the `storage-branches` feature flag (legacy fake-branch
projects) accept `--branch X` writes at the API level, but the
Expand Down
12 changes: 11 additions & 1 deletion plugins/kbagent/skills/kbagent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ description: >
stale local config file, config version overwrite,
default bucket, output bucket, default_bucket, storage.output,
raw mode bucket override, custom output bucket name,
local workspace, project directory, kbagent init.
local workspace, project directory, kbagent init,
invite user, invite member, project invitation, manage members,
list members, remove member, change role, project role,
bulk invite, invite from CSV, project access, member management.
---

# kbagent -- Keboola Agent CLI
Expand Down Expand Up @@ -87,6 +90,12 @@ When working inside a git repository or project directory, run `kbagent init` (o
| Show the effective default project | `kbagent project current` |
| Get the Keboola dashboard project description | `kbagent project description-get --project PROJECT` |
| Set the Keboola dashboard project description (markdown) | `kbagent project description-set --project PROJECT` |
| Invite a user (or many users via CSV) to one or more projects | `kbagent project invite` |
| List active members of a project (and optionally pending invitations) | `kbagent project member-list --project PROJECT` |
| List pending project invitations | `kbagent project invitation-list --project PROJECT` |
| Cancel a pending invitation | `kbagent project invitation-cancel --project PROJECT --email EMAIL` |
| Remove an active member from a project (destructive) | `kbagent project member-remove --project PROJECT --email EMAIL` |
| Change an existing member's role (PATCH) | `kbagent project member-set-role --project PROJECT --email EMAIL --role ROLE` |
| Set up projects and register them in the kbagent config | `kbagent org setup --url URL` |
| List available components from connected projects | `kbagent component list` |
| Show detailed information about a specific component | `kbagent component detail --component-id COMPONENT-ID` |
Expand Down Expand Up @@ -231,6 +240,7 @@ For detailed response parsing rules and common pitfalls, see [gotchas](reference
| Storage Files (upload, download, tags, load/unload) | [storage-files-workflow](references/storage-files-workflow.md) |
| **Storage column types** (native types, NOT NULL, DEFAULT, branch materialize) | [storage-types-workflow](references/storage-types-workflow.md) |
| Bucket sharing & linking | [sharing-workflow](references/sharing-workflow.md) |
| **Project members & invitations** (single + bulk via CSV, role change, remove) | [member-workflow](references/member-workflow.md) |
| Dev branches | [branch-workflow](references/branch-workflow.md) |
| Encrypting secrets for MCP tools | [encrypt-workflow](references/encrypt-workflow.md) |
| Sync & Git-branching (GitOps) | [sync-workflow](references/sync-workflow.md) |
Expand Down
12 changes: 12 additions & 0 deletions plugins/kbagent/skills/kbagent/references/commands-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ All commands support `--json` for structured output. Multi-project flags (`--pro
- `project use ALIAS` -- pin `ALIAS` as the persistent default project. Stored as `default_project` in config.json. Overridden at runtime by `KBAGENT_PROJECT=ALIAS` (env, beats pin) and by `--project ALIAS` (CLI flag, beats both)
- `project current` -- print the effective default project and its source (`env` / `pin` / `none`). Reports both the env override AND the persisted pin so misconfigurations are visible. Returns `{"alias": null, "source": "none"}` when neither is set

## Project Members & Invitations (since v0.26.1)

All seven commands authenticate via `KBC_MANAGE_API_TOKEN` (Manage API), not the project's Storage token. Allowed roles are exactly `admin`, `guest`, `readOnly`, `share` -- the API self-reports this list in its 400 validation error and `constants.PROJECT_ROLES` mirrors it.

- `project invite --project ALIAS --email EMAIL --role admin|guest|readOnly|share [--reason TEXT] [--dry-run]` -- single-shot invitation. Returns `{"status": "ok", "invitation_id": ..., ...}`. Re-inviting an already-invited or already-member email returns `{"status": "noop", "note": "already_invited" | "already_member"}` (HTTP 400 from the Manage API, normalised to a no-op).
- `project invite --from-csv FILE [--default-role ROLE] [--workers N] [--dry-run]` -- bulk invitation. CSV header required; columns: `email`, `project` (alias) or `project_id` (numeric), `role` (optional with `--default-role`), `reason` (optional). Parallelised via `ThreadPoolExecutor` (default 8 workers). Single-stack-URL invariant per file: rows referencing different stacks raise `ConfigError` upfront. Result is `{"total","succeeded","noop","failed","rows":[...]}`; `rows[]` order is *not deterministic*. Exit 0 even with `failed > 0` -- inspect the JSON.
- `project member-list --project ALIAS [--include-pending]` -- list active members. Each member dict carries `id`, `email`, `name`, `role`, `status`, `mfa_enabled`. With `--include-pending`, the response also includes `pending_invitations: [...]`.
- `project invitation-list --project ALIAS` -- list pending (unaccepted) invitations only.
- `project invitation-cancel --project ALIAS --email EMAIL [--invitation-id ID] [--yes]` -- cancel a pending invitation. Without `--invitation-id`, the service resolves it by listing pending invitations and matching `--email` (case-insensitive). 204 No Content on success; `KeboolaApiError(NOT_FOUND)` if the email has no pending invitation.
- `project member-remove --project ALIAS --email EMAIL [--yes]` -- destructive: remove an active member. The service resolves `--email` to the numeric `user_id` (case-insensitive) and DELETEs `/manage/projects/{id}/users/{userId}`. Re-add the user via `project invite`.
- `project member-set-role --project ALIAS --email EMAIL --role admin|guest|readOnly|share` -- change an existing member's role. Uses **PATCH** `/manage/projects/{id}/users/{userId}` with `{"role": "..."}`. PUT does *not* work on this endpoint -- pre-v0.26.1 implementations that tried PUT got a misleading 404.

## Permission flags (top-level, session-only)
- `--deny-writes` -- block all write/destructive/admin operations for this single invocation. Merges with any persisted permission policy; never written to config.json. Exit code 6 (PERMISSION_DENIED) on blocked operations
- `--deny-destructive` -- block only destructive operations (delete-table, delete-bucket, terminate-job, etc.) for this invocation. Pure-write ops like create-table stay allowed. Use this when you want to keep build-up capabilities but lock out tear-downs
Expand Down
31 changes: 31 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Gotchas -- Response Parsing and Common Pitfalls

## `project invite` "already invited / already member" returns HTTP 400, not 422 (since v0.26.1)

- Re-inviting a user the project already knows about returns HTTP **400** with
one of two error strings:
- `"This user has already been invited to this project."` (pending invitation)
- `"This user is already a member of this project."` (active member)
- `MemberService.invite()` translates both cases to `status="noop"` with
`note="already_invited"` / `"already_member"` -- they are *not* exit-1
failures. Bulk runs (`--from-csv`) count them as `noop` in the summary, not
`failed`.
- The 422 heuristic in pre-v0.26.1 orchestrator scripts (`invite_participants.py:25`)
is **wrong** for this API. If you write a parallel implementation, key off
status_code 400 + the substring marker, not 422.

## `project member-set-role` is PATCH, not PUT (since v0.26.1)

- The Manage API role-change endpoint is `PATCH /manage/projects/{id}/users/{userId}`
with body `{"role": "..."}`. **PUT returns 404** ("resource not found") even
on a real, currently-active member -- the endpoint shape is PATCH-only.
- The kbagent `ManageClient.update_project_member_role` method emits PATCH;
any custom code re-implementing the call must do the same.

## `project invite --from-csv` order is not deterministic (since v0.26.1)

- Bulk invitation parallelises via `ThreadPoolExecutor` (default 8 workers).
The `rows[]` array in the result is in completion order, not CSV order.
- Per-row parsing of `failed_rows` should match by `email`, not by index.
- A failed row never aborts the run -- the executor accumulates results and
the command exits 0 with `failed > 0` reflected in the JSON summary. Mirror
the `org setup` partial-success exit semantics.

## `default_bucket` is per-config and only an output prefix (since 0.26.0)

- `kbagent config set-default-bucket` writes
Expand Down
Loading
Loading