feat: make vk safe to drive from scripts and agents #32

Merged
jmarya merged 8 commits from feat/agent-cli into main 2026-09-29 18:00:52 +00:00
Owner

Makes vk safe and practical to drive from scripts and AI agents. The four parts from our discussion each have their own commit, plus two fixes found in live testing and a README section.

1. Errors, exit codes and --json everywhere (ee37bad)

  • Failures used to be panics (exit 101), and list calls silently returned a partial result when a page failed. Every command now returns a typed error with its own exit code: 2 usage, 3 not found, 4 auth, 5 conflict, 6 api, 7 network. With --json, errors come out on stderr as {"error": {"kind", "message"}}.
  • --json / -j works anywhere on the command line, or via VK_JSON=1. Write commands print their result, so vk new --json gives you the new task's id.
  • Interactive commands (check, timeline, and edit/comment with no text) refuse to start without a terminal instead of hanging on an editor.
  • --project foo used to take the first project whose title contained "foo". An exact title now wins, and an ambiguous partial match is an error that lists the candidates.

2. Safe edits (5b6fd82, fix a307f30)

  • --expect-updated <time> on edit, done, fav, label, assign, relation and rm. If the task changed since you read it, the command fails with exit 5 and writes nothing. The editor flow and vk check apply the same check against the version they opened.
  • --dry-run / -n on every write shows a field-by-field diff, or a summary for creates and deletes, without writing. It still resolves the task, project, label or user, so a dry run catches the same errors the real run would.
  • - reads a description or comment from stdin.
  • Users can be given by numeric id. API tokens are refused on Vikunja's user routes, and the error now says so.

3. vk context and vk changes (12175f3, fix 70cfdbe)

  • vk context 42: the task's fields, description, checklist, relations and comments in one Markdown document (or JSON). updated is shown verbatim, ready for --expect-updated.
  • vk changes --since 12h [-p project]: tasks created, done or updated since then, newest first. It uses the server-side updated filter and re-checks every result locally. On a live instance with 667 tasks it matched a full scan exactly.

4. vk claim / vk unclaim / vk --mine (66aee26)

  • Claiming assigns you to the task. It's a conflict if someone else is assigned, unless you pass --force. Repeating a claim or unclaim is a harmless no-op.
  • Claims aren't atomic on the server, so claim reads the task back afterwards. If two agents claim at once, the one with the higher user id backs off: exactly one wins, and they can't both give up.
  • API tokens can't ask the server who they belong to, so identity comes from VK_USER_ID, then user_id in vk.toml, then /user.

Verification

  • Build, fmt, clippy -D warnings and RUSTDOCFLAGS=-D warnings cargo doc are clean. 102 tests pass (up from 68).
  • Read-only checks against a live instance: every error kind and exit code, dry runs, context and changes.
  • Writes were tested end to end in a temporary project that was deleted afterwards: create, guarded edit chains, stale-write refusal, stdin input, claim / already-claimed / claimed-by-other / unclaim, done, and project deletion. This found the second-precision bug fixed in a307f30: create/update responses report updated with nanoseconds, but Vikunja stores whole seconds.
  • The race resolution is covered by unit tests only. Testing it live would need a second user.

🤖 Generated with Claude Code

