@theme-registry/refract-mcp 0.1.5 → 0.1.7

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 CHANGED
@@ -8,6 +8,11 @@
8
8
  It is **project-scoped**: it loads your `theme.config.(ts|js|mjs)` once at startup and serves queries
9
9
  against it, so the agent asks about *your* theme without ever resending it. It reloads on change.
10
10
 
11
+ The reload watches the config's directory recursively — so a theme split across `./tokens/*.ts` reloads
12
+ too, not just an edit to the config itself. Hidden paths are never watched (that includes `.git`,
13
+ `.next`, `.turbo`, and the hidden `.mjs` files a `.ts` config's graph-compile emits beside your sources
14
+ and unlinks again), and neither are `node_modules` / `dist` / `out` / `coverage`. Reloads never overlap.
15
+
11
16
  ## Tools
12
17
 
13
18
  | Tool | Answers |
package/dist/server.js CHANGED
@@ -227,10 +227,25 @@ function diffHeld(state, candidateRaw) {
227
227
  }
228
228
  return { ok: validation.ok, diff, targets: validation.perTarget };
229
229
  }
230
- // Dirs never worth reloading on (vendored deps, build output, VCS). Skipped under the recursive watch.
231
- const WATCH_IGNORE = /(^|[/\\])(node_modules|dist|out|\.git|coverage)([/\\]|$)/;
230
+ // Dirs never worth reloading on (vendored deps, build output). Skipped under the recursive watch.
231
+ const WATCH_IGNORE = /(^|[/\\])(node_modules|dist|out|coverage)([/\\]|$)/;
232
232
  // Source files a theme graph is authored in — a change to any of them may change the built theme.
233
233
  const WATCH_SOURCE = /\.(ts|mjs|cjs|js|json)$/;
234
+ /**
235
+ * Any hidden path segment. A theme graph is never authored in one, and this is the rule that keeps the
236
+ * server from waking on its own output: loading a `.ts` config graph-compiles it to hidden
237
+ * `.<base>.<pid>-<n>.mjs` files emitted **beside each compiled source** (adjacency is load-bearing —
238
+ * it's what keeps relative sibling specifiers resolvable), which are imported and then unlinked. Those
239
+ * writes and deletes match {@link WATCH_SOURCE}, so without this a load wakes the watcher, which
240
+ * reloads, which emits them again — an endless self-triggered loop with no user edit involved, churning
241
+ * the config's directory (visible in an editor as a file tree that never stops refreshing). Also covers
242
+ * `.git`, `.next`, `.turbo`, `.cache` and friends, whose build churn caused spurious reloads too.
243
+ */
244
+ const WATCH_HIDDEN = /(^|[/\\])\./;
245
+ /** Is this watch event worth rebuilding the theme for? Pure, so the loop guard above is unit-testable. */
246
+ function shouldReload(filename) {
247
+ return WATCH_SOURCE.test(filename) && !WATCH_HIDDEN.test(filename) && !WATCH_IGNORE.test(filename);
248
+ }
234
249
  /**
235
250
  * Watch the config's directory **recursively** and rebuild on any source-file change — so a theme split
236
251
  * across `./tokens/*.ts` (imported transitively by the config) reloads, not just an edit to the config
@@ -241,21 +256,40 @@ function watchConfig() {
241
256
  if (!held)
242
257
  return;
243
258
  let timer;
259
+ // Second line of defence behind {@link WATCH_HIDDEN}: never overlap two loads, so anything a load
260
+ // writes can't start another one. Edits landing mid-load aren't lost — they re-arm the debounce once.
261
+ let reloading = false;
262
+ let pending = false;
263
+ const reload = () => {
264
+ if (reloading) {
265
+ pending = true;
266
+ return;
267
+ }
268
+ reloading = true;
269
+ loadTheme(configPathArg)
270
+ .then((next) => {
271
+ held = next;
272
+ process.stderr.write(`refract-mcp: reloaded theme from ${next.path}\n`);
273
+ })
274
+ .catch((e) => process.stderr.write(`refract-mcp: reload failed (kept previous theme): ${e.message}\n`))
275
+ .finally(() => {
276
+ reloading = false;
277
+ if (pending) {
278
+ pending = false;
279
+ schedule();
280
+ }
281
+ });
282
+ };
283
+ const schedule = () => {
284
+ clearTimeout(timer);
285
+ timer = setTimeout(reload, 150);
286
+ };
244
287
  const onChange = (_event, filename) => {
245
288
  if (!filename)
246
289
  return;
247
- const f = filename.toString();
248
- if (WATCH_IGNORE.test(f) || !WATCH_SOURCE.test(f))
290
+ if (!shouldReload(filename.toString()))
249
291
  return;
250
- clearTimeout(timer);
251
- timer = setTimeout(() => {
252
- loadTheme(configPathArg)
253
- .then((next) => {
254
- held = next;
255
- process.stderr.write(`refract-mcp: reloaded theme from ${next.path}\n`);
256
- })
257
- .catch((e) => process.stderr.write(`refract-mcp: reload failed (kept previous theme): ${e.message}\n`));
258
- }, 150);
292
+ schedule();
259
293
  };
260
294
  try {
261
295
  const dir = dirname(held.path);
@@ -479,4 +513,4 @@ function isBinEntry(invokedAs, selfPath, real = realpathOr) {
479
513
  }
480
514
  }
481
515
 
482
- export { callTool, createServer, guideFiles, isBinEntry, loadTheme, serverVersion, start };
516
+ export { callTool, createServer, guideFiles, isBinEntry, loadTheme, serverVersion, shouldReload, start };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theme-registry/refract-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Model Context Protocol server for refract — query + validate a project's theme from an AI agent.",
5
5
  "author": {
6
6
  "name": "Petyo Stoyanov",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@theme-registry/refract": "^0.1.5"
28
+ "@theme-registry/refract": "^0.1.7"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "typescript": "*"
@@ -42,7 +42,7 @@
42
42
  "rollup": "^3.29.4",
43
43
  "typescript": "^5.4.2",
44
44
  "vitest": "^4.1.10",
45
- "@theme-registry/refract-css": "^0.1.5"
45
+ "@theme-registry/refract-css": "^0.1.7"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "rollup -c",