@cascivo/mcp 0.1.8 → 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 CHANGED
@@ -41,6 +41,7 @@ The server speaks the MCP stdio transport. By default it reads the `registry.jso
41
41
  | `get_component` | `{ name }` | The full manifest for one component |
42
42
  | `search_components` | `{ query }` | Components matching name, tags, or description |
43
43
  | `add_to_project` | `{ name, outputDir? }` | Runs `cascivo add <name>` as a child process |
44
+ | `create_app` | `{ name, theme?, sections?, cwd? }` | Scaffolds a full Vite + React app (shell + side nav + theme) via `cascivo create` |
44
45
  | `create_theme` | `{ primary, neutral, accent, name? }` | A custom theme as CSS (semantic token layer) |
45
46
  | `scaffold_page` | `{ description, components? }` | A JSX page layout string |
46
47
  | `scaffold_view` | `{ description, components? }` | A validated starter `ViewConfig` + the bound-vocabulary `grammar` for its components |
package/dist/index.d.mts CHANGED
@@ -22,7 +22,8 @@ interface ThemeColors {
22
22
  }
23
23
  /**
24
24
  * Generate a custom cascivo theme as CSS. Maps the three input colors onto the
25
- * semantic token layer; component tokens inherit automatically.
25
+ * semantic token layer via OKLCH relative color syntax + contrast-color(), each
26
+ * behind a static OKLCH fallback. Component tokens inherit automatically.
26
27
  */
27
28
  declare function generateThemeCss(colors: ThemeColors, name?: string): string;
28
29
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/theme.ts","../src/scaffold.ts","../src/registry.ts","../src/grammar.ts","../src/prompt.ts","../src/index.ts"],"mappings":";;;KA0BK,OAAA,IAAW,GAAA,UAAa,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA,UAE3C,aAAA;EACf,YAAA;EACA,OAAA;;EAEA,OAAA,GAAU,OAAO;AAAA;;iBAeH,YAAA,CAAa,OAAA,GAAS,aAAA,GAAqB,SAAS;;;UC/CnD,WAAA;;EAEf,OAAA;EDwBG;ECtBH,OAAA;;EAEA,MAAA;AAAA;;;;;iBAiBc,gBAAA,CAAiB,MAAA,EAAQ,WAAW,EAAE,IAAA;;;UCvBrC,eAAA;EACf,WAAA;EACA,UAAU;AAAA;;;;;iBAgCI,YAAA,CAAa,OAAwB,EAAf,eAAe;;;UC9BpC,YAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,WAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,KAAA;EACA,KAAA,EAAO,YAAY;EACnB,MAAA;EACA,aAAA;IAAiB,IAAA;IAAc,IAAA;IAAc,QAAA;EAAA;EAC7C,QAAA;IAAY,KAAA;IAAe,IAAA;IAAc,WAAA;EAAA;EACzC,YAAA;EACA,IAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,IAAA;EACA,IAAA,EAAM,iBAAiB;AAAA;AAAA,UAGR,QAAA;EACf,OAAA;EACA,WAAA;EACA,UAAA,EAAY,iBAAiB;AAAA;AAAA,iBAwBf,YAAA,CAAa,IAAA,YAAgB,QAAQ;;iBAUrC,cAAA,CACd,QAAA,EAAU,QAAA,EACV,QAAA,WACA,IAAA,YACC,iBAAiB;;iBAQJ,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,IAAA,WAAe,iBAAiB;;iBAoGjE,gBAAA,CAAiB,QAAA,EAAU,QAAA,EAAU,KAAA,WAAgB,iBAAiB;;;UChLrE,WAAA;EACf,IAAA;EACA,QAAA;EJYU;EIVV,IAAA;EJUkC;EIRlC,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EACA,QAAA;EACA,KAAA,EAAO,WAAW;EAClB,QAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,UAAA,EAAY,gBAAgB;AAAA;;;;;;iBAQd,SAAA,CAAU,IAAY;AAAA,iBA8BtB,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,MAAA,cAAoB,WAAW;;AJpC7D;AAenB;;;;iBI4EgB,aAAA,CAAc,OAAoB,EAAX,WAAW;;;UChHjC,uBAAA;;EAEf,UAAU;AAAA;;;;;;iBAsCI,qBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,uBAA4B;;;cChD1B,OAAA;;iBA0BS,IAAA,IAAQ,OAAO"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/server.ts","../src/theme.ts","../src/scaffold.ts","../src/registry.ts","../src/grammar.ts","../src/prompt.ts","../src/index.ts"],"mappings":";;;KA2BK,OAAA,IAAW,GAAA,UAAa,IAAA,GAAO,WAAA,KAAgB,OAAA,CAAQ,QAAA;AAAA,UAE3C,aAAA;EACf,YAAA;EACA,OAAA;;EAEA,OAAA,GAAU,OAAO;AAAA;;iBAeH,YAAA,CAAa,OAAA,GAAS,aAAA,GAAqB,SAAS;;;UChDnD,WAAA;;EAEf,OAAA;EDyBG;ECvBH,OAAA;;EAEA,MAAA;AAAA;;;;;AD2BiB;iBCmJH,gBAAA,CAAiB,MAAA,EAAQ,WAAW,EAAE,IAAA;;;UCpLrC,eAAA;EACf,WAAA;EACA,UAAU;AAAA;;;;;iBAgCI,YAAA,CAAa,OAAwB,EAAf,eAAe;;;UC9BpC,YAAA;EACf,IAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,WAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,KAAA;EACA,KAAA,EAAO,YAAY;EACnB,MAAA;EACA,aAAA;IAAiB,IAAA;IAAc,IAAA;IAAc,QAAA;EAAA;EAC7C,QAAA;IAAY,KAAA;IAAe,IAAA;IAAc,WAAA;EAAA;EACzC,YAAA;EACA,IAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,KAAA;EACA,YAAA;EACA,IAAA;EACA,IAAA,EAAM,iBAAiB;AAAA;AAAA,UAGR,QAAA;EACf,OAAA;EACA,WAAA;EACA,UAAA,EAAY,iBAAiB;AAAA;AAAA,iBAwBf,YAAA,CAAa,IAAA,YAAgB,QAAQ;;iBAUrC,cAAA,CACd,QAAA,EAAU,QAAA,EACV,QAAA,WACA,IAAA,YACC,iBAAiB;;iBAQJ,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,IAAA,WAAe,iBAAiB;;iBAoGjE,gBAAA,CAAiB,QAAA,EAAU,QAAA,EAAU,KAAA,WAAgB,iBAAiB;;;UChLrE,WAAA;EACf,IAAA;EACA,QAAA;EJaU;EIXV,IAAA;EJWkC;EITlC,IAAA;AAAA;AAAA,UAGe,gBAAA;EACf,IAAA;EACA,QAAA;EACA,KAAA,EAAO,WAAW;EAClB,QAAA;EACA,KAAA;AAAA;AAAA,UAGe,WAAA;EACf,UAAA,EAAY,gBAAgB;AAAA;;;;;;iBAQd,SAAA,CAAU,IAAY;AAAA,iBA8BtB,YAAA,CAAa,QAAA,EAAU,QAAA,EAAU,MAAA,cAAoB,WAAW;;AJnC7D;AAenB;;;;iBI2EgB,aAAA,CAAc,OAAoB,EAAX,WAAW;;;UChHjC,uBAAA;;EAEf,UAAU;AAAA;;;;;;iBAsCI,qBAAA,CACd,QAAA,EAAU,QAAA,EACV,OAAA,GAAS,uBAA4B;;;cChD1B,OAAA;;iBA0BS,IAAA,IAAQ,OAAO"}
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import { z } from "zod";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  //#region src/registry.ts
11
- const HERE$3 = dirname(fileURLToPath(import.meta.url));
11
+ const HERE$4 = dirname(fileURLToPath(import.meta.url));
12
12
  /**
13
13
  * Resolve the registry.json location. Checks, in order: an explicit path, the
14
14
  * `CASCIVO_REGISTRY_PATH` env var, a copy bundled next to the built server
@@ -18,9 +18,9 @@ function resolveRegistryPath(explicit) {
18
18
  const candidates = [
19
19
  explicit,
20
20
  process.env.CASCIVO_REGISTRY_PATH,
21
- join(HERE$3, "registry.json"),
22
- join(HERE$3, "..", "..", "..", "registry.json"),
23
- join(HERE$3, "..", "..", "registry.json")
21
+ join(HERE$4, "registry.json"),
22
+ join(HERE$4, "..", "..", "..", "registry.json"),
23
+ join(HERE$4, "..", "..", "registry.json")
24
24
  ].filter((p) => typeof p === "string" && p.length > 0);
25
25
  return candidates.find((p) => existsSync(p)) ?? candidates[candidates.length - 1] ?? "registry.json";
26
26
  }
@@ -111,64 +111,175 @@ function searchComponents(registry, query) {
111
111
  }
112
112
  //#endregion
113
113
  //#region src/theme.ts
114
- /** Darken a color by mixing in black (modern CSS, no preprocessor). */
115
- function darken(color, amount) {
116
- return `color-mix(in oklab, ${color}, black ${amount}%)`;
114
+ const NAMED = {
115
+ black: "#000000",
116
+ white: "#ffffff",
117
+ red: "#ff0000",
118
+ green: "#008000",
119
+ blue: "#0000ff",
120
+ gray: "#808080",
121
+ grey: "#808080",
122
+ silver: "#c0c0c0",
123
+ maroon: "#800000",
124
+ olive: "#808000",
125
+ lime: "#00ff00",
126
+ aqua: "#00ffff",
127
+ teal: "#008080",
128
+ navy: "#000080",
129
+ fuchsia: "#ff00ff",
130
+ purple: "#800080",
131
+ orange: "#ffa500",
132
+ yellow: "#ffff00"
133
+ };
134
+ const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
135
+ const round = (n, p) => {
136
+ const f = 10 ** p;
137
+ return Math.round(n * f) / f;
138
+ };
139
+ /** sRGB (0–1, gamma-encoded) → OKLCH. Ottosson's reference transform. */
140
+ function srgbToOklch(r, g, b) {
141
+ const lin = (c) => c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
142
+ const rl = lin(r);
143
+ const gl = lin(g);
144
+ const bl = lin(b);
145
+ const l_ = Math.cbrt(.4122214708 * rl + .5363325363 * gl + .0514459929 * bl);
146
+ const m_ = Math.cbrt(.2119034982 * rl + .6806995451 * gl + .1073969566 * bl);
147
+ const s_ = Math.cbrt(.0883024619 * rl + .2817188376 * gl + .6299787005 * bl);
148
+ const L = .2104542553 * l_ + .793617785 * m_ - .0040720468 * s_;
149
+ const a = 1.9779984951 * l_ - 2.428592205 * m_ + .4505937099 * s_;
150
+ const bb = .0259040371 * l_ + .7827717662 * m_ - .808675766 * s_;
151
+ const C = Math.hypot(a, bb);
152
+ let H = Math.atan2(bb, a) * 180 / Math.PI;
153
+ if (H < 0) H += 360;
154
+ return {
155
+ l: clamp(L, 0, 1),
156
+ c: Math.max(0, C),
157
+ h: H
158
+ };
159
+ }
160
+ function hexToOklch(hex) {
161
+ let h = hex.slice(1);
162
+ if (h.length === 3) h = h.split("").map((ch) => ch + ch).join("");
163
+ return srgbToOklch(parseInt(h.slice(0, 2), 16) / 255, parseInt(h.slice(2, 4), 16) / 255, parseInt(h.slice(4, 6), 16) / 255);
164
+ }
165
+ /** Parse a hex / rgb() / oklch() / named color into OKLCH. Throws on unknown input. */
166
+ function parseToOklch(input) {
167
+ const raw = input.trim().toLowerCase();
168
+ const named = NAMED[raw];
169
+ if (named) return hexToOklch(named);
170
+ if (raw.startsWith("#")) return hexToOklch(raw);
171
+ const oklchM = /^oklch\(\s*([\d.]+%?)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(raw);
172
+ if (oklchM) {
173
+ const parsePct = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
174
+ const o = {
175
+ l: clamp(parsePct(oklchM[1]), 0, 1),
176
+ c: parseFloat(oklchM[2]),
177
+ h: parseFloat(oklchM[3])
178
+ };
179
+ if (oklchM[4]) o.a = parsePct(oklchM[4]);
180
+ return o;
181
+ }
182
+ const rgbM = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(raw);
183
+ if (rgbM) return srgbToOklch(parseFloat(rgbM[1]) / 255, parseFloat(rgbM[2]) / 255, parseFloat(rgbM[3]) / 255);
184
+ throw new Error(`create_theme: cannot parse color "${input}" — use hex, rgb(), oklch(), or a named color`);
185
+ }
186
+ /** Serialize OKLCH to a canonical `oklch(L C H[ / a])` literal. */
187
+ function fmt(o) {
188
+ const base = `oklch(${round(o.l, 3)} ${round(o.c, 3)} ${round(o.h, 1)}`;
189
+ return o.a !== void 0 ? `${base} / ${round(o.a, 3)})` : `${base})`;
190
+ }
191
+ /** Apply a step to an OKLCH color (lightness/chroma deltas, optional alpha). */
192
+ function step(o, d) {
193
+ const next = {
194
+ l: clamp(o.l + (d.dl ?? 0), 0, 1),
195
+ c: Math.max(0, o.c * (d.dc ?? 1)),
196
+ h: o.h
197
+ };
198
+ if (d.a !== void 0) next.a = d.a;
199
+ return next;
200
+ }
201
+ /**
202
+ * Emit a fallback-then-derived declaration pair: the static OKLCH literal first
203
+ * (what every browser uses), then the relative-color form (progressive). Both
204
+ * are computed from the same base + delta so they agree.
205
+ */
206
+ function derive(prop, baseToken, base, d) {
207
+ return ` ${prop}: ${fmt(step(base, d))};\n ${prop}: ${`oklch(from var(${baseToken}) ${d.dl ? `calc(l ${d.dl >= 0 ? "+" : "-"} ${Math.abs(d.dl)})` : "l"} ${d.dc !== void 0 && d.dc !== 1 ? `calc(c * ${d.dc})` : "c"} h${d.a !== void 0 ? ` / ${d.a}` : ""})`};\n`;
117
208
  }
118
- /** Lighten a color by mixing in white. */
119
- function lighten(color, amount) {
120
- return `color-mix(in oklab, ${color}, white ${amount}%)`;
209
+ /** A plain static OKLCH literal declaration (no derivation). */
210
+ function lit(prop, o) {
211
+ return ` ${prop}: ${fmt(o)};\n`;
121
212
  }
122
213
  /**
214
+ * Pick a WCAG-safe on-color (used as the static fallback for contrast-color()):
215
+ * dark text on light surfaces, light text on dark ones.
216
+ */
217
+ function onColor(base) {
218
+ return base.l > .6 ? {
219
+ l: .145,
220
+ c: .005,
221
+ h: base.h
222
+ } : {
223
+ l: 1,
224
+ c: 0,
225
+ h: 0
226
+ };
227
+ }
228
+ const DESTRUCTIVE = {
229
+ l: .577,
230
+ c: .215,
231
+ h: 27.3
232
+ };
233
+ const SUCCESS = {
234
+ l: .627,
235
+ c: .176,
236
+ h: 149.6
237
+ };
238
+ const WARNING = {
239
+ l: .769,
240
+ c: .166,
241
+ h: 70.7
242
+ };
243
+ /**
123
244
  * Generate a custom cascivo theme as CSS. Maps the three input colors onto the
124
- * semantic token layer; component tokens inherit automatically.
245
+ * semantic token layer via OKLCH relative color syntax + contrast-color(), each
246
+ * behind a static OKLCH fallback. Component tokens inherit automatically.
125
247
  */
126
248
  function generateThemeCss(colors, name = "custom") {
127
- const { primary, neutral, accent } = colors;
249
+ const primary = parseToOklch(colors.primary);
250
+ const neutral = parseToOklch(colors.neutral);
251
+ const accent = parseToOklch(colors.accent);
252
+ const surface = (l, cMul = .15) => ({
253
+ l,
254
+ c: round(neutral.c * cMul, 3),
255
+ h: neutral.h
256
+ });
257
+ const onAccent = onColor(primary);
258
+ const onDestructive = onColor(DESTRUCTIVE);
128
259
  return `/* cascivo — Generated theme: ${name} */
