Compare commits

...

56 Commits

Author SHA1 Message Date
github-actions[bot]
ab14e46ec3 Version Packages (#237) 2026-07-22 13:43:04 +02:00
jyc.dev
d6c694c778 chore: switch to @changesets/changelog-github (#235) 2026-07-22 13:35:11 +02:00
Paolo Ricciuti
a52c8b63bb fix: ship plugin as js files instead of ts (#236) 2026-07-22 13:34:27 +02:00
Rich Harris
8152ed9fd4 chore: use quotemarks instead of bold text (#234) 2026-07-18 20:02:05 -04:00
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
github-actions[bot]
802b379f7a Version Packages (#208)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-22 12:11:32 +02:00
Paolo Ricciuti
fc391d0128 fix: remove restrictive permissions from subagent (#219) 2026-05-22 11:07:01 +02:00
Elliott Johnson
575dcc7d2b chore: Supply chain hardening (#217) 2026-05-22 08:24:32 +02:00
Elliott Johnson
88cea9e539 Revert "chore: Supply chain hardening" (#216) 2026-05-20 08:53:28 -06:00
Elliott Johnson
fdb1bc7370 chore: Supply chain hardening 2026-05-20 08:46:41 -06:00
Tee Ming
5b4d3aa68a docs: fix broken skills link (#211)
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-05-15 21:51:07 +02:00
github-actions[bot]
04c52f2b72 Version Packages (#197)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-13 20:51:10 +02:00
Paolo Ricciuti
0ea2a617ef docs: overhaul docs focus on all ai tools (#188)
Co-authored-by: jyc.dev <jycouet@gmail.com>
Co-authored-by: Tee Ming <chewteeming01@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
2026-05-13 20:50:40 +02:00
Paolo Ricciuti
484453e5f8 feat: allow stdio mcp to read the content of the file directly (#198) 2026-05-13 20:26:18 +02:00
Paolo Ricciuti
841af5b3a4 fix: handle non call expressions passed to is_rune (#201)
Co-authored-by: Copilot <copilot@github.com>
2026-04-28 17:12:58 +02:00
Paolo Ricciuti
1ce957ac72 chore: add LICENSE 2026-04-28 14:48:50 +02:00
Paolo Ricciuti
e429cd7839 chore: remove db requirement (#196) 2026-04-20 12:50:45 +02:00
github-actions[bot]
2ded13539a Version Packages (#192)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-10 18:39:19 +02:00
paoloricciuti
96c50acae2 fix: add server export to opencode plugin 2026-04-10 18:37:49 +02:00
github-actions[bot]
4920088e5c Version Packages (#191)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-10 16:57:38 +02:00
Paolo Ricciuti
8557f0af6f fix: import ts files directly (#190) 2026-04-10 16:56:23 +02:00
renovate[bot]
972d0f0bb2 chore(deps): update dependency @vercel/analytics to v2 (#177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 10:15:32 +01:00
renovate[bot]
7bf364a9d5 chore(deps): update pnpm/action-setup action to v5 (#186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 10:14:49 +01:00
Paolo Ricciuti
d06d758b81 chore: sync agents with the opencode plugin too (#187) 2026-03-25 09:43:52 +01:00
github-actions[bot]
e5ce7437c4 Version Packages (#179)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-13 23:07:32 +01:00
Paolo Ricciuti
2f422ee190 fix: broaden checks for imported runes because LLMs are unhinged (#185) 2026-03-13 22:59:52 +01:00
paoloricciuti
14f087cd7a chore: simplify sync script/action 2026-03-13 22:22:19 +01:00
Chris Tsongas
1ef5ddf605 fix: update svelte-file-editor agent to use proper name (#183)
Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
2026-03-13 10:37:43 +01:00
github-actions[bot]
0e55ee792d chore: sync skills from svelte.dev (#178)
Co-authored-by: svelte-docs-bot[bot] <196124396+svelte-docs-bot[bot]@users.noreply.github.com>
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-03-12 18:32:21 +01:00
paoloricciuti
84ec24b6f6 fix: don't format code blocks in references 2026-03-12 18:19:43 +01:00
paoloricciuti
710cebe539 docs: opencode config link 2026-03-12 17:59:32 +01:00
paoloricciuti
b2a380c4ce docs: fix markdown blocks 2026-03-12 17:57:45 +01:00
paoloricciuti
eef0a9b4d9 docs: update opencode docs 2026-03-11 21:31:43 +01:00
Paolo Ricciuti
260b36e8af chore: undo rename for publish to registry check 2026-03-11 14:11:15 +01:00
github-actions[bot]
6df3ebe568 Version Packages (#173)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-11 13:08:21 +01:00
Momcilo Miladinovic
27a2fc5653 fix: merge user-configured svelte-file-editor agent settings (#176)
Co-authored-by: Momochiro <x@Mac.sweet.opnsense>
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-03-11 12:59:01 +01:00
Paolo Ricciuti
5cd99d8234 feat: allow enabling a specific skill in opencode plugin (#174) 2026-03-10 17:38:04 +01:00
Paolo Ricciuti
e9f19199cb feat: add sync skill docs (#172) 2026-03-09 17:49:18 +01:00
Willow (GHOST)
480b46df0a feat(opencode): mcp enabled option is passed to opencode (#171)
Co-authored-by: Paolo Ricciuti <ricciutipaolo@gmail.com>
2026-03-07 12:38:45 +01:00
jyc.dev
5f5fb27977 feat: display similar result & error at the end (#161) 2026-03-07 12:20:18 +01:00
Paolo Ricciuti
29cfa77c39 chore: restructure repo to have single source of truth for tools (#170) 2026-03-03 21:43:34 +01:00
148 changed files with 5728 additions and 1883 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

@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/ai-tools" }],
"changelog": ["@changesets/changelog-github", { "repo": "sveltejs/ai-tools", "template": "\n- {summary} {ref}" }],
"commit": false,
"fixed": [],
"linked": [],

View File

@@ -6,7 +6,7 @@
"plugins": [
{
"name": "svelte",
"source": "./plugins/svelte",
"source": "./plugins/claude/svelte",
"description": "A plugin for all things Svelte development, MCP, skills, and more.",
"lspServers": {
"svelte": {

View File

@@ -0,0 +1,13 @@
{
"name": "svelte",
"owner": {
"name": "Svelte"
},
"plugins": [
{
"name": "svelte",
"source": "./plugins/cursor/svelte",
"description": "A plugin for all things Svelte development, MCP, skills, and more."
}
]
}

View File

@@ -13,12 +13,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.28.2
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -32,6 +30,4 @@ jobs:
- name: Run type check
run: pnpm run check
env:
DATABASE_URL: file:test.db
DATABASE_TOKEN: dummy-key
VOYAGE_API_KEY: dummy-key

View File

@@ -13,12 +13,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.28.2
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -32,6 +30,4 @@ jobs:
- name: Run linting
run: pnpm run lint
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key

View File

@@ -1,23 +0,0 @@
name: Publish Any Commit
on: [push, pull_request]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- run: corepack enable
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- run: pnpm dlx pkg-pr-new publish --compact './packages/mcp-stdio' './packages/opencode' --pnpm

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Publish to MCP Registry
working-directory: packages/mcp-stdio

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- 'plugins/svelte/skills/**'
- 'tools/skills/**'
workflow_dispatch:
permissions: {}
@@ -21,12 +21,12 @@ jobs:
skills: ${{ steps.find-skills.outputs.skills }}
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Find all skills
id: find-skills
run: |
skills=$(ls -d plugins/svelte/skills/*/ | xargs -I {} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
skills=$(ls -d tools/skills/*/ | xargs -I {} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "skills=$skills" >> $GITHUB_OUTPUT
release:
@@ -41,7 +41,7 @@ jobs:
skill: ${{ fromJson(needs.detect-skills.outputs.skills) }}
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Get version from date
id: version
@@ -49,18 +49,18 @@ jobs:
- name: Create zip
run: |
cd plugins/svelte/skills
cd tools/skills
zip -r ${{ matrix.skill }}.zip ${{ matrix.skill }}/
- name: Create Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ matrix.skill }}-v${{ steps.version.outputs.version }}
name: ${{ matrix.skill }} v${{ steps.version.outputs.version }}
body: |
Automated release of the ${{ matrix.skill }} skill.
This release was triggered by changes to the `plugins/svelte/skills/${{ matrix.skill }}/` directory.
files: plugins/svelte/skills/${{ matrix.skill }}.zip
This release was triggered by changes to the `tools/skills/${{ matrix.skill }}/` directory.
files: tools/skills/${{ matrix.skill }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest]
steps:
- name: checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
@@ -64,7 +64,7 @@ jobs:
publish-mcp:
needs: release
if: contains(needs.release.outputs.publishedPackages, '"@sveltejs/ai-tools"')
if: contains(needs.release.outputs.publishedPackages, '"@sveltejs/mcp"')
uses: ./.github/workflows/publish-mcp.yml
secrets:
MCP_KEY: ${{ secrets.MCP_KEY }}

View File

@@ -1,71 +0,0 @@
name: Sync Agents Documentation
on:
push:
branches:
- main
paths:
- 'instructions/AGENTS.md'
permissions:
contents: write
pull-requests: write
jobs:
sync-agents:
# prevents this action from running on forks
if: github.repository == 'sveltejs/ai-tools'
name: Sync AGENTS.md to OpenCode Package and Docs
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
- name: Install pnpm
shell: bash
run: |
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
echo installing pnpm version $PNPM_VER
npm i -g pnpm@$PNPM_VER
- name: Sync AGENTS.md
run: pnpm sync-agents-md
- name: Check for changes
id: git-check
run: |
git diff --exit-code packages/opencode/instructions/opencode-agents.md documentation/docs/10-introduction/.generated/agents.md || echo "changed=true" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync AGENTS.md to opencode package and documentation'
branch: chore/sync-agents-md
delete-branch: true
title: 'chore: sync AGENTS.md to opencode package and documentation'
body: |
## Summary
Automatically synced AGENTS.md to the opencode package and documentation.
This PR was triggered by changes to `instructions/AGENTS.md`.
## Changes
- Synced `packages/opencode/instructions/opencode-agents.md` with latest AGENTS.md
- Updated `documentation/docs/10-introduction/.generated/agents.md` with latest content
## Generated by
GitHub Action: Sync Agents Documentation
labels: |
chore
documentation
automated

View File

@@ -1,86 +0,0 @@
name: Sync Cursor Plugin
on:
push:
branches:
- main
paths:
- 'plugins/svelte/skills/**'
- 'plugins/svelte/agents/**'
- 'instructions/AGENTS.md'
permissions:
contents: write
pull-requests: write
jobs:
sync-cursor:
# prevents this action from running on forks
if: github.repository == 'sveltejs/ai-tools'
name: Sync Cursor Plugin from Sources of Truth
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
- name: Install pnpm
shell: bash
run: |
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
echo installing pnpm version $PNPM_VER
npm i -g pnpm@$PNPM_VER
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Sync Cursor plugin
run: pnpm sync-cursor-plugin
- name: Check for changes
id: git-check
run: |
git diff --exit-code svelte-cursor/ || echo "changed=true" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync Cursor plugin from sources of truth'
branch: chore/sync-cursor-plugin
delete-branch: true
title: 'chore: sync Cursor plugin from sources of truth'
body: |
## Summary
Automatically synced the Cursor plugin from sources of truth.
This PR was triggered by changes to one or more of:
- `plugins/svelte/skills/**` (skills)
- `plugins/svelte/agents/**` (agent definitions)
- `instructions/AGENTS.md` (agent instructions/rules)
## Changes
- Synced `svelte-cursor/skills/` with latest skill definitions
- Synced `svelte-cursor/agents/` with latest agent definitions (stripped Claude-specific fields)
- Synced `svelte-cursor/rules/` with latest instructions from AGENTS.md
## Generated by
GitHub Action: Sync Cursor Plugin
labels: |
chore
automated

116
.github/workflows/sync-docs-skills.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Sync Skills
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
actions: write
jobs:
sync-skills:
if: github.repository == 'sveltejs/ai-tools'
name: Sync skills from svelte.dev
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
- name: Install pnpm
shell: bash
run: |
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
echo installing pnpm version "$PNPM_VER"
npm i -g "pnpm@$PNPM_VER"
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Clone svelte.dev
run: git clone --depth 2 https://github.com/sveltejs/svelte.dev.git "${{ runner.temp }}/svelte.dev"
- name: Discover changed skill files
id: discover
env:
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
run: |
skill_files=$(git -C "$SVELTE_DEV_ROOT" diff --name-only --diff-filter=ACMR HEAD~1 HEAD | grep '^apps/svelte.dev/content/docs/.*\.md$' | xargs -I{} grep -l '^skill: *true' "$SVELTE_DEV_ROOT/{}" || true)
echo "skill_files=$skill_files" >> "$GITHUB_OUTPUT"
- name: Sync skills
if: steps.discover.outputs.skill_files != ''
env:
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
DOCS_PREFIX: apps/svelte.dev/content/docs/
run: |
for full_path in ${{ steps.discover.outputs.skill_files }}; do
file="${full_path#$SVELTE_DEV_ROOT/}"
name=$(grep '^name: ' "$full_path" | head -1 | sed 's/^name: *//')
repo="${file#$DOCS_PREFIX}"
repo="${repo#/}"
repo="${repo%%/*}"
output_dir="tools/skills/$name"
rm -rf "$output_dir"
mkdir -p "$output_dir"
pnpm resolve-references --file "$full_path" --repo "$repo" --output "$output_dir"
done
- name: Sync plugins
if: steps.discover.outputs.skill_files != ''
run: |
pnpm sync-claude-plugin
pnpm sync-cursor-plugin
pnpm sync-opencode-plugin
pnpm generate-skill-docs
pnpm bump-plugin-versions
- name: Check for changes
id: git-check
run: |
git diff --exit-code -- tools/skills/ plugins/ packages/opencode/ documentation/docs/ || echo "changed=true" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync skills from svelte.dev'
branch: chore/sync-skills
delete-branch: true
title: 'chore: sync skills from svelte.dev'
body: |
## Summary
Automatically synced skill markdown from `sveltejs/svelte.dev` into `tools/skills/`.
## Changes
- Cloned `sveltejs/svelte.dev`
- Filtered markdown files with `skill: true` frontmatter
- Rebuilt synced skill folders with `scripts/resolve-references.ts`
- Synced `plugins/claude/svelte/` (skills, agents)
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
- Synced `packages/opencode/` (skills, instructions)
- Updated documentation
## Generated by
GitHub Action: Sync Skills
labels: |
chore
automated

View File

@@ -1,25 +1,25 @@
name: Sync OpenCode Skills
name: Sync Plugins
on:
push:
branches:
- main
paths:
- 'plugins/svelte/skills/**'
- 'tools/**'
permissions:
contents: write
pull-requests: write
jobs:
sync-skills:
sync-plugins:
# prevents this action from running on forks
if: github.repository == 'sveltejs/ai-tools'
name: Sync Skills to OpenCode Package and Update Docs
name: Sync Plugins from tools/
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -46,39 +46,50 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Sync skills folder
run: pnpm sync-opencode-skills
- name: Sync plugins
run: pnpm sync-plugins
- name: Generate skills documentation
run: pnpm generate-skill-docs
- name: Generate subagent documentation
run: pnpm generate-subagent-docs
- name: Check for changes
id: git-check
run: |
git diff --exit-code packages/opencode/skills documentation/docs/60-skills/10-skills.md || echo "changed=true" >> $GITHUB_OUTPUT
git diff --exit-code \
plugins/claude/svelte/ \
plugins/cursor/svelte/ \
packages/opencode/skills/ \
packages/opencode/instructions/ \
packages/opencode/schema.json \
documentation/docs/ \
|| echo "changed=true" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.git-check.outputs.changed == 'true'
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: sync skills and update documentation'
branch: chore/sync-opencode-skills
commit-message: 'chore: sync plugins from tools/'
branch: chore/sync-plugins
delete-branch: true
title: 'chore: sync skills and update documentation'
title: 'chore: sync plugins from tools/'
body: |
## Summary
Automatically synced skills and updated documentation.
Automatically synced all plugins from the `tools/` source of truth.
This PR was triggered by changes to the skills folder in `plugins/svelte/skills/`.
This PR was triggered by changes to `tools/**`.
## Changes
- Synced `packages/opencode/skills/` with latest skill definitions
- Updated `documentation/docs/60-skills/10-skills.md` with latest skill documentation
- Synced `plugins/claude/svelte/` (skills, agents with `permissionMode`)
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
- Synced `packages/opencode/` (skills, instructions, schema)
- Updated documentation
## Generated by
GitHub Action: Sync OpenCode Skills
GitHub Action: Sync Plugins
labels: |
chore
documentation
automated

View File

@@ -13,12 +13,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.28.2
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
@@ -32,13 +30,9 @@ jobs:
- name: Build project
run: pnpm run build
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key
- name: Run tests
run: pnpm run test
env:
DATABASE_URL: file:test.db
VOYAGE_API_KEY: dummy-key
DATABASE_TOKEN: dummy-key

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- 'packages/opencode/config.ts'
- 'packages/opencode/config.js'
permissions:
contents: write
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
@@ -67,7 +67,7 @@ jobs:
## Summary
Automatically generated update for OpenCode JSON schema.
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.ts`.
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.js`.
## Changes
- Updated `packages/opencode/schema.json` with latest JSON schema

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0

1
.npmrc
View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -10,6 +10,12 @@
"options": {
"parser": "svelte"
}
},
{
"files": "**/references/*.md",
"options": {
"embeddedLanguageFormatting": "off"
}
}
]
}

View File

@@ -83,7 +83,6 @@ Located in `src/lib/server/analyze/`:
Required environment variables:
- `DATABASE_URL`: SQLite database path (default: `file:test.db`)
- `VOYAGE_API_KEY`: API key for embeddings support (optional)
When connected to the svelte-llm MCP server, you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 [Svelte Contributors](https://github.com/sveltejs/ai-tools/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1 @@
DATABASE_URL=file:test.db
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
VOYAGE_API_KEY=your_actual_api_key_here

View File

@@ -1,12 +0,0 @@
import { defineConfig } from 'drizzle-kit';
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
if (!process.env.DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
export default defineConfig({
schema: './src/lib/server/db/schema.ts',
dialect: 'turso',
dbCredentials: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_TOKEN },
verbose: true,
strict: true,
});

View File

@@ -23,10 +23,6 @@
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"test:watch": "npm run test:unit -- --watch",
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"inspect": "pnpm mcp-inspector"
},
"keywords": [
@@ -39,15 +35,12 @@
"devDependencies": {
"@eslint/compat": "catalog:lint",
"@eslint/js": "catalog:lint",
"@libsql/client": "catalog:orm",
"@modelcontextprotocol/inspector": "catalog:ai",
"@sveltejs/adapter-vercel": "catalog:svelte",
"@sveltejs/kit": "catalog:svelte",
"@sveltejs/vite-plugin-svelte": "catalog:svelte",
"@types/node": "catalog:tooling",
"@typescript-eslint/parser": "catalog:lint",
"drizzle-kit": "catalog:orm",
"drizzle-orm": "catalog:orm",
"eslint-config-prettier": "catalog:lint",
"eslint-plugin-svelte": "catalog:lint",
"globals": "catalog:lint",
@@ -62,7 +55,6 @@
"vitest": "catalog:tooling"
},
"dependencies": {
"@sveltejs/mcp-schema": "workspace:^",
"@sveltejs/mcp-server": "workspace:^",
"@tmcp/transport-http": "catalog:tmcp",
"@vercel/analytics": "catalog:tooling",

View File

@@ -1,6 +1,5 @@
import { dev } from '$app/environment';
import { http_transport } from '$lib/mcp/index.js';
import { db } from '$lib/server/db/index.js';
import { redirect } from '@sveltejs/kit';
import { track } from '@vercel/analytics/server';
@@ -17,7 +16,6 @@ export async function handle({ event, resolve }) {
}
}
const mcp_response = await http_transport.respond(event.request, {
db,
// only add analytics in production
track: dev
? undefined

View File

@@ -1,13 +0,0 @@
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema.js';
import { DATABASE_TOKEN, DATABASE_URL } from '$env/static/private';
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
if (!DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
const client = createClient({
url: DATABASE_URL,
authToken: DATABASE_TOKEN,
});
export const db = drizzle(client, { schema, logger: true });

View File

@@ -1,2 +0,0 @@
// we need to re-export from here to allow for the drizzle config to pick them up for migrations
export * from '@sveltejs/mcp-schema/schema';

View File

@@ -2,21 +2,11 @@
title: Overview
---
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your LLM or agent of choice write better Svelte code. It works by providing documentation relevant to the task at hand, and statically analysing generated code so that it can suggest fixes and best practices.
The following pages will help you set up and use the AI tools officially maintained by the Svelte team.
## Setup
There are four tools, designed to help your agent write correct, robust Svelte code. They are designed to work together, but each can be used individually:
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
- [local setup](local-setup) using `@sveltejs/mcp`
- [remote setup](remote-setup) using `https://mcp.svelte.dev/mcp`
## Usage
To get the most out of the MCP server we recommend including the following prompt in your [`AGENTS.md`](https://agents.md) (or [`CLAUDE.md`](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports), if using Claude Code. Or [`GEMINI.md`](https://geminicli.com/docs/cli/gemini-md/), if using GEMINI). This will tell the LLM which tools are available and when it's appropriate to use them.
> [!NOTE] This is already setup for you when using `npx sv add mcp`
@include .generated/agents.md
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.
- [Instructions](instructions): small prompt always injected into your session to make your agent more aware of the available tools
- [MCP Server](mcp): with tools, prompts and resources to give your agent more context, by pulling directly from the official Svelte documentation and using static analysis to correct common generative AI pitfalls
- [Skills](skills): lazy-loaded descriptions that teach your agent Svelte best practices, and how to use the [`@sveltejs/mcp` cli](cli)
- [Subagents](subagent): focused agents that can be invoked in parallel to execute atomic operations in a separate context window

View File

@@ -0,0 +1,13 @@
---
title: AGENTS.md
---
To get the most out of the [MCP server](mcp) and [skills](skills) we recommend including the following prompt in your [`AGENTS.md`](https://agents.md) (or [`CLAUDE.md`](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports) or [`GEMINI.md`](https://geminicli.com/docs/cli/gemini-md/), if using Claude Code or Gemini). This will tell your agent which tools are available and when it is appropriate to use them.
> [!NOTE] This is already setup for you when using `npx sv add mcp`
<!-- prettier-ignore-start -->
````markdown
@include .generated/agents.md
````
<!-- prettier-ignore-end -->

View File

@@ -0,0 +1,3 @@
---
title: Instructions
---

View File

@@ -1,3 +0,0 @@
---
title: Setup
---

View File

@@ -1,3 +0,0 @@
---
title: Capabilities
---

View File

@@ -5,10 +5,22 @@ This prompt should be used whenever you are asking the model to work on a Svelte
<details>
<summary>Copy the prompt</summary>
```md
<!-- prettier-ignore-start -->
````markdown
You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool `get-documentation` with one of the following paths. However: before invoking the `get-documentation` tool, try to answer the users query using your own knowledge and the `svelte-autofixer` tool. Be mindful of how many section you request, since it is token-intensive!
<available-docs>
- title: Overview, use_cases: use title and path to estimate use case, path: ai/overview
- title: Local setup, use_cases: use title and path to estimate use case, path: ai/local-setup
- title: Remote setup, use_cases: use title and path to estimate use case, path: ai/remote-setup
- title: Tools, use_cases: use title and path to estimate use case, path: ai/tools
- title: Resources, use_cases: use title and path to estimate use case, path: ai/resources
- title: Prompts, use_cases: use title and path to estimate use case, path: ai/prompts
- title: Overview, use_cases: use title and path to estimate use case, path: ai/plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/subagent
- title: Overview, use_cases: use title and path to estimate use case, path: ai/opencode-plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/opencode-subagent
- title: Overview, use_cases: use title and path to estimate use case, path: ai/skills
- title: Overview, use_cases: project setup, creating new svelte apps, scaffolding, cli tools, initializing projects, path: cli/overview
- title: Frequently asked questions, use_cases: project setup, initializing new svelte projects, troubleshooting cli installation, package manager configuration, path: cli/faq
- title: sv create, use_cases: project setup, starting new sveltekit app, initializing project, creating from playground, choosing project template, path: cli/sv-create
@@ -18,7 +30,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: devtools-json, use_cases: development setup, chrome devtools integration, browser-based editing, local development workflow, debugging setup, path: cli/devtools-json
- title: drizzle, use_cases: database setup, sql queries, orm integration, data modeling, postgresql, mysql, sqlite, server-side data access, database migrations, type-safe queries, path: cli/drizzle
- title: eslint, use_cases: code quality, linting, error detection, project setup, code standards, team collaboration, typescript projects, path: cli/eslint
- title: lucia, use_cases: authentication, login systems, user management, registration pages, session handling, auth setup, path: cli/lucia
- title: better-auth, use_cases: use title and path to estimate use case, path: cli/better-auth
- title: mcp, use_cases: use title and path to estimate use case, path: cli/mcp
- title: mdsvex, use_cases: blog, content sites, markdown rendering, documentation sites, technical writing, cms integration, article pages, path: cli/mdsvex
- title: paraglide, use_cases: internationalization, multi-language sites, i18n, translation, localization, language switching, global apps, multilingual content, path: cli/paraglide
@@ -29,6 +41,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: tailwindcss, use_cases: project setup, styling, css framework, rapid prototyping, utility-first css, design systems, responsive design, adding tailwind to svelte, path: cli/tailwind
- title: vitest, use_cases: testing, unit tests, component testing, test setup, quality assurance, ci/cd pipelines, test-driven development, path: cli/vitest
- title: add-on, use_cases: use title and path to estimate use case, path: cli/add-on
- title: sv-utils, use_cases: use title and path to estimate use case, path: cli/sv-utils
- title: Introduction, use_cases: learning sveltekit, project setup, understanding framework basics, choosing between svelte and sveltekit, getting started with full-stack apps, path: kit/introduction
- title: Creating a project, use_cases: project setup, starting new sveltekit app, initial development environment, first-time sveltekit users, scaffolding projects, path: kit/creating-a-project
- title: Project types, use_cases: deployment, project setup, choosing adapters, ssg, spa, ssr, serverless, mobile apps, desktop apps, pwa, offline apps, browser extensions, separate backend, docker containers, path: kit/project-types
@@ -96,17 +109,6 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: Configuration, use_cases: project setup, configuration, adapters, deployment, build settings, environment variables, routing customization, prerendering, csp security, csrf protection, path configuration, typescript setup, path: kit/configuration
- title: Command Line Interface, use_cases: project setup, typescript configuration, generated types, ./$types imports, initial project configuration, path: kit/cli
- title: Types, use_cases: typescript, type safety, route parameters, api endpoints, load functions, form actions, generated types, jsconfig setup, path: kit/types
- title: Overview, use_cases: use title and path to estimate use case, path: mcp/overview
- title: Local setup, use_cases: use title and path to estimate use case, path: mcp/local-setup
- title: Remote setup, use_cases: use title and path to estimate use case, path: mcp/remote-setup
- title: Tools, use_cases: use title and path to estimate use case, path: mcp/tools
- title: Resources, use_cases: use title and path to estimate use case, path: mcp/resources
- title: Prompts, use_cases: use title and path to estimate use case, path: mcp/prompts
- title: Overview, use_cases: use title and path to estimate use case, path: mcp/plugin
- title: Skill, use_cases: use title and path to estimate use case, path: mcp/skill
- title: Subagent, use_cases: use title and path to estimate use case, path: mcp/subagent
- title: Overview, use_cases: use title and path to estimate use case, path: mcp/opencode-plugin
- title: Subagent, use_cases: use title and path to estimate use case, path: mcp/opencode-subagent
- title: Overview, use_cases: always, any svelte project, getting started, learning svelte, introduction, project setup, understanding framework basics, path: svelte/overview
- title: Getting started, use_cases: project setup, starting new svelte project, initial installation, choosing between sveltekit and vite, editor configuration, path: svelte/getting-started
- title: .svelte files, use_cases: always, any svelte project, component creation, project setup, learning svelte basics, path: svelte/svelte-files
@@ -154,6 +156,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
- title: Lifecycle hooks, use_cases: component initialization, cleanup tasks, timers, subscriptions, dom measurements, chat windows, autoscroll features, migration from svelte 4, path: svelte/lifecycle-hooks
- title: Imperative component API, use_cases: project setup, client-side rendering, server-side rendering, ssr, hydration, testing, programmatic component creation, tooltips, dynamic mounting, path: svelte/imperative-component-api
- title: Hydratable data, use_cases: use title and path to estimate use case, path: svelte/hydratable
- title: Best practices, use_cases: use title and path to estimate use case, path: svelte/best-practices
- title: Testing, use_cases: testing, quality assurance, unit tests, integration tests, component tests, e2e tests, vitest setup, playwright setup, test automation, path: svelte/testing
- title: TypeScript, use_cases: typescript setup, type safety, component props typing, generic components, wrapper components, dom type augmentation, project configuration, path: svelte/typescript
- title: Custom elements, use_cases: web components, custom elements, component library, design system, framework-agnostic components, embedding svelte in non-svelte apps, shadow dom, path: svelte/custom-elements
@@ -204,6 +207,7 @@ This is the task you will work on:
</task>
If you are not writing the code into a file, once you have the final version of the code ask the user if it wants to generate a playground link to quickly check the code in it and if it answer yes call the `playground-link` tool and return the url to the user nicely formatted. The playground link MUST be generated only once you have the final version of the code and you are ready to share it, it MUST include an entry point file called `App.svelte` where the main component should live. If you have multiple files to include in the playground link you can include them all at the root.
```
````
<!-- prettier-ignore-end -->
</details>

View File

@@ -0,0 +1,16 @@
---
title: Overview
---
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your agent write better Svelte code. It works by providing relevant documentation, and statically analysing generated code so that it can suggest fixes and best practices.
## Setup
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
- [local setup](local-setup) using `@sveltejs/mcp`
- [remote setup](remote-setup) using `https://mcp.svelte.dev/mcp`
## Usage
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.

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:
@@ -112,6 +117,8 @@ opencode mcp add
## Cursor
You can automatically configure the MCP server using the [Cursor plugin](cursor-plugin) (recommended). If you prefer to configure the MCP server manually you can:
- Open the command palette
- Select "View: Open MCP Settings"
- Click on "Add custom MCP"

View File

@@ -16,7 +16,7 @@ claude mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
You can choose your preferred `scope` (it must be `user`, `project` or `local`) and `name`.
If you prefer you can also install the `svelte` plugin in [the Svelte Claude Code Marketplace](plugin) that will give you both the remote server and useful [skills](skills).
If you prefer you can also install the `svelte` plugin in [the Svelte Claude Code Marketplace](claude-plugin) that will give you both the remote server and useful [skills](skills).
## Claude Desktop
@@ -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:
@@ -102,6 +106,8 @@ opencode mcp add
## Cursor
You can automatically configure the MCP server using the [Cursor plugin](cursor-plugin) (recommended). If you prefer to configure the MCP server manually you can:
- Open the command palette
- Select "View: Open MCP Settings"
- Click on "Add custom MCP"

View File

@@ -0,0 +1,78 @@
---
title: CLI
---
The `@sveltejs/mcp` npm package normally launches the local `stdio` MCP server:
```bash
npx -y @sveltejs/mcp
```
If you invoke it with a subcommand, it behaves like a regular CLI and prints the result directly in your terminal instead. This is useful for agents, scripts and quick manual checks.
## Usage
```bash
npx -y @sveltejs/mcp <command> [options]
```
Available commands:
- `list-sections`
- `get-documentation <sections>`
- `svelte-autofixer <code_or_path>`
You can learn more about the commands with
```bash
npx -y @sveltejs/mcp --help
npx -y @sveltejs/mcp <command> --help
npx -y @sveltejs/mcp --version
```
## `list-sections`
Lists all available Svelte and SvelteKit documentation sections.
```bash
npx -y @sveltejs/mcp list-sections
```
The output is a structured text list of sections, including each section's title, `use_cases`, and documentation path. This is the same catalog the MCP tool uses before calling `get-documentation`.
## `get-documentation`
Fetches the full documentation for one or more sections.
```bash
npx -y @sveltejs/mcp get-documentation 'svelte/$state'
# or
npx -y @sveltejs/mcp get-documentation 'svelte/$state,svelte/await-expressions'
```
Each section can be matched by title or by documentation path. If a section cannot be found, the CLI returns an error plus similar matches when available.
## `svelte-autofixer`
Runs the Svelte autofixer against either inline code or a file path:
```bash
npx -y @sveltejs/mcp svelte-autofixer 'src/routes/+page.svelte'
```
If the argument is an existing path, the CLI reads the file automatically. Otherwise it treats the argument as raw Svelte code.
Because most shells expand `$`, inline code should be quoted or escaped correctly. In practice, passing a file path is usually easier than passing source directly.
Available options:
- `--svelte-version <4|5>` - choose which Svelte version to validate against (defaults to `5`)
- `--async` - enable async Svelte analysis for Svelte 5 projects
The command prints an object with:
- `issues`
- `suggestions`
- `require_another_tool_call_after_fixing`
This makes it easy to use in an agentic loop: run the autofixer, apply fixes, then run it again until it reports no remaining issues or suggestions.

View File

@@ -0,0 +1,3 @@
---
title: MCP server
---

View File

@@ -1,3 +0,0 @@
---
title: Claude Code Plugin
---

View File

@@ -1,11 +0,0 @@
---
title: Subagent
---
The Svelte plugin includes a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
## Benefits
The subagent has access to its own context window, allowing it to fetch the documentation, iterate with the `svelte-autofixer` tool and write to the file system without wasting context in the main agent.
The delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.

View File

@@ -217,7 +217,7 @@ The CSS in a component's `<style>` is scoped to that component. If a parent comp
</style>
```
If this impossible (for example, the child component comes from a library) you can use `:global` to override styles:
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>

View File

@@ -0,0 +1,11 @@
---
title: Overview
---
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 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.
@include .generated/skills.md

View File

@@ -1,3 +0,0 @@
---
title: OpenCode Plugin
---

View File

@@ -1,45 +0,0 @@
---
title: Overview
---
OpenCode has a [plugin system](https://opencode.ai/docs/plugins/) that allows developers to add MCP servers, agents and commands programmatically. Svelte has an OpenCode plugin published under `@sveltejs/opencode`.
## Installation
To install the plugin in OpenCode you can edit your [OpenCode config]() (either the global or the local one), adding `@sveltejs/opencode` to the list of plugins.
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@sveltejs/opencode"]
}
```
That's it! You now have the Svelte MCP server, [skills](skills), and the [file editor subagent](opencode-subagent) configured for you.
## Configuration
The default configuration for the Svelte OpenCode plugin looks like this...
```json
{
"$schema": "https://raw.githubusercontent.com/sveltejs/ai-tools/refs/heads/main/packages/opencode/schema.json",
"mcp": {
"type": "remote",
"enabled": true
},
"subagent": {
"enabled": true
},
"skills": {
"enabled": true
},
"instructions": {
"enabled": true
}
}
```
...but if you prefer, you can enable only the subagent, only the MCP, only the skills, or configure the kind of MCP server you want to use (`local` or `remote`).
You can place this file in `./.opencode/svelte.json` (in your project), in `~/.config/opencode/svelte.json` or, if you have an `OPENCODE_CONFIG_DIR` environment variable specified, at `$OPENCODE_CONFIG_DIR/svelte.json`.

View File

@@ -1,11 +0,0 @@
---
title: Subagent
---
The Svelte plugin includes a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
## Benefits
The subagent has access to its own context window, allowing it to fetch the documentation, iterate with the `svelte-autofixer` tool and write to the file system without wasting context in the main agent.
The delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.

View File

@@ -0,0 +1,67 @@
---
name: svelte-file-editor
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
---
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
## Available MCP Tools
### 1. list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.
### 2. get-documentation
Retrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.
**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`
### 3. svelte-autofixer
Analyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:
- Using `$effect` instead of `$derived` for computations
- Missing cleanup in effects
- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)
- Missing keys in `{#each}` blocks
- And more
## Workflow
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call `list-sections` to see available documentation
2. Call `get-documentation` with relevant section names
### 2. Read the Target File
Read the file to understand the current implementation.
### 3. Make Changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
### 5. Fix Any Issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
After completing your work, provide:
1. Summary of changes made
2. Any issues found and fixed by the autofixer
3. Recommendations for further improvements (if any)

View File

@@ -0,0 +1,20 @@
---
title: Overview
---
Since creating, editing or analyzing a Svelte file is an atomic operation we recommend creating a subagent that your main agent can invoke whenever it needs to interact with a Svelte component. Subagents use a separate context window, allowing them to fetch documentation, iterate with [`svelte-autofixer`](tools#svelte-autofixer) and write to the filesystem without wasting context in the main agent.
Delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.
You can write your own or take inspiration from the one available in the [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools/tree/main/tools/agents) repository: a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
<details>
<summary>View subagent definition</summary>
<!-- prettier-ignore-start -->
````markdown
@include .generated/subagent.md
````
<!-- prettier-ignore-end -->
</details>

View File

@@ -0,0 +1,3 @@
---
title: Subagents
---

View File

@@ -1,12 +1,12 @@
---
title: Overview
title: Claude Code
---
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Claude Code Marketplace plugin.
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Claude Code [plugin marketplace](https://code.claude.com/docs/en/discover-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 as it helps save context by handling Svelte-specific tasks more efficiently.
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

View File

@@ -0,0 +1,73 @@
---
title: OpenCode
---
OpenCode has a [plugin system](https://opencode.ai/docs/plugins/) that allows developers to add MCP servers, agents and commands programmatically. Svelte has an OpenCode plugin published under `@sveltejs/opencode`.
## Installation
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
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@sveltejs/opencode"]
}
```
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. 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`)
```json
{
"$schema": "https://svelte.dev/opencode/schema.json",
"mcp": {
"type": "remote", // or "local" — defaults to remote
"enabled": true
},
"subagent": {
"enabled": true,
"agents": {
"svelte-file-editor": {
"model": "<other-model>", // defaults to the same as main agent
"temperature": 1, // defaults to unset
"top_p": 0.7, // defaults to unset
"maxSteps": 20 // defaults to unlimited
}
}
},
"skills": {
// this can be `true`, or an array of skills to enable
// e.g. ["svelte-core-bestpractices"]
"enabled": true
},
"instructions": {
"enabled": true
}
}
```

View File

@@ -0,0 +1,25 @@
---
title: Cursor
---
Cursor has a [plugin system](https://cursor.com/docs/plugins) that can bundle rules, skills, agents, commands, MCP servers, and hooks.
The Svelte plugin gives you the remote Svelte MCP server, Cursor [skills](skills), an always-on rule that tells the model how to use the Svelte MCP tools correctly, and the `svelte-file-editor` subagent for working on `.svelte` files and `.svelte.ts`/`.svelte.js` modules. The source is available in the [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools/tree/main/plugins/cursor/svelte) repo.
## Installation
Install the plugin from the [Cursor Marketplace](https://cursor.com/marketplace/svelte) with the following command:
```
/add-plugin svelte
```
Plugins can be installed either for the current project or at user level.
Once installed, Cursor will discover the plugin components automatically:
- the Svelte MCP server is added from the plugin's `.mcp.json`
- rules and skills appear in Cursor's rules UI
- the `svelte-file-editor` agent becomes available in chat
> [!NOTE] The Cursor CLI does not support plugins yet. Plugin support in [Cloud Agents](https://cursor.com/docs/cloud-agent) is limited to MCP servers.

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

@@ -0,0 +1,3 @@
---
title: Plugins
---

View File

@@ -1,11 +0,0 @@
---
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.
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.
You can download the latest skills from the [releases page](https://github.com/sveltejs/ai-tools/releases) or find them in the [`plugins/svelte/skills`](https://github.com/sveltejs/ai-tools/tree/main/plugins/svelte/skills) folder.
@include .generated/skills.md

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "The official Svelte MCP server implementation",
"type": "module",
"packageManager": "pnpm@10.28.2",
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
@@ -22,12 +22,18 @@
"generate-summaries": "pnpm --filter @sveltejs/mcp-server run generate-summaries",
"generate-prompt-docs": "node --import node-resolve-ts/register scripts/update-docs-prompts.ts",
"generate-skill-docs": "node --import node-resolve-ts/register scripts/update-docs-skills.ts",
"generate-subagent-docs": "cp ./tools/agents/svelte-file-editor.md ./documentation/docs/50-subagents/.generated/subagent.md",
"debug:generate-summaries": "pnpm --filter @sveltejs/mcp-server run debug:generate-summaries",
"release": "pnpm --filter @sveltejs/mcp run build && changeset publish",
"changeset:version": "changeset version && pnpm --filter @sveltejs/mcp run update:version && git add --all",
"sync-opencode-skills": "rm -rf packages/opencode/skills && cp -r plugins/svelte/skills packages/opencode/skills",
"sync-agents-md": "rm -f packages/opencode/instructions/opencode-agents.md && rm -f documentation/docs/10-introduction/.generated/agents.md && mkdir -p packages/opencode/instructions && mkdir -p documentation/docs/10-introduction/.generated && cp instructions/AGENTS.md packages/opencode/instructions/opencode-agents.md && cp instructions/AGENTS.md documentation/docs/10-introduction/.generated/agents.md",
"sync-cursor-plugin": "node scripts/sync-cursor-plugin.ts"
"sync-plugins": "pnpm sync-claude-plugin && pnpm sync-cursor-plugin && pnpm sync-opencode-plugin && pnpm bump-plugin-versions",
"sync-claude-plugin": "node scripts/sync-claude-plugin.ts",
"sync-cursor-plugin": "node scripts/sync-cursor-plugin.ts",
"sync-opencode-plugin": "node scripts/sync-opencode-plugin.ts && pnpm generate-opencode-jsonschema",
"bump-plugin-versions": "node scripts/bump-plugin-versions.ts",
"postbump-plugin-versions": "pnpm format",
"resolve-references": "node scripts/resolve-references.ts",
"postresolve-references": "pnpm format"
},
"keywords": [
"svelte",
@@ -37,12 +43,12 @@
],
"private": true,
"devDependencies": {
"@changesets/changelog-github": "catalog:tooling",
"@changesets/cli": "catalog:tooling",
"@eslint/compat": "catalog:lint",
"@eslint/js": "catalog:lint",
"@modelcontextprotocol/inspector": "catalog:ai",
"@sveltejs/adapter-vercel": "catalog:svelte",
"@svitejs/changesets-changelog-github-compact": "catalog:tooling",
"eslint": "catalog:lint",
"eslint-config-prettier": "catalog:lint",
"eslint-plugin-import": "catalog:lint",

View File

@@ -1,19 +0,0 @@
{
"name": "@sveltejs/mcp-schema",
"version": "0.0.1",
"private": true,
"description": "",
"main": "index.js",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js",
"./schema": "./src/schema.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"drizzle-orm": "catalog:orm"
}
}

View File

@@ -1,8 +0,0 @@
/**
* @import * as schema from './schema.js'
*/
export * from './schema.js';
/**
* @typedef {typeof schema} Schema
*/

View File

@@ -1,82 +0,0 @@
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { float_32_array } from './utils.js';
/**
* NOTE: if you modify a schema adding a vector column you need to manually add this
*
* CREATE INDEX IF NOT EXISTS name_of_the_index
* ON `name_of_the_table` (
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
* )
*
* to the generated migration file
*/
export const distillations = sqliteTable('distillations', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
version: text('version').notNull(),
content: text('content').notNull(),
size_kb: integer('size_kb').notNull(),
document_count: integer('document_count').notNull(),
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const distillation_jobs = sqliteTable('distillation_jobs', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
batch_id: text('batch_id'),
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
model_used: text('model_used').notNull(),
total_files: integer('total_files').notNull(),
processed_files: integer('processed_files').notNull().default(0),
successful_files: integer('successful_files').notNull().default(0),
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
total_input_tokens: integer('total_input_tokens').notNull().default(0),
total_output_tokens: integer('total_output_tokens').notNull().default(0),
started_at: integer('started_at', { mode: 'timestamp' }),
completed_at: integer('completed_at', { mode: 'timestamp' }),
error_message: text('error_message'),
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content = sqliteTable('content', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content_distilled = sqliteTable('content_distilled', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});

View File

@@ -1,65 +0,0 @@
/**
* @import { Column } from 'drizzle-orm';
*/
import { sql } from 'drizzle-orm';
import { customType } from 'drizzle-orm/sqlite-core';
/**
* Helper function to convert an array of embeddings into a format that can be inserted into a LibSQL vector column.
* @param {number[]} arr The embeddings array.
*/
export function vector(arr) {
return sql`vector32(${JSON.stringify(arr)})`;
}
/**
* Helper function to calculate the distance between a vector column and an array of embeddings and return it as a columns.
* @param {Column} column The drizzle column representing the vector.
* @param {number} arr The embeddings array.
* @param {string} as The name of the returned column. Default is 'distance'.
*
* @example
* await db.select({
* id: vector_table.id,
* text: vector_table.text,
* distance: distance(vector_table.vector, await get_embeddings(sentence)),
* })
* .from(vector_table)
* .orderBy(sql`distance`)
* .execute();
*/
export function distance(column, arr, as = 'distance') {
return /** @type {typeof sql<number>} */ (
sql
)`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
as,
);
}
/**
* Custom drizzle type to use the LibSQL vector column type.
*/
export const float_32_array = /** @type {typeof customType<{
data: number[];
config: { dimensions: number };
configRequired: true;
driverData: Buffer;
}>} */ (customType)({
dataType(config) {
return `F32_BLOB(${config.dimensions})`;
},
/**
* @param {Buffer} value
*/
fromDriver(value) {
return Array.from(new Float32Array(value.buffer));
},
/**
*
* @param {number[]} value
* @returns
*/
toDriver(value) {
return vector(value);
},
});

View File

@@ -17,12 +17,8 @@
".": "./src/index.ts",
"./handlers": "./src/mcp/handlers/tools/handlers.ts"
},
"peerDependencies": {
"drizzle-orm": "^0.45.0"
},
"dependencies": {
"@mcp-ui/server": "catalog:ai",
"@sveltejs/mcp-schema": "workspace:^",
"@tmcp/adapter-valibot": "catalog:tmcp",
"@tmcp/transport-in-memory": "catalog:tmcp",
"@typescript-eslint/parser": "catalog:lint",
@@ -40,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

@@ -344,87 +344,111 @@ describe('add_autofixers_issues', () => {
});
describe('imported_runes', () => {
describe.each([{ source: 'svelte' }, { source: 'svelte/runes' }])(
'from "$source"',
({ source }) => {
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
const content = run_autofixers_on_code(`
describe.each([
{ source: 'svelte' },
{ source: 'svelte/runes' },
{ source: '@sveltejs/runes' },
{ source: '@sveltejs/vite-plugin-svelte' },
])('from "$source"', ({ source }) => {
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import * as ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
});
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount, state, effect } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
);
expect(content.suggestions).toContain(
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
);
});
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
);
expect(content.suggestions).toContain(
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
);
});
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
const content = run_autofixers_on_code(`
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount } from '${source}';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
);
});
},
);
expect(content.suggestions).not.toContain(
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
);
});
});
describe.each(dollarless_runes)('importing $rune from external lib', ({ rune }) => {
it(`should not add suggestions when importing from packages that are not svelte`, () => {
it(`should not add suggestions when importing from packages whose name doesn't contain svelte`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from 'something-something';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "${rune}" from "something-something". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions with a different hint when importing from packages whose name contains svelte but it's not official`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from 'svelte-something-something';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "${rune}" from "svelte-something-something". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
expect(content.suggestions).toContain(
`You are importing "${rune}" from "svelte-something-something". If you are trying to import runes to use them this is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly. If you are importing the function from a separate library ignore this suggestion.`,
);
});
});
it('should not add the imported_runes suggestion when importing derived from svelte/store', () => {
const content = run_autofixers_on_code(`
<script>
import { derived } from 'svelte/store';
</script>`);
expect(content.suggestions).not.toContain(
'You are importing "derived" from "svelte/store". This is not necessary, all runes are globally available. Please remove this import and use "$derived" directly.',
);
});
});
describe('derived_with_function', () => {
@@ -751,5 +775,16 @@ describe('add_autofixers_issues', () => {
`You are reading the stateful variable "$x" with a "$" prefix. Stateful variables are not stores and should be read without the "$". Please read it as a normal variable "x"`,
);
});
// https://github.com/sveltejs/ai-tools/issues/200
it('should not crash when reading $-prefixed identifier for variable initialized with member expression', () => {
expect(() =>
run_autofixers_on_code(`<div>{$x}</div>
<script>
const x = foo.bar;
</script>`),
).not.toThrow();
});
});
});

View File

@@ -3,10 +3,20 @@ import type { Autofixer } from './index.js';
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
function should_suggest_for_source(source: string, rune: string) {
if (!source.includes('svelte')) {
return false;
}
if (source === 'svelte/store' && rune === 'derived') {
return false;
}
return true;
}
export const imported_runes: Autofixer = {
ImportDeclaration(node, { state, next }) {
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
if (source && (source === 'svelte' || source.startsWith('svelte/'))) {
if (source) {
for (const specifier of node.specifiers) {
const id =
specifier.type === 'ImportDefaultSpecifier'
@@ -16,10 +26,25 @@ export const imported_runes: Autofixer = {
: specifier.type === 'ImportSpecifier'
? specifier.imported
: null;
if (id && id.type === 'Identifier' && dollarless_runes.includes(id.name)) {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
);
if (
id &&
id.type === 'Identifier' &&
dollarless_runes.includes(id.name) &&
should_suggest_for_source(source, id.name)
) {
if (
source === 'svelte' ||
source.startsWith('svelte/') ||
source.startsWith('@sveltejs')
) {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
);
} else {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". If you are trying to import runes to use them this is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly. If you are importing the function from a separate library ignore this suggestion.`,
);
}
}
}
}

View File

@@ -91,16 +91,56 @@ export async function get_documentation_handler({
}
});
const has_any_success = results.some((result) => result.success);
let final_text = results.map((r) => r.content).join('\n\n---\n\n');
const successes = results.filter((r) => r.success);
const failed_sections = sections.filter(
(s) =>
!available_sections.some(
(a) => a.title.toLowerCase() === s.toLowerCase() || a.slug === s || a.url === s,
),
);
if (!has_any_success) {
const formatted_sections = await format_sections_list();
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
if (successes.length > 0 && failed_sections.length === 0) {
return successes.map((r) => r.content).join('\n\n---\n\n');
}
return final_text;
const parts: string[] = [];
if (successes.length > 0) {
parts.push(successes.map((r) => r.content).join('\n\n---\n\n'));
}
const fuzzy_results = failed_sections.map((requested) => {
const lower = requested.toLowerCase();
const matches = available_sections.filter(
(a) =>
a.title.toLowerCase().includes(lower) ||
a.slug.includes(lower) ||
lower.includes(a.slug.split('/').pop() ?? '') ||
a.use_cases.toLowerCase().includes(lower),
);
return { requested, matches };
});
const has_fuzzy = fuzzy_results.some((r) => r.matches.length > 0);
// Full list only when no successes and no fuzzy matches
if (successes.length === 0 && !has_fuzzy) {
const formatted_sections = await format_sections_list();
parts.push(`${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`);
}
// Similar results then errors
for (const { requested, matches } of fuzzy_results) {
if (matches.length > 0) {
const match_list = matches.map((m) => `- title: ${m.title}, section: ${m.slug}`).join('\n');
parts.push(
`${matches.length} similar result${matches.length > 1 ? 's' : ''} for "${requested}":\n${match_list}`,
);
}
parts.push(`Section not found: "${requested}".`);
}
return parts.join('\n\n---\n\n');
}
export function get_documentation(server: SvelteMcp) {

View File

@@ -1,4 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { writeFileSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { InMemoryTransport } from '@tmcp/transport-in-memory';
import { beforeEach, describe, expect, it } from 'vitest';
import { server } from '../../index.js';
@@ -12,13 +15,18 @@ async function autofixer_tool_call(
is_error = false,
desired_svelte_version = 5,
async = false,
ctx?: { stdio?: boolean },
) {
const result = await session.callTool('svelte-autofixer', {
code,
desired_svelte_version,
filename: 'App.svelte',
async,
});
const result = await session.callTool(
'svelte-autofixer',
{
code,
desired_svelte_version,
filename: 'App.svelte',
async,
},
ctx,
);
expect(result).toBeDefined();
if (is_error) {
@@ -146,4 +154,40 @@ describe('svelte-autofixer tool', () => {
'The desired_svelte_version MUST be either 4 or 5 but received "3"',
);
});
it('should read file content from path when stdio context is true', async () => {
const tmp_file = join(tmpdir(), `svelte-autofixer-test-${Date.now()}.svelte`);
const file_content = `<script>
$state count = 0;
</script>`;
writeFileSync(tmp_file, file_content, 'utf-8');
try {
// with stdio: true, the file is read from disk and parsed, producing issues
const content = await autofixer_tool_call(tmp_file, false, 5, false, { stdio: true });
expect(content.issues.length).toBeGreaterThan(0);
expect(content.suggestions.length).toBeGreaterThan(0);
} finally {
unlinkSync(tmp_file);
}
});
it('should treat file path as code when stdio context is not set', async () => {
const tmp_file = join(tmpdir(), `svelte-autofixer-test-${Date.now()}.svelte`);
const file_content = `<script>
$state count = 0;
</script>`;
writeFileSync(tmp_file, file_content, 'utf-8');
try {
// without stdio context, the path string is treated as raw code (plain text), no issues
const content = await autofixer_tool_call(tmp_file, false, 5, false);
expect(content.issues).toHaveLength(0);
expect(content.suggestions).toHaveLength(0);
} finally {
unlinkSync(tmp_file);
}
});
});

View File

@@ -1,33 +1,47 @@
import { basename } from 'node:path';
import type { SvelteMcp } from '../../index.js';
import { tool } from 'tmcp/utils';
import * as v from 'valibot';
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
import { add_compile_issues } from '../../autofixers/add-compile-issues.js';
import { add_eslint_issues } from '../../autofixers/add-eslint-issues.js';
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
import { icons } from '../../icons/index.js';
import { tool } from 'tmcp/utils';
import { type SvelteMcp } from '../../index.js';
const autofixer_schema = v.object({
code: v.string(),
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.',
let cached_schema: ReturnType<typeof get_autofixer_schema> | null = null;
function get_autofixer_schema(stdio: boolean) {
let code = v.string();
if (stdio) {
// we only add the description if we are running in stdio, this saves a few tokens for the remote server
code = v.pipe(
v.string(),
v.description(
"The code to be processed by the autofixer. It can also be a path to a file containing the code. If the file doesn't exists the string will be treated as the code",
),
);
}
return v.object({
code,
desired_svelte_version: v.pipe(
v.union([v.string(), v.number()]),
v.description(
'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(
v.optional(v.boolean()),
v.description(
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
async: v.pipe(
v.optional(v.boolean()),
v.description(
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
),
),
),
filename: v.pipe(
v.optional(v.string()),
v.description(
'The filename of the component if available, it MUST be only the Component name with .svelte or .svelte.ts extension and not the entire path.',
filename: v.pipe(
v.optional(v.string()),
v.description(
'The filename of the component if available, it MUST be only the Component name with .svelte or .svelte.ts extension and not the entire path.',
),
),
),
});
});
}
const autofixer_output_schema = v.object({
issues: v.array(v.string()),
@@ -40,7 +54,7 @@ export async function svelte_autofixer_handler({
desired_svelte_version: desired_svelte_version_unchecked,
async,
filename: filename_or_path,
}: v.InferInput<typeof autofixer_schema>) {
}: v.InferInput<ReturnType<typeof get_autofixer_schema>>) {
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
const parsed_version = v.safeParse(
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
@@ -110,7 +124,11 @@ export function svelte_autofixer(server: SvelteMcp) {
title: 'Svelte Autofixer',
description:
'Given a svelte component or module returns a list of suggestions to fix any issues it has. This tool MUST be used whenever the user is asking to write svelte code before sending the code back to the user',
schema: autofixer_schema,
get schema() {
return (
cached_schema ?? (cached_schema = get_autofixer_schema(server.ctx.custom?.stdio ?? false))
);
},
outputSchema: autofixer_output_schema,
annotations: {
title: 'Svelte Autofixer',
@@ -129,6 +147,18 @@ export function svelte_autofixer(server: SvelteMcp) {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer');
}
// we only do this if we know we are running in stdio mode (only stdio pass the context as true)
if (server.ctx.custom?.stdio) {
const [exists_sync, read_file] = await Promise.all([
import('node:fs').then((mod) => mod.existsSync),
import('node:fs/promises').then((mod) => mod.readFile),
]);
if (exists_sync(code)) {
code = await read_file(code, 'utf-8');
}
}
try {
const content = await svelte_autofixer_handler({
code,

View File

@@ -1,8 +1,6 @@
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
import { McpServer } from 'tmcp';
import { setup_prompts, setup_resources, setup_tools } from './handlers/index.js';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import type { Schema } from '@sveltejs/mcp-schema';
import { icons } from './icons/index.js';
export const server = new McpServer(
@@ -25,8 +23,8 @@ export const server = new McpServer(
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.',
},
).withContext<{
db: LibSQLDatabase<Schema>;
track?: (sessionId: string, event: string, extra?: string) => Promise<void>;
stdio?: boolean;
}>();
export type SvelteMcp = typeof server;

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;
}
@@ -69,6 +63,8 @@ export function parse(code: string, file_path: string) {
return this.all_references.find((r) => r.identifier === id);
},
is_rune(call: CallExpression, rune?: (typeof runes)[number][]) {
// it should always be a call expression but we check just in case because sometimes it's any and TS doesn't complain
if (call.type !== 'CallExpression') return false;
if (call.callee.type !== 'Identifier' && call.callee.type !== 'MemberExpression')
return false;
const id = call.callee.type === 'Identifier' ? call.callee : call.callee.object;

View File

@@ -1,5 +1,39 @@
# @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
- feat: allow stdio mcp to read the content of the file directly ([#198](https://github.com/sveltejs/ai-tools/pull/198))
- fix: handle non call expressions passed to `is_rune` ([#201](https://github.com/sveltejs/ai-tools/pull/201))
- chore: remove db requirement ([#196](https://github.com/sveltejs/ai-tools/pull/196))
## 0.1.22
### Patch Changes
- fix: broaden checks for imported runes because LLMs are unhinged ([#185](https://github.com/sveltejs/ai-tools/pull/185))
## 0.1.21
### Patch Changes
- feat: display similar result & error at the end ([#161](https://github.com/sveltejs/ai-tools/pull/161))
## 0.1.20
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@sveltejs/mcp",
"version": "0.1.20",
"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.20",
"version": "0.1.25",
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
"icons": [
{
@@ -25,7 +25,7 @@
{
"registryType": "npm",
"identifier": "@sveltejs/mcp",
"version": "0.1.20",
"version": "0.1.25",
"runtimeHint": "npx",
"transport": {
"type": "stdio"

View File

@@ -14,7 +14,9 @@ const cli = sade('svelte-mcp');
cli.command('__mcp', '', { default: true }).action(() => {
const transport = new StdioTransport(server);
transport.listen();
transport.listen({
stdio: true,
});
});
cli

View File

@@ -1,5 +1,53 @@
# @sveltejs/opencode
## 0.1.11
### Patch Changes
- fix: ship plugin as js files instead of ts ([#236](https://github.com/sveltejs/ai-tools/pull/236))
## 0.1.10
### Patch Changes
- feat: add tui opencode plugin ([#231](https://github.com/sveltejs/ai-tools/pull/231))
## 0.1.9
### Patch Changes
- fix: remove restrictive permissions from subagent ([#219](https://github.com/sveltejs/ai-tools/pull/219))
## 0.1.8
### Patch Changes
- fix: add `server` export to opencode plugin ([`96c50ac`](https://github.com/sveltejs/ai-tools/commit/96c50acae2b4131a6c72d3579a73c44ab9158b18))
## 0.1.7
### Patch Changes
- fix: import `ts` files directly ([#190](https://github.com/sveltejs/ai-tools/pull/190))
## 0.1.6
### Patch Changes
- chore: sync skills from svelte.dev ([#178](https://github.com/sveltejs/ai-tools/pull/178))
- fix: update svelte-file-editor agent to use proper name ([#183](https://github.com/sveltejs/ai-tools/pull/183))
## 0.1.5
### Patch Changes
- fix: merge user-configured svelte-file-editor agent settings ([#176](https://github.com/sveltejs/ai-tools/pull/176))
- feat(opencode): mcp enabled option is passed to opencode ([#171](https://github.com/sveltejs/ai-tools/pull/171))
- feat: allow enabling a specific skill in opencode plugin ([#174](https://github.com/sveltejs/ai-tools/pull/174))
## 0.1.4
### 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
@@ -36,39 +47,76 @@ The plugin injects instructions that teach the agent how to effectively use the
## Configuration
The default configuration:
Create `svelte.json` to customize how the plugin configures MCP, the Svelte subagent, instructions, and skills.
```json
{
"$schema": "https://raw.githubusercontent.com/sveltejs/ai-tools/refs/heads/main/packages/opencode/schema.json",
"$schema": "https://svelte.dev/opencode/schema.json",
"mcp": {
"type": "remote",
"enabled": true
},
"subagent": {
"enabled": true
"enabled": true,
"agents": {
"svelte-file-editor": {
"model": "anthropic/claude-sonnet-4-20250514",
"temperature": 0.7,
"top_p": 0.9,
"maxSteps": 20
}
}
},
"instructions": {
"enabled": true
},
"skills": {
"enabled": ["svelte-code-writer", "svelte-core-bestpractices"]
}
}
```
### Defaults
If omitted, the plugin uses these defaults:
- `mcp.type`: `"remote"`
- `mcp.enabled`: `true`
- `subagent.enabled`: `true`
- `subagent.agents`: `{}`
- `instructions.enabled`: `true`
- `skills.enabled`: `true`
### Configuration Options
| Option | Type | Default | Description |
| ---------------------- | ----------------------- | ---------- | -------------------------------------------------------------------------------- |
| `mcp.type` | `"remote"` \| `"local"` | `"remote"` | Use the remote server at `mcp.svelte.dev` or run locally via `npx @sveltejs/mcp` |
| `mcp.enabled` | `boolean` | `true` | Enable/disable the MCP server |
| `subagent.enabled` | `boolean` | `true` | Enable/disable the Svelte file editor subagent |
| `instructions.enabled` | `boolean` | `true` | Enable/disable agent instructions injection |
| Option | Type | Default | Description |
| ------------------------------------------------ | --------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `mcp.type` | `"remote" \| "local"` | `"remote"` | Use `https://mcp.svelte.dev/mcp` (`remote`) or run `@sveltejs/mcp` via `npx` (`local`). |
| `mcp.enabled` | `boolean` | `true` | Enable or disable the Svelte MCP server entry. |
| `subagent.enabled` | `boolean` | `true` | Enable or disable registration of the `svelte-file-editor` subagent. |
| `subagent.agents.svelte-file-editor.model` | `string` | main agent | Override the model used by the Svelte file editor subagent. |
| `subagent.agents.svelte-file-editor.temperature` | `number` | unset | Set temperature for the subagent. |
| `subagent.agents.svelte-file-editor.top_p` | `number` | unset | Set top-p sampling for the subagent. |
| `subagent.agents.svelte-file-editor.maxSteps` | `number` | unlimited | Limit the number of steps the subagent can execute. |
| `instructions.enabled` | `boolean` | `true` | Enable or disable automatic instruction-file injection. |
| `skills.enabled` | `boolean \| string[]` | `true` | Enable all skills (`true`), disable all skills (`false`), or enable only specific skill names. |
### Config File Location
### Supported Skill Names
Place your configuration at one of these locations:
When using `skills.enabled` as an array, these built-in names are currently available:
- `~/.config/opencode/svelte.json` (global)
- `$OPENCODE_CONFIG_DIR/svelte.json` (if `OPENCODE_CONFIG_DIR` is set, takes priority)
- `svelte-code-writer`
- `svelte-core-bestpractices`
### Config File Locations and Precedence
The plugin reads from these files (lowest priority first, highest priority last):
- `~/.config/opencode/svelte.json`
- `$OPENCODE_CONFIG_DIR/svelte.json` (when `OPENCODE_CONFIG_DIR` is set)
- `.opencode/svelte.json` in the current project
If the same key is defined in multiple files, the later location overrides earlier ones.
## License

View File

@@ -0,0 +1,11 @@
// This file is auto-generated by scripts/sync-opencode-plugin.ts
// Do not edit manually — edit the markdown files in tools/agents/ instead.
export const agents = {
'svelte-file-editor': {
description:
'Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
prompt:
"You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.\n\nIf the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP Tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather Context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the Target File\n\nRead the file to understand the current implementation.\n\n### 3. Make Changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate Changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix Any Issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output Format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
},
};

241
packages/opencode/config.js Normal file
View File

@@ -0,0 +1,241 @@
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import * as v from 'valibot';
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
// Schema for individual agent configuration
const agent_config_schema = v.object({
model: v.pipe(
v.optional(v.string()),
v.description('Model identifier for the agent (e.g., "anthropic/claude-sonnet-4-20250514")'),
),
temperature: v.pipe(
v.optional(v.number()),
v.description('Temperature setting for the agent (e.g., 0.7)'),
),
top_p: v.pipe(
v.optional(v.number()),
v.description(
'Control response diversity with the top_p option. Alternative to temperature for controlling randomness.',
),
),
maxSteps: v.pipe(
v.optional(v.number()),
v.description('Maximum number of steps the agent can take (e.g., 10)'),
),
});
const default_config = {
mcp: {
type: /** @type {'remote' | 'local'} */ ('remote'),
enabled: true,
},
subagent: {
enabled: true,
agents: /** @type {Record<string, v.InferInput<typeof agent_config_schema>>} */ ({}),
},
instructions: {
enabled: true,
},
skills: {
enabled: /** @type {boolean | string[]} */ (true),
},
};
export const config_schema = v.object({
mcp: v.pipe(
v.optional(
v.object({
type: v.optional(v.picklist(['remote', 'local'])),
enabled: v.optional(v.boolean()),
}),
),
v.description(
"Configuration for the MCP. You can chose if it should be enabled or not and the transport to use 'remote' (default) and 'local'.",
),
),
subagent: v.pipe(
v.optional(
v.object({
enabled: v.optional(v.boolean()),
agents: v.optional(v.record(v.string(), agent_config_schema)),
}),
),
v.description('Configuration for the subagent. You can choose if it should be enabled or not.'),
),
instructions: v.pipe(
v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
v.description(
'Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not.',
),
),
skills: v.pipe(
v.optional(
v.object({
enabled: v.pipe(
v.optional(v.union([v.boolean(), v.array(v.string())])),
v.description(
'It can be either a boolean or an array containing the skills that you want to enable',
),
),
}),
),
v.description(
'Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills.',
),
),
});
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'svelte.json');
/** @typedef {{ data: Record<string, unknown> | null, parse_error?: string }} ConfigLoadResult */
function get_config_paths() {
// Global: ~/.config/opencode/svelte.json
/** @type {string | null} */
let global_path = null;
if (existsSync(GLOBAL_CONFIG_PATH)) {
global_path = GLOBAL_CONFIG_PATH;
}
// Custom config directory: $OPENCODE_CONFIG_DIR/svelte.json
/** @type {string | null} */
let config_dir_path = null;
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
if (opencode_config_dir) {
const config_json = join(opencode_config_dir, 'svelte.json');
if (existsSync(config_json)) {
config_dir_path = config_json;
}
}
// Project-local: ./.opencode/svelte.json (cwd)
/** @type {string | null} */
let project_path = null;
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
if (existsSync(project_config)) {
project_path = project_config;
}
// Lowest priority first, highest priority last (project overrides global)
return [global_path, config_dir_path, project_path];
}
/**
* @param {string} config_path
* @returns {ConfigLoadResult}
*/
function load_config_file(config_path) {
/** @type {string} */
let file_content;
try {
file_content = readFileSync(config_path, 'utf-8');
} catch {
// File doesn't exist or can't be read
return { data: null };
}
try {
const parsed = JSON.parse(file_content);
if (parsed === undefined || parsed === null) {
return { data: null, parse_error: 'Config file is empty or invalid' };
}
return { data: parsed };
} catch (error) {
return {
data: null,
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
};
}
}
/**
* @param {Partial<McpConfig>} user_config
* @returns {McpConfig}
*/
function merge_with_defaults(user_config) {
return {
mcp: {
...default_config.mcp,
...user_config.mcp,
},
subagent: {
enabled: default_config.subagent.enabled,
...user_config.subagent,
agents: {
...default_config.subagent.agents,
...user_config.subagent?.agents,
},
},
instructions: {
...default_config.instructions,
...user_config.instructions,
},
skills: {
...default_config.skills,
...user_config.skills,
},
};
}
/** @param {PluginInput} ctx */
export function get_mcp_config(ctx) {
const config_paths = get_config_paths();
/** @type {Partial<McpConfig>} */
let merged = {};
// Iterate from lowest to highest priority, merging as we go
for (const path of config_paths) {
if (path && existsSync(path)) {
const result = load_config_file(path);
if (result.parse_error) {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `${result.parse_error} (${path})\nSkipping this config file`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
continue;
}
const parsed = v.safeParse(config_schema, result.data);
if (parsed.success) {
merged = {
mcp: { ...merged.mcp, ...parsed.output.mcp },
subagent: {
...merged.subagent,
...parsed.output.subagent,
agents: { ...merged.subagent?.agents, ...parsed.output.subagent?.agents },
},
instructions: { ...merged.instructions, ...parsed.output.instructions },
skills: { ...merged.skills, ...parsed.output.skills },
};
} else {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `Invalid config schema (${path})\nSkipping this config file`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
}
}
}
return merge_with_defaults(merged);
}

View File

@@ -1,174 +0,0 @@
import type { PluginInput } from '@opencode-ai/plugin';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import * as v from 'valibot';
const default_config = {
mcp: {
type: 'remote' as 'remote' | 'local',
enabled: true,
},
subagent: {
enabled: true,
},
instructions: {
enabled: true,
},
skills: {
enabled: true,
},
};
export const config_schema = v.object({
mcp: v.optional(
v.object({
type: v.optional(v.picklist(['remote', 'local'])),
enabled: v.optional(v.boolean()),
}),
),
subagent: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
instructions: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
skills: v.optional(
v.object({
enabled: v.optional(v.boolean()),
}),
),
});
export type McpConfig = v.InferInput<typeof config_schema>;
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'svelte.json');
interface ConfigLoadResult {
data: Record<string, unknown> | null;
parse_error?: string;
}
function get_config_paths() {
// Global: ~/.config/opencode/svelte.json
let global_path: string | null = null;
if (existsSync(GLOBAL_CONFIG_PATH)) {
global_path = GLOBAL_CONFIG_PATH;
}
// Custom config directory: $OPENCODE_CONFIG_DIR/svelte.json
let config_dir_path: string | null = null;
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
if (opencode_config_dir) {
const config_json = join(opencode_config_dir, 'svelte.json');
if (existsSync(config_json)) {
config_dir_path = config_json;
}
}
// Project-local: ./.opencode/svelte.json (cwd)
let project_path: string | null = null;
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
if (existsSync(project_config)) {
project_path = project_config;
}
// Lowest priority first, highest priority last (project overrides global)
return [global_path, config_dir_path, project_path];
}
function load_config_file(config_path: string): ConfigLoadResult {
let file_content: string;
try {
file_content = readFileSync(config_path, 'utf-8');
} catch {
// File doesn't exist or can't be read
return { data: null };
}
try {
const parsed = JSON.parse(file_content);
if (parsed === undefined || parsed === null) {
return { data: null, parse_error: 'Config file is empty or invalid' };
}
return { data: parsed };
} catch (error: unknown) {
return {
data: null,
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
};
}
}
function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
return {
mcp: {
...default_config.mcp,
...user_config.mcp,
},
subagent: {
...default_config.subagent,
...user_config.subagent,
},
instructions: {
...default_config.instructions,
...user_config.instructions,
},
skills: {
...default_config.skills,
...user_config.skills,
},
};
}
export function get_mcp_config(ctx: PluginInput) {
const config_paths = get_config_paths();
let merged: Partial<McpConfig> = {};
// Iterate from lowest to highest priority, merging as we go
for (const path of config_paths) {
if (path && existsSync(path)) {
const result = load_config_file(path);
if (result.parse_error) {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `${result.parse_error} (${path})\nSkipping this config file`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
continue;
}
const parsed = v.safeParse(config_schema, result.data);
if (parsed.success) {
merged = {
mcp: { ...merged.mcp, ...parsed.output.mcp },
subagent: { ...merged.subagent, ...parsed.output.subagent },
instructions: { ...merged.instructions, ...parsed.output.instructions },
skills: { ...merged.skills, ...parsed.output.skills },
};
} else {
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: Invalid opencode plugin config',
message: `Invalid config schema (${path})\nSkipping this config file`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
}
}
}
return merge_with_defaults(merged);
}

117
packages/opencode/index.js Normal file
View File

@@ -0,0 +1,117 @@
import { readdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { agents } from './agents.js';
import { get_mcp_config } from './config.js';
/** @typedef {import('@opencode-ai/plugin').Plugin} Plugin */
const current_dir = dirname(fileURLToPath(import.meta.url));
/**
* @param {Parameters<Plugin>[0]} ctx
* @returns {ReturnType<Plugin>}
*/
export async function svelte_plugin(ctx) {
return {
async config(input) {
input.agent ??= {};
input.mcp ??= {};
input.instructions ??= [];
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills ??= {};
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills.paths ??= [];
// by default we use svelte as the name for the svelte MCP server
let svelte_mcp_name = 'svelte';
// we loop over every mcp server to see if any of them is already the svelte MCP server
for (const name in input.mcp) {
const mcp = input.mcp[name];
if (
(mcp?.type === 'remote' && mcp.url.includes('https://mcp.svelte.dev/mcp')) ||
(mcp?.type === 'local' && mcp.command.some((cmd) => cmd.includes('@sveltejs/mcp')))
) {
// if we found the svelte MCP server, we store its name and break
svelte_mcp_name = name;
break;
}
}
const mcp_config = get_mcp_config(ctx);
if (mcp_config.instructions?.enabled !== false) {
const instructions_dir = join(current_dir, 'instructions');
const instructions_paths = await readdir(instructions_dir);
input.instructions.push(...instructions_paths.map((file) => join(instructions_dir, file)));
}
const skills_enabled = mcp_config.skills?.enabled;
if (skills_enabled !== false) {
const skills_dir = join(current_dir, 'skills');
if (Array.isArray(skills_enabled)) {
// only add specific skill directories by name
for (const skill_name of skills_enabled) {
const skill_path = join(skills_dir, skill_name);
// @ts-expect-error -- skills is a new opencode feature
input.skills.paths.push(skill_path);
}
} else {
// @ts-expect-error -- skills is a new opencode feature
input.skills.paths.push(skills_dir);
}
}
// if the user doesn't have the MCP server already we add one based on config
if (!input.mcp[svelte_mcp_name]) {
if (mcp_config.mcp?.type === 'remote') {
input.mcp[svelte_mcp_name] = {
type: 'remote',
url: 'https://mcp.svelte.dev/mcp',
enabled: mcp_config.mcp?.enabled ?? true,
};
} else {
input.mcp[svelte_mcp_name] = {
type: 'local',
command: ['npx', '-y', '@sveltejs/mcp'],
enabled: mcp_config.mcp?.enabled ?? true,
};
}
}
if (mcp_config.subagent?.enabled !== false) {
for (const [agent_name, agent_data] of Object.entries(agents)) {
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
/** @type {(typeof input.agent)[string]} */
const default_config = {
color: '#ff3e00',
mode: 'subagent',
prompt: agent_data.prompt,
description: agent_data.description,
permission: {
[`${svelte_mcp_name}_*`]: 'allow',
},
};
// Get per-agent config from svelte.json (if any)
const agent_config = mcp_config.subagent?.agents?.[agent_name];
// Configure agent from svelte.json only
// Priority: svelte.json agent config > defaults
input.agent[agent_name] = {
...default_config,
...(agent_config?.model !== undefined && {
model: agent_config.model,
}),
...(agent_config?.temperature !== undefined && {
temperature: agent_config.temperature,
}),
...(agent_config?.maxSteps !== undefined && {
maxSteps: agent_config.maxSteps,
}),
...(agent_config?.top_p !== undefined && {
top_p: agent_config.top_p,
}),
};
}
}
},
};
}

View File

@@ -1,144 +0,0 @@
import type { Plugin } from '@opencode-ai/plugin';
import { readdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { get_mcp_config } from './config.js';
const current_dir = dirname(fileURLToPath(import.meta.url));
export const svelte_plugin: Plugin = async (ctx) => {
return {
async config(input) {
input.agent ??= {};
input.mcp ??= {};
input.instructions ??= [];
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills ??= {};
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
input.skills.paths ??= [];
// by default we use svelte as the name for the svelte MCP server
let svelte_mcp_name = 'svelte';
// we loop over every mcp server to see if any of them is already the svelte MCP server
for (const name in input.mcp) {
const mcp = input.mcp[name];
if (
(mcp?.type === 'remote' && mcp.url.includes('https://mcp.svelte.dev/mcp')) ||
(mcp?.type === 'local' &&
mcp.command.some((cmd: string) => cmd.includes('@sveltejs/mcp')))
) {
// if we found the svelte MCP server, we store its name and break
svelte_mcp_name = name;
break;
}
}
const mcp_config = get_mcp_config(ctx);
if (mcp_config.instructions?.enabled !== false) {
const instructions_dir = join(current_dir, 'instructions');
const instructions_paths = await readdir(instructions_dir);
input.instructions.push(...instructions_paths.map((file) => join(instructions_dir, file)));
}
if (mcp_config.skills?.enabled !== false) {
const skills_dir = join(current_dir, 'skills');
// @ts-expect-error -- skills is a new opencode feature
input.skills.paths.push(skills_dir);
}
// if the user doesn't have the MCP server already we add one based on config
if (!input.mcp[svelte_mcp_name] && mcp_config.mcp?.enabled !== false) {
if (mcp_config.mcp?.type === 'remote') {
input.mcp[svelte_mcp_name] = {
type: 'remote',
url: 'https://mcp.svelte.dev/mcp',
};
} else {
input.mcp[svelte_mcp_name] = {
type: 'local',
command: ['npx', '-y', '@sveltejs/mcp'],
};
}
}
if (mcp_config.subagent?.enabled !== false) {
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
input.agent['svelte-file-editor'] = {
color: '#ff3e00',
mode: 'subagent',
prompt: `You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with \`get_documentation\` and validating the code with \`svelte_autofixer\`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the \`svelte-code-editor\` skill to learn how to use the \`@sveltejs/mcp\` cli to access the same tools.
If the skill is not available you can run \`npx @sveltejs/mcp@latest -y --help\` to learn how to use it.
## Available MCP Tools
### 1. list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.
### 2. get-documentation
Retrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after \`list-sections\` to fetch relevant docs for the task at hand.
**Example sections:** \`$state\`, \`$derived\`, \`$effect\`, \`$props\`, \`$bindable\`, \`snippets\`, \`routing\`, \`load functions\`
### 3. svelte-autofixer
Analyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:
- Using \`$effect\` instead of \`$derived\` for computations
- Missing cleanup in effects
- Svelte 4 syntax (\`on:click\`, \`export let\`, \`<slot>\`)
- Missing keys in \`{#each}\` blocks
- And more
## Workflow
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call \`list-sections\` to see available documentation
2. Call \`get-documentation\` with relevant section names
### 2. Read the Target File
Read the file to understand the current implementation.
### 3. Make Changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
After editing, ALWAYS call \`svelte-autofixer\` with the updated code to check for issues.
### 5. Fix Any Issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
After completing your work, provide:
1. Summary of changes made
2. Any issues found and fixed by the autofixer
3. Recommendations for further improvements (if any)
`,
description:
'Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-code-writer` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
permission: {
bash: 'ask',
edit: 'allow',
webfetch: 'ask',
},
tools: {
[`${svelte_mcp_name}_*`]: true,
},
};
}
},
};
};

View File

@@ -1,6 +1,6 @@
{
"name": "@sveltejs/opencode",
"version": "0.1.4",
"version": "0.1.11",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/sveltejs/ai-tools#readme",
@@ -9,14 +9,26 @@
},
"scripts": {
"check": "tsc --noEmit",
"generate-schema": "node --import node-resolve-ts/register scripts/generate-schema.ts"
"generate-schema": "node scripts/generate-schema.js"
},
"files": [
"index.ts",
"config.ts",
"index.js",
"config.js",
"tui.jsx",
"agents.js",
"instructions",
"skills"
],
"exports": {
"./server": {
"types": "./index.js",
"import": "./index.js"
},
"./tui": {
"types": "./tui.jsx",
"import": "./tui.jsx"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/ai-tools.git",
@@ -26,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

@@ -14,16 +14,55 @@
"type": "boolean"
}
},
"required": []
"required": [],
"description": "Configuration for the MCP. You can chose if it should be enabled or not and the transport to use 'remote' (default) and 'local'."
},
"subagent": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"agents": {
"type": "object",
"propertyNames": {
"anyOf": [
{
"enum": [
"svelte-file-editor"
]
},
{
"type": "string"
}
]
},
"additionalProperties": {
"type": "object",
"properties": {
"model": {
"type": "string",
"description": "Model identifier for the agent (e.g., \"anthropic/claude-sonnet-4-20250514\")"
},
"temperature": {
"type": "number",
"description": "Temperature setting for the agent (e.g., 0.7)"
},
"top_p": {
"type": "number",
"description": "Control response diversity with the top_p option. Alternative to temperature for controlling randomness."
},
"maxSteps": {
"type": "number",
"description": "Maximum number of steps the agent can take (e.g., 10)"
}
},
"required": []
}
}
},
"required": []
"required": [],
"description": "Configuration for the subagent. You can choose if it should be enabled or not."
},
"instructions": {
"type": "object",
@@ -32,16 +71,39 @@
"type": "boolean"
}
},
"required": []
"required": [],
"description": "Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not."
},
"skills": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
"anyOf": [
{
"type": "boolean"
},
{
"type": "array",
"items": {
"anyOf": [
{
"enum": [
"svelte-code-writer",
"svelte-core-bestpractices"
]
},
{
"type": "string"
}
]
}
}
],
"description": "It can be either a boolean or an array containing the skills that you want to enable"
}
},
"required": []
"required": [],
"description": "Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills."
}
},
"required": [],

View File

@@ -0,0 +1,64 @@
import { toJsonSchema } from '@valibot/to-json-schema';
import { config_schema } from '../config.js';
import fs from 'node:fs';
import path from 'node:path';
// Read agent names from tools/agents/*.md files
/** @param {string} agents_dir */
function get_agent_names(agents_dir) {
if (!fs.existsSync(agents_dir)) return [];
return fs
.readdirSync(agents_dir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
.map((entry) => entry.name.replace(/\.md$/, ''));
}
/** @param {string} skills_dir */
function get_skill_names(skills_dir) {
if (!fs.existsSync(skills_dir)) return [];
return fs
.readdirSync(skills_dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
}
const skills_dir = path.resolve('./skills');
const skill_names = get_skill_names(skills_dir);
const schema = config_schema;
const json_schema = toJsonSchema(schema);
// Post-process: inject skill name suggestions into the items schema.
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
// editors will autocomplete the known names but any string is still valid.
if (skill_names.length > 0) {
const enabled = /** @type {any} */ (json_schema).properties?.skills?.properties?.enabled;
if (enabled?.anyOf) {
const array_branch = enabled.anyOf.find(
/** @type {(schema: Record<string, unknown>) => boolean} */ (
(schema) => schema.type === 'array'
),
);
if (array_branch) {
array_branch.items = {
anyOf: [{ enum: skill_names }, { type: 'string' }],
};
}
}
}
// Post-process: inject known agent names for intellisense
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
// editors will autocomplete the known names but any string is still valid.
const agents_dir = path.resolve('../../tools/agents');
const agent_names = get_agent_names(agents_dir);
if (agent_names.length > 0) {
const agents = /** @type {any} */ (json_schema).properties?.subagent?.properties?.agents;
if (agents) {
agents.propertyNames = {
anyOf: [{ enum: agent_names }, { type: 'string' }],
};
}
}
fs.writeFileSync(path.resolve('./schema.json'), JSON.stringify(json_schema, null, '\t'));

View File

@@ -1,8 +0,0 @@
import { toJsonSchema } from '@valibot/to-json-schema';
import { config_schema } from '../config.js';
import fs from 'node:fs';
import path from 'node:path';
const json_schema = toJsonSchema(config_schema);
fs.writeFileSync(path.resolve('./schema.json'), JSON.stringify(json_schema, null, '\t'));

View File

@@ -134,7 +134,7 @@ The CSS in a component's `<style>` is scoped to that component. If a parent comp
</style>
```
If this impossible (for example, the child component comes from a library) you can use `:global` to override styles:
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
```svelte
<div>

View File

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

323
packages/opencode/tui.jsx Normal file
View File

@@ -0,0 +1,323 @@
/** @jsxImportSource @opentui/solid */
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 } from './config.js';
/** @typedef {import('@opencode-ai/plugin/tui').TuiPlugin} TuiPlugin */
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginApi} TuiPluginApi */
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginModule} TuiPluginModule */
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
/** @typedef {'project' | 'global'} Scope */
/** @typedef {Partial<McpConfig>} Config */
const plugin_id = 'svelte.configure';
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'];
const agent_name = 'svelte-file-editor';
/** @param {TuiPluginApi} api */
function project_root(api) {
const worktree = api.state.path.worktree;
return worktree && worktree !== '/' ? worktree : api.state.path.directory;
}
/**
* @param {TuiPluginApi} api
* @param {Scope} scope
*/
function config_path(api, 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',
);
}
/**
* @param {string} path
* @returns {Promise<Config>}
*/
async function read_config(path) {
if (!existsSync(path)) return {};
/** @type {unknown} */
const parsed = 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 /** @type {Config} */ (parsed);
}
/**
* @param {string} path
* @param {Config} config
*/
async function save_config(path, config) {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(config, null, '\t')}\n`, 'utf8');
}
/**
* @param {unknown} value
* @param {string} [fallback]
*/
function display(value, fallback = 'default') {
return value === undefined ? fallback : String(value);
}
/** @type {TuiPlugin} */
const tui = 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
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(/** @type {Scope} */ (option.value))}
/>
));
}
/** @param {Scope} scope */
async function open_config(scope) {
const path = config_path(api, scope);
/** @type {Config} */
let 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);
/** @type {string | undefined} */
let current_option;
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' });
}
}
/**
* @param {'temperature' | 'top_p' | 'maxSteps'} key
* @param {string} label
*/
function prompt_agent_number(key, label) {
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
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 === 'temperature' ||
option.value === 'top_p' ||
option.value === 'maxSteps'
)
return prompt_agent_number(option.value, 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));
/** @param {boolean | undefined} value */
function checked(value) {
return value !== false ? '[x]' : '[ ]';
}
/** @param {'remote' | 'local'} value */
function radio(value) {
return (config.mcp?.type ?? 'remote') === value ? '(*)' : '( )';
}
api.ui.dialog.replace(() => (
<api.ui.DialogSelect
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 /** @satisfies {TuiPluginModule & { id: string }} */ ({
id: plugin_id,
tui,
});

View File

@@ -1,7 +1,7 @@
{
"name": "svelte",
"description": "A plugin for all things related to Svelte development, MCP, skills, and more.",
"version": "1.0.1",
"version": "1.0.4",
"author": {
"name": "Svelte"
},

View File

@@ -6,7 +6,7 @@ permissionMode: acceptEdits
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the `svelte-code-editor` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.

Some files were not shown because too many files have changed in this diff Show More