@flareum/mcp 0.2.1 → 0.2.3
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/README.md +78 -10
- package/dist/cli.js +54 -23
- package/dist/client.d.ts +4 -0
- package/dist/client.js +15 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +15 -1
- package/dist/pull.d.ts +3 -0
- package/dist/pull.js +23 -2
- package/dist/server.js +4 -1
- package/dist/watch.d.ts +2 -0
- package/dist/watch.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ typing literal values.
|
|
|
13
13
|
2. Copy the command it shows you. It looks like this:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
claude mcp add flareum --env FLAREUM_TOKEN=pk_… -- npx -y @flareum/mcp
|
|
16
|
+
claude mcp add flareum --env FLAREUM_TOKEN=pk_… -- npx -y -p @flareum/mcp flareum-mcp
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
3. Run it in **your project's** terminal — the repo where you write CSS/SCSS, not the Flareum repo.
|
|
@@ -29,7 +29,7 @@ The key is shown once and cannot be retrieved again. If you lose it, revoke it a
|
|
|
29
29
|
the newest:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx -y @flareum/mcp@latest --help
|
|
32
|
+
npx -y -p @flareum/mcp@latest flareum --help
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Naming `@latest` is what refreshes the cache. Then restart your editor session — a running MCP
|
|
@@ -39,7 +39,7 @@ Check which version you are actually on:
|
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
npm view @flareum/mcp version # the newest published
|
|
42
|
-
npx -y @flareum/mcp@latest --version
|
|
42
|
+
npx -y -p @flareum/mcp@latest flareum --version
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
If you installed it globally instead of through `npx`:
|
|
@@ -50,6 +50,24 @@ npm install -g @flareum/mcp@latest
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
+
## Removing it
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
claude mcp remove flareum
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run it in the same project directory you added it from — `claude mcp add` registers the server for
|
|
60
|
+
that directory, so a removal elsewhere finds nothing. `claude mcp list` shows what is registered. If
|
|
61
|
+
you added it with `-s user` or `-s project`, pass the same scope to remove it.
|
|
62
|
+
|
|
63
|
+
That takes the key with it: it lived in the `--env FLAREUM_TOKEN=…` of the entry it just deleted.
|
|
64
|
+
There is nothing else on disk unless you wrote a `.flareum/config.json` yourself.
|
|
65
|
+
|
|
66
|
+
**The key still works.** Removing the server is a local change — anyone holding that string can
|
|
67
|
+
still read the project's tokens. If it leaked, or the machine is gone, revoke it too.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
53
71
|
## What your agent can do with it
|
|
54
72
|
|
|
55
73
|
Ask in plain language — the agent picks the tool:
|
|
@@ -80,8 +98,8 @@ The tools tell an agent what a token is CALLED. The stylesheets that give those
|
|
|
80
98
|
from a Push, and this fetches them:
|
|
81
99
|
|
|
82
100
|
```bash
|
|
83
|
-
npx flareum pull
|
|
84
|
-
npx flareum watch
|
|
101
|
+
npx -y -p @flareum/mcp flareum pull # into src/styles/flareum
|
|
102
|
+
npx -y -p @flareum/mcp flareum watch # and again every time you Push in Flareum
|
|
85
103
|
```
|
|
86
104
|
|
|
87
105
|
Then import them once, in your global stylesheet:
|
|
@@ -108,6 +126,40 @@ nothing; only a Push does.
|
|
|
108
126
|
|
|
109
127
|
---
|
|
110
128
|
|
|
129
|
+
## Revoking a key
|
|
130
|
+
|
|
131
|
+
A key grants read access to one project's tokens until you revoke it. Revoke as soon as one is
|
|
132
|
+
pasted into a chat, a ticket, a screenshot, or a repository — and when a laptop or a contractor
|
|
133
|
+
leaves.
|
|
134
|
+
|
|
135
|
+
**1. In Flareum**, open **Project settings → Connect**. Each connected editor is a row showing the
|
|
136
|
+
last four characters of its key, when it was added and when it was last used. Click the delete
|
|
137
|
+
button on the row you want gone.
|
|
138
|
+
|
|
139
|
+
It takes effect immediately: every later call with that key answers `401`, and the key cannot be
|
|
140
|
+
un-revoked. A revoked row disappears from the list rather than lingering as history — it is not a
|
|
141
|
+
connection any more.
|
|
142
|
+
|
|
143
|
+
If you cannot tell two rows apart, the key hint matches the last four characters of the key the
|
|
144
|
+
editor is using, and **Last used** tells you which one is live.
|
|
145
|
+
|
|
146
|
+
**2. On the machine that had it**, remove what is still holding the key — revoking stops it
|
|
147
|
+
working, it does not delete the copy:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
claude mcp remove flareum # the entry, and the key inside it
|
|
151
|
+
rm .flareum/config.json # only if you wrote one by hand
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Skip this and everything keeps trying the dead key, which reads as a broken integration rather than
|
|
155
|
+
a revoked one.
|
|
156
|
+
|
|
157
|
+
**3. To reconnect**, mint a new key from the same Connect screen and run the command it gives you.
|
|
158
|
+
Keys are per editor, not per person — connecting a second machine means minting a second key, so
|
|
159
|
+
losing one never means rotating the other.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
111
163
|
## Configuration
|
|
112
164
|
|
|
113
165
|
| Variable | |
|
|
@@ -115,10 +167,24 @@ nothing; only a Push does.
|
|
|
115
167
|
| `FLAREUM_TOKEN` | **required.** The project is read from the key itself. |
|
|
116
168
|
| `FLAREUM_API` | optional. Defaults to production. Set it to point at a local Flareum. |
|
|
117
169
|
| `FLAREUM_PROJECT` | optional. Overrides the project in the key. |
|
|
170
|
+
| `FLAREUM_OUT` | optional. Where stylesheets are written. Same meaning as `out` in the config file, which it beats. |
|
|
171
|
+
| `FLAREUM_AUTO_PULL` | optional. `off` stops the first-connection pull below. Anything else leaves it on. |
|
|
172
|
+
|
|
173
|
+
On start it writes `.claude/skills/flareum/` into the working directory — the rules your agent needs
|
|
174
|
+
to apply the tokens correctly. `SKILL.md` is the workflow and the router: how to read a search
|
|
175
|
+
result, and where to go for the area being worked in. Each `references/*.md` beside it is one such
|
|
176
|
+
area — `naming.md` for how a token name is built, `typography.md` for applying a text style with
|
|
177
|
+
its class rather than the mixin behind it. The whole folder is rewritten every run, so do not edit
|
|
178
|
+
it, and it prints where it wrote (or why it could not) on stderr.
|
|
179
|
+
|
|
180
|
+
On the first connection it also **pulls the stylesheets**, once. It looks for the folder this project
|
|
181
|
+
already keeps its styles in — `src/styles`, `app/styles`, `scss`, and the rest — and writes them to
|
|
182
|
+
`<that folder>/flareum`, falling back to `styles/flareum` in a project that has none. It says on
|
|
183
|
+
stderr where they landed.
|
|
118
184
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
and
|
|
185
|
+
It runs only when that folder does not exist yet. After that the files are yours: `flareum pull`
|
|
186
|
+
updates them on demand and `flareum watch` on every Push, both into the same folder. A failed pull
|
|
187
|
+
is reported and nothing else — the token tools work without it. `FLAREUM_AUTO_PULL=off` turns it off.
|
|
122
188
|
|
|
123
189
|
---
|
|
124
190
|
|
|
@@ -126,7 +192,7 @@ and it prints where it wrote (or why it could not) on stderr.
|
|
|
126
192
|
|
|
127
193
|
| What you see | Cause |
|
|
128
194
|
|---|---|
|
|
129
|
-
| The server fails to connect / closes immediately | The package is not installed. Check `npx -y @flareum/mcp` resolves — if it 404s, it has not been published yet. |
|
|
195
|
+
| The server fails to connect / closes immediately | The package is not installed. Check `npx -y -p @flareum/mcp flareum-mcp` resolves — if it 404s, it has not been published yet. |
|
|
130
196
|
| A command the docs describe is not recognised | `npx` is running a cached older copy. Re-run it as `@latest` — see Updating. |
|
|
131
197
|
| The agent types literal values anyway | The skill did not land. Look for `[flareum] skill written to …` in the MCP server's stderr. |
|
|
132
198
|
| Values labelled with long ids instead of mode names | The Flareum server is older than the client. Restart it. |
|
|
@@ -181,7 +247,9 @@ to fetch.
|
|
|
181
247
|
| `src/tools.ts` | The TEXT each tool returns. This is what the agent reads, so its wording is guarded. |
|
|
182
248
|
| `src/server.ts` | MCP stdio transport. Glue only — no behaviour lives here. |
|
|
183
249
|
| `src/skill.ts` | Writes the skill into the consuming project. |
|
|
184
|
-
| `
|
|
250
|
+
| `src/first-pull.ts` | Chooses the styles folder, and pulls into it on a first connection. |
|
|
251
|
+
| `skill/SKILL.md` | The router shipped to the agent: the workflow, and where each rule lives. |
|
|
252
|
+
| `skill/references/` | One page per rule area. Every file here must be linked from SKILL.md. |
|
|
185
253
|
|
|
186
254
|
`server.ts` has no tests because it holds no logic; everything it calls is covered. Imports inside
|
|
187
255
|
the package carry a `.js` extension — TypeScript does not rewrite specifiers, and Node's ESM loader
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { FlareumClient } from './client.js';
|
|
4
|
+
import { FlareumApiError, FlareumClient } from './client.js';
|
|
5
5
|
import { CONFIG_PATH, cliErrorReport, parseArgs, resolveConfig } from './config.js';
|
|
6
|
-
import { pullReport, pullStyles } from './pull.js';
|
|
7
|
-
import {
|
|
6
|
+
import { isRetryable, pullReport, pullStyles } from './pull.js';
|
|
7
|
+
import { watchIntervalMs, watchPublished, watchStartedReport } from './watch.js';
|
|
8
8
|
const DEFAULT_OUT = 'src/styles/flareum';
|
|
9
9
|
// Read rather than restated: a hardcoded version is wrong the moment `npm version` runs.
|
|
10
10
|
const VERSION = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
@@ -19,28 +19,42 @@ const HELP = `flareum — read a Flareum project's design tokens
|
|
|
19
19
|
|
|
20
20
|
The key is taken from --token, then FLAREUM_TOKEN, then "token" in ${CONFIG_PATH}.
|
|
21
21
|
Mint one in the project's Connect screen in Flareum.`;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// An absent config is normal; one that fails to parse is a typo the user is staring at, and
|
|
23
|
+
// collapsing both to null reported "No Flareum key" about the file holding the key.
|
|
24
|
+
const readConfigFile = async () => {
|
|
25
|
+
const raw = await readFile(join(process.cwd(), CONFIG_PATH), 'utf8').catch(() => null);
|
|
26
|
+
if (raw === null)
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`Could not read ${CONFIG_PATH} — ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
24
35
|
const COMMANDS = ['pull', 'watch'];
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
if (flags.version || command === '-v') {
|
|
28
|
-
console.log(VERSION);
|
|
29
|
-
process.exit(0);
|
|
30
|
-
}
|
|
31
|
-
if (flags.help || command === '-h') {
|
|
32
|
-
console.log(HELP);
|
|
33
|
-
process.exit(0);
|
|
34
|
-
}
|
|
35
|
-
if (!COMMANDS.includes(command)) {
|
|
36
|
-
console.log(HELP);
|
|
37
|
-
process.exit(command ? 1 : 0);
|
|
38
|
-
}
|
|
36
|
+
// Everything the user can get wrong lives inside this one try, argument parsing included: a throw
|
|
37
|
+
// above it printed a Node stack trace, which is the failure cliErrorReport exists to prevent.
|
|
39
38
|
try {
|
|
39
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
40
|
+
// Read from flags, not `command`: parseArgs takes every leading `--x` as a flag, so checking the
|
|
41
|
+
// command alone printed the help instead.
|
|
42
|
+
if (flags.version || command === '-v') {
|
|
43
|
+
console.log(VERSION);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
if (flags.help || command === '-h') {
|
|
47
|
+
console.log(HELP);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
if (!COMMANDS.includes(command)) {
|
|
51
|
+
console.log(HELP);
|
|
52
|
+
process.exit(command ? 1 : 0);
|
|
53
|
+
}
|
|
40
54
|
const config = resolveConfig({ flags, env: process.env, file: await readConfigFile() });
|
|
41
55
|
const outDir = config.out ?? DEFAULT_OUT;
|
|
42
|
-
const root = resolve(process.cwd(), outDir);
|
|
43
56
|
const client = new FlareumClient({ token: config.token, api: config.api });
|
|
57
|
+
const root = resolve(process.cwd(), outDir);
|
|
44
58
|
const write = async (path, contents) => {
|
|
45
59
|
const target = join(root, path);
|
|
46
60
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -52,13 +66,30 @@ try {
|
|
|
52
66
|
return result;
|
|
53
67
|
};
|
|
54
68
|
if (command === 'watch') {
|
|
55
|
-
const intervalMs =
|
|
69
|
+
const intervalMs = watchIntervalMs(flags.interval);
|
|
56
70
|
console.log(watchStartedReport(outDir, intervalMs));
|
|
57
71
|
let running = true;
|
|
58
72
|
process.on('SIGINT', () => { running = false; console.log('\nStopped watching.'); process.exit(0); });
|
|
59
73
|
await watchPublished({
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
// watch treats null as "nothing published yet, wait quietly". Without this the CLI threw
|
|
75
|
+
// the 409 instead, so a never-pushed project printed an error on every tick forever.
|
|
76
|
+
publishedVersion: async () => {
|
|
77
|
+
try {
|
|
78
|
+
return (await client.published()).publishedVersionId;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof FlareumApiError && error.code === 'NOT_PUBLISHED')
|
|
82
|
+
return null;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
// Throwing is what stops watch marking the version done: a file that failed for a
|
|
87
|
+
// retryable reason must be fetched again, not skipped until the next push.
|
|
88
|
+
onPublish: async () => {
|
|
89
|
+
const result = await runPull();
|
|
90
|
+
if (isRetryable(result))
|
|
91
|
+
throw new Error(`${result.failed.length} file(s) failed — retrying on the next tick.`);
|
|
92
|
+
},
|
|
62
93
|
onError: error => console.error(error instanceof Error ? error.message : String(error)),
|
|
63
94
|
sleep: ms => new Promise(done => { setTimeout(done, ms); }),
|
|
64
95
|
keepGoing: () => running,
|
package/dist/client.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class FlareumClient {
|
|
|
22
22
|
catalog(updatedSince?: string): Promise<CatalogResponse>;
|
|
23
23
|
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
24
24
|
/** The stylesheets the last push published. They exist only after a push, never from a save. */
|
|
25
|
+
published(): Promise<PublishedResponse>;
|
|
25
26
|
files(): Promise<FilesResponse>;
|
|
26
27
|
file(path: string): Promise<string>;
|
|
27
28
|
variable(path: string): Promise<VariableResponse>;
|
|
@@ -69,6 +70,9 @@ export type FilesResponse = {
|
|
|
69
70
|
publishedVersionId: string;
|
|
70
71
|
files: string[];
|
|
71
72
|
};
|
|
73
|
+
export type PublishedResponse = {
|
|
74
|
+
publishedVersionId: string;
|
|
75
|
+
};
|
|
72
76
|
export type VariableMode = {
|
|
73
77
|
id: string;
|
|
74
78
|
name: string;
|
package/dist/client.js
CHANGED
|
@@ -60,6 +60,8 @@ export class FlareumClient {
|
|
|
60
60
|
const url = `${this.#api}/api/v1/projects/${this.projectId}${path}`;
|
|
61
61
|
const response = await this.#fetch(url, {
|
|
62
62
|
headers: { Authorization: `Bearer ${this.#token}`, Accept: 'text/plain' },
|
|
63
|
+
}).catch(error => {
|
|
64
|
+
throw new Error(`Could not reach ${this.#api} — ${error instanceof Error ? error.message : String(error)}`);
|
|
63
65
|
});
|
|
64
66
|
if (response.ok)
|
|
65
67
|
return response.text();
|
|
@@ -75,6 +77,19 @@ export class FlareumClient {
|
|
|
75
77
|
return this.#get('/search', { q: query, ...(limit ? { limit: String(limit) } : {}) });
|
|
76
78
|
}
|
|
77
79
|
/** The stylesheets the last push published. They exist only after a push, never from a save. */
|
|
80
|
+
// The watch's every-tick question. `/files` answers it too, but lists the whole bucket first — so
|
|
81
|
+
// ask the cheap endpoint, and fall back for a server older than it.
|
|
82
|
+
async published() {
|
|
83
|
+
try {
|
|
84
|
+
return await this.#get('/published');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!(error instanceof FlareumApiError) || error.code !== 'HTTP_404')
|
|
88
|
+
throw error;
|
|
89
|
+
const { publishedVersionId } = await this.files();
|
|
90
|
+
return { publishedVersionId };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
78
93
|
files() {
|
|
79
94
|
return this.#get('/files');
|
|
80
95
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ export declare const CONFIG_PATH = ".flareum/config.json";
|
|
|
12
12
|
export declare class MissingToken extends Error {
|
|
13
13
|
constructor();
|
|
14
14
|
}
|
|
15
|
+
export declare class BareFlag extends Error {
|
|
16
|
+
constructor(name: string);
|
|
17
|
+
}
|
|
15
18
|
/** A flag beats the environment beats the file — nearest to the command wins. */
|
|
16
19
|
export declare const resolveConfig: ({ flags, env, file }: ConfigSources) => Resolved;
|
|
17
20
|
/** `--out dir --token x` and `--out=dir`, both. Anything else is a positional. */
|
package/dist/config.js
CHANGED
|
@@ -11,17 +11,29 @@ export class MissingToken extends Error {
|
|
|
11
11
|
this.name = 'MissingToken';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
export class BareFlag extends Error {
|
|
15
|
+
constructor(name) {
|
|
16
|
+
super(`--${name} needs a value, e.g. --${name} <value> or --${name}=<value>.`);
|
|
17
|
+
this.name = 'BareFlag';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
14
20
|
/** A flag beats the environment beats the file — nearest to the command wins. */
|
|
15
21
|
export const resolveConfig = ({ flags, env, file }) => {
|
|
16
22
|
const token = flags.token || env.FLAREUM_TOKEN || file?.token;
|
|
17
23
|
if (!token)
|
|
18
24
|
throw new MissingToken();
|
|
25
|
+
// The file's host travels only with the file's token. A checked-in config is somebody else's
|
|
26
|
+
// input, and pointing an ambient FLAREUM_TOKEN at their host is how the key leaves the machine.
|
|
27
|
+
const fileApi = file?.token && token === file.token ? file.api : undefined;
|
|
19
28
|
return {
|
|
20
29
|
token,
|
|
21
|
-
api: flags.api || env.FLAREUM_API ||
|
|
30
|
+
api: flags.api || env.FLAREUM_API || fileApi,
|
|
22
31
|
out: flags.out || file?.out,
|
|
23
32
|
};
|
|
24
33
|
};
|
|
34
|
+
// Flags that mean nothing without a value. --version / --help are deliberately absent: a bare one
|
|
35
|
+
// is the whole point of them.
|
|
36
|
+
const VALUE_FLAGS = new Set(['out', 'token', 'api', 'interval']);
|
|
25
37
|
/** `--out dir --token x` and `--out=dir`, both. Anything else is a positional. */
|
|
26
38
|
export const parseArgs = (argv) => {
|
|
27
39
|
const flags = {};
|
|
@@ -42,6 +54,8 @@ export const parseArgs = (argv) => {
|
|
|
42
54
|
flags[name] = next;
|
|
43
55
|
i += 1;
|
|
44
56
|
}
|
|
57
|
+
else if (VALUE_FLAGS.has(name))
|
|
58
|
+
throw new BareFlag(name);
|
|
45
59
|
else
|
|
46
60
|
flags[name] = 'true';
|
|
47
61
|
}
|
package/dist/pull.d.ts
CHANGED
|
@@ -5,8 +5,11 @@ export type PullResult = {
|
|
|
5
5
|
failed: Array<{
|
|
6
6
|
path: string;
|
|
7
7
|
reason: string;
|
|
8
|
+
permanent: boolean;
|
|
8
9
|
}>;
|
|
9
10
|
};
|
|
10
11
|
export type Writer = (path: string, contents: string) => Promise<void>;
|
|
12
|
+
export declare const isWritablePath: (path: string) => boolean;
|
|
11
13
|
export declare const pullStyles: (client: Pick<FlareumClient, "files" | "file">, write: Writer) => Promise<PullResult>;
|
|
14
|
+
export declare const isRetryable: ({ failed }: PullResult) => boolean;
|
|
12
15
|
export declare const pullReport: ({ publishedVersionId, written, failed }: PullResult, outDir: string) => string;
|
package/dist/pull.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
const STYLESHEET = /\.s?css$/i;
|
|
2
|
+
// The server says where to write, and a --api flag lets anyone be the server. Refused rather than
|
|
3
|
+
// normalised: repairing a path that climbs out of the output directory is how traversal gets through.
|
|
4
|
+
export const isWritablePath = (path) => {
|
|
5
|
+
if (!path || !STYLESHEET.test(path))
|
|
6
|
+
return false;
|
|
7
|
+
if (path.startsWith('/') || path.includes('\\') || path.includes('\0'))
|
|
8
|
+
return false;
|
|
9
|
+
if (/^[a-zA-Z]:/.test(path))
|
|
10
|
+
return false;
|
|
11
|
+
return !path.split('/').includes('..');
|
|
12
|
+
};
|
|
1
13
|
// Copies the ARTIFACT a push generated; never rebuilds CSS from the catalog, so a name here and a
|
|
2
14
|
// name the API reports cannot disagree.
|
|
3
15
|
export const pullStyles = async (client, write) => {
|
|
@@ -6,15 +18,24 @@ export const pullStyles = async (client, write) => {
|
|
|
6
18
|
const failed = [];
|
|
7
19
|
for (const path of files)
|
|
8
20
|
try {
|
|
21
|
+
if (!isWritablePath(path)) {
|
|
22
|
+
failed.push({ path, reason: 'refused — not a stylesheet inside the output directory',
|
|
23
|
+
permanent: true });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
9
26
|
await write(path, await client.file(path));
|
|
10
27
|
written.push(path);
|
|
11
28
|
}
|
|
12
29
|
catch (error) {
|
|
13
|
-
// One unreadable file must not lose the rest, and must not be reported as written.
|
|
14
|
-
|
|
30
|
+
// One unreadable file must not lose the rest, and must not be reported as written. Marked
|
|
31
|
+
// retryable: unlike a refusal, a read or write that failed once can succeed next time.
|
|
32
|
+
failed.push({ path, reason: error instanceof Error ? error.message : String(error),
|
|
33
|
+
permanent: false });
|
|
15
34
|
}
|
|
16
35
|
return { publishedVersionId, written, failed };
|
|
17
36
|
};
|
|
37
|
+
// A refusal will fail identically forever; anything else is worth another attempt.
|
|
38
|
+
export const isRetryable = ({ failed }) => failed.some(f => !f.permanent);
|
|
18
39
|
export const pullReport = ({ publishedVersionId, written, failed }, outDir) => {
|
|
19
40
|
const lines = [`Pulled ${written.length} stylesheet(s) from version ${publishedVersionId} into ${outDir}`];
|
|
20
41
|
for (const path of written)
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Transport glue only. Every behaviour worth guarding lives in client.ts and tools.ts, which have
|
|
3
3
|
// no SDK dependency and are tested without one.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
7
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -12,7 +13,9 @@ const client = new FlareumClient({
|
|
|
12
13
|
api: process.env.FLAREUM_API,
|
|
13
14
|
projectId: process.env.FLAREUM_PROJECT,
|
|
14
15
|
});
|
|
15
|
-
|
|
16
|
+
// Read, never restated: the hardcoded 0.1.0 kept reporting itself from a 0.2.1 package.
|
|
17
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
18
|
+
const server = new Server({ name: 'flareum', version }, { capabilities: { tools: {} } });
|
|
16
19
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
17
20
|
tools: [
|
|
18
21
|
{
|
package/dist/watch.d.ts
CHANGED
|
@@ -7,5 +7,7 @@ export type WatchDeps = {
|
|
|
7
7
|
keepGoing: () => boolean;
|
|
8
8
|
};
|
|
9
9
|
export declare const DEFAULT_INTERVAL_MS = 15000;
|
|
10
|
+
export declare const MIN_INTERVAL_MS = 2000;
|
|
11
|
+
export declare const watchIntervalMs: (seconds: string | undefined) => number;
|
|
10
12
|
export declare const watchPublished: ({ publishedVersion, onPublish, onError, sleep, keepGoing }: WatchDeps, intervalMs?: number) => Promise<void>;
|
|
11
13
|
export declare const watchStartedReport: (outDir: string, intervalMs: number) => string;
|
package/dist/watch.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export const DEFAULT_INTERVAL_MS = 15_000;
|
|
2
|
+
export const MIN_INTERVAL_MS = 2_000;
|
|
3
|
+
// A floor, not a default: `--interval 0.001` asked for a thousand polls a second, which the API
|
|
4
|
+
// answers with 429s rather than data.
|
|
5
|
+
export const watchIntervalMs = (seconds) => {
|
|
6
|
+
const asked = Number(seconds) * 1000;
|
|
7
|
+
if (!Number.isFinite(asked) || asked <= 0)
|
|
8
|
+
return DEFAULT_INTERVAL_MS;
|
|
9
|
+
return Math.max(asked, MIN_INTERVAL_MS);
|
|
10
|
+
};
|
|
2
11
|
// Re-pulls when the PUBLISHED version changes — a push, never a save, so a designer mid-edit cannot
|
|
3
12
|
// rewrite a file the developer is looking at.
|
|
4
13
|
export const watchPublished = async ({ publishedVersion, onPublish, onError, sleep, keepGoing }, intervalMs = DEFAULT_INTERVAL_MS) => {
|