Makes vk safe and practical to drive from scripts and AI agents. The four parts from our discussion each have their own commit, plus two fixes found in live testing and a README section. ### 1. Errors, exit codes and `--json` everywhere (`ee37bad`) - Failures used to be panics (exit 101), and list calls silently returned a partial result when a page failed. Every command now returns a typed error with its own exit code: 2 usage, 3 not found, 4 auth, 5 conflict, 6 api, 7 network. With `--json`, errors come out on stderr as `{"error": {"kind", "message"}}`. - `--json` / `-j` works anywhere on the command line, or via `VK_JSON=1`. Write commands print their result, so `vk new --json` gives you the new task's id. - Interactive commands (`check`, `timeline`, and `edit`/`comment` with no text) refuse to start without a terminal instead of hanging on an editor. - `--project foo` used to take the first project whose title *contained* "foo". An exact title now wins, and an ambiguous partial match is an error that lists the candidates. ### 2. Safe edits (`5b6fd82`, fix `a307f30`) - `--expect-updated <time>` on edit, done, fav, label, assign, relation and rm. If the task changed since you read it, the command fails with exit 5 and writes nothing. The editor flow and `vk check` apply the same check against the version they opened. - `--dry-run` / `-n` on every write shows a field-by-field diff, or a summary for creates and deletes, without writing. It still resolves the task, project, label or user, so a dry run catches the same errors the real run would. - `-` reads a description or comment from stdin. - Users can be given by numeric id. API tokens are refused on Vikunja's user routes, and the error now says so. ### 3. `vk context` and `vk changes` (`12175f3`, fix `70cfdbe`) - `vk context 42`: the task's fields, description, checklist, relations and comments in one Markdown document (or JSON). `updated` is shown verbatim, ready for `--expect-updated`. - `vk changes --since 12h [-p project]`: tasks created, done or updated since then, newest first. It uses the server-side `updated` filter and re-checks every result locally. On a live instance with 667 tasks it matched a full scan exactly. ### 4. `vk claim` / `vk unclaim` / `vk --mine` (`66aee26`) - Claiming assigns you to the task. It's a conflict if someone else is assigned, unless you pass `--force`. Repeating a claim or unclaim is a harmless no-op. - Claims aren't atomic on the server, so claim reads the task back afterwards. If two agents claim at once, the one with the higher user id backs off: exactly one wins, and they can't both give up. - API tokens can't ask the server who they belong to, so identity comes from `VK_USER_ID`, then `user_id` in `vk.toml`, then `/user`. ### Verification - Build, fmt, `clippy -D warnings` and `RUSTDOCFLAGS=-D warnings cargo doc` are clean. 102 tests pass (up from 68). - Read-only checks against a live instance: every error kind and exit code, dry runs, `context` and `changes`. - Writes were tested end to end in a temporary project that was deleted afterwards: create, guarded edit chains, stale-write refusal, stdin input, claim / already-claimed / claimed-by-other / unclaim, done, and project deletion. This found the second-precision bug fixed in `a307f30`: create/update responses report `updated` with nanoseconds, but Vikunja stores whole seconds. - The race resolution is covered by unit tests only. Testing it live would need a second user. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
vk was hard to drive from scripts and agents: failures were panics with
exit code 101, list endpoints silently returned a partial result when a
page failed, and JSON output existed on four commands only.

Errors. Every command now returns a VkError with a kind, reported on
stderr (as {"error": {"kind", "message"}} in JSON mode) with its own exit
code: 2 usage, 3 not found, 4 auth, 5 conflict, 6 api, 7 network, 1
other. HTTP statuses from Vikunja map onto these kinds, and the server's
message is kept. Paging stops on an error instead of treating it as the
end of the list.

JSON. --json / -j is accepted anywhere on the command line, or via
VK_JSON=1. Commands that change something print the resulting object:
the created or updated task (so `vk new` reports the new id), the new
comment, project or label, or {"deleted": ...}. Interactive commands
(check, timeline, edit and comment without text) refuse JSON mode and a
missing terminal instead of hanging on an editor. sync prints its stats.

Project names. `--project foo` used to take the first project whose
title contained "foo", which could silently pick the wrong one. An exact
title match now wins, a single partial match is still accepted, and
several are an error listing the candidates. Labels and users resolve
through the same not-found errors instead of unwrapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk already re-reads a task before writing it back, so omitted fields are
no longer cleared. That only protects the milliseconds between vk's own
read and write, though. An agent that reads a task, thinks, and writes
minutes later still overwrites whatever a person changed in between.