260
+ /* Built like the first-party themes: OKLCH + relative color syntax + */
261
+ /* contrast-color(), every progressive value behind a static fallback. */
129
262
 
130
263
  @layer cascivo.theme {
131
264
  [data-theme='${name}'] {
132
265
  color-scheme: light;
133
266
 
134
267
  /* ── Surface (derived from neutral) ───────────────── */
135
- --cascivo-color-bg: ${lighten(neutral, 96)};
136
- --cascivo-color-bg-subtle: ${lighten(neutral, 92)};
137
- --cascivo-color-surface: ${lighten(neutral, 98)};
138
- --cascivo-color-surface-raised: ${lighten(neutral, 94)};
139
- --cascivo-color-surface-overlay: ${lighten(neutral, 98)};
140
- --cascivo-color-border: ${lighten(neutral, 80)};
141
- --cascivo-color-border-strong: ${lighten(neutral, 65)};
142
-
268
+ ${lit("--cascivo-color-bg", surface(.99))}${lit("--cascivo-color-bg-subtle", surface(.975))}${lit("--cascivo-color-surface", surface(.985))}${lit("--cascivo-color-surface-raised", surface(.97))}${lit("--cascivo-color-surface-overlay", surface(.99))}${lit("--cascivo-color-border", surface(.9, .25))}${lit("--cascivo-color-border-strong", surface(.8, .3))}
143
269
  /* ── Text (derived from neutral) ──────────────────── */
144
- --cascivo-color-text: ${darken(neutral, 80)};
145
- --cascivo-color-text-subtle: ${darken(neutral, 50)};
146
- --cascivo-color-text-muted: ${lighten(neutral, 30)};
147
- --cascivo-color-text-on-accent: #ffffff;
148
- --cascivo-color-text-on-destructive: #ffffff;
149
-
150
- /* ── Accent / primary interactive ─────────────────── */
151
- --cascivo-color-accent: ${primary};
152
- --cascivo-color-accent-hover: ${darken(primary, 12)};
153
- --cascivo-color-accent-active: ${darken(primary, 24)};
154
- --cascivo-color-accent-subtle: ${lighten(primary, 88)};
155
- --cascivo-color-accent-muted: ${lighten(primary, 76)};
270
+ ${lit("--cascivo-color-text", surface(.22, .4))}${lit("--cascivo-color-text-subtle", surface(.45, .35))}${lit("--cascivo-color-text-muted", surface(.6, .3))}
271
+ /* on-color: static fallback then auto-contrast (WCAG) */
272
+ --cascivo-color-text-on-accent: ${fmt(onAccent)};
273
+ --cascivo-color-text-on-accent: contrast-color(var(--cascivo-color-accent));
274
+ --cascivo-color-text-on-destructive: ${fmt(onDestructive)};
275
+ --cascivo-color-text-on-destructive: contrast-color(var(--cascivo-color-destructive));
156
276
 
277
+ /* ── Accent / primary interactive (derived from one base) ── */
278
+ ${lit("--cascivo-color-accent", primary)}${derive("--cascivo-color-accent-hover", "--cascivo-color-accent", primary, { dl: -.077 })}${derive("--cascivo-color-accent-active", "--cascivo-color-accent", primary, { dl: -.154 })}${derive("--cascivo-color-accent-subtle", "--cascivo-color-accent", primary, { a: .1 })}${derive("--cascivo-color-accent-muted", "--cascivo-color-accent", primary, { a: .2 })}
157
279
  /* ── Secondary accent (info / focus) ──────────────── */
158
- --cascivo-color-info: ${accent};
159
- --cascivo-color-info-subtle: ${lighten(accent, 88)};
160
- --cascivo-color-focus-ring: ${accent};
161
- --cascivo-focus-ring: 0 0 0 3px ${lighten(accent, 20)};
162
-
163
- /* ── Status ───────────────────────────────────────── */
164
- --cascivo-color-destructive: #dc2626;
165
- --cascivo-color-destructive-hover: ${darken("#dc2626", 12)};
166
- --cascivo-color-destructive-subtle: ${lighten("#dc2626", 88)};
167
- --cascivo-color-success: #16a34a;
168
- --cascivo-color-success-subtle: ${lighten("#16a34a", 88)};
169
- --cascivo-color-warning: #f59e0b;
170
- --cascivo-color-warning-subtle: ${lighten("#f59e0b", 88)};
171
- }
280
+ ${lit("--cascivo-color-info", accent)}${derive("--cascivo-color-info-subtle", "--cascivo-color-info", accent, { a: .1 })}${lit("--cascivo-color-focus-ring", accent)}
281
+ /* ── Status (in-system OKLCH; no hex) ─────────────── */
282
+ ${lit("--cascivo-color-destructive", DESTRUCTIVE)}${derive("--cascivo-color-destructive-hover", "--cascivo-color-destructive", DESTRUCTIVE, { dl: -.077 })}${derive("--cascivo-color-destructive-subtle", "--cascivo-color-destructive", DESTRUCTIVE, { a: .1 })}${lit("--cascivo-color-success", SUCCESS)}${derive("--cascivo-color-success-subtle", "--cascivo-color-success", SUCCESS, { a: .1 })}${lit("--cascivo-color-warning", WARNING)}${derive("--cascivo-color-warning-subtle", "--cascivo-color-warning", WARNING, { a: .1 })} }
172
283
  }
