Files
kolaente e271f75cad feat(init): use the hierarchical fuzzy picker for project selection
Replaces the flat numbered project list during 'veans init' with the interactive picker. --project <id> still bypasses it; non-TTY stdin fails cleanly asking for --project.
2026-06-10 13:51:46 +02:00
..

veans

A beans-shaped CLI for Vikunja. Drop it into a repo, run veans init, paste a hook snippet into your coding agent's settings, and the agent immediately knows to track its work in Vikunja instead of in TodoWrite or .beans/.

veans is a thin Go binary that wraps Vikunja's REST API with an opinionated agent-friendly surface and emits a system prompt teaching agents the workflow (claim → work → in-review → human closes). The agent prompt is re-emitted on every SessionStart and PreCompact, so context never goes stale.

Quick start

# 1. Build (or download) the binary
cd veans && mage build && sudo install ./veans /usr/local/bin/

# 2. In a repo with a Vikunja instance reachable
veans init --server https://vikunja.example.com

# 3. Wire it into Claude Code (.claude/settings.json):
{
  "hooks": {
    "SessionStart": [{ "hooks": [{ "type": "command", "command": "veans prime" }] }],
    "PreCompact":   [{ "hooks": [{ "type": "command", "command": "veans prime" }] }]
  }
}

# OpenCode (.opencode/plugin/veans-prime.ts):
export const VeansPrime = {
  event: ["session.start", "compact.before"],
  handler: async ({ exec }) => exec("veans prime"),
}

veans prime exits silently with status 0 when no .veans.yml is reachable upward from cwd, so the hook is safe to install in a global ~/.claude/ without breaking sessions in unrelated repos.

What veans init does

  1. Authenticates as you. Default is OAuth 2.0 Authorization Code + PKCE against Vikunja's built-in authorization server (Vikunja 2.3+ — no client registration needed). veans prints an authorize URL; you open it in your browser, sign in, and paste the resulting vikunja-veans-cli://callback?code=... URL back into the CLI. The browser will fail to open the custom scheme — that's expected; the address bar still has what we need.

    Alternative auth modes:

    • --token <jwt-or-personal-api-token> — paste-in, useful for SSO/OIDC
    • --use-password — fall back to POST /login (local accounts only)
    • --username + --password (non-interactive; implies --use-password)
  2. Asks you to pick a project and a Kanban view.

  3. Bootstraps the canonical buckets if missing: Todo, In Progress, In Review, Done, Scrapped.

  4. Creates a bot-<repo-name> user (Vikunja bot user — no password, no email, can't log in interactively).

  5. Shares the project with the bot at read+write.

  6. Mints a long-lived API token for the bot via PUT /tokens with owner_id, scoped to the discovered route groups (tasks, comments, labels, relations, assignees, etc.) the server actually exposes.

  7. Stores the token in your OS keychain (or ~/.config/veans/credentials.yml if no keychain is available).

  8. Writes .veans.yml to the repo root.

The token stored is the bot's, not yours. The human's transient session is discarded as soon as init finishes — rotate or revoke the bot independently without affecting your own session.

Commands

veans init                     OAuth/login → create bot → mint token → write .veans.yml
veans prime                    emit system prompt for agents (silent if no .veans.yml)
veans list                     filtered list (--ready, --mine, --branch, --filter, --status); emits JSON
veans show <id>                view a task (JSON)
veans create "title"           --description, --label, --status, --priority, --parent, --blocked-by
veans update <id>              --status, --title, --priority, --label-add/remove,
                               --description, --description-replace-old/new, --description-append,
                               --comment, --reason, --if-unchanged-since
veans claim <id>               assign the bot, move to In Progress, tag with current branch label
veans api METHOD PATH          raw REST passthrough — escape hatch for endpoints not wrapped here
veans login                    re-mint the bot's token (rotation)
veans version

Task IDs accept PROJ-NN (when the project has an identifier), #NN (when it doesn't), or a bare integer.

.veans.yml

Committed to the repo root. The numeric IDs are the source of truth; cached identifiers and bot username are for human-readable output.

server: https://vikunja.example.com
project_id: 42
project_identifier: PROJ        # may be "" — task IDs render as #NN then
view_id: 7
buckets:
  todo: 11
  in_progress: 12
  in_review: 13
  done: 14
  scrapped: 15
bot:
  username: bot-myrepo
  user_id: 99

Credentials

Resolved in order on every command:

  1. OS keychain (macOS Keychain, Windows Credential Manager, libsecret/gnome-keyring on Linux), via github.com/zalando/go-keyring.
  2. VEANS_TOKEN env var (read-only). Optionally pin to a server with VEANS_SERVER. Intended for CI / containers.
  3. ~/.config/veans/credentials.yml (mode 0600) — automatic fallback when the keychain is unavailable. Honors XDG_CONFIG_HOME.

Mage targets

mage build              # go build -o ./veans ./cmd/veans
mage test               # unit tests across the module
mage test:filter EXPR   # go test -run EXPR ./...
mage test:e2e           # e2e suite (needs VEANS_E2E_API_URL)
mage lint / lint:fix    # golangci-lint
mage fmt                # go fmt ./...
mage clean              # remove built binary

End-to-end tests

The suite in e2e/ assumes a running Vikunja API. Locally, point it at any dev instance:

export VEANS_E2E_API_URL=http://localhost:3456
export VEANS_E2E_ADMIN_USER=user1
export VEANS_E2E_ADMIN_PASS=12345678   # canonical fixture password
mage test:e2e

CI spins Vikunja up the same way the frontend Playwright suite does — see .github/workflows/veans-e2e.yml. The workflow builds the parent API binary, starts it with VIKUNJA_DATABASE_TYPE=sqlite, VIKUNJA_DATABASE_PATH=memory, fixtures from pkg/db/fixtures/, and runs mage test:e2e from this directory.

E2E tests never touch the developer's keychain — they override HOME and XDG_CONFIG_HOME per test, which forces the credential store to fall through to its file backend.

Status model

Status Bucket name Done flag Who moves there?
todo Todo false created here by default
in-progress In Progress false veans claim / update -s in-progress
in-review In Review false the agent, when work is finished
completed Done true humans / merge hook only
scrapped Scrapped true the agent, with --reason

The agent never moves tasks to completed itself — it parks them in In Review and a human (or the future merge hook) closes them once the PR lands.

Out of scope (for now)

  • OAuth 2.0 device flow (RFC 8628) — would let SSH'd / headless setups authenticate without a browser-on-the-same-machine; not implemented upstream yet.
  • Project-scoped API tokens — Vikunja doesn't ship them yet. The credential schema's scope field is forward-compatible for when it does.
  • Auto-installing hook snippets. We print them; you paste them.
  • Merge-hook GitHub Action that auto-closes tasks on PR merge — separate repo, future work.