@flareum/mcp 0.1.0 → 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/README.md +34 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +62 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +23 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +58 -0
- package/dist/pull.d.ts +12 -0
- package/dist/pull.js +30 -0
- package/dist/watch.d.ts +11 -0
- package/dist/watch.js +24 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -47,6 +47,40 @@ so, because "no results" reads as "no such token exists" and gets acted on.
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## Pulling the stylesheets
|
|
51
|
+
|
|
52
|
+
The tools tell an agent what a token is CALLED. The stylesheets that give those names a value come
|
|
53
|
+
from a Push, and this fetches them:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx flareum pull # into src/styles/flareum
|
|
57
|
+
npx flareum watch # and again every time you Push in Flareum
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then import them once, in your global stylesheet:
|
|
61
|
+
|
|
62
|
+
```scss
|
|
63
|
+
@use './styles/flareum/main';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It copies the files a Push generated — it never rebuilds CSS from the token data, so a name in the
|
|
67
|
+
file and a name the tools report cannot drift.
|
|
68
|
+
|
|
69
|
+
`watch` polls for a new published version and pulls when one appears. A save in Flareum changes
|
|
70
|
+
nothing; only a Push does.
|
|
71
|
+
|
|
72
|
+
### Where the key comes from
|
|
73
|
+
|
|
74
|
+
`--token`, then `FLAREUM_TOKEN`, then `token` in `.flareum/config.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{ "token": "pk_…", "out": "src/styles/flareum" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Add `.flareum/` to your `.gitignore`** — that file holds a key.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
50
84
|
## Configuration
|
|
51
85
|
|
|
52
86
|
| Variable | |
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { FlareumClient } from './client.js';
|
|
5
|
+
import { CONFIG_PATH, cliErrorReport, parseArgs, resolveConfig } from './config.js';
|
|
6
|
+
import { pullReport, pullStyles } from './pull.js';
|
|
7
|
+
import { DEFAULT_INTERVAL_MS, watchPublished, watchStartedReport } from './watch.js';
|
|
8
|
+
const DEFAULT_OUT = 'src/styles/flareum';
|
|
9
|
+
const HELP = `flareum — read a Flareum project's design tokens
|
|
10
|
+
|
|
11
|
+
flareum pull [--out <dir>] [--token pk_…] [--api <url>]
|
|
12
|
+
Write the stylesheets the last push published into <dir>
|
|
13
|
+
(default: ${DEFAULT_OUT}).
|
|
14
|
+
|
|
15
|
+
flareum watch [--out <dir>] [--interval <seconds>]
|
|
16
|
+
Stay running and pull again every time you Push in Flareum.
|
|
17
|
+
|
|
18
|
+
The key is taken from --token, then FLAREUM_TOKEN, then "token" in ${CONFIG_PATH}.
|
|
19
|
+
Mint one in the project's Connect screen in Flareum.`;
|
|
20
|
+
const readConfigFile = async () => readFile(join(process.cwd(), CONFIG_PATH), 'utf8').then(JSON.parse).catch(() => null);
|
|
21
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
22
|
+
const COMMANDS = ['pull', 'watch'];
|
|
23
|
+
if (!COMMANDS.includes(command)) {
|
|
24
|
+
console.log(HELP);
|
|
25
|
+
process.exit(command ? 1 : 0);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const config = resolveConfig({ flags, env: process.env, file: await readConfigFile() });
|
|
29
|
+
const outDir = config.out ?? DEFAULT_OUT;
|
|
30
|
+
const root = resolve(process.cwd(), outDir);
|
|
31
|
+
const client = new FlareumClient({ token: config.token, api: config.api });
|
|
32
|
+
const write = async (path, contents) => {
|
|
33
|
+
const target = join(root, path);
|
|
34
|
+
await mkdir(dirname(target), { recursive: true });
|
|
35
|
+
await writeFile(target, contents, 'utf8');
|
|
36
|
+
};
|
|
37
|
+
const runPull = async () => {
|
|
38
|
+
const result = await pullStyles(client, write);
|
|
39
|
+
console.log(pullReport(result, outDir));
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
if (command === 'watch') {
|
|
43
|
+
const intervalMs = Number(flags.interval) > 0 ? Number(flags.interval) * 1000 : DEFAULT_INTERVAL_MS;
|
|
44
|
+
console.log(watchStartedReport(outDir, intervalMs));
|
|
45
|
+
let running = true;
|
|
46
|
+
process.on('SIGINT', () => { running = false; console.log('\nStopped watching.'); process.exit(0); });
|
|
47
|
+
await watchPublished({
|
|
48
|
+
publishedVersion: async () => (await client.files()).publishedVersionId,
|
|
49
|
+
onPublish: async () => { await runPull(); },
|
|
50
|
+
onError: error => console.error(error instanceof Error ? error.message : String(error)),
|
|
51
|
+
sleep: ms => new Promise(done => { setTimeout(done, ms); }),
|
|
52
|
+
keepGoing: () => running,
|
|
53
|
+
}, intervalMs);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
const result = await runPull();
|
|
57
|
+
process.exit(result.failed.length ? 1 : 0);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error(cliErrorReport(error));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare class FlareumClient {
|
|
|
21
21
|
constructor({ token, api, projectId, fetchImpl }: ClientOptions);
|
|
22
22
|
catalog(updatedSince?: string): Promise<CatalogResponse>;
|
|
23
23
|
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
24
|
+
/** The stylesheets the last push published. They exist only after a push, never from a save. */
|
|
25
|
+
files(): Promise<FilesResponse>;
|
|
26
|
+
file(path: string): Promise<string>;
|
|
24
27
|
variable(path: string): Promise<VariableResponse>;
|
|
25
28
|
}
|
|
26
29
|
export type CatalogResponse = {
|
|
@@ -62,6 +65,10 @@ export type SearchResponse = {
|
|
|
62
65
|
truncated: boolean;
|
|
63
66
|
candidates: Candidate[];
|
|
64
67
|
};
|
|
68
|
+
export type FilesResponse = {
|
|
69
|
+
publishedVersionId: string;
|
|
70
|
+
files: string[];
|
|
71
|
+
};
|
|
65
72
|
export type VariableMode = {
|
|
66
73
|
id: string;
|
|
67
74
|
name: string;
|
package/dist/client.js
CHANGED
|
@@ -36,8 +36,12 @@ export class FlareumClient {
|
|
|
36
36
|
for (const [key, value] of Object.entries(query))
|
|
37
37
|
if (value)
|
|
38
38
|
url.searchParams.set(key, value);
|
|
39
|
+
// Node reports an unreachable host as a bare "fetch failed", which in a watch loop repeats
|
|
40
|
+
// forever without ever saying WHICH host is down.
|
|
39
41
|
const response = await this.#fetch(url.toString(), {
|
|
40
42
|
headers: { Authorization: `Bearer ${this.#token}`, Accept: 'application/json' },
|
|
43
|
+
}).catch(error => {
|
|
44
|
+
throw new Error(`Could not reach ${this.#api} — ${error instanceof Error ? error.message : String(error)}`);
|
|
41
45
|
});
|
|
42
46
|
if (response.ok)
|
|
43
47
|
return response.json();
|
|
@@ -52,12 +56,31 @@ export class FlareumClient {
|
|
|
52
56
|
: 'Retry; if it persists, check the project is reachable.',
|
|
53
57
|
});
|
|
54
58
|
}
|
|
59
|
+
async #getText(path) {
|
|
60
|
+
const url = `${this.#api}/api/v1/projects/${this.projectId}${path}`;
|
|
61
|
+
const response = await this.#fetch(url, {
|
|
62
|
+
headers: { Authorization: `Bearer ${this.#token}`, Accept: 'text/plain' },
|
|
63
|
+
});
|
|
64
|
+
if (response.ok)
|
|
65
|
+
return response.text();
|
|
66
|
+
const body = await response.json().catch(() => null);
|
|
67
|
+
throw new FlareumApiError(body?.error ?? {
|
|
68
|
+
code: `HTTP_${response.status}`, message: `The Flareum API returned ${response.status}.`, hint: '',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
55
71
|
catalog(updatedSince = '') {
|
|
56
72
|
return this.#get('/catalog', { updatedSince });
|
|
57
73
|
}
|
|
58
74
|
search(query, limit) {
|
|
59
75
|
return this.#get('/search', { q: query, ...(limit ? { limit: String(limit) } : {}) });
|
|
60
76
|
}
|
|
77
|
+
/** The stylesheets the last push published. They exist only after a push, never from a save. */
|
|
78
|
+
files() {
|
|
79
|
+
return this.#get('/files');
|
|
80
|
+
}
|
|
81
|
+
file(path) {
|
|
82
|
+
return this.#getText(`/files/${path.split('/').map(encodeURIComponent).join('/')}`);
|
|
83
|
+
}
|
|
61
84
|
variable(path) {
|
|
62
85
|
return this.#get(`/variables/${path.split('/').map(encodeURIComponent).join('/')}`);
|
|
63
86
|
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Resolved = {
|
|
2
|
+
token: string;
|
|
3
|
+
api?: string;
|
|
4
|
+
out?: string;
|
|
5
|
+
};
|
|
6
|
+
export type ConfigSources = {
|
|
7
|
+
flags: Record<string, string>;
|
|
8
|
+
env: Record<string, string | undefined>;
|
|
9
|
+
file: Partial<Resolved> | null;
|
|
10
|
+
};
|
|
11
|
+
export declare const CONFIG_PATH = ".flareum/config.json";
|
|
12
|
+
export declare class MissingToken extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
/** A flag beats the environment beats the file — nearest to the command wins. */
|
|
16
|
+
export declare const resolveConfig: ({ flags, env, file }: ConfigSources) => Resolved;
|
|
17
|
+
/** `--out dir --token x` and `--out=dir`, both. Anything else is a positional. */
|
|
18
|
+
export declare const parseArgs: (argv: string[]) => {
|
|
19
|
+
command: string;
|
|
20
|
+
flags: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
export declare const cliErrorReport: (error: unknown) => string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { FlareumApiError } from './client.js';
|
|
2
|
+
export const CONFIG_PATH = '.flareum/config.json';
|
|
3
|
+
export class MissingToken extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
// Names every place it looked, so the reader does not have to guess which one to set.
|
|
6
|
+
super('No Flareum key. Give it one of:\n'
|
|
7
|
+
+ ' --token pk_…\n'
|
|
8
|
+
+ ' FLAREUM_TOKEN=pk_… in the environment\n'
|
|
9
|
+
+ ` a "token" in ${CONFIG_PATH}\n`
|
|
10
|
+
+ 'Mint a key in the project\'s Connect screen in Flareum.');
|
|
11
|
+
this.name = 'MissingToken';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** A flag beats the environment beats the file — nearest to the command wins. */
|
|
15
|
+
export const resolveConfig = ({ flags, env, file }) => {
|
|
16
|
+
const token = flags.token || env.FLAREUM_TOKEN || file?.token;
|
|
17
|
+
if (!token)
|
|
18
|
+
throw new MissingToken();
|
|
19
|
+
return {
|
|
20
|
+
token,
|
|
21
|
+
api: flags.api || env.FLAREUM_API || file?.api,
|
|
22
|
+
out: flags.out || file?.out,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** `--out dir --token x` and `--out=dir`, both. Anything else is a positional. */
|
|
26
|
+
export const parseArgs = (argv) => {
|
|
27
|
+
const flags = {};
|
|
28
|
+
const positional = [];
|
|
29
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
if (!arg.startsWith('--')) {
|
|
32
|
+
positional.push(arg);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const [name, inline] = arg.slice(2).split('=');
|
|
36
|
+
if (inline !== undefined) {
|
|
37
|
+
flags[name] = inline;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const next = argv[i + 1];
|
|
41
|
+
if (next && !next.startsWith('--')) {
|
|
42
|
+
flags[name] = next;
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
flags[name] = 'true';
|
|
47
|
+
}
|
|
48
|
+
return { command: positional[0] ?? '', flags };
|
|
49
|
+
};
|
|
50
|
+
// Every CLI failure reaches a user through this, so an unhandled one must read as a sentence
|
|
51
|
+
// rather than the Node stack trace a rethrow prints.
|
|
52
|
+
export const cliErrorReport = (error) => {
|
|
53
|
+
if (error instanceof MissingToken)
|
|
54
|
+
return error.message;
|
|
55
|
+
if (error instanceof FlareumApiError)
|
|
56
|
+
return `${error.message}\n${error.hint}`;
|
|
57
|
+
return error instanceof Error ? error.message : String(error);
|
|
58
|
+
};
|
package/dist/pull.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FlareumClient } from './client.js';
|
|
2
|
+
export type PullResult = {
|
|
3
|
+
publishedVersionId: string;
|
|
4
|
+
written: string[];
|
|
5
|
+
failed: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
export type Writer = (path: string, contents: string) => Promise<void>;
|
|
11
|
+
export declare const pullStyles: (client: Pick<FlareumClient, "files" | "file">, write: Writer) => Promise<PullResult>;
|
|
12
|
+
export declare const pullReport: ({ publishedVersionId, written, failed }: PullResult, outDir: string) => string;
|
package/dist/pull.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copies the ARTIFACT a push generated; never rebuilds CSS from the catalog, so a name here and a
|
|
2
|
+
// name the API reports cannot disagree.
|
|
3
|
+
export const pullStyles = async (client, write) => {
|
|
4
|
+
const { publishedVersionId, files } = await client.files();
|
|
5
|
+
const written = [];
|
|
6
|
+
const failed = [];
|
|
7
|
+
for (const path of files)
|
|
8
|
+
try {
|
|
9
|
+
await write(path, await client.file(path));
|
|
10
|
+
written.push(path);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
// One unreadable file must not lose the rest, and must not be reported as written.
|
|
14
|
+
failed.push({ path, reason: error instanceof Error ? error.message : String(error) });
|
|
15
|
+
}
|
|
16
|
+
return { publishedVersionId, written, failed };
|
|
17
|
+
};
|
|
18
|
+
export const pullReport = ({ publishedVersionId, written, failed }, outDir) => {
|
|
19
|
+
const lines = [`Pulled ${written.length} stylesheet(s) from version ${publishedVersionId} into ${outDir}`];
|
|
20
|
+
for (const path of written)
|
|
21
|
+
lines.push(` ${path}`);
|
|
22
|
+
if (failed.length) {
|
|
23
|
+
lines.push(`${failed.length} could NOT be written:`);
|
|
24
|
+
for (const { path, reason } of failed)
|
|
25
|
+
lines.push(` ${path} — ${reason}`);
|
|
26
|
+
}
|
|
27
|
+
if (!written.length && !failed.length)
|
|
28
|
+
lines.push(' nothing — this version published no stylesheets.');
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
};
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type WatchDeps = {
|
|
2
|
+
/** The published version right now, or null when nothing is published yet. */
|
|
3
|
+
publishedVersion: () => Promise<string | null>;
|
|
4
|
+
onPublish: (version: string) => Promise<void>;
|
|
5
|
+
onError: (error: unknown) => void;
|
|
6
|
+
sleep: (ms: number) => Promise<void>;
|
|
7
|
+
keepGoing: () => boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_INTERVAL_MS = 15000;
|
|
10
|
+
export declare const watchPublished: ({ publishedVersion, onPublish, onError, sleep, keepGoing }: WatchDeps, intervalMs?: number) => Promise<void>;
|
|
11
|
+
export declare const watchStartedReport: (outDir: string, intervalMs: number) => string;
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DEFAULT_INTERVAL_MS = 15_000;
|
|
2
|
+
// Re-pulls when the PUBLISHED version changes — a push, never a save, so a designer mid-edit cannot
|
|
3
|
+
// rewrite a file the developer is looking at.
|
|
4
|
+
export const watchPublished = async ({ publishedVersion, onPublish, onError, sleep, keepGoing }, intervalMs = DEFAULT_INTERVAL_MS) => {
|
|
5
|
+
let seen = null;
|
|
6
|
+
while (keepGoing()) {
|
|
7
|
+
try {
|
|
8
|
+
const version = await publishedVersion();
|
|
9
|
+
if (version && version !== seen) {
|
|
10
|
+
// Marked seen only AFTER the pull lands: a failed write must be retried, not skipped.
|
|
11
|
+
await onPublish(version);
|
|
12
|
+
seen = version;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
// A server restart or a dropped connection must not end the watch — the next tick retries.
|
|
17
|
+
onError(error);
|
|
18
|
+
}
|
|
19
|
+
if (keepGoing())
|
|
20
|
+
await sleep(intervalMs);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const watchStartedReport = (outDir, intervalMs) => `Watching for a Push in Flareum — pulling into ${outDir} every ${Math.round(intervalMs / 1000)}s.`
|
|
24
|
+
+ '\nPress Ctrl+C to stop.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flareum/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Connect a coding agent to a Flareum project's design tokens.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Flareum",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"bin": {
|
|
23
|
+
"flareum": "dist/cli.js",
|
|
23
24
|
"flareum-mcp": "dist/server.js"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|