The previous init flow took whatever the user typed for --server and
called GET <url>/api/v1/info on it. If the user typed
"vikunja.example.com" (no scheme), or pasted the URL with /api/v1 in
it (double-suffix), or pointed at a localhost install on the default
:3456 port without typing the port, we'd hand back a raw HTTP error.
New `client.DiscoverServer` ports the frontend's
helpers/checkAndSetApiUrl.ts discovery: probe a small ordered set of
plausible bases for /api/v1/info, return the first one that returns
parseable Info. Candidate order:
1. scheme://host[:port]/path (as the user typed it)
2. scheme://host:3456/path (default API port)
3. opposite scheme of (1)
4. opposite scheme of (2)
Heuristics:
- Missing scheme → https for public hosts, http for localhost /
127.0.0.1 / [::1] (matches most CLIs' behaviour)
- Trailing /api/v1 from a pasted URL is stripped before probing, so
we don't double up to /api/v1/api/v1/info
- Trailing slashes normalized
Errors now list everything we tried + the last underlying network
error, so the user can see why a URL failed instead of just
"GET /info: connection refused":
veans: VALIDATION_ERROR: couldn't find a Vikunja instance reachable
from "vikunja.example.com" — tried:
- https://vikunja.example.com/api/v1/info
- https://vikunja.example.com:3456/api/v1/info
- http://vikunja.example.com/api/v1/info
- http://vikunja.example.com:3456/api/v1/info
last error: dial tcp: lookup vikunja.example.com: no such host
bootstrap.Init now defers URL canonicalisation to DiscoverServer and
caches the matched info from the probe (no second /info round-trip).
Unit tests cover the candidate-builder across the common shapes:
bare hostname, localhost, /api/v1-suffixed paste, explicit port,
subpath install, 127.0.0.1:3456, trailing slash. e2e green.
Adds a final step to bootstrap.Init that offers to wire `veans prime`
into Claude Code and OpenCode automatically. Per-agent yes/no prompts
default to "yes" for Claude Code and "no" for OpenCode; --install-claude
/ --install-opencode flags skip the prompt for scripted contexts;
--no-hooks falls back to the previous behaviour of just printing the
snippets.
Claude Code:
- Writes/merges .claude/settings.json
- JSON merge preserves existing keys (model, permissions, other hooks)
and only appends a `veans prime` command entry under SessionStart
and PreCompact if one isn't already there
- Idempotent: re-running reports "Already configured" without
duplicating entries
OpenCode:
- Writes .opencode/plugin/veans-prime.ts with the standard handler
skeleton
- Existing files are left alone (no TS-merge story for v0)
Failures during hook install are non-fatal: the repo is already
configured, so the user gets a warning + the printed snippets as a
fallback path.
Unit tests cover the merge logic (fresh file, idempotent rerun,
preserving user's other hooks/keys), the install actions
("Wrote"/"Updated"/"Already configured"), and the offer flow
(flags-bypass-prompt vs prompt-when-unset vs no-hooks).
Vikunja's built-in OAuth server (Vikunja 2.3+) does not require client
registration and accepts arbitrary client_ids — it just enforces PKCE
(S256) and constrains redirect URIs to the vikunja- scheme. Earlier I
deferred OAuth on the assumption it needed a registered client; that
was wrong, and the docs make the path much smoother than POST /login.
The custom-scheme constraint (no http:// loopback) is side-stepped by
manual paste-back: veans prints the authorize URL, the user signs in,
their browser fails to open vikunja-veans-cli://callback?code=... and
shows an error, the user copies the URL from the address bar and
pastes it back. CLI extracts code + state, verifies state for CSRF,
exchanges via POST /api/v1/oauth/token (JSON body — Vikunja rejects
form-encoded), and returns the access token.
Resolution order in AcquireHumanToken:
1. --token (paste-in JWT or personal API token; SSO/OIDC users)
2. --use-password / --username + --password (POST /login)
3. OAuth flow (interactive default)
login command supports the same --use-password / --token escape hatches
for token rotation on instances with OAuth disabled.
Includes unit tests for the PKCE generator (verifier shape per RFC 7636,
challenge = SHA256(verifier) base64url-no-pad), authorize-URL
construction, and the lenient callback parser (full URL / query-only /
bare code).