Skip to content

feat(server): serve Skill Stack browse and install over the v2 HttpApi - #364

Merged
quickbeard merged 1 commit into
mainfrom
feat/skill-stack-server
Sep 14, 2026
Merged

quickbeard merged 1 commit into
mainfrom
feat/skill-stack-server

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

P1 of the Skill Stack integration (P0 was #361): the server endpoints both UIs will drive, plus the regenerated clients. No user-facing change on its own — the TUI dialog (P2) and desktop Stacks tab (P3) land on top of this.

Routes (fork-only v2 group server.skillhub)

GET    /api/skillhub/stack                      list public stacks (search/page/limit)
POST   /api/skillhub/stack/{slug}/prepare       download + plan, write nothing → stagingID
POST   /api/skillhub/stack/install              install a staging (skip-existing | overwrite)
DELETE /api/skillhub/stack/staging/{stagingID}  drop a staging

Two-step install so a client can show what's already installed and ask before overwriting, without downloading twice. prepare parks the ZIP on disk under Global.Path.cache/skillhub/stacks/ with a 10-minute TTL (not in memory — a stack can be hundreds of MB). install re-plans against the disk as it is then, so a skill that appeared between the two calls is still honoured by skip-existing.

Location-scoped, never client-pathed. scope: "project" resolves server-side to the request location's project root. This is the architectural fix for the off-host install that #363 currently blocks in the desktop app — once the desktop moves onto these routes, that block can go.

Errors carry a code (auth / not-found / already-installed / network / expired / unknown) matching the desktop's existing SkillHubError codes, so P3 maps 1:1 and nobody parses English. Unknown and expired stagings deliberately look the same — the archive is gone either way and the fix is identical.

Layering

Schema → Protocol → Server, as CLAUDE.md requires. packages/server takes the wire types through the protocol group's re-export rather than a new direct schema dependency, matching every other handler. Hub auth is the server host's shared ~/.codev-hub session — the user's own machine for every local server, which is how the TUI always runs. @codev/skillhub's transport seam from #363 means the server gets Node's default transport with no extra plumbing.

CI: the exerciser

test:httpapi runs with --fail-on-missing --fail-on-skip, so todo placeholders would fail and the four routes needed real scenarios that never touch the live hub. skillhub-fake.ts swaps in a fixture hub through the package's injectable transport — the same seam the desktop uses for Chromium's net.fetch — so there are no mocks: the real handler downloads, parses, plans, and stages a fixture stack. All three modes (effect, coverage, auth) pass for the new routes.

Generated clients

@opencode-ai/client gains client.skillhub.{listStacks,prepareStack,installStack,discardStack} (endpoint names overridden — the codegen's default of the last id segment would give list/prepare/install, which collide the moment single-skill routes join the group). The legacy JS SDK the TUI uses is regenerated too. check:generated will be green once this is on a branch; locally it only diffs against HEAD.

Verification

  • Typecheck clean: schema, protocol, server, codev-skillhub, opencode (incl. the exerciser), tui, app.
  • packages/client 16 pass; packages/codev-skillhub 245 pass.
  • Exerciser --include skillhub in all three modes: 4 pass / 0 fail / missing=0.
  • Four DIVERGENCES.md rows added (routes, generated clients, exerciser).

🤖 Generated with Claude Code

https://claude.ai/code/session_01P1MfYkJD3tQZRgqBKB4gig

Puts CoDev SkillHub Skill Stack traffic behind the server as the fork-only
v2 group `server.skillhub`, so the TUI — which reaches the hub only through
the SDK — and the desktop app share one implementation, and an install
lands on the host that actually runs the agent rather than wherever the
client happens to be.

Install is two steps so a client can show what is already installed and
ask before overwriting, without downloading the archive twice: prepare
downloads, parses, and plans against the request location's project root
(resolved server-side, never taken from the client) and parks the ZIP on
disk under a stagingID with a 10-minute TTL; install consumes it, re-
planning against the disk as it is then. Failures carry a machine-readable
code (auth, not-found, already-installed, network, expired, unknown) so the
clients never parse English.

The exerciser gates every fork route in CI with --fail-on-missing
--fail-on-skip and must never reach the live hub, so it installs a fake
through the package's injectable transport — the same seam the desktop
uses for Chromium's net.fetch — and the four scenarios drive the real
handler end to end against a fixture stack.

Clients regenerated: @opencode-ai/client (skillhub.listStacks/
prepareStack/installStack/discardStack) and the legacy JS SDK the TUI
uses. No user-facing change yet; the TUI dialog and desktop tab build on
this next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1MfYkJD3tQZRgqBKB4gig
@quickbeard
quickbeard merged commit 09fa0c0 into main Sep 14, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the feat/skill-stack-server branch September 14, 2026 02:01
quickbeard added a commit that referenced this pull request Sep 14, 2026
Adds the TUI's first way onto the CoDev SkillHub: `/stacks` (and the
unbound `prompt_stacks` keybind) opens a browser for the hub's Skill
Stacks, then walks scope (this project or global), an agent preset, and
— only when prepare reports collisions — a skip-or-overwrite choice
before installing. Escaping the conflict prompt discards the staging
rather than leaving it to the server's TTL.

Everything goes through the server's v2.skillhub routes (#364), so the
install lands on the host that runs the agent, in the same folders
`codev skill stack pull` writes. After a successful install the dialog
disposes the server instance — what a config update does too — because
skill discovery is cached per instance and nothing else invalidates it;
without that the model would not see the new skills until a restart.

Flow state lives in an awaited controller rather than components, as in
dialog-configure.tsx: dialog.replace stores the element factory and can
re-invoke it. And since replace() fires every prior step's onClose even
when this flow advances itself, "did the user escape mid-download?" is
answered by checking whether the dialog still shows the step we put up.

The agent picker is a preset list because DialogSelect is single-choice
(a per-row toggle needs its own keybind, like dialog.mcp.toggle); CoDev
Code is in every preset and is folded in server-side regardless.


Claude-Session: https://claude.ai/code/session_01P1MfYkJD3tQZRgqBKB4gig

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
quickbeard added a commit that referenced this pull request Sep 14, 2026
The Skill Hub gains a Skills | Skill Stacks tab strip. The Stacks tab
lists the hub's stacks — owner, skill and member counts, a preview of the
skills inside — and installs one through the server the app is connected
to (the v2.skillhub routes from #364), so the install lands on the host
that runs the agent and works against a remote server. No Electron bridge
is involved: this is the first Skill Hub surface in the app that the web
build could serve too.

The install flow is a state machine over injected server calls
(stack-install-model.ts) so its transitions — prepare, stop to ask about
collisions, install, then dispose the instance so the model sees the new
skills — are unit-tested without rendering. The dialog is a view over it,
reusing the single-skill dialog's agent rows and remembered choice; scope
"project" is carried as the request directory and resolved server-side,
never as a path the renderer chooses.

Numeric fields from the generated client are coerced at the two places
they are compared or subtracted: Effect's Schema.Number admits the
"NaN"/"Infinity" string encodings, so the generated type is not plain
number.


Claude-Session: https://claude.ai/code/session_01P1MfYkJD3tQZRgqBKB4gig

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant