@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 +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +326 -57
- package/dist/index.mjs.map +1 -1
- package/dist/registry.json +2810 -448
- package/package.json +1 -1
- package/readme.body.md +1 -0
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
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -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":";;;
|
|
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$
|
|
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$
|
|
22
|
-
join(HERE$
|
|
23
|
-
join(HERE$
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
/**
|
|
119
|
-
function
|
|
120
|
-
return `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
--cascivo-color-text-
|
|
147
|
-
--cascivo-color-text-on-accent:
|
|
148
|
-
--cascivo-color-text-on-destructive:
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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", "
|
|
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", "
|
|
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
|
-
},
|
|
1330
|
+
}, {
|
|
1331
|
+
...tokenNames ? { tokenNames } : {},
|
|
1332
|
+
...aliasMap && aliasMap.size > 0 ? { aliasMap } : {}
|
|
1333
|
+
}));
|
|
1065
1334
|
});
|
|
1066
1335
|
server.registerTool("get_context", {
|
|
1067
1336
|
title: "Get context",
|