Files
marketplace/cli/internal/scaffold/assets/skills/kettle-api/SKILL.md
T
naudachu 8b1b11001a feat: drop the kettle plugin; the binary writes its own skills
The plugin and the binary shipped on two release cadences and nothing on an
operator's machine ever checked that the one they installed described the other.
The generated flag block existed precisely so a renamed flag could not ship with
documentation recommending the old one — and then shipped one version behind the
registry it came from, which is the same bug one hop downstream.

So the prose moved into the binary. `internal/scaffold` embeds every document;
`kettle init` and `kettle gen scaffold` write them into a project's own
`.claude/`. The two cannot disagree because there is one artefact.

The namespace survived the move. A project's skills are flat, so the prefix is
spelled into the directory name (`kettle-issue`); a project's *commands* take
their namespace from a subdirectory, so `commands/kettle/init.md` is still
`/kettle:init`. Four of the six command files are thin pointers at a skill, and
that is what kept ~1,600 lines of `/kettle:…` cross-references true without a
rewrite. `init` and `auth` lost `disable-model-invocation: true` — being a
command is that property — and `auth` now restricts `allowed-tools` so a model
cannot reach `kettle auth add` at all.

`gen scaffold` writes files whole rather than splicing a region. The old
refusal protected somebody's hand-written prose around the block; that prose is
embedded now, so there is none to protect, and preserving local edits would
freeze a project's documentation at whatever version first initialized it.
`--check` warns before an upgrade discards one.

The plugin's `agents-sync.sh` — 141 lines of Python behind a filename that said
`.sh` — became `internal/mirror` and `kettle mirror`. Same seven branches, same
refusal to merge two real files that differ, now with a table test per branch
and a check that a repair converges in one pass. `--hook` is the PreToolUse
form and exits 0 on every path including a panic. It is opt-in per project,
which is strictly narrower than the plugin hook that was on for everybody who
installed it.

`kettle init --interactive` walks a person through the login, the token (read
with the echo off, so it lands in no history and no file), the repository, the
`.claude/` tree and the mirror hook. It refuses a stdin that is not a terminal
and names the flags instead: every question it asks has one, and it performs
nothing itself, so an interactive run and a flag run are one code path.

Two rules that used to be prose are now the binary's: init refuses a linked
worktree and names the main checkout, and writing into an existing
`.claude/settings.json` is refused with the snippet printed rather than
reformatting a file the operator commits.

The scaffold version stamp went to its own `.kettle/scaffold.yaml` rather than
into `config.yaml`, because unknown keys there are a hard error and that file
may be committed and read by whatever build each machine has.

golang.org/x/term becomes a direct dependency; it was already in the tree
indirectly, so no module was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:17:24 +05:00

12 KiB


name: kettle-api description: Everything Gitea has that is not an issue — pull requests, releases, tags, branches, milestones, labels, commits, actions, webhooks, notifications, tracked times, users, repositories — reached with kettle api, one endpoint per request, under the login the project already pins. Load when the user asks to open or review a PR, cut or edit a release, make a milestone or a tag, look at branches or commits, read notifications or actions, or hit any Gitea endpoint by hand. Issues are NOT handled here: /kettle:issue works on them offline and /kettle:sync moves them to and from the tracker.

/kettle:api — Gitea beyond issues

kettle api <endpoint> is one authenticated request to the Gitea this project is pinned to. No second tool, no second login: the URL, the token and the repository are the ones /kettle:auth and kettle init already resolved, and the request body is filed under .kettle/payload/ like every other request kettle makes.

This skill is the map of what to ask for. kettle help api is the flag reference, and it is generated from the binary — the block at the bottom of this file is the same text.

Issues are somewhere else

kettle api can reach an issue and must not be used to. An issue read this way comes back as a full JSON payload — every label object, every URL, the whole comment thread — which is precisely what the other two skills exist to keep out of the context window.

Skill Scope
/kettle:issue issues as units of work — create, read, grep, validate, tick, dependency graph. Offline.
/kettle:sync moving issues between the local store and the tracker — pull, push, comment, close, evict.

The one exception is an issue endpoint that is not about the issue's content: issues/{n}/comments is also a pull request's comment thread, and PATCH issues/{n} is also how a pull request's title and body are edited. Gitea numbers issues and pull requests in one sequence and serves both under /issues/.

What is a command and what is a request

Reach for the command where there is one: it knows the format, the store and the round trip. Everything else is an endpoint.

