Compare commits

...

16 Commits

Author SHA1 Message Date
github-actions[bot]
cccb61197f Version Packages (#233) 2026-07-18 15:19:54 +02:00
Paolo Ricciuti
51bf537e23 feat: add tui opencode plugin (#231) 2026-07-18 15:15:17 +02:00
paoloricciuti
35b6a5a51c chore: add writing skill skill and opencode plugin skill 2026-07-10 23:49:19 +02:00
paoloricciuti
8a7763868a docs: remove confusing wording in skill docs 2026-07-06 10:07:24 +02:00
paoloricciuti
7239d7cb13 docs: add codex plugin to skills docs 2026-06-29 15:33:32 +02:00
paoloricciuti
ff78a85ddf docs: add links to plugin in setup docs 2026-06-29 15:31:16 +02:00
Paolo Ricciuti
8984bb0303 docs: codex plugin (#230) 2026-06-29 15:18:32 +02:00
Paolo Ricciuti
3fa951f1b6 docs: copilot plugin (#212) 2026-06-29 10:42:38 +02:00
github-actions[bot]
a668188dfa Version Packages (#229) 2026-06-26 18:20:51 +02:00
paoloricciuti
59dcbff136 fix: better wording for desired_svelte_version 2026-06-26 12:37:50 +02:00
prajwal
129a307df0 docs: migrate to agy (#228)
Co-authored-by: prajwal <prajwal@hopbox.in>
2026-06-24 18:16:22 +02:00
Paolo Ricciuti
d051d7ce6c chore: remove log 2026-06-12 11:12:22 +02:00
Copilot
54a84c5512 Updating Svelte version in mcp-remote app (#226)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-12 11:09:38 +02:00
Paolo Ricciuti
2e8f79038e chore: test version 2026-06-12 10:56:20 +02:00
github-actions[bot]
1b6b7589b5 Version Packages (#223) 2026-06-04 18:31:47 +02:00
Paolo Ricciuti
fea691996f chore: support declaration tags (#222) 2026-06-04 17:07:27 +02:00
26 changed files with 2437 additions and 188 deletions

View File

@@ -0,0 +1,195 @@
# Glossary — Building Great Skills
The domain model for what makes a skill great. A skill exists to wrangle determinism out of a stochastic system; the root virtue is **Predictability**, and every term below is a lever on it. This is the disclosed reference for [`writing-great-skills`](SKILL.md).
The terms are grouped by axis: **Invocation** (how a skill is reached), **Information Hierarchy** (how its content is arranged), **Steering** (how the agent's runtime behaviour is shaped), and **Pruning** (how it is kept lean). Each **failure mode** lives beside the lever that cures it, tagged _failure mode_.
**Bold terms** in any definition are themselves defined in this glossary; find them by their heading.
## Predictability
The degree to which a skill makes the agent behave the same _way_ on every run — the same process, not the same output (a brainstorming skill should _predictably_ diverge; its tokens vary, its behaviour doesn't). The root virtue every other term serves — cost and maintainability are symptoms of it, not rivals.
_Avoid_: consistency, reliability, robustness, output-determinism
## Invocation
How a skill is reached — and the two loads you pay for the choice.
### Model-Invoked
A skill that keeps its **description** field, so the agent can see it and fire it autonomously — and the human can still type its name, so model-invocation always _includes_ user reach. There is no model-only state: a description only ever _adds_ agent discovery, never removes the human's. Pays a permanent **context load** on every turn in exchange for that discoverability. Reachable by other skills, because the description that makes it agent-discoverable makes it invocable. A model-invoked skill whose content is all **reference** is also one home for shared reference: another skill can invoke it, so reference needed by several skills lives in one place. Pick model-invocation only when the agent must reach the skill on its own; if it never fires except by hand, drop the description and pay no context load.
_Avoid_: ability, tool, capability
### User-Invoked
A skill with its **description** stripped — invisible to the agent and reachable only by the human typing its name (user-_only_, where **model-invoked** is user-_and-agent_). Trades agent-discoverability for zero **context load**. Because it has no description, nothing but the human can reach it: no other skill can fire it.
_Avoid_: procedure, workflow, command
### Description
The skill's machine-readable trigger, and the one **context pointer** a **model-invoked** skill is forced to keep loaded at all times. Its mere presence _is_ the invocation axis: keep it and the skill is model-invoked (and reachable by other skills); delete it and the skill is **user-invoked**, reachable only by the human. The source of a model-invoked skill's **context load**.
_Avoid_: frontmatter, summary
### Context Pointer
A reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. The **description** is the top-level context pointer (context window → skill); pointers to disclosed files are the same object one level down. Its wording, not the target, decides _when_ the agent reaches — and _how reliably_. A must-have target behind a weakly worded pointer is a variance bug: fix the wording first, and inline the material only if sharpening fails.
_Avoid_: link, reference, import
### Context Load
The cost a **model-invoked** skill imposes on the agent's context window — its **description**, always loaded, spending both tokens and attention. What **user-invoked** skills escape by having no description, and the brake on splitting into more model-invoked skills.
_Avoid_: token cost, context bloat
### Cognitive Load
The cost a **user-invoked** skill imposes on the human — what they must hold in their head: which skills exist and when to reach for each (the human is the index). What **model-invocation** removes by being agent-discoverable, and the brake on splitting into more user-invoked skills. Not a cost to minimise: it is the price of human agency, the reason some skills stay user-invoked. Spend it where human judgement matters; remove it where it does not.
_Avoid_: human index, burden, overhead
### Router Skill
A **user-invoked** skill whose job is to point at your other user-invoked skills — naming each and when to reach for it — so the human has one skill to remember instead of many. It can only hint, never fire them: user-invoked skills have no **description**, so nothing but the human can reach them. The cure for **cognitive load** when user-invoked skills multiply.
_Avoid_: dispatcher, menu, registry, index, router procedure
### Granularity
How finely you divide skills. Finer division spends one of the two loads: more **model-invoked** skills spend **context load** (more descriptions crowding the window and competing for attention); more **user-invoked** skills spend **cognitive load** (more for the human to remember and reach for). Two cuts guide the division. By **invocation**, split off a model-invoked skill where you have a distinct **leading word** to trigger it — a trigger word you actually use in your prompts. By **sequence**, split a run of **steps** where a step's **post-completion steps** need hiding, since isolating it in its own context clears what follows. Beware the reverse: merging sequences exposes each step's post-completion steps to what follows, inviting premature completion.
_Avoid_: chunking, modularity
## Information Hierarchy
How a skill's content is arranged, and how far down the ladder each piece sits.
### Information Hierarchy
A skill's content ranked by how immediately the agent needs it — a single ladder, produced by two cuts: in-file or behind a pointer, and step or reference. The rungs:
- **Steps** — in-file, primary
- **Reference**, in-file — secondary
- **Reference**, disclosed — behind a **context pointer**
A skill with no **steps** uses just the bottom two rungs — often a legitimately flat peer-set (e.g. every rule of a review on one rung), which is a fine arrangement, not a smell. The hierarchy is independent of invocation: a skill can be model- or user-invoked whether it is all steps, all reference, or both. When a skill has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one. Keep the top of the ladder legible; push down it whatever you can.
_Avoid_: structure, organization, layout
### Steps
The ordered actions the agent performs — when a skill has them, the primary tier of its content, and the part that earns its place in SKILL.md. Not every skill has steps: a skill can be all steps (`tdd`), all **reference** (a review), or both, independent of invocation. Every step ends on a **completion criterion**, clear or vague.
_Avoid_: workflow, instructions, choreography
### Reference
Material the agent refers to on demand — definitions, facts, parameters, examples, conditional instructions. When a skill has **steps** it is secondary to them; when a skill has none it is the entire content; or it lives outside any skill entirely — see **External Reference**. Reached via **context pointers**, and the prime candidate for **progressive disclosure**.
_Avoid_: supporting material, docs, background
### External Reference
**Reference** that lives outside the skill system — a plain file, no **description**, no **steps**, not invocable — that any skill can point at. The home for shared reference that needn't fire on its own, and the only shared home two **user-invoked** skills can use, since neither has a description and so neither can fire the other.
_Avoid_: doc, resource, knowledge base
### Progressive Disclosure
Moving **reference** down the ladder — out of SKILL.md and behind a **context pointer** — so the top stays legible. Not primarily a token optimisation; it is how the **information hierarchy** is protected. Licensed by **branching**: disclose what only some branches need, inline what every path needs, and if a pointer fires unreliably on must-have material, sharpen its wording, and pull it back inline only if that fails.
_Avoid_: lazy loading, chunking
### Co-location
Keeping the material an agent needs at once in one place — a concept's definition, rules, and caveats under a single heading, not scattered across the file — so reading one part brings its neighbours with it. The within-file companion to the **Information Hierarchy**: the hierarchy ranks _how far down_ a piece sits; co-location decides _what sits beside it_ once there. There is no formula for the right format of a body of **reference**; the test is that a skill should read like documentation written for the agent, and grouped material reads that way where scattered material does not. Distinct from **Duplication**: that repeats one meaning in two places, where scattering fragments a single meaning across many.
_Avoid_: grouping, clustering, cohesion
### Sprawl
_Failure mode._ A skill that is simply too long — too many lines in SKILL.md — independent of whether they are stale or repeated. Even an all-live, all-unique skill can sprawl. It costs readability (the agent wades through more before it can act, and attention thins across the excess), maintainability (every extra line is one more to keep **relevant**), and tokens. The cure is the **information hierarchy**: push **reference** down behind **context pointers**, and split by **branch** or sequence so each path carries only what it needs. Distinct from **sediment** (length from stale accumulation) and **duplication** (length from repeated meaning) — sprawl is length itself, whatever its cause.
_Avoid_: bloat, length, size, verbosity
## Steering
The levers that shape the agent's runtime behaviour toward **Predictability**.
### Branch
A distinct way a skill can be invoked — a case the skill handles — so different runs take different paths through it. A skill with many steps may carry many branches; a linear one has none.
_Avoid_: path, case, fork
### Leading Word
A compact concept — also called a _Leitwort_ — already living in the model's pretraining, that the agent thinks with while running the skill. It encodes a behavioural principle in the fewest possible tokens by invoking priors the model already holds (e.g. _lesson_, _proximal zone of development_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition across the skill and anchors a whole region of behaviour. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free. Reach for an existing word first.
A leading word serves **predictability** twice. In the body it anchors **execution** — the agent reaches for the same behaviour every time the concept appears, and inside flat reference it focuses attention on a class of thing to look for, recruiting the right checks each run. In the **description** it anchors **invocation** — and not only within the skill: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the skill and fires it more reliably. Word a description with the leading words you actually use when you want the skill.
_Avoid_: keyword, term, motif
### Completion Criterion
The condition that tells the agent a unit of work is done — the target it judges against. Two properties make it a lever, not just a quality. Its **clarity** (can the agent tell done from not-done?) resists **premature completion** — a vague bound ("understanding reached") lets the agent declare done and slip to the next step; this axis needs _steps_ to bite, since premature completion is a between-steps failure. Its **demand** (how much it requires) sets **legwork** — "every modified model accounted for" forces thorough work where "produce a change list" does not — and this axis is _not_ step-bound: it can bind a body of flat reference too, which is how a skill with no steps still carries an exhaustiveness bar ("every rule applied"). The strongest criteria are both checkable and exhaustive.
_Avoid_: done condition, exit condition, stopping rule
### Legwork
The work an agent does behind the scenes within a single step — reading files, exploring the codebase, making changes, digging up what it needs rather than offloading to the user. It lives below the step structure: never written as its own step, latent in the wording, controlled by the agent rather than the skill. The within-step counterpart to **post-completion steps**' across-step pull. Raised by a **leading word** (_comprehensive_, _thorough_) or a **completion criterion** that demands the work be exhaustive — including the demand axis applied to flat reference, which is what drives a skill of flat reference to cover all its rungs. Goes thin either when that demand is missing or when **premature completion** cuts the step short.
_Avoid_: scope, effort, diligence, coverage
### Post-Completion Steps
The **steps** that follow the current step. Visible, they pull the agent forward into **premature completion** — the more it sees, the stronger the tug; the defence is to hide them by splitting the sequence of steps into two.
_Avoid_: horizon, fog of war, lookahead
### Premature Completion
_Failure mode._ Ending the current step before it is genuinely done, because the agent's attention slips to being done rather than to the work. A between-steps failure: it needs **steps** to occur — a skill with no steps that quits early isn't premature completion but thin **legwork** under an unmet demand. A tug-of-war between two forces: visible **post-completion steps** (the pull forward) and the **completion criterion**'s clarity (the resistance — a sharp, checkable bar holds; a vague one gives way). Fuzziness is the necessary condition: a sharp bound resists the pull no matter how many later steps are visible, so a step that never rushes needs no defending. Two levers hold a step that does, but reach for them in order: **sharpen the bound first** — it is local and cheap. Only when the criterion is irreducibly fuzzy _and_ you actually observe the rush do you **hide the later steps** — and hiding only works across a real context boundary (a user-invoked hand-off or a subagent dispatch; an inline model-invoked call leaves the later steps in context and clears nothing). One cause of thin legwork, but distinct from it: legwork can be thin even when a step runs to full completion.
_Avoid_: premature closure, the rush, rushing, shortcutting
## Pruning
Keeping a skill lean — each remedy paired with the failure it cures.
### Single Source of Truth
The desired state where each meaning lives in exactly one authoritative place, so a change to the skill's behaviour is a change in one place. **Duplication** is its violation.
_Avoid_: home, canonical location
### Duplication
_Failure mode._ The same meaning given more than one **single source of truth**. It costs maintenance (change one place, you must change the others), costs tokens, and inflates prominence — repeating a meaning weights it on the ladder past its real rank. The accidental inverse of a **leading word**, which raises attention on purpose by repeating a token, never the meaning.
_Avoid_: repetition, redundancy
### Relevance
Whether a line still bears on what the skill does — the lens for what to keep. A line loses relevance either by never bearing on the task (mere exposition, or a **branch** that should be disclosed) or by going stale: drifting out of date as the behaviour or world it describes changes. Shorter skills are easier to keep relevant, because each line is cheaper to check. Distinct from **no-op**: relevance asks whether a line bears on the task, not whether it changes behaviour.
_Avoid_: load-bearing, staleness, freshness
### Sediment
_Failure mode._ Layers of old content that settle in a skill and are never cleared, because adding feels safe and removing feels risky — so stale and irrelevant lines accumulate and you must core down through them to find what is still live. The default fate of any skill without a pruning discipline; the slow erosion of **relevance**, as opposed to **duplication**'s repeated meaning.
_Avoid_: accretion, bloat, cruft, rot
### No-Op
_Failure mode._ An instruction that changes nothing because the model already does it by default — you pay load to tell the agent what it would do anyway. The test: does a line change behaviour versus the default? A line can be perfectly **relevant** and still be a no-op. The same priors that make a **leading word** free make a no-op worthless.
A leading word is a _technique_; No-Op is a _verdict_ on a line — and they cross. A leading word too weak to beat the default is a no-op (_be thorough_ when the agent is already thorough-ish), and the fix is a stronger word that passes the verdict (_relentless_), not a different technique. So the No-Op test — does it change behaviour versus the default? — is also how you grade whether a leading word is earning its repetitions. This is model-relative, not reader-relative: two people disagreeing over whether a line is a no-op disagree about the default, and settle it by running the skill, not by debate.
_Avoid_: redundant instruction, restating the obvious, belaboring

View File

@@ -0,0 +1,84 @@
---
name: writing-great-skills
description: Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
disable-model-invocation: true
metadata:
internal: true
---
A skill exists to wrangle determinism out of a stochastic system. **Predictability** — the agent taking the same _process_ every run, not producing the same output — is the root virtue; every lever below serves it.
**Bold terms** are defined in [`GLOSSARY.md`](GLOSSARY.md); look them up there for the full meaning.
## Invocation
Two choices, trading different costs:
- A **model-invoked** skill keeps a **description**, so the agent can fire it autonomously _and_ other skills can reach it (you can still type its name too). It contributes to **context load** — the description sits in the window every turn. Mechanics: omit `disable-model-invocation`, and write a model-facing description with rich trigger phrasing ("Use when the user wants…, mentions…").
- A **user-invoked** skill strips the description from the agent's reach: only you, typing its name, can invoke it — and no other skill can. Zero context load, but it spends **cognitive load**: _you_ are the index that must remember it exists. Mechanics: set `disable-model-invocation: true`; the `description` becomes human-facing — a one-line summary, trigger lists stripped.
Pick model-invocation only when the agent must reach the skill on its own, or another skill must. If it only ever fires by hand, make it user-invoked and pay no context load.
When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each.
## Writing the description
A model-invoked **description** does two jobs — state what the skill is, and list the **branches** that should trigger it. Every word increases **context load**, so a description earns even harder pruning than the body:
- **Front-load the skill's leading word** — the description is where it does its invocation work.
- **One trigger per branch.** Synonyms that rename a single branch are **duplication** — "build features using TDD … asks for test-first development" is one branch written twice. Collapse them; keep only genuinely distinct branches.
- **Cut identity that's already in the body.** Keep the description to triggers, plus any "when another skill needs…" reach clause.
## Information hierarchy
A skill is built from two content types — **steps** and **reference** — that mix freely: a skill can be all steps, all reference, or both. The core decision is which to use and where each sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
1. **In-skill step** — an ordered action in `SKILL.md`, the primary tier: what the agent does, in order. Each step ends on a **completion criterion**, the condition that tells the agent the work is done. Make it _checkable_ (can the agent tell done from not-done?) and, where it matters, _exhaustive_ ("every modified model accounted for", not "produce a change list") — a vague criterion invites **premature completion**.
2. **In-skill reference** — a definition, rule, or fact in `SKILL.md`, consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell. _This skill is all reference._
3. **External reference** — reference pushed out of `SKILL.md` into a separate file, reached by a **context pointer**, loaded only when the pointer fires. (Spans _disclosed_ reference — a sibling file like `GLOSSARY.md`, still part of the skill — through fully **external reference** that lives outside the skill system and any skill can point at.)
A demanding completion criterion drives thorough **legwork** — the digging the agent does within the work — whether the skill has steps or not, since "every rule applied" binds flat reference just as "every step done" binds a sequence.
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
**Progressive disclosure** is the move down the ladder — out of `SKILL.md` into a linked file — so the top stays legible. Mechanics: a linked `.md` file in the skill folder, named for what it holds (this skill discloses its full definitions to `GLOSSARY.md`). Some skills are used in more than one way, and each distinct way is a **branch** — different runs taking different paths through the skill. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. A **context pointer**'s _wording_, not its target, decides when and how reliably the agent reaches the material.
Where the ladder decides _how far down_ a piece sits, **co-location** decides _what sits beside it_ once there: keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it.
## When to split
**Granularity** is how finely you divide skills, and each cut spends one of the two loads, so split only when the cut earns it. Two cuts:
- **By invocation** — split off a **model-invoked** skill when you have a distinct **leading word** that should trigger it on its own, or another skill must reach it. You pay **context load** for the new always-loaded **description**, so that independent reach has to be worth it.
- **By sequence** — split a run of **steps** when the steps still ahead (a step's **post-completion steps**) tempt the agent to rush the one in front of it (**premature completion**). Keeping them out of view encourages the agent to do more **legwork** on the current task.
## Pruning
Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit.
Check every line for **relevance**: does it still bear on what the skill does?
Then hunt **no-ops** sentence by sentence, not just line by line: run the no-op test on each sentence in isolation, and when one fails, delete the whole sentence rather than trim words from it. Be aggressive — most prose that fails should go, not be rewritten.
## Leading words
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the skill (e.g. _lesson_, _fog of war_, _tracer bullets_). Repeated throughout the text (though not necessarily - a strong leading word might only be needed once), it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds.
It serves predictability twice. In the body it anchors _execution_: the agent reaches for the same behaviour every time the word appears. In the description it anchors _invocation_: when the same word lives in your prompts, docs, and code, the agent links that shared language to the skill and fires it more reliably.
Hunt for opportunities to refactor skills to use leading words. A triad spelled out at three sites (**duplication**), a description spending a sentence to gesture at one idea — each is a passage begging to **collapse** into a single token. Examples include:
- "fast, deterministic, low-overhead" -> _tight_ — one quality restated across a phase — into a single pretrained word (a _tight_ loop).
- "a loop you believe in" -> _red_ — converts a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't).
You win twice over: fewer tokens, _and_ a sharper hook for the agent to hang its thinking on. Assume every skill is carrying restatements that leading words retire — go find them.
## Failure modes
Use these to diagnose issues the user may be having with the skill.
- **Premature completion** — ending a step before it's genuinely done, attention slipping to _being done_. Defence, in order: sharpen the completion criterion first (cheap, local); only if it is irreducibly fuzzy _and_ you observe the rush, hide the post-completion steps by splitting (the sequence cut).
- **Duplication** — the same meaning in more than one place. Costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank.
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.

View File

@@ -0,0 +1,126 @@
---
name: writing-opencode-plugins
description: OpenCode plugins, @opencode-ai/plugin, @opencode-ai/plugin/tui, plugin hooks, custom tools, TUI routes, slots, keymaps, and packaging. Use when creating, editing, reviewing, testing, or publishing server or TUI plugins for OpenCode.
metadata:
internal: true
---
# Writing OpenCode Plugins
Use this skill to implement production-quality OpenCode plugins. Treat the repository's exported types and runtime as authoritative because plugin APIs are evolving and public docs may lag.
## Start Here
1. Decide which runtime owns the feature.
2. Read the relevant public type before writing code.
3. Find one focused in-repository example using the same API.
4. Implement the smallest target-specific module.
5. Test loading, behavior, failure, and cleanup in the owning package.
| Need | Plugin target | Import | Configuration |
| -------------------------------------------------------------------- | --------------------------- | ------------------------------ | ---------------------------------------------------------------- |
| Hooks, tools, auth, providers, model parameters, shell environment | Server | `@opencode-ai/plugin` | `opencode.json` or auto-discovered `.opencode/plugins/*.{ts,js}` |
| Commands, keybindings, routes, dialogs, slots, themes, notifications | TUI | `@opencode-ai/plugin/tui` | Explicit `tui.json` `plugin` entry |
| Both | Two target-only entrypoints | Both imports in separate files | Package exports `./server` and `./tui` |
Never export `server` and `tui` from the same module. Do not use server event hooks as a substitute for interactive TUI APIs.
## Verify The Current Contract
Read these files before implementing unfamiliar behavior:
- `packages/plugin/src/index.ts`: authoritative server plugin and hook types.
- `packages/plugin/src/tool.ts`: custom tool schema, context, permission, metadata, attachments, and result types.
- `packages/plugin/src/tui.ts`: authoritative TUI API and module types.
- `packages/opencode/specs/tui-plugins.md`: TUI loading, packaging, lifecycle, and API semantics.
- `packages/opencode/src/plugin/shared.ts`: target validation, IDs, and entrypoint resolution.
- `packages/opencode/src/plugin/loader.ts`: install, compatibility, and import behavior.
If these disagree with examples or website docs, follow exported types and runtime behavior, then update stale documentation when appropriate.
## Choose A Module Shape
Prefer the explicit module object for new server plugins:
```ts
import type { Plugin, PluginModule } from '@opencode-ai/plugin';
const server: Plugin = async ({ client, directory }, options) => ({
dispose: async () => {},
});
export default {
id: 'acme.example',
server,
} satisfies PluginModule & { id: string };
```
Legacy server-only local plugins may export a plugin function directly. In a legacy module every distinct named export is interpreted as a plugin, so do not export unrelated constants. Prefer a default module object for new code.
TUI plugins always use a default module object:
```tsx
/** @jsxImportSource @opentui/solid */
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';
const tui: TuiPlugin = async (api) => {
api.ui.toast({ message: 'Plugin loaded' });
};
export default {
id: 'acme.example-tui',
tui,
} satisfies TuiPluginModule & { id: string };
```
File plugins require a stable, non-empty `id`. npm plugins may derive the ID from the package name, but an explicit namespaced ID makes state, diagnostics, and collision handling clearer.
## Engineering Rules
- Use TypeScript and `satisfies` against the public plugin type.
- Parse and validate `options`; they arrive as unvalidated `Record<string, unknown>`.
- Namespace plugin IDs, command IDs, route names, modes, slot names, and shared KV keys.
- Use the directory supplied by the plugin or tool context, not `process.cwd()`.
- Honor `AbortSignal` for long-running or cancellable work.
- Use `client.app.log()` for structured server logging instead of `console.log`.
- Request permission before sensitive or consequential custom-tool work.
- Keep notifications privacy-safe; do not expose prompts, secrets, paths, commands, or raw errors.
- Register only needed hooks and UI resources. Avoid broad event subscriptions when a specific hook exists.
- Make cleanup bounded, idempotent, and safe after partial initialization.
- Do not depend on undocumented load order to resolve ownership conflicts.
## Testing Workflow
Server plugin tests belong under `packages/opencode/test/plugin/` or the closest owning subsystem. TUI runtime tests belong under `packages/opencode/test/cli/tui/`; component-level TUI tests may belong in `packages/tui`.
Test at least:
- valid loading and target/entrypoint selection;
- configured options and malformed options;
- the observable behavior, not a duplicate of implementation logic;
- abort, failure, and partial-initialization behavior;
- cleanup or disposal;
- duplicate IDs or registrations when relevant;
- local file and npm packaging behavior when publishing.
Run tests from the package directory, never the repository root. Use `bun typecheck` from the owning package for type checking.
## Review Checklist
- The feature is in the correct server or TUI runtime.
- Module shape and import path match the target.
- Server and TUI entrypoints are separate.
- IDs and persistent keys are stable and namespaced.
- Options and external data are validated.
- Hook output mutation preserves other plugins' changes.
- Tools use context directory, permission, metadata, and abort correctly.
- TUI keybindings are mode-gated unless intentionally global.
- TUI resources and custom side effects are disposed.
- Package exports, `engines.opencode`, and config target are correct.
- Tests cover behavior and lifecycle.
## References
- [Server plugins](references/server-plugins.md): hooks, custom tools, lifecycle, and examples.
- [TUI plugins](references/tui-plugins.md): keymaps, routes, dialogs, slots, state, and lifecycle.
- [Packaging and testing](references/packaging-testing.md): config, package exports, compatibility, and test locations.

View File

@@ -0,0 +1,129 @@
# Packaging And Testing
## Local Configuration
Server plugin in `opencode.json`:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["./plugins/server.ts", ["package-name", { "key": "value" }]]
}
```
Server files under `.opencode/plugin/` or `.opencode/plugins/` are also auto-discovered. Relative configured paths resolve from the config file that declared them.
TUI plugin in `tui.json`:
```json
{
"$schema": "https://opencode.ai/tui.json",
"plugin": [["./plugins/tui.tsx", { "key": "value" }]],
"plugin_enabled": {
"acme.demo": true
}
}
```
`plugin_enabled` uses the resolved plugin ID, not its package or file spec. Persisted runtime enablement can override config.
After editing plugin or config-time files, restart OpenCode; the running session keeps its loaded configuration and modules.
## npm Package Shape
Publish separate target-only entrypoints:
```json
{
"name": "@acme/opencode-plugin",
"type": "module",
"exports": {
"./server": {
"import": "./dist/server.js",
"config": { "serverOption": true }
},
"./tui": {
"import": "./dist/tui.js",
"config": { "tuiOption": true }
}
},
"engines": {
"opencode": "^1.0.0"
},
"peerDependencies": {
"@opencode-ai/plugin": "^1.0.0"
}
}
```
- Server resolution prefers `exports["./server"]` and may fall back to `main`.
- TUI resolution requires `exports["./tui"]`; it does not use `main`.
- A package supporting both targets needs separate source and output files.
- `exports[target].config` may provide default options written during first install.
- Use `engines.opencode` to declare tested compatibility.
- npm compatibility is checked; local file plugins bypass the engine check.
- Pin package versions when reproducibility matters.
- Plugin package install runs with lifecycle scripts disabled, so do not require `postinstall`.
- Keep resolved entrypoints and theme paths inside the package directory.
Theme-only TUI packages may use `oc-themes`; consult `packages/opencode/specs/tui-plugins.md` for path and synchronization rules.
## Resolution And Identity
- npm declarations deduplicate by package name; higher-precedence/later declarations win.
- File server and TUI specs have target-specific resolution behavior.
- External modules may resolve/import in parallel, but activate sequentially.
- IDs must not collide with built-ins or other loaded plugins.
- Dynamic import failures are effectively permanent for the current process because Bun caches them.
- `--pure` or `OPENCODE_PURE` skips external plugins.
Read `packages/opencode/src/plugin/shared.ts`, `loader.ts`, and `install.ts` before changing packaging behavior.
## Test Locations
Server plugin coverage:
- `packages/opencode/test/plugin/trigger.test.ts`: hook sequencing and failures.
- `packages/opencode/test/plugin/loader-shared.test.ts`: resolution and module validation.
- `packages/opencode/test/plugin/shared.test.ts`: shared target rules.
- `packages/opencode/test/plugin/install.test.ts`: package install and config patching.
- `packages/opencode/test/plugin/install-concurrency.test.ts`: concurrent writes.
- `packages/opencode/test/plugin/auth-override.test.ts`: auth precedence.
- `packages/opencode/test/tool/registry.test.ts`: schemas, results, and attachments.
TUI plugin coverage:
- `packages/opencode/test/cli/tui/plugin-loader.test.ts`: loading and ordering.
- `packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts`: target entrypoints.
- `packages/opencode/test/cli/tui/plugin-lifecycle.test.ts`: rollback and cleanup.
- `packages/opencode/test/cli/tui/plugin-toggle.test.ts`: persisted enablement.
- `packages/opencode/test/cli/tui/plugin-add.test.ts`: runtime addition.
- `packages/opencode/test/cli/tui/plugin-install.test.ts`: installation.
- `packages/opencode/test/cli/tui/plugin-loader-pure.test.ts`: pure mode.
Use fixture helpers under `packages/opencode/test/fixture/` rather than reimplementing the loader in tests.
## Verification Commands
Run from the owning package, never the repository root:
```sh
cd packages/opencode
bun typecheck
bun test test/plugin/trigger.test.ts
bun test test/cli/tui/plugin-lifecycle.test.ts
```
Select the smallest relevant tests first, then broader plugin suites. For interactive TUI verification, follow `packages/opencode/AGENTS.md`: run `bun dev` in detached `tmux`, capture output, and explicitly stop the session.
## Publishing Checklist
- Build output is ESM-compatible and contains no source-only path aliases.
- Every advertised target has the correct package export.
- Each target module exports only its own target shape.
- Peer/runtime dependencies are classified correctly.
- `engines.opencode` matches tested versions.
- Default options are backward-compatible and validated at runtime.
- Local file, pinned npm, and bare npm specs have been considered.
- Loading, failure, cleanup, and upgrade behavior are tested.
- README examples match the exported API and config target.

View File

@@ -0,0 +1,122 @@
# Server Plugins
## Contract And Lifecycle
The public contract is `packages/plugin/src/index.ts`:
```ts
type Plugin = (input: PluginInput, options?: Record<string, unknown>) => Promise<Hooks>
```
`PluginInput` provides the SDK `client`, `project`, `directory`, `worktree`, `serverUrl`, Bun shell `$`, and experimental workspace registration.
The server runtime is `packages/opencode/src/plugin/index.ts`.
- Built-in plugins initialize before external plugins.
- External modules may resolve concurrently, but activation is sequential for deterministic hook order.
- `config` hooks run sequentially against the mutable merged config.
- `event` subscribes to location-filtered events and is fire-and-forget.
- Ordinary hooks run sequentially and share a mutable output object.
- Ordinary hook failures propagate and stop later hooks for that trigger.
- Initialization, config, and disposal failures are isolated and logged by the host.
- `dispose` runs when the per-directory plugin scope closes.
Mutate hook output in place. Preserve values contributed by earlier plugins: append arrays, merge maps, and change only fields the plugin owns.
## Hook Selection
Use the narrowest hook that expresses the behavior:
| Goal | Hook |
| ---------------------------------- | --------------------------------- |
| Observe SDK events | `event` |
| Modify merged configuration | `config` |
| Add tools | `tool` |
| Add provider authentication | `auth` |
| Add or change provider models | `provider` |
| Modify incoming user message | `chat.message` |
| Modify LLM parameters or headers | `chat.params`, `chat.headers` |
| Modify command parts | `command.execute.before` |
| Validate or rewrite tool arguments | `tool.execute.before` |
| Transform tool presentation/result | `tool.execute.after` |
| Modify model-facing tool schemas | `tool.definition` |
| Add shell environment variables | `shell.env` |
| Influence permission decisions | `permission.ask` |
| Customize compaction | `experimental.session.compacting` |
Read the complete `Hooks` interface before using experimental hooks.
## Custom Tools
Use `tool()` and Zod schemas from `tool.schema`:
```ts
import { type Plugin, tool } from "@opencode-ai/plugin"
export default (async () => ({
tool: {
lookup_issue: tool({
description: "Look up one issue by numeric ID",
args: {
id: tool.schema.number().int().positive().describe("Issue ID"),
},
async execute(args, context) {
await context.ask({
permission: "lookup_issue",
patterns: [String(args.id)],
always: ["*"],
metadata: { id: args.id },
})
context.metadata({ title: `Issue ${args.id}` })
return {
title: `Issue ${args.id}`,
output: "Result",
metadata: { id: args.id },
}
},
}),
},
})) satisfies Plugin
```
Tool rules:
- Write descriptions for the model, including when to use the tool and important constraints.
- Describe arguments individually and constrain them in the schema.
- Use `context.directory` and `context.worktree` for path resolution.
- Pass `context.abort` into cancellable I/O.
- Call `context.ask()` before performing work covered by a permission boundary.
- Use `context.metadata()` for in-progress presentation; return final metadata in the result.
- Return attachments only as declared file attachments with a MIME type and URL.
- Keep output useful and bounded. The host may truncate large results and add truncation metadata.
Plugin tools with built-in IDs take precedence, but overriding built-ins should be explicit and tested.
## Auth And Providers
Use existing built-ins as references rather than inventing OAuth behavior:
- `packages/opencode/src/plugin/azure.ts`: simple API-key prompt.
- `packages/opencode/src/plugin/xai.ts`: OAuth, refresh, and custom fetch behavior.
- `packages/opencode/src/plugin/openai/codex.ts`: auth plus chat parameter hooks.
- `packages/opencode/src/plugin/github-copilot/copilot.ts`: full auth/provider integration.
Do not log credentials, tokens, authorization codes, provider headers, or raw auth responses. Preserve provider identity and refresh semantics defined by `AuthHook`.
## Useful Examples
- `.opencode/plugins/model-task.ts`: custom subagent tool with permission, abort, metadata, and SDK calls when present in the worktree.
- `packages/plugin/src/example.ts`: minimal package example.
- `packages/opencode/test/fixture/agent-plugin.ts`: config mutation fixture.
- `packages/opencode/src/plugin/*.ts`: built-in auth/provider implementations.
## Common Failures
- Exporting constants beside legacy plugin functions: every exported value may be treated as a plugin.
- Using `process.cwd()` in a multi-directory process.
- Replacing a shared output map or array and deleting earlier plugin contributions.
- Forgetting that `event` is not awaited like ordinary hooks.
- Assuming thrown hook errors are isolated.
- Installing a missing dependency after a dynamic import failed and expecting the same process to recover; Bun caches failed imports.
- Trusting options without validation.

View File

@@ -0,0 +1,130 @@
# TUI Plugins
## Contract And Loading
The public contract is `packages/plugin/src/tui.ts`; technical behavior is documented in `packages/opencode/specs/tui-plugins.md`.
```ts
type TuiPlugin = (api: TuiPluginApi, options: Record<string, unknown> | undefined, meta: TuiPluginMeta) => Promise<void>
```
- Import from `@opencode-ai/plugin/tui`.
- Export one default `{ id?, tui }` object. Named exports are ignored by the loader.
- File plugins require an explicit non-empty ID.
- Configure TUI plugins explicitly in `tui.json`; there is no directory auto-discovery.
- JSX uses OpenTUI Solid, normally with `/** @jsxImportSource @opentui/solid */`.
- TUI packages resolve only `exports["./tui"]`; they do not fall back to package `main` or root exports.
## API Routing
| Need | API |
| -------------------------------- | ------------------------------------------ |
| Commands and shortcuts | `api.keymap.registerLayer(...)` |
| Temporary input context | `api.mode.push(...)` |
| Full-screen UI | `api.route.register(...)`, `navigate(...)` |
| Host dialogs and toast | `api.ui.dialog`, `Dialog*`, `toast(...)` |
| Reuse the host prompt | `api.ui.Prompt` |
| Inject host UI | `api.slots.register(...)` |
| Theme tokens and switching | `api.theme` |
| Synced sessions/providers/status | `api.state` |
| SDK operations | `api.client` |
| TUI event stream | `api.event.on(...)` |
| Persistent shared values | `api.kv` |
| Host-mediated notification/sound | `api.attention` |
| Extra cleanup | `api.lifecycle.onDispose(...)` |
Do not use deprecated `api.command` in new plugins. Register commands and bindings through keymap layers.
## Commands And Modes
```tsx
api.keymap.registerLayer({
mode: "base",
commands: [
{
name: "acme.demo.open",
title: "Open demo",
category: "Plugin",
namespace: "palette",
slashName: "demo",
run() {
api.route.navigate("acme.demo")
},
},
],
bindings: [{ key: "ctrl+shift+m", cmd: "acme.demo.open", desc: "Open demo" }],
})
```
Built-in modes are `base`, `modal`, and `autocomplete`. A layer without `mode` remains active across dialogs and autocomplete, so omit mode only for intentionally global behavior.
For plugin-owned full-screen interaction, push a namespaced mode inside the component and dispose it with Solid cleanup:
```tsx
import { onCleanup } from "solid-js"
const pop_mode = api.mode.push("acme.demo")
onCleanup(pop_mode)
```
## Routes, Dialogs, And Slots
- Reserve `home` and `session` for host routes.
- Namespace route names; duplicate routes are last-registration-wins.
- Unknown routes render fallback UI rather than throwing.
- Use host dialog components for standard interactions and `api.ui.dialog.replace()` for custom dialog content.
- Use route params for serializable navigation state; keep component-local transient state in Solid primitives when appropriate.
- `api.slots.register(...)` returns an assigned ID, not an unregister function.
- Slot registration and other host API resources are scope-tracked automatically.
- Read current slot names and props from `TuiHostSlotMap`, not copied lists.
## State And Persistence
- `api.tuiConfig` and `api.state` are live views, not initialization snapshots.
- `api.kv` is shared by all plugins. Prefix every key with the plugin ID.
- Check readiness where the API exposes it.
- Persist only user preferences or durable plugin state, not derived host state.
- Runtime enablement in KV overrides `tui.json` on startup.
`meta.state` is `first`, `updated`, or `same`. Use it for bounded migration or asset synchronization, not normal rendering behavior.
## Lifecycle
The host automatically scope-tracks commands, keymap resources, routes, event subscriptions, slots, pushed modes, and sound packs.
- `api.lifecycle.signal` aborts before cleanup begins.
- Use `api.lifecycle.onDispose()` for timers, sockets, file watchers, workers, or other plugin-owned resources.
- Initialization failure rolls back tracked resources and does not prevent later plugins from loading.
- Cleanup is reverse-order, awaited, idempotent, and constrained by a total five-second budget.
- Keep cleanup fast and independently safe after partial initialization.
## UI Quality
- Use `api.theme.current` tokens instead of hard-coded colors.
- Use `api.keys` to display shortcuts according to host formatting.
- Make routes responsive to terminal dimensions and usable with keyboard-only input.
- Avoid taking over global shortcuts without a strong reason.
- Prefer host dialogs, prompts, and slots over visually inconsistent reimplementations.
- Send attention through `api.attention.notify()` so the host owns focus, notification, and sound policy.
- Keep notification text privacy-safe.
## Useful Examples
- `.opencode/plugins/tui-smoke.tsx`: broad API smoke implementation.
- `packages/tui/src/feature-plugins/system/which-key.tsx`: focused keymap UI.
- `packages/tui/src/feature-plugins/system/notifications.ts`: attention behavior.
- `packages/tui/src/feature-plugins/system/diff-viewer.tsx`: route/UI integration.
- `packages/tui/src/feature-plugins/home/tips.tsx`: host slot usage.
- `packages/tui/src/feature-plugins/sidebar/context.tsx`: sidebar extension.
## Common Failures
- Expecting `.opencode/plugins` auto-discovery for TUI modules.
- Exporting `{ server, tui }` from one module.
- Omitting the default export, or relying on named exports.
- Omitting an ID for a file plugin.
- Registering an ungated keymap layer accidentally active in modal/autocomplete modes.
- Treating KV as plugin-private.
- Treating `slots.register()` as returning a disposer.
- Expecting `plugins.install()` to activate a plugin; installation and runtime addition are separate.
- Leaking timers or network resources because host tracking only covers host registrations.

View File

@@ -37,7 +37,7 @@ In the Settings > Developer section, click on Edit Config. It will open the fold
## Codex CLI
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
You can automatically configure the MCP server using the [Codex plugin](codex-plugin) (recommended). If you prefer to configure the MCP server manually, add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
```toml
[mcp_servers.svelte]
@@ -47,7 +47,7 @@ args = ["-y", "@sveltejs/mcp"]
## Copilot CLI
Use the Copilot CLI to interactively add the MCP server:
You can automatically configure the MCP server using the [Copilot plugin](copilot-plugin) (recommended). If you prefer to configure the MCP server manually, use the Copilot CLI to interactively add the MCP server:
```bash
/mcp add
@@ -66,16 +66,21 @@ Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following
}
```
## Gemini CLI
## Antigravity CLI
To include the local MCP version in Gemini CLI, simply run the following command:
To use the local MCP version in Antigravity CLI, create or edit `~/.gemini/config/mcp_config.json` and add the following configuration:
```bash
gemini mcp add -t stdio -s [scope] svelte npx -y @sveltejs/mcp
```json
{
"mcpServers": {
"svelte": {
"command": "npx",
"args": ["-y", "@sveltejs/mcp"]
}
}
}
```
The `[scope]` must be `user`, `project` or `local`.
## OpenCode
You can automatically configure the MCP server using the [OpenCode plugin](opencode-plugin) (recommended). If you prefer to configure the MCP server manually, run:

View File

@@ -28,7 +28,7 @@ If you prefer you can also install the `svelte` plugin in [the Svelte Claude Cod
## Codex CLI
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
You can automatically configure the MCP server using the [Codex plugin](codex-plugin) (recommended). If you prefer to configure the MCP server manually, add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
```toml
experimental_use_rmcp_client = true
@@ -38,7 +38,7 @@ url = "https://mcp.svelte.dev/mcp"
## Copilot CLI
Use the Copilot CLI to interactively add the MCP server:
You can automatically configure the MCP server using the [Copilot plugin](copilot-plugin) (recommended). If you prefer to configure the MCP server manually, use the Copilot CLI to interactively add the MCP server:
```bash
/mcp add
@@ -56,16 +56,20 @@ Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following
}
```
## Gemini CLI
## Antigravity CLI
To use the remote MCP server with Gemini CLI, simply run the following command:
To use the remote MCP version in Antigravity CLI, create or edit `~/.gemini/config/mcp_config.json` and add the following configuration:
```bash
gemini mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
```json
{
"mcpServers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}
```
The `[scope]` must be `user` or `project`.
## OpenCode
You can automatically configure the MCP server using the [OpenCode plugin](opencode-plugin) (recommended). If you prefer to configure the MCP server manually, run:

View File

@@ -2,9 +2,9 @@
title: Overview
---
This is the list of available skills provided by the Svelte MCP package. Skills are sets of instructions that AI agents can load on-demand to help with specific tasks.
This is the list of available skills provided by Svelte. Skills are sets of instructions that AI agents can load on-demand to help with specific tasks.
Skills are available in both the Claude Code plugin (installed via the marketplace) and the OpenCode plugin (`@sveltejs/opencode`). They can also be manually installed in your `.claude/skills` or `.opencode/skills` folder.
Skills are available in the Claude Code plugin, the Codex CLI plugin, the GitHub Copilot CLI plugin, and the OpenCode plugin (`@sveltejs/opencode`). They can also be manually installed in your `.claude/skills`, `.copilot/skills`, or `.opencode/skills` folder.
You can download the latest skills from the [releases page](https://github.com/sveltejs/ai-tools/releases) of the repo, or find them in the [`tools/skills`](https://github.com/sveltejs/ai-tools/tree/main/tools/skills) folder.

View File

@@ -6,7 +6,13 @@ OpenCode has a [plugin system](https://opencode.ai/docs/plugins/) that allows de
## Installation
To install the plugin you can edit your [OpenCode config](https://opencode.ai/docs/config/) (either the global or the local one), adding `@sveltejs/opencode` to the list of plugins.
With OpenCode 1.3.4 or newer, install the plugin from the command line:
```sh
opencode plugin @sveltejs/opencode
```
Alternatively, edit your [OpenCode config](https://opencode.ai/docs/config/) (either the global or the local one) and add `@sveltejs/opencode` to the list of plugins:
```json
{
@@ -17,9 +23,22 @@ To install the plugin you can edit your [OpenCode config](https://opencode.ai/do
That's it! You now have the Svelte [MCP server](mcp), [skills](skills), and the `svelte-file-editor` [subagent](subagent) configured for you.
### TUI configuration
The package also includes a TUI plugin for configuring these features interactively. Add `@sveltejs/opencode` to your global or project-local `tui.json`:
```json
{
"$schema": "https://opencode.ai/tui.json",
"plugin": ["@sveltejs/opencode"]
}
```
Restart OpenCode, then run `/svelte-plugin` or select **Configure Svelte plugin** from the command palette. Choose whether to edit the project or global configuration, then use the checkboxes and radio options to configure the plugin. Changes are saved automatically, and **Revert changes** restores the values from when the dialog was opened.
## Configuration
By default, everything is enabled, but you can configure the plugin by adding a configuration file:
By default, everything is enabled. The TUI plugin writes the same configuration files that you can create or edit manually:
- locally, in `.opencode/svelte.json`
- globally, in `~/.config/opencode/svelte.json` (or, if you have specified the environment variable, in `$OPENCODE_CONFIG_DIR/svelte.json`)

View File

@@ -0,0 +1,36 @@
---
title: GitHub Copilot CLI
---
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a GitHub Copilot CLI [plugin marketplace](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing).
The marketplace allows you to install the `svelte` plugin which will give you the remote MCP server, [skills](skills) to instruct the LLM on how to properly write Svelte 5 code, and a specialized agent for editing Svelte files.
If possible, we recommend that you instruct the LLM to execute MCP calls with the agent (you can explicitly mention an agent in your message to delegate work to it) when creating or editing `.svelte` files or `.svelte.ts`/`.svelte.js` modules — this will help save context by handling Svelte-specific tasks more efficiently.
## Installation
In VS Code, run the **Install plugin from source** command and use the repository URL:
```text
https://github.com/sveltejs/ai-tools
```
You can also add the repository as a marketplace from the Copilot CLI:
```bash
copilot plugin marketplace add sveltejs/ai-tools
```
Then, install the Svelte plugin:
```bash
copilot plugin install svelte@ai-tools
```
You can also run the same commands from an interactive Copilot CLI session:
```bash
/plugin marketplace add sveltejs/ai-tools
/plugin install svelte@ai-tools
```

View File

@@ -0,0 +1,28 @@
---
title: Codex CLI
---
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Codex CLI [plugin marketplace](https://developers.openai.com/codex/plugins).
The marketplace allows you to install the `svelte` plugin which will give you the remote MCP server, [skills](skills) to instruct the LLM on how to properly write Svelte 5 code, and a specialized agent for editing Svelte files.
If possible, we recommend that you instruct the LLM to execute MCP calls with the agent (you can explicitly mention an agent in your message to delegate work to it) when creating or editing `.svelte` files or `.svelte.ts`/`.svelte.js` modules — this will help save context by handling Svelte-specific tasks more efficiently.
## Installation
Add the repository as a marketplace from the Codex CLI:
```bash
codex plugin marketplace add sveltejs/ai-tools
```
Then, open the plugin directory from an interactive Codex CLI session:
```bash
codex
/plugins
```
Choose the Svelte marketplace, select the `svelte` plugin, and install it.
Codex can read the repository's legacy-compatible `.claude-plugin/marketplace.json` marketplace file, so the same marketplace source works for both Claude Code and Codex CLI.

View File

@@ -36,7 +36,6 @@
"devDependencies": {
"@anthropic-ai/sdk": "catalog:ai",
"@sveltejs/kit": "catalog:svelte",
"@types/eslint-scope": "catalog:lint",
"@types/estree": "catalog:tooling",
"@typescript-eslint/types": "catalog:lint",
"dotenv": "catalog:tooling"

View File

@@ -25,7 +25,7 @@ function get_autofixer_schema(stdio: boolean) {
desired_svelte_version: v.pipe(
v.union([v.string(), v.number()]),
v.description(
'The desired svelte version...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
'The desired major svelte version as an integer (must be 4 or 5)...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
),
),
async: v.pipe(

View File

@@ -1,17 +1,11 @@
import ts_parser from '@typescript-eslint/parser';
import type * as eslint from 'eslint';
import type { CallExpression, Identifier } from 'estree';
import type { Reference, Variable } from 'eslint-scope';
import { parseForESLint as svelte_eslint_parse } from 'svelte-eslint-parser';
import { runes } from '../constants.js';
type Scope = {
variables?: Variable[];
references?: Reference[];
childScopes?: Scope[];
};
type ScopeManager = {
globalScope: Scope;
};
type Scope = eslint.Scope.Scope;
type ScopeManager = eslint.Scope.ScopeManager;
function collect_scopes(scope: Scope, acc: Scope[] = []) {
acc.push(scope);
@@ -27,12 +21,12 @@ export function parse(code: string, file_path: string) {
parser: { ts: ts_parser, typescript: ts_parser },
});
let all_scopes: Scope[] | undefined;
let all_variables: Variable[] | undefined;
let all_references: Reference[] | undefined;
let all_variables: eslint.Scope.Variable[] | undefined;
let all_references: eslint.Scope.Reference[] | undefined;
function get_all_scopes() {
if (!all_scopes) {
all_scopes = collect_scopes(parsed.scopeManager!.globalScope);
all_scopes = collect_scopes(parsed.scopeManager!.globalScope!);
}
return all_scopes;
}

View File

@@ -1,5 +1,17 @@
# @sveltejs/mcp
## 0.1.25
### Patch Changes
- fix: better wording for `desired_svelte_version` ([`59dcbff`](https://github.com/sveltejs/ai-tools/commit/59dcbff136a91efad5a0e978e8208e1b1d277f97))
## 0.1.24
### Patch Changes
- chore: support declaration tags ([#222](https://github.com/sveltejs/ai-tools/pull/222))
## 0.1.23
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@sveltejs/mcp",
"version": "0.1.23",
"version": "0.1.25",
"type": "module",
"license": "MIT",
"mcpName": "dev.svelte/mcp",

View File

@@ -9,7 +9,7 @@
"subfolder": "packages/mcp-stdio",
"source": "github"
},
"version": "0.1.23",
"version": "0.1.25",
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
"icons": [
{
@@ -25,7 +25,7 @@
{
"registryType": "npm",
"identifier": "@sveltejs/mcp",
"version": "0.1.23",
"version": "0.1.25",
"runtimeHint": "npx",
"transport": {
"type": "stdio"

View File

@@ -1,5 +1,11 @@
# @sveltejs/opencode
## 0.1.10
### Patch Changes
- feat: add tui opencode plugin ([#231](https://github.com/sveltejs/ai-tools/pull/231))
## 0.1.9
### Patch Changes

View File

@@ -15,6 +15,17 @@ Add `@sveltejs/opencode` to your OpenCode config (either global or local):
That's it! You now have the Svelte MCP server and the file editor subagent configured automatically.
To configure the plugin from OpenCode's TUI, also add the package to `tui.json`:
```json
{
"$schema": "https://opencode.ai/tui.json",
"plugin": ["@sveltejs/opencode"]
}
```
Run `/svelte-plugin` or choose **Configure Svelte plugin** from the command palette. The dialog lets you choose project or global scope before editing the available options, and guides you through finite choices such as built-in skills.
## Features
### Svelte MCP Server

View File

@@ -1,6 +1,6 @@
{
"name": "@sveltejs/opencode",
"version": "0.1.9",
"version": "0.1.10",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/sveltejs/ai-tools#readme",
@@ -14,6 +14,7 @@
"files": [
"index.ts",
"config.ts",
"tui.tsx",
"agents.ts",
"instructions",
"skills"
@@ -22,6 +23,10 @@
"./server": {
"types": "./index.ts",
"import": "./index.ts"
},
"./tui": {
"types": "./tui.tsx",
"import": "./tui.tsx"
}
},
"repository": {
@@ -33,10 +38,14 @@
"access": "public"
},
"dependencies": {
"valibot": "catalog:tooling"
"valibot": "catalog:tooling",
"@opentui/core": "catalog:opencode",
"@opentui/keymap": "catalog:opencode",
"@opentui/solid": "catalog:opencode",
"solid-js": "catalog:opencode"
},
"devDependencies": {
"@opencode-ai/plugin": "catalog:ai",
"@opencode-ai/plugin": "catalog:opencode",
"@valibot/to-json-schema": "catalog:tooling",
"@types/node": "catalog:tooling"
}

View File

@@ -2,8 +2,10 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "preserve",
"jsxImportSource": "@opentui/solid",
"types": ["@types/node"]
},
"include": ["index.ts", "config.ts", "agents.ts", "scripts/*"],
"include": ["index.ts", "config.ts", "agents.ts", "tui.tsx", "scripts/*"],
"exclude": ["node_modules"]
}

291
packages/opencode/tui.tsx Normal file
View File

@@ -0,0 +1,291 @@
/** @jsxImportSource @opentui/solid */
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';
import { existsSync } from 'node:fs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import * as v from 'valibot';
import { config_schema, type McpConfig } from './config.ts';
const plugin_id = 'svelte.configure';
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'] as const;
const agent_name = 'svelte-file-editor';
type Scope = 'project' | 'global';
type Config = Partial<McpConfig>;
function project_root(api: Parameters<TuiPlugin>[0]) {
const worktree = api.state.path.worktree;
return worktree && worktree !== '/' ? worktree : api.state.path.directory;
}
function config_path(api: Parameters<TuiPlugin>[0], scope: Scope) {
if (scope === 'project') return join(project_root(api), '.opencode', 'svelte.json');
return join(
process.env.OPENCODE_CONFIG_DIR ?? join(homedir(), '.config', 'opencode'),
'svelte.json',
);
}
async function read_config(path: string): Promise<Config> {
if (!existsSync(path)) return {};
const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
const result = v.safeParse(config_schema, parsed);
if (!result.success)
throw new Error('The existing file does not match the Svelte plugin schema.');
// Keep schema annotations and future fields that this version does not edit.
return parsed as Config;
}
async function save_config(path: string, config: Config) {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(config, null, '\t')}\n`, 'utf8');
}
function display(value: unknown, fallback = 'default') {
return value === undefined ? fallback : String(value);
}
const tui: TuiPlugin = async (api) => {
function open_scope() {
if (!api.state.path.directory) {
api.ui.toast({
variant: 'warning',
message: 'Paths are still syncing. Try again in a moment.',
});
return;
}
api.ui.dialog.replace(() => (
<api.ui.DialogSelect<Scope>
title="Configure Svelte plugin"
options={[
{
title: 'Project',
value: 'project',
description: 'Write .opencode/svelte.json for this project',
},
{
title: 'Global',
value: 'global',
description: 'Write svelte.json in the OpenCode config directory',
},
]}
onSelect={(option) => void open_config(option.value)}
/>
));
}
async function open_config(scope: Scope) {
const path = config_path(api, scope);
let config: Config;
try {
config = await read_config(path);
} catch (error) {
api.ui.toast({
variant: 'error',
title: 'Cannot edit Svelte configuration',
message: error instanceof Error ? error.message : 'Failed to read configuration',
});
return;
}
const original_config = structuredClone(config);
let current_option: string | undefined;
async function persist(show_toast = true) {
try {
await save_config(path, config);
if (show_toast)
api.ui.toast({ variant: 'success', message: `Saved ${scope} Svelte configuration` });
} catch {
api.ui.toast({ variant: 'error', message: 'Failed to save Svelte configuration' });
}
}
function prompt_agent_number(key: 'temperature' | 'top_p' | 'maxSteps', label: string) {
const agent = config.subagent?.agents?.[agent_name];
api.ui.dialog.replace(() => (
<api.ui.DialogPrompt
title={`${agent_name}: ${label}`}
placeholder="Leave empty to use the default"
value={agent?.[key] === undefined ? '' : String(agent[key])}
onCancel={open_agent}
onConfirm={async (value) => {
const number = value.trim() === '' ? undefined : Number(value);
if (number !== undefined && !Number.isFinite(number)) {
api.ui.toast({ variant: 'warning', message: `${label} must be a number` });
return;
}
config.subagent ??= {};
config.subagent.agents ??= {};
config.subagent.agents[agent_name] = { ...agent, [key]: number };
await persist();
open_agent();
}}
/>
));
}
function open_agent() {
const agent = config.subagent?.agents?.[agent_name];
api.ui.dialog.replace(() => (
<api.ui.DialogSelect<string>
title={`Configure ${agent_name}`}
options={[
{ title: 'Model', value: 'model', description: display(agent?.model) },
{
title: 'Temperature',
value: 'temperature',
description: display(agent?.temperature),
},
{ title: 'Top P', value: 'top_p', description: display(agent?.top_p) },
{ title: 'Maximum steps', value: 'maxSteps', description: display(agent?.maxSteps) },
{ title: 'Back', value: 'back' },
]}
onSelect={(option) => {
if (option.value === 'back') return open_menu();
if (option.value !== 'model')
return prompt_agent_number(
option.value as 'temperature' | 'top_p' | 'maxSteps',
option.title,
);
api.ui.dialog.replace(() => (
<api.ui.DialogPrompt
title={`${agent_name}: model`}
placeholder="provider/model, or empty for default"
value={agent?.model ?? ''}
onCancel={open_agent}
onConfirm={async (value) => {
config.subagent ??= {};
config.subagent.agents ??= {};
config.subagent.agents[agent_name] = {
...agent,
model: value.trim() || undefined,
};
await persist();
open_agent();
}}
/>
));
}}
/>
));
}
function open_menu() {
const skills = config.skills?.enabled;
const selected_skills = new Set(
Array.isArray(skills) ? skills : skills === false ? [] : skill_names,
);
const all_skills_selected = skill_names.every((name) => selected_skills.has(name));
function checked(value: boolean | undefined) {
return value !== false ? '[x]' : '[ ]';
}
function radio(value: 'remote' | 'local') {
return (config.mcp?.type ?? 'remote') === value ? '(*)' : '( )';
}
api.ui.dialog.replace(() => (
<api.ui.DialogSelect<string>
title={`Svelte plugin (${scope})`}
{...(current_option === undefined ? {} : { current: current_option })}
skipFilter
options={[
{
title: `${checked(config.mcp?.enabled)} MCP server`,
value: 'mcp-enabled',
category: 'MCP',
},
{ title: `${radio('remote')} Remote`, value: 'mcp-remote', category: 'MCP transport' },
{ title: `${radio('local')} Local`, value: 'mcp-local', category: 'MCP transport' },
{
title: `${checked(config.subagent?.enabled)} Subagent`,
value: 'subagent-enabled',
category: 'Subagent',
},
{ title: 'Subagent settings', value: 'agent' },
{
title: `${checked(config.instructions?.enabled)} Instructions`,
value: 'instructions',
category: 'Instructions',
},
{
title: `${all_skills_selected ? '[x]' : '[ ]'} Select all`,
value: 'skills-all',
category: 'Skills',
},
...skill_names.map((name) => ({
title: `${selected_skills.has(name) ? '[x]' : '[ ]'} ${name}`,
value: `skill:${name}`,
category: 'Skills',
})),
{
title: 'Revert changes',
value: 'revert',
category: 'Actions',
description: 'Restore values from when this dialog opened',
},
{ title: 'Change scope', value: 'scope', category: 'Actions' },
{ title: 'Close', value: 'close', category: 'Actions' },
]}
onMove={(option) => (current_option = option.value)}
onSelect={async (option) => {
current_option = option.value;
if (option.value === 'close') return api.ui.dialog.clear();
if (option.value === 'scope') return open_scope();
if (option.value === 'agent') return open_agent();
if (option.value === 'revert') {
config = structuredClone(original_config);
await persist();
return open_menu();
}
if (option.value === 'mcp-enabled')
config.mcp = { ...config.mcp, enabled: config.mcp?.enabled === false };
if (option.value === 'mcp-remote') config.mcp = { ...config.mcp, type: 'remote' };
if (option.value === 'mcp-local') config.mcp = { ...config.mcp, type: 'local' };
if (option.value === 'subagent-enabled')
config.subagent = { ...config.subagent, enabled: config.subagent?.enabled === false };
if (option.value === 'instructions')
config.instructions = {
...config.instructions,
enabled: config.instructions?.enabled === false,
};
if (option.value === 'skills-all') {
config.skills = { enabled: all_skills_selected ? [] : [...skill_names] };
}
if (option.value.startsWith('skill:')) {
const name = option.value.slice('skill:'.length);
if (selected_skills.has(name)) {
selected_skills.delete(name);
} else {
selected_skills.add(name);
}
config.skills = { enabled: [...selected_skills] };
}
await persist(false);
open_menu();
}}
/>
));
}
open_menu();
}
api.keymap.registerLayer({
commands: [
{
name: `${plugin_id}.open`,
title: 'Configure Svelte plugin',
category: 'Plugin',
namespace: 'palette',
slashName: 'svelte-plugin',
run: open_scope,
},
],
});
};
export default {
id: plugin_id,
tui,
} satisfies TuiPluginModule & { id: string };

1289
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,39 @@
minimumReleaseAge: 2880
minimumReleaseAgeExclude:
- '@sveltejs/*'
- svelte
- esrap
- devalue
- zimmerframe
- prettier-plugin-svelte
- svelte-check
- esm-env
blockExoticSubdeps: true
engineStrict: true
packages:
- ./packages/*
- ./apps/*
blockExoticSubdeps: true
catalogs:
ai:
'@anthropic-ai/sdk': ^0.71.0
'@mcp-ui/server': ^6.0.0
'@modelcontextprotocol/inspector': ^0.19.0
'@opencode-ai/plugin': ^1.1.44
opencode:
'@opencode-ai/plugin': 1.17.9
'@opentui/core': ^0.4.3
'@opentui/keymap': ^0.4.3
'@opentui/solid': ^0.4.3
solid-js: 1.9.12
lint:
'@eslint/compat': ^2.0.0
'@eslint/js': ^9.36.0
'@types/eslint-scope': ^8.3.2
'@typescript-eslint/parser': ^8.44.0
'@typescript-eslint/types': ^8.44.0
eslint: ^9.36.0
eslint-config-prettier: ^10.0.1
eslint-plugin-import: ^2.32.0
eslint-plugin-pnpm: ^1.3.0
eslint-plugin-svelte: ^3.12.5
eslint-plugin-svelte: ^3.19.0
globals: ^17.0.0
prettier: ^3.4.2
prettier-plugin-svelte: ^3.3.3
svelte-eslint-parser: ^1.4.0
svelte-eslint-parser: ^1.7.1
typescript-eslint: ^8.44.0
svelte:
'@sveltejs/adapter-vercel': ^6.0.0
'@sveltejs/kit': ^2.42.2
'@sveltejs/vite-plugin-svelte': ^6.0.0
svelte: ^5.47.0
svelte: ^5.56.1
svelte-check: ^4.0.0
tmcp:
'@tmcp/adapter-valibot': ^0.1.5
@@ -68,5 +60,16 @@ catalogs:
vite-plugin-devtools-json: ^1.0.0
vitest: ^4.0.0
zimmerframe: ^1.1.4
engineStrict: true
minimumReleaseAge: 2880
minimumReleaseAgeExclude:
- '@sveltejs/*'
- svelte
- esrap
- devalue
- zimmerframe
- prettier-plugin-svelte
- svelte-check
- esm-env
useNodeVersion: 22.19.0
useNodeVersion: 22.22.2

11
skills-lock.json Normal file
View File

@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"writing-great-skills": {
"source": "mattpocock/skills",
"sourceType": "github",
"skillPath": "skills/productivity/writing-great-skills/SKILL.md",
"computedHash": "dd555ce552f82784c3d2b8d13a8e26a6677a07ddc00032e142dec33bfd5438c6"
}
}
}