@flareum/mcp 0.5.0 → 0.5.8

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/dist/cli.js CHANGED
@@ -69,7 +69,9 @@ try {
69
69
  // The server reads this lock to decide whether to pull. Without it a CLI pull was invisible,
70
70
  // and the next connection fetched the same version again.
71
71
  if (!isRetryable(result))
72
- await writeLock(process.cwd(), makeLock(projectIdFromToken(config.token), result.publishedVersionId, outDir, new Date().toISOString()));
72
+ // An explicit pull is the deliberate act applying.md ends on, so it advances BOTH: these
73
+ // stylesheets, and the version the code is now written against.
74
+ await writeLock(process.cwd(), makeLock(projectIdFromToken(config.token), result.publishedVersionId, outDir, new Date().toISOString(), result.publishedVersionId));
73
75
  console.log(pullReport(result, outDir));
74
76
  return result;
75
77
  };
@@ -101,14 +101,17 @@ export const configuredOut = (cwd, env) => {
101
101
  };
102
102
  export const runFirstPull = async (client, cwd, env, configuredOut, knownVersion, now = () => new Date().toISOString()) => {
103
103
  const exists = path => existsSync(resolve(cwd, path));
104
- const isEmpty = path => {
104
+ // Any FILE, at any depth. An empty directory tree is what a git clean leaves behind, not somebody
105
+ // else's work — counting those as content refused to ever pull again.
106
+ const holdsNoFiles = (dir) => {
105
107
  try {
106
- return readdirSync(resolve(cwd, path)).length === 0;
108
+ return readdirSync(dir, { withFileTypes: true }).every(entry => entry.isDirectory() && holdsNoFiles(join(dir, entry.name)));
107
109
  }
108
110
  catch {
109
111
  return true;
110
112
  }
111
113
  };
114
+ const isEmpty = path => holdsNoFiles(resolve(cwd, path));
112
115
  const dir = chooseStylesDir(exists, configuredOut);
113
116
  if (!autoPullEnabled(env))
114
117
  return { ran: false, reason: 'disabled', dir };
@@ -128,12 +131,15 @@ export const runFirstPull = async (client, cwd, env, configuredOut, knownVersion
128
131
  // Claimed BEFORE the first write: an interrupted pull leaves files with no completed version,
129
132
  // and without this it would read as somebody else's folder and never be finished.
130
133
  await mkdir(resolve(cwd, dir), { recursive: true });
131
- await writeLock(cwd, makeLock(projectId, IN_FLIGHT, dir, now()));
134
+ // The applied version is carried across every write here: this pull is unattended, and
135
+ // advancing it would consume the diff window before an agent could read it.
136
+ const applied = lock?.appliedVersionId ?? syncedVersion(lock, dir) ?? undefined;
137
+ await writeLock(cwd, makeLock(projectId, IN_FLIGHT, dir, now(), applied));
132
138
  const result = await pullStyles(client, fileWriter(resolve(cwd, dir)));
133
139
  // Recorded only when the WHOLE version landed: pullStyles resolves even when files failed, so
134
140
  // stamping on its return marked a half-pulled folder complete and never retried it.
135
141
  if (!isRetryable(result))
136
- await writeLock(cwd, makeLock(projectId, result.publishedVersionId, dir, now()));
142
+ await writeLock(cwd, makeLock(projectId, result.publishedVersionId, dir, now(), applied));
137
143
  return { ran: true, dir, result };
138
144
  }
139
145
  catch (error) {
package/dist/lock.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const LOCK_PATH = ".flareum/lock.json";
2
2
  export type Lock = {
3
3
  projectId: string;
4
4
  syncedVersionId: string;
5
+ appliedVersionId?: string;
5
6
  syncedAt: string;
6
7
  out: string;
7
8
  prefix?: string;
@@ -12,7 +13,8 @@ export declare const readLock: (cwd: string) => Lock | null;
12
13
  export declare const owns: (lock: Lock | null, dir: string) => boolean;
13
14
  export declare const syncedVersion: (lock: Lock | null, dir: string) => string | null;
14
15
  export declare const writeLock: (cwd: string, lock: Lock) => Promise<void>;
15
- export declare const makeLock: (projectId: string, syncedVersionId: string, out: string, at: string, prefix?: string) => Lock;
16
+ export declare const makeLock: (projectId: string, syncedVersionId: string, out: string, at: string, appliedVersionId?: string, prefix?: string) => Lock;
17
+ export declare const appliedVersion: (lock: Lock | null) => string;
16
18
  export declare const LEGACY_STAMP = ".flareum-version";
17
19
  export declare const legacyVersion: (cwd: string, dir: string) => string | null;
18
20
  export declare const legacyStampExists: (cwd: string, dir: string) => boolean;
package/dist/lock.js CHANGED
@@ -20,7 +20,17 @@ export const writeLock = async (cwd, lock) => {
20
20
  await mkdir(dirname(path), { recursive: true });
21
21
  await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
22
22
  };
23
- export const makeLock = (projectId, syncedVersionId, out, at, prefix) => ({ projectId, syncedVersionId, syncedAt: at, out, ...(prefix ? { prefix } : {}) });
23
+ export const makeLock = (projectId, syncedVersionId, out, at, appliedVersionId, prefix) => ({
24
+ projectId, syncedVersionId, syncedAt: at, out,
25
+ ...(appliedVersionId ? { appliedVersionId } : {}),
26
+ ...(prefix ? { prefix } : {}),
27
+ });
28
+ // What an agent diffs FROM. Falls back to the stylesheets for a lock written before this field, and
29
+ // never returns the in-flight sentinel — a pull half-done is not a version anything was applied to.
30
+ export const appliedVersion = (lock) => {
31
+ const applied = lock?.appliedVersionId ?? lock?.syncedVersionId;
32
+ return !applied || applied === IN_FLIGHT ? '' : applied;
33
+ };
24
34
  // A project pulled by 0.2.x recorded the version in a stamp beside the stylesheets. Adopted rather
25
35
  // than ignored, so an upgrade does not re-pull a version already on disk.
26
36
  export const LEGACY_STAMP = '.flareum-version';
package/dist/server.js CHANGED
@@ -11,10 +11,10 @@ import { installSkill, skillInstallReport } from './skill.js';
11
11
  import { configuredOut, firstPullReport, runFirstPull } from './first-pull.js';
12
12
  import { projectConfigReport, writeProjectConfig } from './project-config.js';
13
13
  import { checkKey } from './key-check.js';
14
- import { readLock } from './lock.js';
14
+ import { appliedVersion, readLock } from './lock.js';
15
15
  // The agent should not have to be told which version to diff from — the lock in the project is the
16
16
  // answer, and asking the model to supply it invites a guess.
17
- const lockedVersion = () => readLock(process.cwd())?.syncedVersionId ?? '';
17
+ const lockedVersion = () => appliedVersion(readLock(process.cwd()));
18
18
  // A missing key threw at module load, and an editor shows that as a Node stack trace with the
19
19
  // message buried in it. Say it in one line and exit, the way the CLI already does.
20
20
  let client;
@@ -75,8 +75,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
75
75
  try {
76
76
  if (request.params.name === 'flareum_search')
77
77
  return text(formatSearch(await client.search(String(args.query ?? ''), Number(args.limit) || undefined)));
78
- if (request.params.name === 'flareum_changes_since')
79
- return text(formatChanges(await client.changes(String(args.since ?? '')), lockedVersion()));
78
+ if (request.params.name === 'flareum_changes_since') {
79
+ // Defaulted here, not left to the model: the lock is the answer, and asking for it invites a guess.
80
+ const locked = lockedVersion();
81
+ const since = String(args.since ?? '') || locked;
82
+ return text(formatChanges(await client.changes(since), locked));
83
+ }
80
84
  if (request.params.name === 'flareum_get')
81
85
  return text(formatVariable(await client.variable(String(args.path ?? ''))));
82
86
  return text(`Unknown tool: ${request.params.name}`);
package/dist/tools.js CHANGED
@@ -72,7 +72,8 @@ export const TOOL_DESCRIPTIONS = {
72
72
  get: 'Read one design token in full: its value in every mode, its type, which components use it, and '
73
73
  + 'which other tokens reference it. Use it to judge what a change would affect before making one.',
74
74
  changes: 'List what changed in Flareum since the version this project last synced. Call it when the user '
75
- + 'asks to update, sync or migrate tokens, and at the start of a session in a project that has a '
75
+ + 'says "update flareum", or asks to sync, update or migrate tokens, and at the start of a '
76
+ + 'session in a project that has a '
76
77
  + '.flareum/lock.json. A rename reads as a rename with both names, so rewrite the call sites '
77
78
  + 'rather than deleting and adding. A removed token carries what to use instead. Read every '
78
79
  + 'entry before editing anything, and say what you are about to change.',
@@ -80,14 +81,14 @@ export const TOOL_DESCRIPTIONS = {
80
81
  // Written for a model that is about to EDIT files: the CSS names first, because those are what it
81
82
  // greps for, and the action stated per line so a rename is never applied as a delete plus an add.
82
83
  export const formatChanges = ({ since, totalChanges, entries, truncated, liveVersionId }, lockedVersion = '') => {
83
- if (!totalChanges)
84
+ if (!totalChanges || !entries.length)
84
85
  return since
85
86
  ? `No changes since ${since}. The project is at ${liveVersionId}.`
86
87
  : `This project has no tokens yet.`;
87
88
  const lines = [
88
89
  since
89
- ? `${totalChanges} change(s) since ${since}. The project is now at ${liveVersionId}.`
90
- : `${totalChanges} change(s). No baseline was given, so this is everything the project has.`,
90
+ ? `${entries.length} change(s) since ${since}. The project is now at ${liveVersionId}.`
91
+ : `${entries.length} change(s). No baseline was given, so this is everything the project has.`,
91
92
  '',
92
93
  ];
93
94
  if (lockedVersion && since && lockedVersion !== since)
@@ -114,7 +115,8 @@ const changeLine = (entry) => {
114
115
  if (change === 'typeChanged')
115
116
  return `${kind} type: ${nameOf(before)} is now ${after?.type} (was ${before?.type})`;
116
117
  if (change === 'valueChanged')
117
- return `${kind} value${mode}: ${nameOf(before)} ${before?.value} → ${after?.value}`;
118
+ return `${kind} value${mode}: ${nameOf(before)} ${before?.value ?? '(not set)'} → `
119
+ + `${after?.value ?? '(not set)'}`;
118
120
  if (change === 'movedCollection')
119
121
  return `${kind} moved collection: ${nameOf(before)} → ${nameOf(after)}`;
120
122
  return `${kind} ${change}: ${nameOf(after ?? before)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flareum/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.8",
4
4
  "description": "Connect a coding agent to a Flareum project's design tokens.",
5
5
  "license": "MIT",
6
6
  "author": "Flareum",
package/skill/SKILL.md CHANGED
@@ -5,106 +5,90 @@ description: Use the project's Flareum design tokens instead of literal values.
5
5
 
6
6
  # Flareum design tokens
7
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.
8
+ Tokens live in Flareum. `flareum_search` and `flareum_get` read them; the CSS/SCSS in the repo is a
9
+ rendering of them.
11
10
 
12
- Every name here is written with **`[prefix]`** where your project's own token prefix goes — the
13
- string every `cssName` in a search result starts with. Substitute it; never write `[prefix]` itself.
11
+ Names below use **`[prefix]`** for this project's own prefix — the string every `cssName` starts
12
+ with. Substitute it; never write `[prefix]` itself.
14
13
 
15
14
  ## The one rule
16
15
 
17
16
  **Search before you write a literal value, and before you propose a token name.**
18
17
 
19
- A hardcoded `#5B8DEF` is a token that escaped the system. A newly invented `color/border/muted` beside
20
- an existing `color/border/secondary` is worse — it looks like a decision, and nobody will ever
21
- reconcile the two. `flareum_search` takes a hex, an rgb(), a path fragment, a phrase from a comment,
22
- or a component name.
18
+ A hardcoded `#5B8DEF` is a token that escaped the system. An invented `color/border/muted` beside an
19
+ existing `color/border/secondary` is worse — it looks like a decision, and nobody reconciles the two.
20
+ Search takes a hex, an rgb(), a path fragment, a comment phrase, or a component name.
23
21
 
24
- ## The rules, by topic
22
+ ## Before the first token in a project
25
23
 
26
- This page is the workflow. Open the one below that covers what you are about to write — each is a
27
- page, and it is worth reading in full before the first line of code.
24
+ Three checks. All three fail silently a correct `var([prefix]-…)` renders as nothing, which reads
25
+ as the token being wrong rather than missing.
28
26
 
29
- | Writing | Read |
30
- |---|---|
31
- | Any text — a heading, a label, body copy, a pseudo-element's `content` | [references/typography.md](references/typography.md) |
32
- | A token name, or judging one that already exists | [references/naming.md](references/naming.md) |
33
- | Applying what `flareum_changes_since` returned — a sync, an update, a migration | [references/applying.md](references/applying.md) |
34
-
35
- ## Before the first token: check the stylesheets are imported
36
-
37
- A token only resolves if the pulled stylesheets are actually loaded. `flareum pull` writes them into
38
- the project — it does not wire them in, because which file is the global entry point is the
39
- project's decision, not the tool's.
27
+ **1. Are they on disk?** Look in `src/styles/flareum/`, or wherever `out` in `.flareum/config.json`
28
+ points. If it is missing or holds no stylesheets:
40
29
 
41
- So the first time you use a token in a project, check the import exists, and add it if it does not.
42
- Otherwise every `var([prefix]-…)` you write is correct and renders as nothing — the failure is
43
- silent, and it looks like the token is wrong rather than absent.
44
-
45
- **Find the pulled files** (`src/styles/flareum/`, or wherever `out` in `.flareum/config.json`
46
- points), then **find the global stylesheet** — the one the app already loads for everything:
30
+ ```bash
31
+ npx -y -p @flareum/mcp flareum pull
32
+ ```
47
33
 
48
- | Project | Usually |
49
- |---|---|
50
- | Angular | the `styles` entry in `angular.json` — commonly `src/styles.scss` |
51
- | Vite / React / Vue | the CSS imported by the entry module — `src/main.tsx`, `src/index.css` |
52
- | Next.js | `app/globals.css`, imported by the root layout |
53
- | Plain | whatever the HTML `<link>`s |
34
+ Pull into the folder this project already keeps styles in. If the default would miss it:
35
+ `npx -y -p @flareum/mcp flareum pull --out app/styles/flareum`.
54
36
 
55
- Add the import at the **top**, before anything that uses a token:
37
+ **2. Are they imported?** Add it to the global stylesheet, at the top:
56
38
 
57
39
  ```scss
58
- @use './styles/flareum/main'; // SCSS
59
- ```
60
- ```css
61
- @import './styles/flareum/main.css'; /* plain CSS */
40
+ @use './styles/flareum/main';
62
41
  ```
63
42
 
64
- Then confirm it resolves — build, or grep the built CSS for a `--[prefix]-` declaration. A token
65
- that is imported but still not applying is usually the `font:` shorthand or a missing
66
- `--line-height`; [references/typography.md](references/typography.md) covers both.
43
+ That file is the `styles` entry in `angular.json` (usually `src/styles.scss`), `app/globals.css` in
44
+ Next.js, or whatever the entry module imports. Confirm it worked build, or grep the built CSS for a
45
+ `--[prefix]-` declaration.
67
46
 
68
- If the pulled folder does not exist at all, the project has never pulled: say so and give the
69
- command`npx -p @flareum/mcp flareum pull` rather than writing tokens that cannot resolve.
47
+ **3. Are they current?** Call `flareum_changes_since`. If it reports changes, the local stylesheets
48
+ are behind the designer a token you are about to use may have been renamed or removed. **Say what
49
+ changed and ask before updating. Do not pull silently.** On yes, follow "Update Flareum". On no, use
50
+ the tokens as they are on disk and say which choices may be affected.
70
51
 
71
- ## When the project is behind
52
+ **Never write tokens against stylesheets that are not there.**
72
53
 
73
- A project with a `.flareum/lock.json` was synced at a particular version. `flareum_changes_since`
74
- says what has changed since — a rename reads as a rename, a removed token carries what to use
75
- instead. Reach for it when the user asks to sync, update or migrate tokens, and at the start of a
76
- session in a project that has a lock.
54
+ ## "Update Flareum"
77
55
 
78
- **Applying those entries has an order that matters**, and getting it wrong leaves a project that
79
- reports itself synced while still referencing dead tokens:
80
- [references/applying.md](references/applying.md).
56
+ When the user says **update flareum**, or asks to sync or migrate tokens:
81
57
 
82
- ## Reading a search result
83
-
84
- The header tells you which kind of answer you got, and they mean different things:
58
+ 1. `flareum_changes_since`.
59
+ 2. **Say what it means** — how many renames, removals and value changes, and how many files each
60
+ rename touches. A removal with a recorded successor is substituted; one **without** is reported
61
+ and left alone: you have no basis for choosing a successor, and a guess is how a design system
62
+ quietly acquires wrong semantics.
63
+ 3. **Ask before changing anything.** A no stops there.
64
+ 4. On yes: **rewrite the call sites first, pull last.** The pull brings the stylesheets AND records
65
+ the project as synced, so pulling first then failing halfway leaves a project that reports itself
66
+ current while still using dead tokens. Rewrite names only — a value change needs no code edit.
85
67
 
86
- - **`Match for "…"`** use it. The `cssName` is ready to paste.
87
- - **`No confident match for "…"`** — the listed tokens are **nearest neighbours, not matches**. One
88
- of them is often the thing you want under a name you would not have guessed. **Do not create a
89
- token or hardcode a value on the strength of this.** Ask which to use, or search again with the
90
- value rather than the name.
68
+ If nothing has changed, say so in one line and stop.
91
69
 
92
- An empty project says so explicitly. Anything else always returns candidates.
70
+ ## Reading a search result
93
71
 
94
- ## If the token genuinely does not exist
72
+ - **`Match for "…"`** use it. The `cssName` is ready to paste.
73
+ - **`No confident match for "…"`** — these are nearest neighbours, not matches. One is often what
74
+ you want under a name you would not have guessed. **Do not invent a token or hardcode a value on the
75
+ strength of this.** Ask which to use, or search again by value.
95
76
 
96
- This integration is **read-only** there is no tool that writes to Flareum, by design. So:
77
+ ## If the token does not exist
97
78
 
98
- 1. Say which token is missing and what it would be for.
99
- 2. Propose a name that fits [the grammar](references/naming.md), so the designer can create it in one step.
100
- 3. Do not silently hardcode the value and move on.
79
+ The integration is **read-only** — nothing writes to Flareum. Say which token is missing and what it
80
+ is for, propose a name that fits [the grammar](references/naming.md), and do not hardcode the value
81
+ and move on.
101
82
 
102
- ## Blast radius before a change
83
+ ## Before changing a token
103
84
 
104
- `flareum_get` reports two independent things, and they answer different questions:
85
+ `flareum_get` reports **component usage** (which components consume it) and **referrers** (which
86
+ tokens reference it, and would break). No usage data means **unknown**, not unused — grep the repo
87
+ before treating a token as safe to change.
105
88
 
106
- - **component usage** — which components consume it (from an imported analysis).
107
- - **referrers** — which other tokens reference it, and would break.
89
+ ## By topic
108
90
 
109
- If it says no usage data has been imported, that means **unknown**, not unused. Grep the repo
110
- yourself before treating a token as safe to change.
91
+ | Writing | Read |
92
+ |---|---|
93
+ | Any text — heading, label, body copy, a pseudo-element's `content` | [references/typography.md](references/typography.md) |
94
+ | A token name, or judging one that exists | [references/naming.md](references/naming.md) |
@@ -1,70 +0,0 @@
1
- # Applying what changed
2
-
3
- Read this when `flareum_changes_since` returns entries, or the user asks to sync, update or migrate
4
- tokens.
5
-
6
- ## The order is the safety
7
-
8
- **Rewrite the call sites first. Pull last.**
9
-
10
- `flareum pull` brings the new stylesheets AND stamps `.flareum/lock.json`, which is what records the
11
- project as synced. Pull first and a rewrite that fails halfway leaves the lock advanced over a
12
- codebase still referencing dead tokens — the next session reports nothing to do, and the breakage is
13
- invisible. Pull last and a failed rewrite simply leaves the project where it was.
14
-
15
- ```
16
- 1. flareum_changes_since read every entry before editing anything
17
- 2. show the developer what you are about to change, and what you cannot
18
- 3. rewrite the call sites names only
19
- 4. npx -p @flareum/mcp flareum pull the new tree, and the lock
20
- ```
21
-
22
- Do not update `lock.json` by hand. It is stamped by a successful pull, and stamping it any other way
23
- is claiming work that did not happen.
24
-
25
- ## What you may change, and what you may not
26
-
27
- | You may | You may not |
28
- |---|---|
29
- | Rewrite `var(--old)` → `var(--new)` across the repo | Change a **value**. Only a name. |
30
- | Substitute a removed token's recorded replacement | Guess a replacement when none is recorded |
31
- | Replace the pulled `variables/` tree by pulling | Hand-edit anything inside it — it is generated |
32
- | Report what you could not resolve | Silently skip an entry |
33
-
34
- ## Entry by entry
35
-
36
- **`renamed` / `moved`** — one entry carries both names. Rewrite every occurrence of `before.cssName`
37
- to `after.cssName`. It is a rename: do not delete the old and add the new, and do not touch the value.
38
-
39
- **`viaFolder`** — this rename is part of a whole folder moving. Apply it as one prefix rewrite across
40
- the members rather than N independent edits, and say so once rather than N times.
41
-
42
- **`deleted` with a replacement** — substitute it. The replacement is what the designer recorded as
43
- the successor, and it may be a token, a formula or a literal.
44
-
45
- **`deleted` with NO replacement** — **report it and leave it alone.** You have no basis for choosing
46
- a successor, and a plausible-looking guess is how a design system quietly acquires wrong semantics.
47
- Name the token, name the files that use it, and let the developer decide.
48
-
49
- **`valueChanged`** — nothing to do in the code. The value lives in the stylesheets the pull replaces;
50
- a call site referencing the token gets the new value for free. Mention it only if the change is large
51
- enough that someone should look at the result.
52
-
53
- **`typeChanged`** — the token still exists under the same name, but it is a different kind of value
54
- now (a colour that became a number). The call sites may still compile and be wrong. Report each one;
55
- do not rewrite them silently.
56
-
57
- **`added`** — nothing to apply. Worth mentioning only if it replaces something you are about to
58
- report as removed.
59
-
60
- **`reordered`** — ignore. It affects the order tokens are emitted in, not any call site.
61
-
62
- ## Before you touch a file
63
-
64
- Say what you are about to do, in the developer's terms: how many call sites, in how many files, and
65
- what you will not be touching. A migration nobody agreed to is a migration nobody can review.
66
-
67
- ## When it does not go cleanly
68
-
69
- Report what failed and **do not pull**. A partial rewrite with the old stylesheets still in place is
70
- a working project; a partial rewrite with the lock advanced is a broken one that claims to be current.