vikunja/veans/AGENTS.md

7.6 KiB

AGENT Instructions for veans

Things to know before touching this submodule that aren't obvious from reading the code. The parent repo's CLAUDE.md covers the rest of Vikunja; this file is veans-specific.

Module layout

  • veans/ is its own Go module (code.vikunja.io/veans), separate from the parent. Don't try to import code.vikunja.io/api/... — that pulls XORM into the CLI binary. Wire types live in internal/client/types.go as plain JSON-tagged structs that mirror the parent models.
  • License headers are enforced by goheader in veans/.golangci.yml. Every new .go file needs the AGPLv3 banner from veans/code-header-template.txt (a copy of the parent's, kept local so the linter resolves the path relative to this module).

Building and testing

  • mage build./veans binary. The Aliases map in magefile.go routes bare names like mage test to Test.All — without aliases, mage rejects namespace invocations ("Unknown target specified").
  • Unit tests: mage test or go test ./....
  • E2e tests: assume an externally-running Vikunja at VEANS_E2E_API_URL and admin creds in env (VEANS_E2E_ADMIN_TOKEN, or VEANS_E2E_ADMIN_USER + VEANS_E2E_ADMIN_PASS). The package self-skips when VEANS_E2E_API_URL is empty, so plain go test is safe locally.
  • Local e2e loop: from the parent repo root, build the API (mage build:build), run it with sqlite-memory + a known JWT secret, register an admin user via POST /register, then go test ./e2e/... from veans/ with the env vars above.
  • CI: the test-veans-e2e job in .github/workflows/test.yml consumes the existing vikunja_bin artifact from api-build; don't recompile the API in a parallel workflow. The veans-test job runs unit tests independently and gives fast feedback.

Vikunja wire-format gotchas

Most failures surface when crossing the JSON boundary. The list below is what's bitten me; if a new endpoint behaves oddly, suspect one of these:

  • ProjectView.view_kind and bucket_configuration_mode are strings, not ints. The parent enums (ProjectViewKind, BucketConfigurationModeKind) have custom MarshalJSON that emits "kanban" / "manual" etc. Use the string constants in internal/client/types.go.
  • Task.BucketID is always 0 in GET /tasks/:id. The model has xorm:"-" on it — the actual bucket lives in a separate task_buckets table. Fetch with ?expand=buckets and use task.CurrentBucketID(viewID) to read it.
  • POST /tasks/{id} does NOT move tasks between buckets. The task↔bucket relation is row-shaped; use client.MoveTaskToBucket() which hits POST /projects/{p}/views/{v}/buckets/{b}/tasks. The Update path on the server only auto-moves on done flips.
  • Bot user creation is PUT /user/bots, not /bots — the routes are registered under the /user subgroup. Same prefix for GET /user/bots.
  • APIToken.expires_at is required. The struct field has valid:"required" upstream; sending it omitted or zero fails validation. Use client.FarFuture (year 9999) when you mean "no expiry" — the frontend does the same.

API token permissions

  • Vikunja validates token permissions against apiTokenRoutes, a map built dynamically from registered routes. Group names are derived from the URL path (params stripped, joined by _). Examples:
    • /projects/:project/views/:view/buckets/:bucket/tasks → group projects, action views_buckets_tasks
    • /tasks/:task/comments → group tasks_comments, action create
  • client.PermissionsForBot() calls GET /routes at runtime and grants only the intersection of what we want and what the server exposes. Don't hard-code permission group names — they drift across Vikunja versions, and discovery keeps the bot's grant valid across upgrades.

Bot ownership and token minting

  • Creating a bot via PUT /user/bots automatically sets the bot's bot_owner_id to the calling user. Only the owner can mint tokens for the bot via PUT /tokens with owner_id=<bot_id>. The init flow does these as a single human-JWT-authenticated batch.
  • Bots have no password and cannot authenticate via POST /login. After init, veans login re-authenticates as the human (not the bot) and mints a fresh bot token.

OAuth flow

  • Vikunja's authorization server requires PKCE/S256 and accepts either vikunja-…:// custom schemes or RFC 8252 loopback URIs (http://127.0.0.1:NNN/, http://localhost:NNN/, http://[::1]:NNN/). No client registration needed — client_id can be any consistent string (we use veans-cli).
  • internal/auth/oauth.go binds a free port on 127.0.0.1, opens the browser, and captures the callback. The Shutdown defer uses context.WithoutCancel(ctx) so cancellation at the outer scope still drains the loopback server cleanly.
  • Token exchange is JSON only. Form-encoded POSTs to /oauth/token fail; the standard golang.org/x/oauth2 client speaks form encoding, which is why we have a hand-rolled client.ExchangeOAuthCode.

Credential store

  • Lookup chain: keychain → env (VEANS_TOKEN, optionally pinned by VEANS_SERVER) → file (~/.config/veans/credentials.yml, mode 0600, honors XDG_CONFIG_HOME).
  • Chain.Set falls through to the next backend on error so a missing dbus on a CI runner doesn't block writes — the file backend is the reliable last-resort.
  • E2e tests override HOME and XDG_CONFIG_HOME per test to keep the developer's keyring untouched. Don't bypass the credentials package in tests — leaks between tests will surface as the wrong bot token.

Project identifiers and bot usernames

  • Project Identifier is runelength(0|10), can be empty. When empty, Config.FormatTaskID renders #NN; otherwise PROJ-NN. Both are accepted by runtime.resolveTaskID along with bare integers.
  • Bot username must start with bot-; the server enforces it. Hyphens, digits, lowercase letters allowed; no spaces, no commas, no link-share-N pattern. config.SuggestedBotUsername does the folding for repo names.
  • E2e tests deriving identifiers from a unique suffix should use the trailing chars of strconv.FormatInt(time.Now().UnixNano(), 36). The leading chars barely change between consecutive runs and will collide if you take [:N].

Cobra surface conventions

  • Global --json flag flips both successful output (raw object for show, raw array for list) and the error envelope.
  • Stable error codes in internal/output/errors.go. Don't add new ad-hoc strings — wrap with output.New / output.Wrap.
  • RunE handlers that don't use args []string should rename it to _ to satisfy revive's unused-parameter rule.
  • The bucket-move dance (MoveTaskToBucket) runs after the field update on update, so a status transition can't clobber freshly attached labels. Comments for --status scrapped post before the bucket move so the audit trail reads in chronological order.

Things to not do

  • Don't add an os/exec.Command without ctx — noctx is enabled. Use exec.CommandContext(ctx, …) and thread the context through.
  • Don't commit the built binary. veans/.gitignore covers ./veans and ./veans.exe.
  • Don't write to stdout from prime when no .veans.yml is found. The hook contract is silent + exit 0 so the snippet is safe to install globally in ~/.claude/settings.json.
  • Don't change canonical bucket titles without updating internal/status/CanonicalBucketTitles, the prompt template, and the e2e assertions in lockstep — agents and humans both treat them as fixed strings.