Subject How to reach it Pages?
issues (create, read, tick, validate) /kettle:issue — offline, no request at all
issues (pull, push, comment, close, evict) /kettle:sync handled
the canonical type/* and severity/* labels kettle labels handled
this repository's own releases, with binaries cd cli && make release TAG=v1.2.3
everything below kettle api see the column
Entity Endpoint Pages?
pull requests repos/{owner}/{repo}/pulls yes
one pull request repos/{owner}/{repo}/pulls/{n} no
create a pull request POST repos/{owner}/{repo}/pulls no
edit a PR's title or body PATCH repos/{owner}/{repo}/issues/{n} no
a PR's or issue's comments repos/{owner}/{repo}/issues/{n}/comments yes
edit one comment PATCH repos/{owner}/{repo}/issues/comments/{id} no
reviews on a PR repos/{owner}/{repo}/pulls/{n}/reviews yes
merge a PR POST repos/{owner}/{repo}/pulls/{n}/merge no
releases repos/{owner}/{repo}/releases yes
one release by tag repos/{owner}/{repo}/releases/tags/{tag} no
tags repos/{owner}/{repo}/tags yes
branches repos/{owner}/{repo}/branches yes
commits repos/{owner}/{repo}/commits yes
milestones repos/{owner}/{repo}/milestones yes
labels (all of them, not just canonical) repos/{owner}/{repo}/labels yes
webhooks repos/{owner}/{repo}/hooks yes
action tasks repos/{owner}/{repo}/actions/tasks yes
tracked times repos/{owner}/{repo}/times yes
the repository itself repos/{owner}/{repo} no
notifications notifications yes
who this token is user no
an organization's repositories orgs/{org}/repos yes

{owner} and {repo} are filled in from the project's configuration. A path that names a repository in full is left alone — repos/other-owner/other/releases reads another repository on the same instance, which is why there is no --repo flag. Another instance is KETTLE_URL and KETTLE_TOKEN, not a flag.

What the instance actually serves is its own version's business; its API docs at <instance-url>/api/swagger are the authority when an endpoint answers 404.

Pagination is yours

One invocation is one HTTP request. kettle api never follows a list to its end, because a passthrough that silently stitched pages together would report as one answer something that was several.

So for every row marked yes above:

kettle api 'repos/{owner}/{repo}/pulls?state=open&limit=50'   # first page, 50 rows
kettle api 'repos/{owner}/{repo}/pulls?state=open&limit=50&page=2'
  • limit is capped by the instance (MAX_RESPONSE_ITEMS, 50 by default); the default page size is 30.
  • A short page is the last one. Ask for 50, count what came back: fewer than 50 means there is no page 3. That is the same rule the binary's own listings use, and it needs no response headers.
  • Quote any endpoint holding ? or &, or the shell takes it apart.
  • Walking many pages of anything into your own context is a mistake before it is a request. Narrow the query (state=, since=, q=), or pipe through jq and keep the two fields you needed.

Writing a body

Two ways, and the choice is about the body, not the endpoint:

# small and flat: every value is a string
kettle api --field body=lgtm repos/{owner}/{repo}/issues/7/comments

# anything real — multi-line, markdown, booleans, numbers, nesting
mkdir -p tmp/release
cat > tmp/release/v0-2-0.json <<'EOF'
{"tag_name": "v0.2.0", "name": "v0.2.0", "draft": false,
 "body": "## Changes\n\nMulti-line markdown with `code`."}
EOF
kettle api --data @tmp/release/v0-2-0.json repos/{owner}/{repo}/releases
  • A body implies POST; anything else is -X PUT, -X PATCH, -X DELETE.
  • Newlines inside a JSON string are \n. Composing from a file: jq -Rs '{body: .}' < body.md > tmp/pull/x.json.
  • --field values are always strings. A draft: false or a number is a --data body — guessing types is how a tag_name of 1.0 goes up as a number.
  • Keep tmp/ gitignored and keep the file: a PATCH is usually the same body with one line changed. kettle files its own copy under .kettle/payload/ automatically; that directory is the transport's and nothing hand-made goes in it.
  • Attachments are multipart/form-data and this command sends JSON. Upload release binaries with the release tooling (make release), or the web UI.

Deleting

-X DELETE needs --yes in the same invocation, and the refusal happens before anything is sent:

kettle api -X DELETE --yes repos/{owner}/{repo}/releases/12

That flag is the whole gate. Whether a thing should be deleted is the operator's call, not a step in a plan — ask, do not assume, and never widen a deletion past what was named.

When it says 403

Gitea scopes a token as <read|write>:<category>, and a token minted to file issues carries write:issue and nothing else. Releases, pull requests, branches, tags and actions are all repository, so that token answers 403 on the first kettle api outside issues — and the 403 names no scope.

kettle auth list shows what each login on this machine recorded; kettle config shows what this project resolved. Nothing can be read back off the instance (Gitea's own token listing needs a password, not a token), so a scope that was never written down is a scope nobody knows. Minting a new token is the operator's job, in the web UI — /kettle:auth has the procedure.

What is not an API call at all

Want Do
check out a PR branch, clone, push git. This is git's job and always was
who am I kettle api user
open something in a browser nothing here; hand the user the URL
add a login, list logins, ssh keys /kettle:auth, and adding one is the operator's
administer users or the instance nothing here. Not an agent's work

The canonical issue format lives in ../issue/references/format.md — it describes local files, not requests.

Generated from the kettle command registry by kettle gen scaffold. Everything between the two markers is replaced on the next run — the prose around it is embedded in the binary and replaced with it.

kettle api <endpoint>

one request to this project's Gitea, for everything that is not an issue

Releases, pull requests, milestones, branches, tags, actions, webhooks, notifications: everything Gitea has that this binary has no command for. One invocation is ONE request — the credentials, the repository and the payload scratchpad are the ones this project already resolved, so there is nothing to configure and no second tool to log in.

THE ENDPOINT IS SPELLED THE WAY GITEA'S OWN DOCUMENTATION SPELLS IT. A bare path is taken as relative to /api/v1/; a path that already begins /api/ is sent as it stands, which is how anything outside v1 is reached; a full URL is allowed only on the instance this project points at, because every request here carries the project's token in a header and a URL somewhere else would hand that token over. {owner} and {repo} are filled in from the project's configuration. Quote an endpoint that contains ? or & or the shell will take it apart.

ANOTHER REPOSITORY NEEDS NO FLAG — write its address into the path (repos/other-owner/other-repo/releases) and nothing is substituted. There is no --repo and no --login here for the same reason there is none on push or pull: which login a project runs under is a fact about the project. Another INSTANCE is KETTLE_URL and KETTLE_TOKEN, which is also what a CI run uses.

THE ANSWER IS THE SERVER'S BYTES ON STDOUT, unparsed and unreformatted — pipe it to jq, redirect it to a file. There is no flag that names an output file: in this tree --out is the issue store, and one word meaning two things is exactly the trap the tool this replaces set with an -o that wrote a file called "json".

IT DOES NOT PAGINATE. One call is one request, so a listing answers with one page: ask for the next with ?page=2, and for a bigger one with ?limit=50 (the server's own default is 30, its maximum is usually 50). A passthrough that stitched pages together silently would report as one answer something that was several.

ISSUES ARE NOT THIS COMMAND'S JOB even though it can reach them. An issue read this way arrives as a full JSON payload — every comment, every label object, every URL — which is what /kettle:issue and /kettle:sync exist to keep out of a context window. Use pull, push, comment and close.

A 403 here is usually the token rather than the request: a token minted for issues carries write:issue, and releases, pull requests, branches and tags are all under repository. kettle auth list shows what each login records.

-X DELETE NEEDS --yes. Everything else goes through as typed; a deletion does not, because a flag typed on purpose is an operator's decision and the URL of a release is one character away from the URL of the wrong release.

What it cannot do: an upload. Release attachments are multipart/form-data and this sends JSON — the release tooling in cmd/release does those.

flag default what it does
--X the same flag as --method, spelled the way curl and the tool this replaces spell it
--data the request body: @file, @- for standard input, or the JSON itself
--field key=value, added to a JSON body as a string; repeatable
--method GET, POST, PUT, PATCH or DELETE (default GET, or POST when there is a body)
--status false print the status line on standard error
--yes false confirm a DELETE
kettle api repos/{owner}/{repo}/releases  # the latest page of releases, as JSON
kettle api user                           # who this project's token belongs to
kettle api 'repos/{owner}/{repo}/pulls?state=open&limit=50'  # quote anything with ? or & in it
kettle api --data @tmp/release/v0-2-0.json repos/{owner}/{repo}/releases  # a body from a file; POST is implied
kettle api --field body=lgtm repos/{owner}/{repo}/issues/7/comments  # a small body without a file
kettle api -X DELETE --yes repos/{owner}/{repo}/releases/12  # a deletion, said out loud
kettle api repos/{owner}/{repo}/milestones | jq '.[].title'  # the bytes are the server's; jq is yours