desktop-api
Skill41 starsChanged 7 days agoMITView on GitHub
Observe the Hive Desktop notification pipeline and force a re-poll through its loopback MCP server, instead of reading desktop-pipeline.db. Use to assert what an event produced in the inbox, force a refresh after a devserver overlay/action/scenario, discover the webhook port for a push, or confirm an actions.yml/flow edit actually loaded.
- Reads credentials
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- hay-kot/hive-desktopedited, first seen Sep 16, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- hay-kot/hive-desktop★ 490% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository17 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/AGENTS.mdAGENTS.md
- cli-autocalibrateSkill
- cli-smokeSkill
- cli-tuiSkill
- desktop-commands-auditSkill
- desktop-devserverSkill
- desktop-shipped-skillsSkill
- desktop-ui-auditSkill
- desktop-ui-perfSkill
- docs-auditSkill
- docs-pageSkill
- go-enumSkill
- release-notesSkill
- release-prepSkill
- releaseSkill
- sqlcSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 28
If /api/status refuses the connection, the app is not running, was not built from this worktree, or the HTTP server is disabled …
- Very specific: line 171
A failing tool answers with a result, not an error: Look for .result.isError and read .result.content[].text, which opens with a stable …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Observe the pipeline through the MCP server
The desktop exposes its capabilities as MCP tools over app.App (ADR mcp-replaces-the-agent-facing-http-api), so
a test asserts against a supported surface instead of desktop-pipeline.db.
One loopback http server (on by default) serves webhook push (/hooks/…),
the MCP server (/mcp), and the liveness probe (/api/status) on one port.
Pair this with the devserver skill: devserver acts (rewrites GitHub, pushes webhooks), these tools observe and reload.
1. Find the port and confirm the app is up
The HTTP port is written to the worktree's launch.env. Liveness stays a plain
GET, because a JSON-RPC handshake is the wrong shape for "is it running?":
API=127.0.0.1:$(grep HIVE_DESKTOP_HTTP_PORT launch.env | cut -d'"' -f2)
curl -s $API/api/status | jq . # {webhook:{running,host,port,pathPrefix}, api:{pathPrefix}}
curl -s $API/api/version | jq . # revision + dirty flag of the running buildIf /api/status refuses the connection, the app is not running, was not built
from this worktree, or the HTTP server is disabled (http.enabled: false). A
fresh mise run desktop:dev:prepare turns it on.
2. Connect
The endpoint is http://$API/mcp, Streamable HTTP, no authentication (loopback
bind, and no tool here spawns a process). Prefer a real MCP client — add it
once and the tools appear as ordinary tools:
claude mcp add --transport http hive-desktop "http://$API/mcp"The server is stateless and answers application/json rather than SSE, so a
single curl also works when a client is not set up. Ask it what it serves —
the schemas are generated from the server's own Go types, so this never drifts:
mcp() { curl -s "http://$API/mcp" -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":${2:-\{\}}}"; }
mcp tools/list | jq -r '.result.tools[] | "\(.name)\t\(.title)"'tools/call takes {"name": …, "arguments": {…}}, and a tool's answer is in
.result.structuredContent:
call() { mcp tools/call "{\"name\":\"$1\",\"arguments\":${2:-\{\}}}" \
| jq '.result.structuredContent // .result'; }3. The loop: reload → read → retry
There is no server-side wait. source_refresh returns once the event log
is appended; the flow engine commits a moment later on its own goroutine. So
force the poll, then read and retry a few times:
call source_refresh
for i in $(seq 1 10); do
hit=$(call inbox_list '{"profileId":"'"$P"'","detail":"full","limit":50}' \
| jq --arg repo acme/widgets --argjson num 42 \
'[.items[] | select(.payload.repo==$repo and .payload.num==$num)][0]')
[ "$hit" != "null" ] && echo "$hit" | jq '{lifecycle, sourceState, unread, reason: .payload.reason}' && break
sleep 1
doneSelecting on payload fields is what detail: "full" is for, and why this
narrows with limit — see Read tools before widening it.
A webhook push appends directly (no refresh needed) — push via devserver, then read + retry the same way.
4. Read tools
call profile_list # ids to scope everything else by
call flow_get '{"profileId":"'"$P"'"}' # the graph: node ids, types, wires
call inbox_list '{"profileId":"'"$P"'"}' # newest first; title/state/lifecycle, no payloads
call inbox_list '{"profileId":"'"$P"'","feed":"'"$FEED"'"}' # presence == routed there
call inbox_list '{"profileId":"'"$P"'","feed":"'"$FEED"'","archived":true}'
call inbox_list '{"externalId":"'"$EXT"'","detail":"full"}' # a GitHub global node id, NOT repo#num
call feed_list '{"profileId":"'"$P"'"}' # every declared feed, counts joined on
call inbox_event_list '{"itemId":'"$ID"'}' # one item's history
call inbox_session_list '{"itemId":'"$ID"'}' # hive sessions this item startedflow_get is where node ids come from — flow_preview's nodeId and the
node-image tools take them, and nothing else on the surface reports one.
Payloads are opt-in. inbox_list and inbox_event_list default to
detail: "summary", which omits the raw source payload — a PR body alone is
kilobytes and a listing repeats it per item, so a whole feed at "full" runs to
megabytes and will blow a tool-response limit. Narrow first (by feed, by
externalId, or with limit), then ask for "full". Everything the app itself
names — title, url, lifecycle, sourceState, unread, feedId,
timestamps — is in the summary. Each answer echoes the detail it used, so an
omitted payload is never mistaken for an absent one.
A missing thing is not_found, never an empty collection: a profile id, a feed
id or an item id that resolves to nothing is an error, so an empty list always
means "nothing has landed here". feed_list reports a declared feed at zero
rather than omitting it, and marks declared: false on a feed only stale inbox
rows still claim.
5. Test a flow without deploying it
flow_preview runs a flow against input you supply and returns what every node
did, committing nothing (ADR flows-are-dry-run-against-supplied-input). Reach for it before the deploy → refresh
→ read loop above: that loop writes feed membership, kv and notifications to
answer a question about a script, and it cannot tell a bug from "hasn't polled
yet".
call flow_preview '{
"profileId": "triage", "nodeId": "src-github",
"messages": [{"Key": "acme/app#1", "Payload": {"state": "open"}}]
}' | jq '.nodes[] | {nodeId, in, out, dropped, ok,
emitted: [.emitted[] | {port, keys: [.messages[].Key]}],
console: [.console[].text], error}'- Isolate one node.
nodeIdis any node, not only a source — inject a captured payload straight at thefunctionnode under test and the source in front of it never runs. - Test an unsaved edit. Send
flowYaml(the file's text) orflow(the same document as a JSON object) instead ofprofileId. - Seed
kv({"<nodeId>": {"<key>": <value>}}) to exercise notify-once against a known starting state. The real store is neither read nor written, so repeated calls give the same answer; what the run would have stored comes back in.kvMutations, and what it would have committed in.outputs. - Keep the response bounded with
detail. A payload is reported once per node it reaches, so a real one through a deep graph comes back many times over."emitted"(the default) drops each node'sreceived— it is the upstream'semitted;"counts"drops message bodies entirely and is what a snapshot run wants;"full"restores both. console.login a function node is readable here and nowhere else — a live run discards it.- A script error's
lineand the positions inside itsmessageandstackare all relative to theon_messagebody, not to the wrapper the engine compiles.
6. Did my config edit load?
Both config surfaces report their own load status, so an edit is verified by
reading it back rather than by clicking through the UI. A file that fails to
parse leaves the previous version in effect, so valid is the only signal that
separates "accepted" from "rejected and ignored":
call action_list | jq '{path, valid, error, ids: [.actions[].id]}'
call profile_list | jq '[.profiles[] | {id, valid}]'action_list returns each action's type-specific config too, so a template
edit is confirmed by the value that came back — .actions[] |
select(.id=="…") | .clipboard.textTemplate.
Guardrails
- A failing tool answers with a result, not an error. Look for
.result.isErrorand read.result.content[].text, which opens with a stable kind —invalid,not_found,conflict,unavailable,internal. Branch on that, never on the prose. - Match on the payload, not the external id:
external_idis the source's own id (a GitHub global node id), while the devserver and your test think inrepo/num. Filter.items[] | select(.payload.repo==… and .payload.num==…)— which needsdetail: "full", so scope the read before you widen it. The same external id can exist across profiles/scopes, so by-id reads return a slice. - Reload only helps pull sources.
source_refreshre-pollssources.github; it does nothing for webhook-fed feeds (those append on delivery). It answersunavailablein mock modes (no producer). - The tools need the HTTP server on. They share that loopback server (on by
default); with
http.enabled: falsethere is nothing to mount on. - Reads, reloads and safe mutations only. Session control is deliberately
absent — starting a session is command execution and lives behind the
token-guarded
/api/terminal/prefix. Drive triage through the app or the devserver, and assert here. A dry run is part of that: it reports the outputs and KV writes a live run would have made instead of making them, so what it tells you about the commit is the graph's half. Two things the commit itself does are deliberately outside it — minting an inbox row for a key no source ingested, and dropping feed rows a snapshot no longer lists. Assert those through the inbox after a real refresh.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