173
284
  `;
174
285
  }
@@ -496,10 +607,10 @@ function buildGenerationPrompt(registry, options = {}) {
496
607
  }
497
608
  //#endregion
498
609
  //#region src/tokens.ts
499
- const HERE$2 = dirname(fileURLToPath(import.meta.url));
610
+ const HERE$3 = dirname(fileURLToPath(import.meta.url));
500
611
  const CATALOG_BASE_URL = "https://cascivo.com";
501
612
  async function loadTokenCatalog(fetchFn) {
502
- const localPaths = [join(HERE$2, "..", "..", "..", "apps", "docs", "public", "tokens.catalog.json"), join(HERE$2, "tokens.catalog.json")];
613
+ const localPaths = [join(HERE$3, "..", "..", "..", "apps", "site", "public", "tokens.catalog.json"), join(HERE$3, "tokens.catalog.json")];
503
614
  for (const p of localPaths) if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
504
615
  const res = await (fetchFn ?? fetch)(`${CATALOG_BASE_URL}/tokens.catalog.json`);
505
616
  if (!res.ok) throw new Error(`Failed to fetch token catalog: ${res.status}`);
@@ -507,10 +618,10 @@ async function loadTokenCatalog(fetchFn) {
507
618
  }
508
619
  //#endregion
509
620
  //#region src/variants.ts
510
- const HERE$1 = dirname(fileURLToPath(import.meta.url));
621
+ const HERE$2 = dirname(fileURLToPath(import.meta.url));
511
622
  const BASE_URL = "https://cascivo.com";
512
623
  async function loadVariantMatrix(fetchFn) {
513
- const localPaths = [join(HERE$1, "..", "..", "..", "apps", "docs", "public", "tokens.variants.json"), join(HERE$1, "tokens.variants.json")];
624
+ const localPaths = [join(HERE$2, "..", "..", "..", "apps", "site", "public", "tokens.variants.json"), join(HERE$2, "tokens.variants.json")];
514
625
  for (const p of localPaths) if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
515
626
  const res = await (fetchFn ?? fetch)(`${BASE_URL}/tokens.variants.json`);
516
627
  if (!res.ok) throw new Error(`Failed to fetch variant matrix: ${res.status}`);
@@ -647,6 +758,27 @@ function checkTokens(css, tokenNames) {
647
758
  }
648
759
  return violations;
649
760
  }
761
+ /** Flag `var(--cascivo-…)` references that use an alias where a canonical token exists. */
762
+ function checkAliasTokens(css, aliasMap) {
763
+ if (aliasMap.size === 0) return [];
764
+ const violations = [];
765
+ const re = /var\(\s*(--cascivo-[\w-]+)/g;
766
+ let m;
767
+ const seen = /* @__PURE__ */ new Set();
768
+ while ((m = re.exec(css)) !== null) {
769
+ const token = m[1];
770
+ const canonical = aliasMap.get(token);
771
+ if (!canonical || seen.has(token)) continue;
772
+ seen.add(token);
773
+ violations.push({
774
+ rule: "alias-token-used",
775
+ severity: "warning",
776
+ line: lineOf(css, m.index),
777
+ detail: `"${token}" is an alias — prefer the canonical "${canonical}"`
778
+ });
779
+ }
780
+ return violations;
781
+ }
650
782
  /** Run all structural invariants over a candidate component's source. */
651
783
  function validateComponentSource(input, options = {}) {
652
784
  const violations = [];
@@ -655,6 +787,7 @@ function validateComponentSource(input, options = {}) {
655
787
  violations.push(...checkBreakpoints(input.css));
656
788
  violations.push(...checkFallbacks(input.css));
657
789
  if (options.tokenNames) violations.push(...checkTokens(input.css, options.tokenNames));
790
+ if (options.aliasMap) violations.push(...checkAliasTokens(input.css, options.aliasMap));
658
791
  }
659
792
  violations.sort((a, b) => a.line - b.line);
660
793
  return {
@@ -665,10 +798,10 @@ function validateComponentSource(input, options = {}) {
665
798
  }
666
799
  //#endregion
667
800
  //#region src/context.ts
668
- const HERE = dirname(fileURLToPath(import.meta.url));
801
+ const HERE$1 = dirname(fileURLToPath(import.meta.url));
669
802
  const CONTEXT_BASE_URL = "https://cascivo.com";
670
803
  async function loadContext(fetchFn) {
671
- const localPaths = [join(HERE, "..", "..", "..", "apps", "docs", "public", "context.json"), join(HERE, "context.json")];
804
+ const localPaths = [join(HERE$1, "..", "..", "..", "apps", "site", "public", "context.json"), join(HERE$1, "context.json")];
672
805
  for (const p of localPaths) if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
673
806
  const res = await (fetchFn ?? fetch)(`${CONTEXT_BASE_URL}/context.json`);
674
807
  if (!res.ok) throw new Error(`Failed to fetch context bundle: ${res.status}`);
@@ -676,7 +809,7 @@ async function loadContext(fetchFn) {
676
809
  }
677
810
  async function loadComponentMarkdown(name, fetchFn) {
678
811
  const slug = name.toLowerCase().replace(/\s+/g, "-");
679
- const localPaths = [join(HERE, "..", "..", "..", "apps", "docs", "public", "context", `${slug}.md`), join(HERE, "context", `${slug}.md`)];
812
+ const localPaths = [join(HERE$1, "..", "..", "..", "apps", "site", "public", "context", `${slug}.md`), join(HERE$1, "context", `${slug}.md`)];
680
813
  for (const p of localPaths) if (existsSync(p)) return readFileSync(p, "utf8");
681
814
  const url = `${CONTEXT_BASE_URL}/context/${slug}.md`;
682
815
  const fn = fetchFn ?? fetch;
@@ -739,6 +872,57 @@ function selectComponent(need, components) {
739
872
  })).filter((r) => r.score >= 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, 5);
740
873
  }
741
874
  //#endregion
875
+ //#region src/templates.ts
876
+ const HERE = dirname(fileURLToPath(import.meta.url));
877
+ const EMPTY = {
878
+ generatedAt: "",
879
+ templates: [],
880
+ facets: {
881
+ categories: [],
882
+ frameworks: [],
883
+ tags: []
884
+ }
885
+ };
886
+ /**
887
+ * Resolve the marketplace catalog location. Checks, in order: an explicit path,
888
+ * the `CASCIVO_MARKETPLACE_PATH` env var, a copy bundled next to the built
889
+ * server, and the in-repo `apps/site/public/marketplace.json` (dev).
890
+ */
891
+ function resolveCatalogPath(explicit) {
892
+ const candidates = [
893
+ explicit,
894
+ process.env.CASCIVO_MARKETPLACE_PATH,
895
+ join(HERE, "marketplace.json"),
896
+ join(HERE, "..", "..", "..", "apps", "site", "public", "marketplace.json"),
897
+ join(HERE, "..", "..", "apps", "site", "public", "marketplace.json")
898
+ ].filter((p) => typeof p === "string" && p.length > 0);
899
+ return candidates.find((p) => existsSync(p)) ?? candidates[candidates.length - 1] ?? "";
900
+ }
901
+ /** Load the marketplace catalog, returning an empty catalog if none is found. */
902
+ function loadCatalog(explicit) {
903
+ const path = resolveCatalogPath(explicit);
904
+ if (!path || !existsSync(path)) return EMPTY;
905
+ try {
906
+ return JSON.parse(readFileSync(path, "utf8"));
907
+ } catch {
908
+ return EMPTY;
909
+ }
910
+ }
911
+ /** Filter the catalog's templates. Pure. */
912
+ function listTemplates(catalog, filter = {}) {
913
+ return catalog.templates.filter((t) => {
914
+ if (filter.category && t.category !== filter.category) return false;
915
+ if (filter.framework && t.framework !== filter.framework) return false;
916
+ if (filter.verifiedOnly && !t.verified) return false;
917
+ if (filter.tag && !t.tags.includes(filter.tag)) return false;
918
+ return true;
919
+ });
920
+ }
921
+ /** Find one template by name or installSpec. Pure. */
922
+ function getTemplate(catalog, name) {
923
+ return catalog.templates.find((t) => t.name === name || t.installSpec === name);
924
+ }
925
+ //#endregion
742
926
  //#region src/server.ts
743
927
  const json = (value) => ({ content: [{
744
928
  type: "text",
@@ -860,6 +1044,84 @@ function createServer(options = {}) {
860
1044
  if (result.status !== 0) return error(result.stderr || result.error?.message || `Failed to add "${name}".`);
861
1045
  return text(result.stdout || `Added ${name}.`);
862
1046
  });
1047
+ server.registerTool("list_templates", {
1048
+ title: "List templates",
1049
+ description: "List marketplace templates (whole-page compositions) from the static catalog, optionally filtered by category, tag, framework, or verified status.",
1050
+ inputSchema: {
1051
+ category: z.string().optional().describe("Filter by category, e.g. \"dashboard\""),
1052
+ tag: z.string().optional().describe("Filter by tag"),
1053
+ framework: z.enum(["react-vite", "react-next"]).optional().describe("Filter by target framework"),
1054
+ verifiedOnly: z.boolean().optional().describe("Only verified templates")
1055
+ }
1056
+ }, ({ category, tag, framework, verifiedOnly }) => {
1057
+ const catalog = loadCatalog();
1058
+ const filter = {};
1059
+ if (category) filter.category = category;
1060
+ if (tag) filter.tag = tag;
1061
+ if (framework) filter.framework = framework;
1062
+ if (verifiedOnly) filter.verifiedOnly = verifiedOnly;
1063
+ return json(listTemplates(catalog, filter));
1064
+ });
1065
+ server.registerTool("get_template", {
1066
+ title: "Get template",
1067
+ description: "Get one marketplace template by name or install spec — its components, install command, demo link, and screenshots.",
1068
+ inputSchema: { name: z.string().describe("Template name or install spec, e.g. \"@cascivo/dashboard\"") }
1069
+ }, ({ name }) => {
1070
+ const tpl = getTemplate(loadCatalog(), name);
1071
+ if (!tpl) return error(`Template "${name}" not found in the marketplace catalog.`);
1072
+ return json(tpl);
1073
+ });
1074
+ server.registerTool("add_template", {
1075
+ title: "Add template",
1076
+ description: "Install a marketplace template (its components + page/fixture files) into the current project by running the cascade CLI.",
1077
+ inputSchema: {
1078
+ name: z.string().describe("Template name or install spec, e.g. \"@cascivo/dashboard\""),
1079
+ cwd: z.string().optional().describe("Project directory (default: current directory)")
1080
+ }
1081
+ }, ({ name, cwd }) => {
1082
+ const spec = getTemplate(loadCatalog(), name)?.installSpec ?? name;
1083
+ const result = spawnSync("npx", [
1084
+ "-y",
1085
+ "cascivo",
1086
+ "add",
1087
+ spec
1088
+ ], {
1089
+ encoding: "utf8",
1090
+ ...cwd ? { cwd } : {}
1091
+ });
1092
+ if (result.status !== 0) return error(result.stderr || result.error?.message || `Failed to add template "${spec}".`);
1093
+ return text(result.stdout || `Added template ${spec}.`);
1094
+ });
1095
+ server.registerTool("create_app", {
1096
+ title: "Create app",
1097
+ description: "Scaffold a complete, ready-to-run cascivo app (Vite + React + TypeScript) wired with the app shell, side navigation, header, and a theme — one page per nav section, with signal-driven section switching. Runs `cascivo create` as a child process, writing the project into a new <name> directory.",
1098
+ inputSchema: {
1099
+ name: z.string().describe("Project name and directory, e.g. \"my-app\""),
1100
+ theme: z.enum([
1101
+ "light",
1102
+ "dark",
1103
+ "warm"
1104
+ ]).optional().describe("Theme to wire in (default: light)"),
1105
+ sections: z.array(z.string()).optional().describe("Side-nav section labels; one page is generated per section (default: Dashboard, Reports, Settings)"),
1106
+ cwd: z.string().optional().describe("Directory to create the app in (default: current directory)")
1107
+ }
1108
+ }, ({ name, theme, sections, cwd }) => {
1109
+ const args = [
1110
+ "-y",
1111
+ "cascivo",
1112
+ "create",
1113
+ name,
1114
+ "--yes"
1115
+ ];
1116
+ if (theme) args.push("--theme", theme);
1117
+ if (sections && sections.length > 0) args.push("--sections", sections.join(", "));
1118
+ const result = spawnSync("npx", args, {
1119
+ encoding: "utf8",
1120
+ ...cwd ? { cwd } : {}
1121
+ });
1122
+ if (result.status !== 0) return error(result.stderr || result.error?.message || `Failed to create "${name}".`);
1123
+ return text(result.stdout || `Created ${name}.`);
1124
+ });
863
1125
  server.registerTool("create_theme", {
864
1126
  title: "Create theme",
865
1127
  description: "Generate a cascade theme CSS file from three colors.",
@@ -990,18 +1252,20 @@ export function Diagram() {
990
1252
  });
991
1253
  server.registerTool("get_tokens", {
992
1254
  title: "Get tokens",
993
- description: "Get the cascade token catalog (closed set). Agents must select from this catalog rather than hard-coding values.",
1255
+ description: "Get the cascade token catalog (closed set). Agents must select from this catalog rather than hard-coding values. Returns the CANONICAL token set by default — exactly one name per purpose. Pass includeAliases: true to also list backwards-compat aliases, each tagged with its canonical name.",
994
1256
  inputSchema: {
995
1257
  group: z.string().optional().describe("Filter by token group, e.g. \"color\", \"space\", \"radius\""),
996
1258
  layer: z.enum([
997
1259
  "primitive",
998
1260
  "semantic",
999
1261
  "component"
1000
- ]).optional().describe("Filter by layer")
1262
+ ]).optional().describe("Filter by layer"),
1263
+ includeAliases: z.boolean().optional().describe("Include backwards-compat alias tokens (e.g. --cascivo-color-bg → --cascivo-color-background). Off by default so agents see one correct token per purpose.")
1001
1264
  }
1002
- }, async ({ group, layer }) => {
1265
+ }, async ({ group, layer, includeAliases }) => {
1003
1266
  try {
1004
1267
  let tokens = (await loadTokenCatalog(fetchFn)).tokens;
1268
+ if (!includeAliases) tokens = tokens.filter((t) => !t.aliasOf);
1005
1269
  if (group) tokens = tokens.filter((t) => t.group === group);
1006
1270
  if (layer) tokens = tokens.filter((t) => t.layer === layer);
1007
1271
  return json({
@@ -1053,15 +1317,20 @@ export function Diagram() {
1053
1317
  }
1054
1318
  }, async ({ tsx, css, name }) => {
1055
1319
  let tokenNames;
1320
+ let aliasMap;
1056
1321
  try {
1057
1322
  const catalog = await loadTokenCatalog(fetchFn);
1058
1323
  tokenNames = new Set(catalog.tokens.map((t) => t.name));
1324
+ aliasMap = new Map(catalog.tokens.filter((t) => t.aliasOf).map((t) => [t.name, t.aliasOf]));
1059
1325
  } catch {}
1060
1326
  return json(validateComponentSource({
1061
1327
  ...tsx ? { tsx } : {},
1062
1328
  ...css ? { css } : {},
1063
1329
  ...name ? { name } : {}
1064
- }, tokenNames ? { tokenNames } : {}));
1330
+ }, {
1331
+ ...tokenNames ? { tokenNames } : {},
1332
+ ...aliasMap && aliasMap.size > 0 ? { aliasMap } : {}
1333
+ }));
1065
1334
  });
1066
1335
  server.registerTool("get_context", {
1067
1336
  title: "Get context",