--expect-updated <time> on edit, done, fav, label, assign, relation and
rm takes the task's `updated` value from an earlier read. If the task
has changed since, the command fails with a conflict (exit 5) and writes
nothing. Any RFC 3339 spelling of the same instant matches. The editor
flow of `vk edit` and the `vk check` checklist apply the same check
implicitly, against the version they opened, so a long editing session
cannot save over changes made meanwhile.

--dry-run / -n on every command that changes something reports what it
would do without doing it: a field-by-field diff for task updates, or a
summary of the create/delete/relate. It still resolves the task,
project, label or user, so a dry run catches the same not-found errors
the real run would. With --json the report is
{"dry_run": true, "action", "task", "changes": {field: {from, to}}}.

Task updates are split into pure functions (TaskEdit::apply, with_done,
with_favorite) and one update_task call, which is what makes the dry
run exact.

Also:
- `-` reads a description or comment from stdin, so long markdown
  doesn't have to go through shell quoting
- users can be given by numeric id. API tokens are refused on
  Vikunja's user routes, so name lookups cannot work with them, and the
  error now says so

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two read commands that replace several round trips.

`vk context <id>` gathers one task with everything around it: its
fields, the description as markdown, the checklist, related tasks and
comments. By default it is a markdown document that a person or a
language model can read as is. The `updated` value is printed verbatim,
because that is what --expect-updated compares against. With --json
the same sections come as structured data next to the full task.
Previously this took info, comments and a lookup per relation.

`vk changes [--since 24h] [-p project]` lists tasks created, completed
or updated since a point in time (30m, 12h, 3d, 2w, or a date), most
recent first, so an agent starting a new session can catch up in one
call. The server-side `updated > '<time>'` filter is used and each result
re-checked locally. Against a live instance it matched a full scan
exactly. Changes that do not touch a task's `updated` field, and
deleted tasks, cannot show up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vikunja stores whole seconds, but its responses to creating or updating
a task carry the unrounded time (…:35.69486587). An `updated` value taken
from `vk new --json` or `vk edit --json` therefore never matched the
stored …:35, and every guarded write that followed failed with a false
conflict. Found by chaining writes against a live instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The description's markdown included the task list as loose bullets, and
the Checklist section repeated it. Task lists are now left out of the
Description section, which is skipped when nothing else remains. The
JSON output keeps the full description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lets several agents, or an agent and a person, share a board without
doing the same work twice. A claim is an assignment to yourself.

- `vk claim <id>` assigns you. It is a conflict (exit 5) if someone else
  is assigned, unless --force, which adds you alongside them. Claiming a
  task you already hold is a no-op success, so retries are safe. A done
  task cannot be claimed.
- `vk unclaim <id>` unassigns you. Also a no-op when it is not yours.
- `vk --mine` lists the tasks assigned to you.
- Both support --dry-run and --json, and claim takes --expect-updated.

Claims are not atomic on the server, so two agents claiming at once can
both end up assigned. After assigning, claim reads the task back. If a
rival appeared meanwhile, the one with the higher user id unassigns
itself and gets a conflict. Every racer applies the same rule, so
exactly one keeps the task and neither livelocks.

Identity: API tokens are refused on /user, so vk cannot ask whom they
belong to. The user is taken from VK_USER_ID, then `user_id` in
~/.config/vk.toml, then /user (password logins). The error explains how
to find your id otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A README section on driving vk from scripts and agents: context and
changes, --json, the exit codes and error kinds, --expect-updated,
--dry-run, reading text from stdin, and claiming (including the user_id
setting API tokens need).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vk claim` and `--mine` need to know which user vk acts as, and API
tokens cannot ask the server. That meant editing the config by hand.

- A password login now looks up the user id once and saves it with the
  token.
- `vk login --token tk_… --user-id <id>` sets up an API token from the
  command line and records whose it is. Without --user-id it still logs
  in and says what claim needs.
- Credentials are checked before the config is written, so a bad token
  or password leaves the existing config alone.
- The config is written through the Config type, so values are escaped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jmarya merged commit 0f0c1848a9 into main 2026-09-29 18:00:52 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jmarya/vk!32
No description provided.