leglas-mcp 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +276 -300
- package/dist/bin.js +10 -1
- package/dist/channel.d.ts +58 -0
- package/dist/channel.js +92 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/tools.js +36 -9
- package/package.json +8 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fred Amartey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,84 +1,221 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/main/.github/assets/logo-duo.svg" width="640" alt="The Leglas mark, in light and dark" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://raw.githubusercontent.com/FredAmartey/leglas/main/.github/assets/wordmark.svg" width="190" alt="Leglas" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">Your app is the canvas.</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/leglas"><img src="https://img.shields.io/npm/v/leglas" alt="npm"></a>
|
|
13
|
+
<a href="https://github.com/FredAmartey/leglas/actions/workflows/ci.yml"><img src="https://github.com/FredAmartey/leglas/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
14
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/leglas" alt="license"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
Code is becoming the source of truth. Features go from prompt to
|
|
18
|
+
working code in minutes, and mockups/design files eventually lag behind the product and drift out of sync. The
|
|
19
|
+
fastest teams already design in the medium they ship. Leglas is built
|
|
20
|
+
for working that way: it lets you explore many design directions at
|
|
21
|
+
once, live, in your own app.
|
|
22
|
+
|
|
23
|
+
The goal is to help devs and designers try many variations of a component, feature, page or user-flow quickly and make coming up with ideas extremely easy.
|
|
24
|
+
|
|
25
|
+
Ask your agent for a handful of directions for the landing page,
|
|
26
|
+
or the checkout component, or the empty states, or your onboarding flow. Leglas runs them all as your
|
|
27
|
+
actual app, side by side in one place, and holds your notes on each. Explore far and wide without losing focus: you see more ideas
|
|
28
|
+
without losing your opinion of any of them.
|
|
29
|
+
|
|
30
|
+
And because every variation is the real product, your judgment is real
|
|
31
|
+
too. Everything behaves the way it will in production, motion and data
|
|
32
|
+
included. Choosing between two directions is choosing between two
|
|
33
|
+
things that already exist, and the winner never has to be rebuilt from
|
|
34
|
+
a picture or design file.
|
|
35
|
+
|
|
36
|
+
Your app doesn't change to make any of this work. Leglas proxies the same
|
|
37
|
+
dev server in your project: one config file to delete when you're done
|
|
38
|
+
and sessions that clean up after themselves.
|
|
39
|
+
|
|
40
|
+
## What you can do with Leglas
|
|
41
|
+
|
|
42
|
+
- Pick any two variations for side-by-side comparison when it gets hard to choose.
|
|
43
|
+
- Name each direction, drag to reorder and organise your variants, set aside the ones that
|
|
44
|
+
don't feel right. Your actions on every idea survive a long exploration.
|
|
45
|
+
- Send a teammate the link to a direction. They open the live version instead of a screenshot and a paragraph of explanation.
|
|
46
|
+
- Compare things no design tool can hold: 3 git branches, a local
|
|
47
|
+
build against production, yesterday's direction against today's, or even 7 different worktrees.
|
|
48
|
+
- `leglas init` teaches any coding agent the workflow, and
|
|
49
|
+
`leglas explore` briefs an exploration. Five directions can be five
|
|
50
|
+
separate ideas, or five variants of the one you already like: you choose
|
|
51
|
+
the spread, your agent supplies the taste.
|
|
52
|
+
- Ask for changes without leaving the comparison: describe what you
|
|
53
|
+
want on the direction you're looking at, and Leglas turns it into a
|
|
54
|
+
precise request for your agent, file path included. Leave
|
|
55
|
+
`leglas watch --run "claude -p {prompt}"` running in another terminal
|
|
56
|
+
and your agent picks each request up as you send it. In Claude Code,
|
|
57
|
+
the Leglas MCP server can also push each request straight into your
|
|
58
|
+
open session as a channel event (channels are a research preview:
|
|
59
|
+
start Claude Code with `--dangerously-load-development-channels
|
|
60
|
+
server:<your leglas server name>`).
|
|
61
|
+
- Keep the winner with one command. Leglas moves it into your source
|
|
62
|
+
tree and clears the exploration away.
|
|
63
|
+
- No app yet or want plain HTML comparisons? Some people want exactly that, and it works fine. Same comparison, no dev server.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
Start your dev server, then run Leglas from the project directory:
|
|
2
68
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
already running, list the URLs you want to compare, and flip between them in
|
|
7
|
-
one interface. Every preview is your real application: real data, real
|
|
8
|
-
authentication, real behaviour. Nothing is mocked, nothing is cloned, and
|
|
9
|
-
your project needs no changes to work with it.
|
|
10
|
-
|
|
11
|
-
## Requirements
|
|
12
|
-
|
|
13
|
-
- Node.js 24 or newer, developed and tested against Node 26
|
|
14
|
-
- A dev server you can run locally
|
|
15
|
-
|
|
16
|
-
TypeScript config files rely on Node stripping types natively, which is
|
|
17
|
-
available without a flag from Node 23.6. On an older runtime, use
|
|
18
|
-
`leglas.config.mjs` or `leglas.config.json` instead.
|
|
19
|
-
|
|
20
|
-
Leglas never imports or executes your framework, so the target can be Next,
|
|
21
|
-
Vite, Remix, Create React App, SvelteKit, Astro, or static output.
|
|
22
|
-
|
|
23
|
-
## Getting started
|
|
69
|
+
```sh
|
|
70
|
+
npx leglas
|
|
71
|
+
```
|
|
24
72
|
|
|
25
|
-
|
|
73
|
+
Leglas starts on port 4100, proxies your app, and opens
|
|
74
|
+
`http://localhost:4100/leglas`. With no configuration you get a single
|
|
75
|
+
preview of your app root. Add a config file to compare more than one thing.
|
|
76
|
+
|
|
77
|
+
It works with whatever you're building in. Leglas never imports or
|
|
78
|
+
executes your framework, so the target can be Next, Vite, Remix,
|
|
79
|
+
SvelteKit, Astro, or a folder of static files. Requires Node 24 or
|
|
80
|
+
newer.
|
|
81
|
+
|
|
82
|
+
## Working with coding agents
|
|
83
|
+
|
|
84
|
+
Run `leglas init` once. It writes a section into your project's
|
|
85
|
+
`AGENTS.md`, creates a starter config, and gitignores Leglas's working
|
|
86
|
+
directory. That section travels with the repo, so Claude Code, Cursor,
|
|
87
|
+
Codex, or whatever you switch to next opens the project already knowing
|
|
88
|
+
how to add design directions to it. Every command accepts `--json` and
|
|
89
|
+
prints a single machine-readable envelope, so agents drive the same CLI
|
|
90
|
+
you do.
|
|
91
|
+
|
|
92
|
+
The instructions center on one rule: add beside what exists, never
|
|
93
|
+
rewrite it. Two directions that rewrite the same file cannot render from
|
|
94
|
+
one server, and asking an agent to "make the hero calmer" tempts it to
|
|
95
|
+
edit the hero. The supporting commands:
|
|
96
|
+
|
|
97
|
+
- `leglas explore hero --count 6` briefs the exploration: what the set
|
|
98
|
+
is for, why it only works if the six genuinely disagree, and how each
|
|
99
|
+
direction registers. Unbriefed, six requests come back as six variants
|
|
100
|
+
of one idea. With `--based-on "Aurora"` the goal flips: six deliberate
|
|
101
|
+
variants of a direction you already like, and drifting into a new
|
|
102
|
+
direction is the failure. The designs themselves are the agent's;
|
|
103
|
+
Leglas prescribes none.
|
|
104
|
+
- `leglas new hero --from src/Hero.tsx` scaffolds a switcher under
|
|
105
|
+
`.leglas/variants/hero/`. With `--from`, the baseline re-exports your
|
|
106
|
+
real component, so you never compare against a stale copy. Leglas
|
|
107
|
+
prints the one line to add in your component and does not edit it,
|
|
108
|
+
because rewriting a file it does not understand is how a tool breaks a
|
|
109
|
+
codebase. Scaffolded branch points return the fallback in production
|
|
110
|
+
builds, so a committed one cannot expose an unreleased direction.
|
|
111
|
+
- `leglas classify --change package.json --rewrite src/theme.css` answers
|
|
112
|
+
where a direction should live before it is written. Changing
|
|
113
|
+
dependencies, build configuration, or an existing file's behaviour
|
|
114
|
+
cannot be additive, so those directions build on their own git branch
|
|
115
|
+
and register with `leglas add --branch`. Everything else stays in-app,
|
|
116
|
+
where switching is instant.
|
|
117
|
+
- `leglas show "Aurora" --json` answers for one direction: its entry, the
|
|
118
|
+
source file behind it, the variants based on it, what it is being compared
|
|
119
|
+
against, and anything still pending on it. Copying a direction from the
|
|
120
|
+
rail hands over a block that ends in this command, so an agent given the
|
|
121
|
+
block can go and get the rest.
|
|
122
|
+
- `leglas keep "Aurora" --to src/components/hero.tsx` moves the winner
|
|
123
|
+
into real source and ends the exploration.
|
|
124
|
+
|
|
125
|
+
Asking for a change works from the interface too. Type what you want
|
|
126
|
+
changed into the field under the rail (or press `R`) and Leglas composes a
|
|
127
|
+
prompt naming the direction and the file behind it, copies it to your
|
|
128
|
+
clipboard, and queues it. The direction it means is the one highlighted
|
|
129
|
+
directly above the field. Your agent drains the queue with `leglas requests --json` and clears
|
|
130
|
+
it with `--clear`. Leglas runs no model of its own; your agent already
|
|
131
|
+
knows your conventions and your taste.
|
|
132
|
+
|
|
133
|
+
### MCP server
|
|
134
|
+
|
|
135
|
+
For agent hosts that cannot run shell commands, `leglas-mcp` exposes the
|
|
136
|
+
same operations as MCP tools over stdio: `start`, `add`, `list`, `show`,
|
|
137
|
+
`classify`, `explore`, `scaffold`, `keep`, `requests`, and `init`. Each
|
|
138
|
+
tool calls exactly what the CLI calls and returns the same envelope.
|
|
26
139
|
|
|
27
140
|
```sh
|
|
28
|
-
npx leglas
|
|
141
|
+
claude mcp add leglas -- npx -y leglas-mcp
|
|
29
142
|
```
|
|
30
143
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
144
|
+
Or in `.mcp.json`:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "mcpServers": { "leglas": { "command": "npx", "args": ["-y", "leglas-mcp"] } } }
|
|
148
|
+
```
|
|
34
149
|
|
|
35
|
-
|
|
150
|
+
The host's working directory names the project. The `start` tool boots
|
|
151
|
+
the viewer and returns its URL, and anything it started stops when the
|
|
152
|
+
session ends.
|
|
36
153
|
|
|
37
154
|
## Configuration
|
|
38
155
|
|
|
39
|
-
Create `leglas.config.ts` at the root
|
|
40
|
-
|
|
41
|
-
|
|
156
|
+
Create `leglas.config.ts` at the project root. `.js`, `.mjs`, and `.json`
|
|
157
|
+
work too. Resolution walks upward from the working directory, so in a
|
|
158
|
+
monorepo the nearest file wins. Node reads the TypeScript config natively;
|
|
159
|
+
there is no compiler or extra dependency involved.
|
|
42
160
|
|
|
43
161
|
```ts
|
|
44
162
|
export default {
|
|
45
163
|
devServer: "http://localhost:3000",
|
|
46
164
|
previews: [
|
|
47
165
|
{ title: "Current", url: "/" },
|
|
48
|
-
{
|
|
49
|
-
title: "Wave",
|
|
50
|
-
url: "/?v-hero=wave",
|
|
51
|
-
note: "Client artwork, bottom anchored.",
|
|
52
|
-
tags: ["Hero"],
|
|
53
|
-
},
|
|
166
|
+
{ title: "Wave", url: "/?v-hero=wave", note: "Full-bleed, anchored low.", tags: ["Hero"] },
|
|
54
167
|
{
|
|
55
168
|
title: "Dot grid",
|
|
56
169
|
url: "/?v-hero=dotgrid",
|
|
57
|
-
note: "
|
|
170
|
+
note: "Lattice that wakes near the pointer.",
|
|
58
171
|
tags: ["Hero"],
|
|
59
172
|
},
|
|
60
173
|
],
|
|
61
174
|
};
|
|
62
175
|
```
|
|
63
176
|
|
|
64
|
-
| Field
|
|
65
|
-
|
|
|
66
|
-
| `title`
|
|
67
|
-
| `url`
|
|
68
|
-
| `note`
|
|
69
|
-
| `tags`
|
|
70
|
-
| `branch`
|
|
71
|
-
| `file`
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
177
|
+
| Field | Required | Purpose |
|
|
178
|
+
| ---------------- | ------------- | ----------------------------------------------------------------------- |
|
|
179
|
+
| `title` | yes | Label in the rail, and the key for your saved layout. Must be unique. |
|
|
180
|
+
| `url` | unless `file` | Root relative (`/pricing`) or absolute (`https://staging.example.com`) |
|
|
181
|
+
| `note` | no | Second line under the title |
|
|
182
|
+
| `tags` | no | The first tag renders as a pill |
|
|
183
|
+
| `branch` | no | Preview a git branch instead of the running dev server |
|
|
184
|
+
| `file` | no | An HTML file served by Leglas itself, instead of `url` |
|
|
185
|
+
| `basedOn` | no | Title of the direction this is a variant of; the rail groups the family |
|
|
186
|
+
| `devServer` | no | Defaults to `http://localhost:3000` |
|
|
187
|
+
| `devCommand` | with `branch` | How to start the app. Must contain `{port}`. |
|
|
188
|
+
| `installCommand` | no | Defaults to `npm install` |
|
|
189
|
+
|
|
190
|
+
A broken config never stops the server. Leglas starts anyway and the
|
|
191
|
+
interface reports what to fix, so you are not hunting through a stack
|
|
192
|
+
trace.
|
|
193
|
+
|
|
194
|
+
## The interface
|
|
195
|
+
|
|
196
|
+
Directions live in a rail on the left. The stage shows the active one in
|
|
197
|
+
a framed viewport at Full, 1440, 834, or 390 wide. Rename, reorder, hide,
|
|
198
|
+
and tag directions from the rail; layout is saved per project and
|
|
199
|
+
survives restarts and port changes.
|
|
200
|
+
|
|
201
|
+
Flipping shows a difference over time. A split shows it at once, which is
|
|
202
|
+
what you want for the last two directions in contention: press `C`, or
|
|
203
|
+
hover a direction and press its compare button, and it becomes the right
|
|
204
|
+
pane while the active direction holds the left.
|
|
205
|
+
|
|
206
|
+
Arrows move between directions, `1` to `9` jump straight to one, `R` asks
|
|
207
|
+
for a change to the one you are on, `Cmd K` (`Ctrl K` elsewhere) searches
|
|
208
|
+
and `B` collapses the rail. Press `?` for the whole keymap.
|
|
209
|
+
|
|
210
|
+
A small tools widget floats over the stage and can be dragged to any
|
|
211
|
+
corner, because a floating control has a habit of sitting exactly where
|
|
212
|
+
you need to look. Its popover holds the viewport presets and a few
|
|
213
|
+
preferences.
|
|
214
|
+
|
|
215
|
+
Frameworks paint a dev badge over the corner of the running app. It
|
|
216
|
+
belongs to your app, so Leglas leaves it alone; when it lands on the part
|
|
217
|
+
you are judging, the popover hides it, and does that by styling inside
|
|
218
|
+
the preview frame, never by altering what the proxy forwards.
|
|
82
219
|
|
|
83
220
|
## Command line
|
|
84
221
|
|
|
@@ -86,10 +223,11 @@ bundler or extra dependency involved.
|
|
|
86
223
|
leglas init Prepare a project and teach its agents
|
|
87
224
|
leglas [options] Start the server and open the interface
|
|
88
225
|
leglas new <surface> Scaffold a branch point for a surface
|
|
89
|
-
leglas explore <surface>
|
|
226
|
+
leglas explore <surface> Brief an agent's exploration of a surface
|
|
90
227
|
leglas classify Decide where a direction should live
|
|
91
228
|
leglas add --title T --url U Register a preview on this machine
|
|
92
229
|
leglas list Show every preview, shared and local
|
|
230
|
+
leglas show <title> Everything Leglas knows about one direction
|
|
93
231
|
leglas requests Collect change requests made from the interface
|
|
94
232
|
leglas keep <title> Keep a winner and end the exploration
|
|
95
233
|
|
|
@@ -97,224 +235,53 @@ leglas keep <title> Keep a winner and end the exploration
|
|
|
97
235
|
--port <port> Port for Leglas (default 4100, next free if taken)
|
|
98
236
|
--config <path> Use this config file instead of searching upward
|
|
99
237
|
--no-open Do not open the browser
|
|
100
|
-
--json Print one machine
|
|
101
|
-
-h, --help
|
|
102
|
-
-v, --version
|
|
238
|
+
--json Print one machine-readable envelope
|
|
103
239
|
|
|
104
240
|
--print (new) Print the scaffold instead of writing it
|
|
105
|
-
--
|
|
106
|
-
--
|
|
241
|
+
--from <path> (new) Use an existing component as the baseline
|
|
242
|
+
--count <n> (explore) How many directions, default 3
|
|
243
|
+
--based-on <title> (explore) Variants of an existing direction instead of new ones
|
|
244
|
+
--based-on <title> (add) The direction this preview is a variant of; groups the family
|
|
245
|
+
--change <path> (classify) A file the direction creates or wires up
|
|
246
|
+
--rewrite <path> (classify) An existing file whose behaviour must change
|
|
107
247
|
--note <text> (add) Second line under the title
|
|
108
248
|
--tag <text> (add) Repeatable
|
|
109
249
|
--branch <name> (add) Back the preview with a checkout of this branch
|
|
110
|
-
--
|
|
111
|
-
--
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### Shared and local previews
|
|
115
|
-
|
|
116
|
-
`leglas.config.ts` is the shared description of a project: commit it, and a
|
|
117
|
-
teammate gets the same directions on clone.
|
|
118
|
-
|
|
119
|
-
`leglas add` registers a preview on your machine only, in
|
|
120
|
-
`.leglas/previews.json`. Exploration is short-lived and its code lives in a
|
|
121
|
-
gitignored directory, so a teammate must never receive a config entry
|
|
122
|
-
pointing at something they do not have. `leglas list` shows both, marking
|
|
123
|
-
which are local. Any command that writes into `.leglas/` also makes sure the
|
|
124
|
-
directory is gitignored.
|
|
125
|
-
|
|
126
|
-
### Working with coding agents
|
|
127
|
-
|
|
128
|
-
`leglas init` writes a section into your project's `AGENTS.md`, creates a
|
|
129
|
-
starter config, and ignores the working directory. The section is read by
|
|
130
|
-
Cursor, Claude Code and most other agents, and needs no per-user setup: it
|
|
131
|
-
travels with the clone, so anyone who opens the repository gets an agent that
|
|
132
|
-
already knows how to add design directions to it.
|
|
133
|
-
|
|
134
|
-
The instruction that matters most in that section is to add directions beside
|
|
135
|
-
what exists rather than rewriting it. Asked to make a hero calmer, an agent's
|
|
136
|
-
instinct is to edit the hero, and two directions that both rewrite the same
|
|
137
|
-
file cannot render from one server. Nothing in Leglas can prevent that,
|
|
138
|
-
because it never sees your source, so the contract has to say it.
|
|
139
|
-
|
|
140
|
-
Every command accepts `--json` and prints a single envelope with a stable exit
|
|
141
|
-
code, so an agent can drive the tool without parsing prose.
|
|
142
|
-
|
|
143
|
-
### Agent hosts that cannot run a shell
|
|
144
|
-
|
|
145
|
-
`leglas-mcp` is a stdio MCP server exposing the same operations as tools:
|
|
146
|
-
`start`, `add`, `list`, `classify`, `explore`, `scaffold`, `keep`,
|
|
147
|
-
`requests`, and `init`. It holds no logic of its own; each tool calls
|
|
148
|
-
exactly what the CLI calls and returns the same JSON envelope.
|
|
149
|
-
|
|
150
|
-
Register it in your agent host from the project directory, for example in
|
|
151
|
-
Claude Code:
|
|
152
|
-
|
|
153
|
-
```sh
|
|
154
|
-
claude mcp add leglas -- npx -y leglas-mcp
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
or in a `.mcp.json`:
|
|
158
|
-
|
|
159
|
-
```json
|
|
160
|
-
{ "mcpServers": { "leglas": { "command": "npx", "args": ["-y", "leglas-mcp"] } } }
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
The host's working directory names the project, the same contract as the
|
|
164
|
-
CLI. The `start` tool boots the viewer and returns the interface URL, and
|
|
165
|
-
the server it started stops when the session ends, so a dead host never
|
|
166
|
-
leaves a dev server running on a forgotten port.
|
|
167
|
-
|
|
168
|
-
### When a direction cannot be additive
|
|
169
|
-
|
|
170
|
-
Most directions are additive: new files beside what exists, rendering from
|
|
171
|
-
the dev server you already run. Some genuinely are not. A direction that
|
|
172
|
-
changes dependencies, changes build configuration, or only works by
|
|
173
|
-
rewriting what an existing file renders cannot share the running server
|
|
174
|
-
with its siblings.
|
|
175
|
-
|
|
176
|
-
`leglas classify` decides which kind you have, before the code is written.
|
|
177
|
-
Declare what the direction will touch and it answers with the route and the
|
|
178
|
-
reason:
|
|
179
|
-
|
|
180
|
-
```sh
|
|
181
|
-
leglas classify --change package.json --rewrite src/theme.css --json
|
|
250
|
+
--file <path> (add) An HTML file served by Leglas itself
|
|
251
|
+
--to <path> (keep) Where the winner should live
|
|
182
252
|
```
|
|
183
253
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
`leglas
|
|
189
|
-
previews need `devCommand` in the config so Leglas can start the checkout,
|
|
190
|
-
and they appear in the same rail as everything else.
|
|
191
|
-
|
|
192
|
-
The escalation is the exception, and it is never silent: isolation costs an
|
|
193
|
-
install and a boot, so a direction goes to a checkout only when it names a
|
|
194
|
-
reason it cannot sit in the running app.
|
|
195
|
-
|
|
196
|
-
### Exploring several directions at once
|
|
197
|
-
|
|
198
|
-
`leglas explore hero --count 6` prints six distinct angles to build, each with
|
|
199
|
-
a line naming the obvious approach that would collapse the difference. Left to
|
|
200
|
-
itself an agent iterates narrowly around its first idea, so six requests come
|
|
201
|
-
back as six shades of one design. The angles vary composition, medium, density,
|
|
202
|
-
motion, texture and palette rather than colour alone, and they are ordered for
|
|
203
|
-
spread so asking for three still explores widely.
|
|
204
|
-
|
|
205
|
-
The `AGENTS.md` section tells agents to run this first, so "give me a few
|
|
206
|
-
options" reaches for the angles instead of inventing variations of what is
|
|
207
|
-
already there.
|
|
208
|
-
|
|
209
|
-
### Asking for a change without leaving
|
|
210
|
-
|
|
211
|
-
The tools popover has a field for the direction you are looking at. Type what
|
|
212
|
-
you want changed and press Enter: Leglas composes a prompt naming that
|
|
213
|
-
direction and the file behind it, copies it to your clipboard, and queues it.
|
|
214
|
-
|
|
215
|
-
Leglas runs no model of its own. Your agent already knows your conventions,
|
|
216
|
-
your design system and your taste, which is context no external worker has, so
|
|
217
|
-
it does the work. `leglas requests --json` hands over anything pending and
|
|
218
|
-
`leglas requests --clear` empties the queue once it is done. If you would
|
|
219
|
-
rather not wire that up, the clipboard copy is the whole feature: paste and
|
|
220
|
-
go.
|
|
221
|
-
|
|
222
|
-
### Scaffolding a surface
|
|
223
|
-
|
|
224
|
-
`leglas new hero --from src/Hero.tsx` creates a switcher and a first direction
|
|
225
|
-
under `.leglas/variants/hero/`, and adds `.leglas/` to your `.gitignore`.
|
|
226
|
-
|
|
227
|
-
`--from` points at whatever renders that surface today. The baseline then
|
|
228
|
-
re-exports that component rather than copying it, so editing the real
|
|
229
|
-
component changes the baseline too and you are never comparing against a stale
|
|
230
|
-
duplicate of your own code. Without `--from` you get a placeholder to fill in
|
|
231
|
-
yourself. The
|
|
232
|
-
generated code is ordinary application code: it imports nothing from Leglas,
|
|
233
|
-
so removing the tool leaves it working. It also renders the fallback in
|
|
234
|
-
production regardless of the URL, so a branch point that reaches a deployed
|
|
235
|
-
build cannot expose an unreleased direction.
|
|
236
|
-
|
|
237
|
-
One step is left to you. Leglas prints the import and the element to use, but
|
|
238
|
-
does not edit the component itself, because rewriting a file it does not
|
|
239
|
-
understand is how a tool breaks a codebase. Use `--print` to see the scaffold
|
|
240
|
-
without writing anything.
|
|
241
|
-
|
|
242
|
-
`--json` exists so coding agents can drive the tool. It prints a single
|
|
243
|
-
object with the interface URL, the resolved port, whether the dev server
|
|
244
|
-
answered, and any configuration errors.
|
|
245
|
-
|
|
246
|
-
## Keyboard
|
|
247
|
-
|
|
248
|
-
| Key | Action |
|
|
249
|
-
| --- | --- |
|
|
250
|
-
| Up, Down | Move between directions |
|
|
251
|
-
| `\` | Split the stage against the direction you were last on |
|
|
252
|
-
| `/` | Focus search |
|
|
253
|
-
| `[` | Collapse or open the rail |
|
|
254
|
-
| Escape | Clear search, or close the tools popover |
|
|
255
|
-
|
|
256
|
-
Directions can be renamed, removed, restored and dragged into any order.
|
|
257
|
-
Layout is saved per project, so it survives restarts and a change of port.
|
|
258
|
-
|
|
259
|
-
### Comparing two at once
|
|
260
|
-
|
|
261
|
-
Flipping shows a difference over time. A split shows it at once, which is what
|
|
262
|
-
you want for the last two directions still in contention.
|
|
263
|
-
|
|
264
|
-
Hover any direction in the rail and press its compare button. It becomes the
|
|
265
|
-
right pane, the active direction stays on the left, and the row is marked so
|
|
266
|
-
you can see what you are comparing against without hovering. Press it again to
|
|
267
|
-
close the split. `\` splits against whichever direction you were looking at
|
|
268
|
-
before this one.
|
|
269
|
-
|
|
270
|
-
The tools widget can be dragged to any corner, since a floating control has a
|
|
271
|
-
habit of sitting exactly where you need to look. Its corner is remembered.
|
|
272
|
-
|
|
273
|
-
Frameworks paint their own dev badge over the running app, which lands on the
|
|
274
|
-
same corner and is tooling rather than design. Leglas hides those by default,
|
|
275
|
-
and the popover turns them back on. This is done by styling inside the
|
|
276
|
-
preview frame, never by altering what the proxy forwards, so what your dev
|
|
277
|
-
server sent is what your app receives.
|
|
278
|
-
|
|
279
|
-
## How it works
|
|
280
|
-
|
|
281
|
-
Leglas runs a local server that does two things. It serves the interface at
|
|
282
|
-
`/leglas`, and it forwards every other request to your dev server. Because
|
|
283
|
-
previews load through that proxy, they are same origin with the interface,
|
|
284
|
-
which means no CORS configuration and no special cases for cookies.
|
|
285
|
-
|
|
286
|
-
The proxy is designed to be invisible. Hot module replacement survives the
|
|
287
|
-
hop, so editing a file still updates every preview. Redirects that point at
|
|
288
|
-
your dev server are rewritten to keep you inside the interface, and
|
|
289
|
-
redirects to anywhere else are left alone. Responses stream rather than
|
|
290
|
-
buffer. If an app behaves differently through Leglas than it does on its own
|
|
291
|
-
port, that is a bug.
|
|
254
|
+
`leglas.config.ts` is the shared description of a project: commit it and a
|
|
255
|
+
teammate gets the same directions on clone. `leglas add` registers a
|
|
256
|
+
preview on your machine only, in `.leglas/previews.json`, because
|
|
257
|
+
exploration is short-lived and its code lives in a gitignored directory.
|
|
258
|
+
`leglas list` shows both and marks which are local.
|
|
292
259
|
|
|
293
|
-
|
|
260
|
+
Renaming a direction in the rail is local in the same way, recorded in
|
|
261
|
+
`.leglas/renames.json`. Leglas will not edit your config to rename
|
|
262
|
+
something you only renamed for yourself, so the config title stays the
|
|
263
|
+
one a teammate sees, and `leglas show` and `leglas keep` take either
|
|
264
|
+
name.
|
|
294
265
|
|
|
295
|
-
|
|
296
|
-
serve:
|
|
266
|
+
## Comparing branches
|
|
297
267
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
268
|
+
A preview with a `branch` field is served from its own checkout: Leglas
|
|
269
|
+
creates a worktree, installs, starts the app with your `devCommand` on a
|
|
270
|
+
free port, and tears it all down when you quit. In the interface it looks
|
|
271
|
+
like any other direction, so a branch against your working tree, or three
|
|
272
|
+
branches against each other, compares the same way two query parameters
|
|
273
|
+
do.
|
|
301
274
|
|
|
302
|
-
|
|
303
|
-
subject to the target's frame policy. A site that refuses to be framed will
|
|
304
|
-
not preview, and the interface says so rather than showing an empty pane.
|
|
305
|
-
|
|
306
|
-
## Starting before you have an app
|
|
275
|
+
## Without a dev server
|
|
307
276
|
|
|
308
277
|
Leglas does not require a running app.
|
|
309
278
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
proxies it, and stops it when you quit, exactly as it does for branch
|
|
313
|
-
checkouts. This is reported as status, never asked as a question. When
|
|
279
|
+
If the project exists but nothing is listening, set `devCommand` and
|
|
280
|
+
Leglas starts your app itself, proxies it, and stops it on exit. When
|
|
314
281
|
`--user-port` names a server explicitly, Leglas never starts a different
|
|
315
282
|
one behind that flag.
|
|
316
283
|
|
|
317
|
-
|
|
284
|
+
If there is no app at all, a direction can be a plain HTML file:
|
|
318
285
|
|
|
319
286
|
```ts
|
|
320
287
|
export default {
|
|
@@ -325,44 +292,48 @@ export default {
|
|
|
325
292
|
};
|
|
326
293
|
```
|
|
327
294
|
|
|
328
|
-
Leglas serves each file
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.
|
|
333
|
-
AGENTS.md section teaches agents the same loop, so "show me three landing
|
|
334
|
-
page directions" works in an empty repository. When the real app arrives,
|
|
335
|
-
directions graduate to app code and nothing about the interface changes.
|
|
336
|
-
|
|
337
|
-
## When two directions render the same page
|
|
295
|
+
Leglas serves each file from its own origin, so the full interface works
|
|
296
|
+
with no dev server anywhere. The file's directory is mounted rather than
|
|
297
|
+
the lone file, so stylesheets and images beside it resolve. When the real
|
|
298
|
+
app arrives, directions graduate to app code and nothing about the
|
|
299
|
+
interface changes.
|
|
338
300
|
|
|
339
|
-
|
|
340
|
-
identical. This catches the failure that is otherwise invisible: a typo like
|
|
341
|
-
`?v-hero=wavee` that your app ignores, serving its default page while the rail
|
|
342
|
-
implies you are comparing something.
|
|
343
|
-
|
|
344
|
-
The comparison reads the rendered page rather than the server's response, so it
|
|
345
|
-
works whether your app renders on the server or in the browser. A single-page
|
|
346
|
-
app returns the same HTML for every URL, which makes any server-side comparison
|
|
347
|
-
useless there.
|
|
301
|
+
## How it works
|
|
348
302
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
303
|
+
Leglas runs one local server that serves the interface at `/leglas` and
|
|
304
|
+
forwards every other request to your dev server. Previews load through
|
|
305
|
+
that proxy, so they are same origin with the interface: no CORS
|
|
306
|
+
configuration, no cookie special cases.
|
|
307
|
+
|
|
308
|
+
The proxy is designed to be invisible. Hot module replacement survives
|
|
309
|
+
the hop, redirects that point at your dev server are rewritten to keep
|
|
310
|
+
you inside the interface, and responses stream rather than buffer. If an
|
|
311
|
+
app behaves differently through Leglas than on its own port, that is a
|
|
312
|
+
bug.
|
|
313
|
+
|
|
314
|
+
Because a preview is a URL, the same interface compares two routes, two
|
|
315
|
+
implementations behind a query parameter, or a local server against a
|
|
316
|
+
deployed one. Absolute URLs load directly rather than through the proxy,
|
|
317
|
+
so a site that refuses to be framed will not preview; the interface says
|
|
318
|
+
so instead of showing an empty pane.
|
|
319
|
+
|
|
320
|
+
Leglas also compares what each preview actually draws and warns when two
|
|
321
|
+
are identical. This catches a typo like `?v-hero=wavee` that your app
|
|
322
|
+
silently ignores while the rail implies a comparison. The check reads the
|
|
323
|
+
rendered page, runs only on previews you have opened, and skips
|
|
324
|
+
cross-origin previews, which the browser will not let it read.
|
|
353
325
|
|
|
354
326
|
## Limitations
|
|
355
327
|
|
|
356
|
-
Leglas shows
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
not intended to ship in a production runtime.
|
|
328
|
+
- Leglas shows directions; it does not create them. Comparing existing
|
|
329
|
+
routes costs nothing, but a new direction is still code you or your
|
|
330
|
+
agent writes.
|
|
331
|
+
- The duplicate check compares rendered markup only, and only when the
|
|
332
|
+
server renders some. Two previews that differ solely in a script are
|
|
333
|
+
reported as identical, and in a fully client-rendered app the check
|
|
334
|
+
says nothing.
|
|
335
|
+
- The interface is built for desktop widths.
|
|
336
|
+
- Leglas is a development tool. Nothing in it ships to production.
|
|
366
337
|
|
|
367
338
|
## Development
|
|
368
339
|
|
|
@@ -375,17 +346,22 @@ pnpm test # run the test suite
|
|
|
375
346
|
pnpm typecheck # type check every package
|
|
376
347
|
```
|
|
377
348
|
|
|
378
|
-
| Package
|
|
379
|
-
|
|
|
380
|
-
| `packages/server` | Config loading, the proxy, and the local server
|
|
381
|
-
| `packages/shell`
|
|
382
|
-
| `packages/cli`
|
|
383
|
-
| `packages/mcp`
|
|
349
|
+
| Package | Contents |
|
|
350
|
+
| ----------------- | -------------------------------------------------- |
|
|
351
|
+
| `packages/server` | Config loading, the proxy, and the local server |
|
|
352
|
+
| `packages/shell` | The interface, a React application built with Vite |
|
|
353
|
+
| `packages/cli` | The `leglas` binary |
|
|
354
|
+
| `packages/mcp` | The `leglas-mcp` stdio server for agent hosts |
|
|
384
355
|
|
|
385
356
|
To work on the interface with live reload, run a Leglas server in one
|
|
386
|
-
terminal and `pnpm --filter @leglas/shell dev` in another.
|
|
387
|
-
|
|
357
|
+
terminal and `pnpm --filter @leglas/shell dev` in another.
|
|
358
|
+
|
|
359
|
+
Two packages are published, both unscoped: `leglas`, which bundles the
|
|
360
|
+
server and the built interface, and `leglas-mcp`. Releases are
|
|
361
|
+
tag-driven: bump both versions, push a `v<version>` tag, and CI runs the
|
|
362
|
+
suite and publishes through npm trusted publishing. No npm token exists
|
|
363
|
+
anywhere in the project.
|
|
388
364
|
|
|
389
365
|
## License
|
|
390
366
|
|
|
391
|
-
MIT
|
|
367
|
+
[MIT](LICENSE)
|
package/dist/bin.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, startChannel } from "./channel.js";
|
|
5
6
|
import { registerLeglasTools } from "./tools.js";
|
|
6
7
|
/**
|
|
7
8
|
* The stdio face. An agent host spawns this in the project directory, which
|
|
@@ -12,13 +13,19 @@ function version() {
|
|
|
12
13
|
const pkg = require("../package.json");
|
|
13
14
|
return pkg.version;
|
|
14
15
|
}
|
|
15
|
-
const server = new McpServer({ name: "leglas", version: version() }
|
|
16
|
+
const server = new McpServer({ name: "leglas", version: version() },
|
|
17
|
+
// The channel capability and instructions are inert on hosts that do not
|
|
18
|
+
// speak channels; on Claude Code they let change requests from the
|
|
19
|
+
// interface arrive in the open session as events.
|
|
20
|
+
{ capabilities: { experimental: CHANNEL_CAPABILITY }, instructions: CHANNEL_INSTRUCTIONS });
|
|
16
21
|
const tools = registerLeglasTools(server, { cwd: process.cwd() });
|
|
22
|
+
let channel = null;
|
|
17
23
|
let stopping = false;
|
|
18
24
|
const shutdown = async () => {
|
|
19
25
|
if (stopping)
|
|
20
26
|
return;
|
|
21
27
|
stopping = true;
|
|
28
|
+
channel?.stop();
|
|
22
29
|
await tools.shutdown();
|
|
23
30
|
process.exit(0);
|
|
24
31
|
};
|
|
@@ -31,3 +38,5 @@ transport.onclose = () => {
|
|
|
31
38
|
void shutdown();
|
|
32
39
|
};
|
|
33
40
|
await server.connect(transport);
|
|
41
|
+
// Only after connect: a notification with no transport throws.
|
|
42
|
+
channel = startChannel(server, { cwd: process.cwd() });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type PendingRequest } from "leglas";
|
|
3
|
+
/**
|
|
4
|
+
* The push face, for hosts that treat an MCP server as a channel.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code's channels let a server inject events into the open session, so
|
|
7
|
+
* a change request typed into the interface can reach the agent that is
|
|
8
|
+
* already sitting in the project with the codebase warm. Declaring the
|
|
9
|
+
* capability and emitting the events is safe everywhere: a host that does not
|
|
10
|
+
* speak channels ignores the experimental capability and drops the
|
|
11
|
+
* notifications without error, and the queue file stays the durable copy that
|
|
12
|
+
* the requests tool and leglas watch drain. Push is a latency improvement,
|
|
13
|
+
* never the record.
|
|
14
|
+
*/
|
|
15
|
+
/** Channel events queue into the session; the host reads this to know what they are. */
|
|
16
|
+
export declare const CHANNEL_INSTRUCTIONS: string;
|
|
17
|
+
/** Merged into the server's capabilities; presence is what registers the listener. */
|
|
18
|
+
export declare const CHANNEL_CAPABILITY: {
|
|
19
|
+
"claude/channel": {};
|
|
20
|
+
};
|
|
21
|
+
export declare const CHANNEL_POLL_MS = 2000;
|
|
22
|
+
export type ChannelEvent = {
|
|
23
|
+
content: string;
|
|
24
|
+
meta: Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* One request as one event. The content is the composed prompt, which is
|
|
28
|
+
* already written for an agent with no other context; the meta names the
|
|
29
|
+
* direction and the id so the event can be traced back to the queue.
|
|
30
|
+
*/
|
|
31
|
+
export declare function channelEvent(request: PendingRequest): ChannelEvent;
|
|
32
|
+
/**
|
|
33
|
+
* The requests that deserve an event this poll: still queued, not yet pushed.
|
|
34
|
+
*
|
|
35
|
+
* Picked-up requests are excluded because something already has them, and a
|
|
36
|
+
* re-emitted id would ask two agents to make the same change. Requests that
|
|
37
|
+
* were queued before this process started do emit once, deliberately: a
|
|
38
|
+
* session that opens onto a backlog should hear about it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function unpushed(requests: readonly PendingRequest[], pushed: ReadonlySet<string>): PendingRequest[];
|
|
41
|
+
export type Channel = {
|
|
42
|
+
stop(): void;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Poll the queue and push each new request as a channel event.
|
|
46
|
+
*
|
|
47
|
+
* Start this only after the server is connected: a notification before the
|
|
48
|
+
* transport exists throws, and nothing here is worth crashing the host's
|
|
49
|
+
* server over, so emission failures are swallowed. Delivery is fire-and-forget
|
|
50
|
+
* by the channel contract anyway; an event that never lands costs nothing,
|
|
51
|
+
* because the queue file still holds the request.
|
|
52
|
+
*/
|
|
53
|
+
export declare function startChannel(server: McpServer, options: {
|
|
54
|
+
cwd: string;
|
|
55
|
+
pollMs?: number;
|
|
56
|
+
/** Tests gate this to force overlapping polls; production reads the file. */
|
|
57
|
+
read?: (cwd: string) => Promise<PendingRequest[]>;
|
|
58
|
+
}): Channel;
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readRequests } from "leglas";
|
|
2
|
+
/**
|
|
3
|
+
* The push face, for hosts that treat an MCP server as a channel.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code's channels let a server inject events into the open session, so
|
|
6
|
+
* a change request typed into the interface can reach the agent that is
|
|
7
|
+
* already sitting in the project with the codebase warm. Declaring the
|
|
8
|
+
* capability and emitting the events is safe everywhere: a host that does not
|
|
9
|
+
* speak channels ignores the experimental capability and drops the
|
|
10
|
+
* notifications without error, and the queue file stays the durable copy that
|
|
11
|
+
* the requests tool and leglas watch drain. Push is a latency improvement,
|
|
12
|
+
* never the record.
|
|
13
|
+
*/
|
|
14
|
+
/** Channel events queue into the session; the host reads this to know what they are. */
|
|
15
|
+
export const CHANNEL_INSTRUCTIONS = 'Change requests from the Leglas interface arrive as <channel source="leglas"> events, ' +
|
|
16
|
+
"each carrying an agent-ready prompt. Before acting on one, collect the queue with the " +
|
|
17
|
+
"requests tool, which marks it picked up in the interface. Then make the change the " +
|
|
18
|
+
"prompt describes and pass clear to the requests tool once it is done. These events are " +
|
|
19
|
+
"one-way; no reply is expected.";
|
|
20
|
+
/** Merged into the server's capabilities; presence is what registers the listener. */
|
|
21
|
+
export const CHANNEL_CAPABILITY = { "claude/channel": {} };
|
|
22
|
+
export const CHANNEL_POLL_MS = 2000;
|
|
23
|
+
/**
|
|
24
|
+
* One request as one event. The content is the composed prompt, which is
|
|
25
|
+
* already written for an agent with no other context; the meta names the
|
|
26
|
+
* direction and the id so the event can be traced back to the queue.
|
|
27
|
+
*/
|
|
28
|
+
export function channelEvent(request) {
|
|
29
|
+
return {
|
|
30
|
+
content: request.prompt,
|
|
31
|
+
meta: { direction: request.title, request_id: request.id },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The requests that deserve an event this poll: still queued, not yet pushed.
|
|
36
|
+
*
|
|
37
|
+
* Picked-up requests are excluded because something already has them, and a
|
|
38
|
+
* re-emitted id would ask two agents to make the same change. Requests that
|
|
39
|
+
* were queued before this process started do emit once, deliberately: a
|
|
40
|
+
* session that opens onto a backlog should hear about it.
|
|
41
|
+
*/
|
|
42
|
+
export function unpushed(requests, pushed) {
|
|
43
|
+
return requests.filter((request) => request.status === "queued" && !pushed.has(request.id));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Poll the queue and push each new request as a channel event.
|
|
47
|
+
*
|
|
48
|
+
* Start this only after the server is connected: a notification before the
|
|
49
|
+
* transport exists throws, and nothing here is worth crashing the host's
|
|
50
|
+
* server over, so emission failures are swallowed. Delivery is fire-and-forget
|
|
51
|
+
* by the channel contract anyway; an event that never lands costs nothing,
|
|
52
|
+
* because the queue file still holds the request.
|
|
53
|
+
*/
|
|
54
|
+
export function startChannel(server, options) {
|
|
55
|
+
const read = options.read ?? readRequests;
|
|
56
|
+
const pushed = new Set();
|
|
57
|
+
let busy = false;
|
|
58
|
+
const poll = async () => {
|
|
59
|
+
// The interval does not wait for the previous run. Without this guard two
|
|
60
|
+
// overlapping polls read the same queue before either records a push, and
|
|
61
|
+
// the same request goes out twice.
|
|
62
|
+
if (busy)
|
|
63
|
+
return;
|
|
64
|
+
busy = true;
|
|
65
|
+
try {
|
|
66
|
+
await push();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
busy = false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const push = async () => {
|
|
73
|
+
const fresh = unpushed(await read(options.cwd), pushed);
|
|
74
|
+
for (const request of fresh) {
|
|
75
|
+
pushed.add(request.id);
|
|
76
|
+
try {
|
|
77
|
+
await server.server.notification({
|
|
78
|
+
method: "notifications/claude/channel",
|
|
79
|
+
params: channelEvent(request),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Not connected, or the host refused the write. The request is still
|
|
84
|
+
// in the queue for the requests tool, so losing the push loses only
|
|
85
|
+
// the immediacy.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const timer = setInterval(() => void poll(), options.pollMs ?? CHANNEL_POLL_MS);
|
|
90
|
+
void poll();
|
|
91
|
+
return { stop: () => clearInterval(timer) };
|
|
92
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { registerLeglasTools } from "./tools.js";
|
|
2
2
|
export type { LeglasTools } from "./tools.js";
|
|
3
|
+
export { CHANNEL_CAPABILITY, CHANNEL_INSTRUCTIONS, CHANNEL_POLL_MS, channelEvent, startChannel, unpushed, } from "./channel.js";
|
|
4
|
+
export type { Channel, ChannelEvent } from "./channel.js";
|
package/dist/index.js
CHANGED
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { run, runAdd, runClassify, runExplore, runInit, runKeep, runList, runNew, runRequests, } from "leglas";
|
|
1
|
+
import { run, runAdd, runClassify, runExplore, runInit, runKeep, runList, runNew, runRequests, runShow, } from "leglas";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
async function capture(invoke) {
|
|
4
4
|
const lines = [];
|
|
@@ -31,7 +31,9 @@ export function registerLeglasTools(server, options) {
|
|
|
31
31
|
server.registerTool("start", {
|
|
32
32
|
title: "Start the Leglas viewer",
|
|
33
33
|
description: "Boot the Leglas server for this project and return the interface URL. " +
|
|
34
|
-
"Idempotent per session: calling it again returns the running viewer."
|
|
34
|
+
"Idempotent per session: calling it again returns the running viewer. " +
|
|
35
|
+
"When building a set, call this first and give the user the URL before " +
|
|
36
|
+
"any direction exists: the rail updates live, so they watch the set fill in.",
|
|
35
37
|
inputSchema: {
|
|
36
38
|
port: z.number().int().min(0).max(65535).optional()
|
|
37
39
|
.describe("Port for Leglas itself; defaults to 4100, next free if taken."),
|
|
@@ -53,6 +55,8 @@ export function registerLeglasTools(server, options) {
|
|
|
53
55
|
server.registerTool("add", {
|
|
54
56
|
title: "Register a preview",
|
|
55
57
|
description: "Register a design direction on this machine so it appears in the rail. " +
|
|
58
|
+
"The rail picks it up within seconds, so when building a set register " +
|
|
59
|
+
"each direction as it lands rather than the whole set at the end. " +
|
|
56
60
|
"Use branch for a direction that lives on its own git branch.",
|
|
57
61
|
inputSchema: {
|
|
58
62
|
title: z.string().min(1).describe("Unique title; identifies the preview."),
|
|
@@ -64,13 +68,28 @@ export function registerLeglasTools(server, options) {
|
|
|
64
68
|
.describe("Back the preview with a checkout of this git branch."),
|
|
65
69
|
file: z.string().optional()
|
|
66
70
|
.describe("Project-relative HTML file for Leglas to serve itself; no dev server needed."),
|
|
71
|
+
basedOn: z.string().optional()
|
|
72
|
+
.describe("Title of the direction this is a variant of; the rail groups the family."),
|
|
67
73
|
},
|
|
68
|
-
}, async ({ title, url, note, tags, branch, file }) => capture((deps) => runAdd({ preview: { title, url, note, tags, branch, file }, json: true, cwd }, deps)));
|
|
74
|
+
}, async ({ title, url, note, tags, branch, file, basedOn }) => capture((deps) => runAdd({ preview: { title, url, note, tags, branch, file, basedOn }, json: true, cwd }, deps)));
|
|
69
75
|
server.registerTool("list", {
|
|
70
76
|
title: "List previews",
|
|
71
77
|
description: "Every preview, shared and local, with its URL and backing branch if any.",
|
|
72
78
|
inputSchema: {},
|
|
73
79
|
}, async () => capture((deps) => runList({ json: true, cwd }, deps)));
|
|
80
|
+
server.registerTool("show", {
|
|
81
|
+
title: "Inspect one direction",
|
|
82
|
+
description: "Everything Leglas knows about one direction: its full entry, the source file behind " +
|
|
83
|
+
"it, the variants based on it, the directions it is being compared against, and any " +
|
|
84
|
+
"change requests still pending on it. Call this when handed a direction's reference " +
|
|
85
|
+
"block.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
title: z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.describe("The direction's title as the config spells it, not a renamed display name."),
|
|
91
|
+
},
|
|
92
|
+
}, async ({ title }) => capture((deps) => runShow({ title, json: true, cwd }, deps)));
|
|
74
93
|
server.registerTool("classify", {
|
|
75
94
|
title: "Decide where a direction should live",
|
|
76
95
|
description: "Before writing a direction, declare what it will touch and learn whether it can be " +
|
|
@@ -86,14 +105,20 @@ export function registerLeglasTools(server, options) {
|
|
|
86
105
|
},
|
|
87
106
|
}, async ({ changes }) => capture((deps) => runClassify({ changes, json: true, cwd }, deps)));
|
|
88
107
|
server.registerTool("explore", {
|
|
89
|
-
title: "
|
|
90
|
-
description: "
|
|
91
|
-
"
|
|
108
|
+
title: "Brief an exploration",
|
|
109
|
+
description: "What a set for a surface needs and how it registers here. Directions must genuinely " +
|
|
110
|
+
"disagree; with basedOn, variants of that direction must not. The designs themselves are " +
|
|
111
|
+
"yours. Run before building a set.",
|
|
92
112
|
inputSchema: {
|
|
93
113
|
surface: z.string().min(1),
|
|
94
|
-
count: z.number().int().min(1).max(24).optional().describe("How many
|
|
114
|
+
count: z.number().int().min(1).max(24).optional().describe("How many; default 3."),
|
|
115
|
+
basedOn: z
|
|
116
|
+
.string()
|
|
117
|
+
.min(1)
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("An existing direction's title: ask for variants of it instead of new directions."),
|
|
95
120
|
},
|
|
96
|
-
}, async ({ surface, count }) => capture((deps) => runExplore({ surface, count: count ?? 3, json: true }, deps)));
|
|
121
|
+
}, async ({ surface, count, basedOn }) => capture((deps) => runExplore({ surface, count: count ?? 3, basedOn: basedOn ?? null, json: true }, deps)));
|
|
97
122
|
server.registerTool("scaffold", {
|
|
98
123
|
title: "Scaffold a branch point",
|
|
99
124
|
description: "Create a switcher and a first direction for a surface under .leglas/variants/. " +
|
|
@@ -116,7 +141,9 @@ export function registerLeglasTools(server, options) {
|
|
|
116
141
|
server.registerTool("requests", {
|
|
117
142
|
title: "Collect change requests",
|
|
118
143
|
description: "Pending change requests made from the interface, each naming the direction and the file " +
|
|
119
|
-
"behind it.
|
|
144
|
+
"behind it. Call this when starting work in a project that uses Leglas, and again before " +
|
|
145
|
+
"changing any direction: the user may have described the change from the interface while " +
|
|
146
|
+
"you worked, and collecting marks it picked up there. Pass clear once they are done.",
|
|
120
147
|
inputSchema: {
|
|
121
148
|
clear: z.boolean().optional(),
|
|
122
149
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leglas-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Stdio MCP server exposing Leglas to agent hosts that cannot run a shell.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,9 +15,15 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
17
17
|
"zod": "^3.25.76",
|
|
18
|
-
"leglas": "^0.
|
|
18
|
+
"leglas": "^0.2.0"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/FredAmartey/leglas.git",
|
|
24
|
+
"directory": "packages/mcp"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/FredAmartey/leglas#readme",
|
|
21
27
|
"engines": {
|
|
22
28
|
"node": ">=24"
|
|
23
29
|
},
|