@flareum/mcp 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flareum
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 ADDED
@@ -0,0 +1,126 @@
1
+ # @flareum/mcp
2
+
3
+ Connects a coding agent to a Flareum project's design tokens, so it uses your tokens instead of
4
+ typing literal values.
5
+
6
+ **Read-only.** Nothing this package does can change your Flareum project. There is no write tool.
7
+
8
+ ---
9
+
10
+ ## Install
11
+
12
+ 1. In Flareum, open **Project settings → Connect** and click **Connect to VS Code**.
13
+ 2. Copy the command it shows you. It looks like this:
14
+
15
+ ```bash
16
+ claude mcp add flareum --env FLAREUM_TOKEN=pk_… -- npx -y @flareum/mcp
17
+ ```
18
+
19
+ 3. Run it in **your project's** terminal — the repo where you write CSS/SCSS, not the Flareum repo.
20
+ 4. Restart your editor session. MCP servers are not picked up by a session already running.
21
+
22
+ The key is shown once and cannot be retrieved again. If you lose it, revoke it and make another.
23
+
24
+ ---
25
+
26
+ ## What your agent can do with it
27
+
28
+ Ask in plain language — the agent picks the tool:
29
+
30
+ | Ask | What happens |
31
+ |---|---|
32
+ | "Is there a token for `#5B8DEF`?" | searches by colour; a hex, `rgb()` or a bare triplet all work |
33
+ | "What spacing tokens exist?" | searches by path fragment |
34
+ | "Which token is the one used by the Card?" | searches by component name |
35
+ | "What breaks if I change `space/4`?" | lists the tokens referencing it, and the components using it |
36
+ | "Style this banner" | the agent looks a token up rather than typing `padding: 16px` |
37
+
38
+ ### The two tools
39
+
40
+ | Tool | Answers |
41
+ |---|---|
42
+ | `flareum_search(query, limit?)` | *Is there already a token for this?* Matches path, CSS name, resolved value, comment and component names. |
43
+ | `flareum_get(path)` | *What is this token, and what breaks if I change it?* Values per mode, plus what references it. |
44
+
45
+ **A search never comes back empty.** If nothing matches well it returns the nearest tokens and says
46
+ so, because "no results" reads as "no such token exists" and gets acted on.
47
+
48
+ ---
49
+
50
+ ## Configuration
51
+
52
+ | Variable | |
53
+ |---|---|
54
+ | `FLAREUM_TOKEN` | **required.** The project is read from the key itself. |
55
+ | `FLAREUM_API` | optional. Defaults to production. Set it to point at a local Flareum. |
56
+ | `FLAREUM_PROJECT` | optional. Overrides the project in the key. |
57
+
58
+ On start it writes `.claude/skills/flareum/SKILL.md` into the working directory — the naming rules
59
+ your agent needs to propose a token name correctly. It is regenerated every run, so do not edit it,
60
+ and it prints where it wrote (or why it could not) on stderr.
61
+
62
+ ---
63
+
64
+ ## If it is not working
65
+
66
+ | What you see | Cause |
67
+ |---|---|
68
+ | 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. |
69
+ | The agent types literal values anyway | The skill did not land. Look for `[flareum] skill written to …` in the MCP server's stderr. |
70
+ | Values labelled with long ids instead of mode names | The Flareum server is older than the client. Restart it. |
71
+ | `401` on every call | The key was revoked, or belongs to another project. Mint a new one. |
72
+ | `429` | More than 120 requests a minute on one key. |
73
+
74
+ Every error the API returns carries a `hint` saying what to do next — your agent sees it.
75
+
76
+ ---
77
+
78
+ ## Development
79
+
80
+ Working on this package rather than using it.
81
+
82
+ ```bash
83
+ cd packages/mcp
84
+ npm install
85
+ npm run build # tsc → dist/
86
+ npm test # this package's tests
87
+ ```
88
+
89
+ To try a local build without publishing, point the command at the built file instead of npm:
90
+
91
+ ```bash
92
+ claude mcp add flareum \
93
+ --env FLAREUM_TOKEN=pk_… \
94
+ --env FLAREUM_API=http://localhost:9999 \
95
+ -- node /absolute/path/to/packages/mcp/dist/server.js
96
+ ```
97
+
98
+ ### Publishing
99
+
100
+ Only a maintainer does this, and only to let other people `npx` the package. It is not needed to
101
+ develop or test.
102
+
103
+ ```bash
104
+ npm adduser # once ever — links your machine to an npm account
105
+ npm org create flareum # once ever — claims the @flareum scope
106
+ cd packages/mcp
107
+ npm version patch # every release; npm refuses to republish a version
108
+ npm publish # builds first, publishes public
109
+ ```
110
+
111
+ Until this is published, the command the Connect screen generates cannot work — `npx` has nothing
112
+ to fetch.
113
+
114
+ ### Layout
115
+
116
+ | File | |
117
+ |---|---|
118
+ | `src/client.ts` | HTTP against the Flareum read API. `fetch` is injected, so it is testable without a network. |
119
+ | `src/tools.ts` | The TEXT each tool returns. This is what the agent reads, so its wording is guarded. |
120
+ | `src/server.ts` | MCP stdio transport. Glue only — no behaviour lives here. |
121
+ | `src/skill.ts` | Writes the skill into the consuming project. |
122
+ | `skill/SKILL.md` | The naming rules shipped to the agent. |
123
+
124
+ `server.ts` has no tests because it holds no logic; everything it calls is covered. Imports inside
125
+ the package carry a `.js` extension — TypeScript does not rewrite specifiers, and Node's ESM loader
126
+ will not resolve an extensionless one at runtime.
@@ -0,0 +1,77 @@
1
+ export type FlareumError = {
2
+ code: string;
3
+ message: string;
4
+ hint?: string;
5
+ };
6
+ export type ClientOptions = {
7
+ token: string;
8
+ api?: string;
9
+ projectId?: string;
10
+ fetchImpl?: typeof fetch;
11
+ };
12
+ export declare class FlareumApiError extends Error {
13
+ readonly code: string;
14
+ readonly hint: string;
15
+ constructor({ code, message, hint }: FlareumError);
16
+ }
17
+ export declare const projectIdFromToken: (token: string) => string;
18
+ export declare class FlareumClient {
19
+ #private;
20
+ readonly projectId: string;
21
+ constructor({ token, api, projectId, fetchImpl }: ClientOptions);
22
+ catalog(updatedSince?: string): Promise<CatalogResponse>;
23
+ search(query: string, limit?: number): Promise<SearchResponse>;
24
+ variable(path: string): Promise<VariableResponse>;
25
+ }
26
+ export type CatalogResponse = {
27
+ projectId: string;
28
+ prefix: string;
29
+ liveVersionId: string;
30
+ publishedVersionId: string | null;
31
+ collections: Array<{
32
+ id: string;
33
+ name: string;
34
+ prefix: string;
35
+ variables: unknown[];
36
+ }>;
37
+ };
38
+ export type Candidate = {
39
+ id: string;
40
+ path: string;
41
+ cssName: string;
42
+ type?: string;
43
+ collection?: string;
44
+ collectionName?: string;
45
+ values?: Record<string, {
46
+ value: string;
47
+ resolved: string;
48
+ }>;
49
+ usage?: {
50
+ componentCount: number;
51
+ components: string[];
52
+ };
53
+ state?: string;
54
+ score: number;
55
+ matchedOn: string[];
56
+ };
57
+ export type SearchResponse = {
58
+ query: string;
59
+ matched: boolean;
60
+ reason?: string;
61
+ total: number;
62
+ truncated: boolean;
63
+ candidates: Candidate[];
64
+ };
65
+ export type VariableMode = {
66
+ id: string;
67
+ name: string;
68
+ selector: string;
69
+ };
70
+ export type VariableResponse = Candidate & {
71
+ modes?: VariableMode[];
72
+ referrers: Array<{
73
+ id: string;
74
+ path: string;
75
+ cssName: string;
76
+ }>;
77
+ };
package/dist/client.js ADDED
@@ -0,0 +1,64 @@
1
+ // The HTTP client. `fetch` is injected so every behaviour here is testable without a network, and
2
+ // so a host with its own fetch (a proxy, a retry policy) can supply one.
3
+ export class FlareumApiError extends Error {
4
+ code;
5
+ hint;
6
+ constructor({ code, message, hint }) {
7
+ super(message);
8
+ this.name = 'FlareumApiError';
9
+ this.code = code;
10
+ this.hint = hint ?? '';
11
+ }
12
+ }
13
+ // Must match src/environments/environment.prod.ts. A wrong default sends a bearer token to a
14
+ // domain nobody here owns.
15
+ const DEFAULT_API = 'https://api.flareum.app';
16
+ // A token is `pk_<project>_<random>`; the project is recoverable from it so a user pastes one value.
17
+ export const projectIdFromToken = (token) => token.split('_')[1] ?? '';
18
+ export class FlareumClient {
19
+ projectId;
20
+ #token;
21
+ #api;
22
+ #fetch;
23
+ constructor({ token, api, projectId, fetchImpl }) {
24
+ if (!token)
25
+ throw new Error('FLAREUM_TOKEN is not set. Mint one in the project\'s Connect screen.');
26
+ this.#token = token;
27
+ this.#api = (api ?? DEFAULT_API).replace(/\/+$/, '');
28
+ this.projectId = projectId || projectIdFromToken(token);
29
+ const impl = fetchImpl ?? globalThis.fetch;
30
+ if (!impl)
31
+ throw new Error('No fetch implementation available.');
32
+ this.#fetch = impl;
33
+ }
34
+ async #get(path, query = {}) {
35
+ const url = new URL(`${this.#api}/api/v1/projects/${this.projectId}${path}`);
36
+ for (const [key, value] of Object.entries(query))
37
+ if (value)
38
+ url.searchParams.set(key, value);
39
+ const response = await this.#fetch(url.toString(), {
40
+ headers: { Authorization: `Bearer ${this.#token}`, Accept: 'application/json' },
41
+ });
42
+ if (response.ok)
43
+ return response.json();
44
+ // The server's `hint` is written for a model to act on, so it must survive to the tool result
45
+ // rather than being flattened into a status code.
46
+ const body = await response.json().catch(() => null);
47
+ throw new FlareumApiError(body?.error ?? {
48
+ code: `HTTP_${response.status}`,
49
+ message: `The Flareum API returned ${response.status}.`,
50
+ hint: response.status === 401
51
+ ? 'The token is invalid or revoked. Mint a new one in the project\'s Connect screen.'
52
+ : 'Retry; if it persists, check the project is reachable.',
53
+ });
54
+ }
55
+ catalog(updatedSince = '') {
56
+ return this.#get('/catalog', { updatedSince });
57
+ }
58
+ search(query, limit) {
59
+ return this.#get('/search', { q: query, ...(limit ? { limit: String(limit) } : {}) });
60
+ }
61
+ variable(path) {
62
+ return this.#get(`/variables/${path.split('/').map(encodeURIComponent).join('/')}`);
63
+ }
64
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // Transport glue only. Every behaviour worth guarding lives in client.ts and tools.ts, which have
3
+ // no SDK dependency and are tested without one.
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { FlareumApiError, FlareumClient } from './client.js';
8
+ import { TOOL_DESCRIPTIONS, formatError, formatSearch, formatVariable } from './tools.js';
9
+ import { installSkill, skillInstallReport } from './skill.js';
10
+ const client = new FlareumClient({
11
+ token: process.env.FLAREUM_TOKEN ?? '',
12
+ api: process.env.FLAREUM_API,
13
+ projectId: process.env.FLAREUM_PROJECT,
14
+ });
15
+ const server = new Server({ name: 'flareum', version: '0.1.0' }, { capabilities: { tools: {} } });
16
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
17
+ tools: [
18
+ {
19
+ name: 'flareum_search',
20
+ description: TOOL_DESCRIPTIONS.search,
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ query: { type: 'string', description: 'A name, a path fragment, a hex or rgb() colour, a comment phrase, or a component name.' },
25
+ limit: { type: 'number', description: 'Maximum candidates to return (default 10, max 50).' },
26
+ },
27
+ required: ['query'],
28
+ },
29
+ },
30
+ {
31
+ name: 'flareum_get',
32
+ description: TOOL_DESCRIPTIONS.get,
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ path: { type: 'string', description: 'The token path, e.g. color/semantic/border/warning. A CSS custom-property name also works.' },
37
+ },
38
+ required: ['path'],
39
+ },
40
+ },
41
+ ],
42
+ }));
43
+ const text = (body) => ({ content: [{ type: 'text', text: body }] });
44
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
45
+ const args = (request.params.arguments ?? {});
46
+ try {
47
+ if (request.params.name === 'flareum_search')
48
+ return text(formatSearch(await client.search(String(args.query ?? ''), Number(args.limit) || undefined)));
49
+ if (request.params.name === 'flareum_get')
50
+ return text(formatVariable(await client.variable(String(args.path ?? ''))));
51
+ return text(`Unknown tool: ${request.params.name}`);
52
+ }
53
+ catch (error) {
54
+ // A failure is reported as a readable tool result, never a protocol error: the model can act on
55
+ // the hint, and cannot act on a transport exception.
56
+ if (error instanceof FlareumApiError)
57
+ return text(formatError(error));
58
+ return text(formatError({ code: 'UNREACHABLE', message: String(error),
59
+ hint: 'Check FLAREUM_API and that the machine is online.' }));
60
+ }
61
+ });
62
+ // Never silently: if the skill does not land, the agent hardcodes values and nothing says why.
63
+ console.error(skillInstallReport(await installSkill(process.cwd())));
64
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,11 @@
1
+ export type SkillInstall = {
2
+ ok: true;
3
+ path: string;
4
+ } | {
5
+ ok: false;
6
+ path: string;
7
+ reason: string;
8
+ };
9
+ export declare const installSkill: (cwd: string) => Promise<SkillInstall>;
10
+ /** Reported on stderr — stdout carries the JSON-RPC stream and must never be written to. */
11
+ export declare const skillInstallReport: (result: SkillInstall) => string;
package/dist/skill.js ADDED
@@ -0,0 +1,23 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const SKILL_DIR = ['.claude', 'skills', 'flareum'];
5
+ // Written on every start, not once: a stale copy of the naming rules is worse than none, and the
6
+ // file is generated rather than hand-edited.
7
+ export const installSkill = async (cwd) => {
8
+ const source = join(dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'SKILL.md');
9
+ const path = join(cwd, ...SKILL_DIR, 'SKILL.md');
10
+ try {
11
+ await mkdir(join(cwd, ...SKILL_DIR), { recursive: true });
12
+ await writeFile(path, await readFile(source, 'utf8'), 'utf8');
13
+ return { ok: true, path };
14
+ }
15
+ catch (error) {
16
+ return { ok: false, path, reason: error instanceof Error ? error.message : String(error) };
17
+ }
18
+ };
19
+ /** Reported on stderr — stdout carries the JSON-RPC stream and must never be written to. */
20
+ export const skillInstallReport = (result) => result.ok
21
+ ? `[flareum] skill written to ${result.path}`
22
+ : `[flareum] could NOT write the skill to ${result.path} — ${result.reason}. `
23
+ + 'Without it the agent will not know to search for a token before writing a literal value.';
@@ -0,0 +1,15 @@
1
+ import type { SearchResponse, VariableResponse } from './client.js';
2
+ export declare const NO_MATCH_INSTRUCTION: string;
3
+ /** What `flareum_search` hands back. */
4
+ export declare const formatSearch: (result: SearchResponse) => string;
5
+ /** What `flareum_get` hands back — detail plus the blast radius of changing it. */
6
+ export declare const formatVariable: (v: VariableResponse) => string;
7
+ export declare const formatError: ({ code, message, hint }: {
8
+ code: string;
9
+ message: string;
10
+ hint?: string;
11
+ }) => string;
12
+ export declare const TOOL_DESCRIPTIONS: {
13
+ readonly search: string;
14
+ readonly get: string;
15
+ };
package/dist/tools.js ADDED
@@ -0,0 +1,74 @@
1
+ // The text a tool returns IS the product: it is what the model reads before deciding whether a
2
+ // token exists. Kept pure and separate from the MCP transport so every wording here is guarded.
3
+ // A collection can carry 28 modes, so the whole list would bury the row it is describing.
4
+ const MAX_VALUES_SHOWN = 4;
5
+ const valueLine = (values) => {
6
+ if (!values)
7
+ return '';
8
+ // A stored value can span lines (a multi-line var() fallback), which would break the row apart.
9
+ const all = [...new Set(Object.values(values)
10
+ .map(v => (v.resolved || v.value).replace(/\s+/g, ' ').trim())
11
+ .filter(Boolean))];
12
+ return all.length > MAX_VALUES_SHOWN
13
+ ? `${all.slice(0, MAX_VALUES_SHOWN).join(' · ')} (+${all.length - MAX_VALUES_SHOWN} more)`
14
+ : all.join(' · ');
15
+ };
16
+ const describe = (c) => {
17
+ const bits = [c.cssName, c.type, valueLine(c.values)].filter(Boolean);
18
+ const usage = c.usage?.componentCount ? ` — used by ${c.usage.componentCount} component(s)` : '';
19
+ const flag = c.state && c.state !== 'ok' ? ` [${c.state}]` : '';
20
+ return ` ${c.path}\n ${bits.join(' | ')}${usage}${flag}`;
21
+ };
22
+ export const NO_MATCH_INSTRUCTION = 'These are the NEAREST tokens, not matches. Do not create a new token or hardcode a value on the '
23
+ + 'strength of this result — one of these may already be what you want, under a different name. '
24
+ + 'Ask which to use, or refine the search.';
25
+ /** What `flareum_search` hands back. */
26
+ export const formatSearch = (result) => {
27
+ if (!result.candidates.length)
28
+ return `No variables in this project yet (searched "${result.query}").`;
29
+ const header = result.matched
30
+ ? `Match for "${result.query}":`
31
+ : `No confident match for "${result.query}". ${result.reason ?? ''}`.trim();
32
+ const listed = result.candidates.map(describe).join('\n');
33
+ const footer = [];
34
+ if (!result.matched)
35
+ footer.push(NO_MATCH_INSTRUCTION);
36
+ if (result.truncated)
37
+ footer.push(`Showing ${result.candidates.length} of ${result.total}. Narrow the query to see others.`);
38
+ return [header, listed, ...footer].filter(Boolean).join('\n\n');
39
+ };
40
+ /** What `flareum_get` hands back — detail plus the blast radius of changing it. */
41
+ export const formatVariable = (v) => {
42
+ const lines = [
43
+ v.path,
44
+ ` CSS name: ${v.cssName}`,
45
+ v.collectionName ? ` Collection: ${v.collectionName}` : '',
46
+ v.type ? ` Type: ${v.type}` : '',
47
+ ].filter(Boolean);
48
+ // Values are keyed by mode ID; label them with the mode's name, which is what a reader knows.
49
+ const modeName = new Map((v.modes ?? []).map(m => [m.id, m.name || m.selector || m.id]));
50
+ if (v.values)
51
+ for (const [mode, value] of Object.entries(v.values))
52
+ lines.push(` ${modeName.get(mode) ?? mode}: ${value.resolved || value.value || '(empty)'}`);
53
+ // Both halves of blast radius, and each says what it does NOT know — absent usage data is not
54
+ // evidence that nothing uses it.
55
+ lines.push(v.usage
56
+ ? ` Used by ${v.usage.componentCount} component(s): ${v.usage.components.join(', ') || 'names unavailable'}`
57
+ : ' No component-usage data has been imported, so component usage is unknown.');
58
+ lines.push(v.referrers.length
59
+ ? ` Referenced by ${v.referrers.length} other variable(s): ${v.referrers.map(r => r.path).join(', ')}`
60
+ : ' No other variable references it.');
61
+ if (v.state && v.state !== 'ok')
62
+ lines.push(` Status: ${v.state}`);
63
+ return lines.join('\n');
64
+ };
65
+ export const formatError = ({ code, message, hint }) => [`${message} (${code})`, hint].filter(Boolean).join('\n');
66
+ export const TOOL_DESCRIPTIONS = {
67
+ search: 'Search the project\'s design tokens in Flareum. ALWAYS call this before proposing a new token '
68
+ + 'name or writing a literal colour, size or spacing value — the token you need usually already '
69
+ + 'exists under a name you would not guess. Matches on path, CSS name, resolved value (a hex '
70
+ + 'works), comment and component names. A result may be nearest-neighbours rather than a match; '
71
+ + 'read the header before acting.',
72
+ get: 'Read one design token in full: its value in every mode, its type, which components use it, and '
73
+ + 'which other tokens reference it. Use it to judge what a change would affect before making one.',
74
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@flareum/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Connect a coding agent to a Flareum project's design tokens.",
5
+ "license": "MIT",
6
+ "author": "Flareum",
7
+ "homepage": "https://flareum.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://gitlab.com/flareum-group/thegproject.git",
11
+ "directory": "packages/mcp"
12
+ },
13
+ "keywords": [
14
+ "flareum",
15
+ "design-tokens",
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "claude",
19
+ "design-system"
20
+ ],
21
+ "type": "module",
22
+ "bin": {
23
+ "flareum-mcp": "dist/server.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "skill"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json",
34
+ "test": "cd ../.. && npx mocha --exit 'packages/mcp/src/**/*.test.ts'",
35
+ "prepublishOnly": "npm run build"
36
+ },
37
+ "dependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "typescript": "^5.4.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ }
47
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: flareum
3
+ description: Use the project's Flareum design tokens instead of literal values. Invoke whenever you are about to write a colour, size, spacing, radius, shadow or duration in CSS/SCSS, or need to know which token a value corresponds to.
4
+ ---
5
+
6
+ # Flareum design tokens
7
+
8
+ This project's design tokens live in Flareum. The generated CSS/SCSS in the repo is a **rendering**
9
+ of them — the tokens themselves, with their types, modes, comments and usage, are reachable through
10
+ the `flareum_search` and `flareum_get` tools.
11
+
12
+ ## The one rule
13
+
14
+ **Search before you write a literal value, and before you propose a token name.**
15
+
16
+ A hardcoded `#5B8DEF` is a token that escaped the system. A newly invented `color/border/muted` beside
17
+ an existing `color/border/secondary` is worse — it looks like a decision, and nobody will ever
18
+ reconcile the two. `flareum_search` takes a hex, an rgb(), a path fragment, a phrase from a comment,
19
+ or a component name.
20
+
21
+ ## Reading a search result
22
+
23
+ The header tells you which kind of answer you got, and they mean different things:
24
+
25
+ - **`Match for "…"`** — use it. The `cssName` is ready to paste.
26
+ - **`No confident match for "…"`** — the listed tokens are **nearest neighbours, not matches**. One
27
+ of them is often the thing you want under a name you would not have guessed. **Do not create a
28
+ token or hardcode a value on the strength of this.** Ask which to use, or search again with the
29
+ value rather than the name.
30
+
31
+ An empty project says so explicitly. Anything else always returns candidates.
32
+
33
+ ## If the token genuinely does not exist
34
+
35
+ This integration is **read-only** — there is no tool that writes to Flareum, by design. So:
36
+
37
+ 1. Say which token is missing and what it would be for.
38
+ 2. Propose a name that fits the grammar below, so the designer can create it in one step.
39
+ 3. Do not silently hardcode the value and move on.
40
+
41
+ ## The naming grammar
42
+
43
+ A token name is a serialized hierarchy, general → specific, terminal value last:
44
+
45
+ ```
46
+ --[prefix]-[category]-[layer?]-[path...]-[value]
47
+ ```
48
+
49
+ - **`category` always comes first** after the prefix: `color`, `space`, `size`, `radius`, `shadow`,
50
+ `blur`, `duration`, `z`, `opacity`, `font`, `border`.
51
+ - **`layer`** is optional and names the tier. For colour: `primitive`, `global`, `semantic`,
52
+ `action_palette`, `action_state`, `action`. A raw scale (`--fui-space-4`) has none.
53
+ - **A component is a `path` segment, never a top-level layer.**
54
+
55
+ ```css
56
+ /* ✓ */ --fui-color-semantic-button-background-primary-hover
57
+ /* ✗ */ --fui-button-color-background-primary-hover /* component-first */
58
+ /* ✗ */ --fui-semantic-color-text-primary /* layer hoisted above the category */
59
+ ```
60
+
61
+ Spell each segment the way the system already spells it — `disabled` not `disable`, `background` not
62
+ `bg`, `accent` not `acent`. Search for the segment before coining a second spelling of a word that
63
+ already exists; a new word is for a genuinely new concept only.
64
+
65
+ ## Blast radius before a change
66
+
67
+ `flareum_get` reports two independent things, and they answer different questions:
68
+
69
+ - **component usage** — which components consume it (from an imported analysis).
70
+ - **referrers** — which other tokens reference it, and would break.
71
+
72
+ If it says no usage data has been imported, that means **unknown**, not unused. Grep the repo
73
+ yourself before treating a token as safe to change.