vectorvesper 2.0.2 → 2.0.4

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.
@@ -0,0 +1,839 @@
1
+ import {
2
+ detectProject,
3
+ fetchRegistryIndex
4
+ } from "./chunk-5YSVDVLK.js";
5
+ import {
6
+ findHook,
7
+ loadHookManifest
8
+ } from "./chunk-XN3PPCIM.js";
9
+ import {
10
+ getAuthToken
11
+ } from "./chunk-SFP5K3ZO.js";
12
+
13
+ // src/mcp/server.ts
14
+ import { z } from "zod";
15
+
16
+ // src/mcp/check-motion.ts
17
+ import fs from "fs";
18
+ import path from "path";
19
+ async function loadBabel() {
20
+ const mod = await import("@babel/core");
21
+ const parse = mod.parseSync ?? mod.default?.parseSync;
22
+ const traverseRaw = mod.traverse ?? mod.default?.traverse;
23
+ const traverse = traverseRaw?.default ?? traverseRaw;
24
+ if (typeof parse !== "function" || typeof traverse !== "function") {
25
+ throw new Error("@babel/core parse/traverse unavailable");
26
+ }
27
+ return { parse, traverse };
28
+ }
29
+ var HOT_EVENTS = /* @__PURE__ */ new Set(["scroll", "wheel", "pointermove", "mousemove", "touchmove", "pointerdown"]);
30
+ var BROWSER_GLOBALS = /* @__PURE__ */ new Set(["window", "document", "navigator", "localStorage", "sessionStorage"]);
31
+ var OBSERVERS = /* @__PURE__ */ new Set(["IntersectionObserver", "ResizeObserver", "MutationObserver"]);
32
+ function calleeName(callee) {
33
+ if (!callee) return void 0;
34
+ if (callee.type === "Identifier") return callee.name;
35
+ if ((callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") && callee.property) {
36
+ return callee.property.name ?? callee.property.value;
37
+ }
38
+ return void 0;
39
+ }
40
+ function isSetter(name) {
41
+ return !!name && /^set[A-Z]/.test(name) && name !== "setInterval" && name !== "setTimeout";
42
+ }
43
+ function collect(ast, traverse) {
44
+ const f = {
45
+ hasUseClient: (ast.program.directives ?? []).some((d) => d.value?.value === "use client"),
46
+ vvHooks: [],
47
+ importedModules: /* @__PURE__ */ new Set(),
48
+ usesUseGSAP: false,
49
+ usesR3F: false,
50
+ raf: [],
51
+ hasCancelRaf: false,
52
+ addEventListener: [],
53
+ hasRemoveEventListener: false,
54
+ setInterval: [],
55
+ hasClearInterval: false,
56
+ newObserver: [],
57
+ hasObserverDisconnect: false,
58
+ gsapTimeline: [],
59
+ hasGsapCleanup: false,
60
+ threeDisposable: [],
61
+ hasThreeDispose: false,
62
+ moduleScopeBrowserApi: [],
63
+ setStateInHot: [],
64
+ transformAuthorities: [],
65
+ looping: [],
66
+ hasReducedMotionGuard: false
67
+ };
68
+ const manifest = loadHookManifest();
69
+ const hookByName = /* @__PURE__ */ new Map();
70
+ for (const h of manifest?.hooks ?? []) hookByName.set(h.name, h);
71
+ let fnDepth = 0;
72
+ const scanHotCallback = (cbNode, ctx, fromRaf) => {
73
+ if (!cbNode || cbNode.type !== "ArrowFunctionExpression" && cbNode.type !== "FunctionExpression") return;
74
+ traverse(
75
+ { type: "File", program: { type: "Program", body: [{ type: "ExpressionStatement", expression: cbNode }], directives: [] } },
76
+ {
77
+ CallExpression(p) {
78
+ if (isSetter(calleeName(p.node.callee))) {
79
+ f.setStateInHot.push({ line: cbNode.loc?.start.line ?? 0, ctx, fromRaf });
80
+ }
81
+ },
82
+ OptionalCallExpression(p) {
83
+ if (isSetter(calleeName(p.node.callee))) {
84
+ f.setStateInHot.push({ line: cbNode.loc?.start.line ?? 0, ctx, fromRaf });
85
+ }
86
+ }
87
+ }
88
+ );
89
+ };
90
+ const handleCall = (node) => {
91
+ const name = calleeName(node.callee);
92
+ const line = node.loc?.start.line ?? 0;
93
+ switch (name) {
94
+ case "requestAnimationFrame":
95
+ f.raf.push(line);
96
+ scanHotCallback(node.arguments?.[0], "a requestAnimationFrame loop", true);
97
+ break;
98
+ case "cancelAnimationFrame":
99
+ f.hasCancelRaf = true;
100
+ break;
101
+ case "setInterval":
102
+ f.setInterval.push(line);
103
+ break;
104
+ case "clearInterval":
105
+ f.hasClearInterval = true;
106
+ break;
107
+ case "addEventListener": {
108
+ f.addEventListener.push(line);
109
+ const ev = node.arguments?.[0];
110
+ if (ev?.type === "StringLiteral" && HOT_EVENTS.has(ev.value)) {
111
+ scanHotCallback(node.arguments?.[1], `a "${ev.value}" listener`, false);
112
+ }
113
+ break;
114
+ }
115
+ case "removeEventListener":
116
+ f.hasRemoveEventListener = true;
117
+ break;
118
+ case "disconnect":
119
+ f.hasObserverDisconnect = true;
120
+ break;
121
+ case "dispose":
122
+ f.hasThreeDispose = true;
123
+ break;
124
+ case "matchMedia":
125
+ f.hasReducedMotionGuard = true;
126
+ break;
127
+ case "kill":
128
+ case "revert":
129
+ f.hasGsapCleanup = true;
130
+ break;
131
+ case "timeline":
132
+ if (f.importedModules.has("gsap") || f.importedModules.has("@gsap/react")) {
133
+ f.gsapTimeline.push(line);
134
+ }
135
+ break;
136
+ }
137
+ };
138
+ traverse(ast, {
139
+ Function: {
140
+ enter() {
141
+ fnDepth++;
142
+ },
143
+ exit() {
144
+ fnDepth--;
145
+ }
146
+ },
147
+ ImportDeclaration(p) {
148
+ const src = p.node.source.value;
149
+ f.importedModules.add(src);
150
+ if (src === "@gsap/react") f.usesUseGSAP = true;
151
+ if (src === "@react-three/fiber") f.usesR3F = true;
152
+ if (src === "@vectorvesper/motion" || src.startsWith("@vectorvesper/motion/")) {
153
+ for (const spec of p.node.specifiers ?? []) {
154
+ const name = spec.imported?.name ?? spec.local?.name;
155
+ const hook = name && hookByName.get(name);
156
+ if (hook) f.vvHooks.push({ hook, line: p.node.loc?.start.line ?? 0 });
157
+ }
158
+ }
159
+ },
160
+ StringLiteral(p) {
161
+ const v = p.node.value;
162
+ if (v.includes("prefers-reduced-motion")) f.hasReducedMotionGuard = true;
163
+ if (/\binfinite\b/.test(v)) f.looping.push({ line: p.node.loc?.start.line ?? 0, kind: "CSS `infinite` animation" });
164
+ if (/\btransition\b|\banimation\b/.test(v) && /\btransform\b|\ball\b/.test(v)) {
165
+ f.transformAuthorities.push({ line: p.node.loc?.start.line ?? 0, source: "a CSS transition/animation" });
166
+ }
167
+ },
168
+ // `repeat: Infinity` (framer-motion) / `repeat: -1` (gsap) — looping motion.
169
+ ObjectProperty(p) {
170
+ const key = p.node.key?.name ?? p.node.key?.value;
171
+ if (key !== "repeat") return;
172
+ const val = p.node.value;
173
+ const infinite = val?.type === "Identifier" && val.name === "Infinity" || val?.type === "NumericLiteral" && val.value === -1 || val?.type === "UnaryExpression" && val.operator === "-" && val.argument?.value === 1;
174
+ if (infinite) f.looping.push({ line: p.node.loc?.start.line ?? 0, kind: "an infinite `repeat`" });
175
+ },
176
+ // A `transform` in a JSX style object/string means this node's transform is
177
+ // being animated or set outside any hook.
178
+ JSXAttribute(p) {
179
+ if (p.node.name?.name !== "style") return;
180
+ const line = p.node.loc?.start.line ?? 0;
181
+ const val = p.node.value;
182
+ const expr = val?.type === "JSXExpressionContainer" ? val.expression : null;
183
+ if (expr?.type === "ObjectExpression") {
184
+ for (const prop of expr.properties ?? []) {
185
+ const k = prop.key?.name ?? prop.key?.value;
186
+ if (k === "transform") f.transformAuthorities.push({ line, source: "an inline style `transform`" });
187
+ }
188
+ }
189
+ },
190
+ Identifier(p) {
191
+ const name = p.node.name;
192
+ if (!BROWSER_GLOBALS.has(name)) return;
193
+ if (fnDepth !== 0) return;
194
+ const parent = p.parent;
195
+ if (parent?.type === "UnaryExpression" && parent.operator === "typeof") return;
196
+ if (parent?.type === "MemberExpression" && parent.property === p.node && !parent.computed) return;
197
+ if (parent?.type === "VariableDeclarator" && parent.id === p.node) return;
198
+ f.moduleScopeBrowserApi.push({ line: p.node.loc?.start.line ?? 0, api: name });
199
+ },
200
+ NewExpression(p) {
201
+ const n = p.node.callee?.name;
202
+ if (n && OBSERVERS.has(n)) f.newObserver.push(p.node.loc?.start.line ?? 0);
203
+ if (n && /(?:Geometry|Material|Texture|RenderTarget)$/.test(n)) {
204
+ f.threeDisposable.push(p.node.loc?.start.line ?? 0);
205
+ }
206
+ },
207
+ CallExpression(p) {
208
+ handleCall(p.node);
209
+ },
210
+ OptionalCallExpression(p) {
211
+ handleCall(p.node);
212
+ }
213
+ });
214
+ return f;
215
+ }
216
+ function checkClientBoundary(f, file, framework) {
217
+ const out = [];
218
+ const servery = framework === "next" || framework === "unknown";
219
+ if (!servery || f.hasUseClient) {
220
+ }
221
+ if (servery && !f.hasUseClient) {
222
+ const clientHook = f.vvHooks.find((v) => v.hook.runtime.requiresClient);
223
+ if (clientHook) {
224
+ out.push({
225
+ rule: "client-boundary",
226
+ severity: "error",
227
+ file,
228
+ line: clientHook.line,
229
+ message: `\`${clientHook.hook.name}\` is client-only but this file has no \`"use client"\` directive.`,
230
+ fix: 'Add `"use client";` as the first line of this file.'
231
+ });
232
+ }
233
+ }
234
+ for (const b of f.moduleScopeBrowserApi) {
235
+ out.push({
236
+ rule: "ssr-module-scope",
237
+ severity: "error",
238
+ file,
239
+ line: b.line,
240
+ message: `\`${b.api}\` is used at module scope; it is undefined during server rendering and will throw at import time.`,
241
+ fix: `Move the \`${b.api}\` access inside a component, an effect, or an event handler \u2014 anywhere that only runs in the browser.`
242
+ });
243
+ }
244
+ return out;
245
+ }
246
+ function isRafLoop(f) {
247
+ return f.raf.length >= 2;
248
+ }
249
+ function checkOrphanRaf(f, file) {
250
+ if (!isRafLoop(f)) return [];
251
+ return [
252
+ {
253
+ rule: "orphan-raf",
254
+ severity: "warning",
255
+ file,
256
+ line: f.raf[0],
257
+ message: `This file runs its own \`requestAnimationFrame\` loop${f.raf.length > 1 ? ` (\xD7${f.raf.length})` : ""}. Independent loops each schedule their own frame and cannot shed work under load \u2014 the coordination the runtime exists to provide is lost.`,
258
+ fix: "Drive the animation from the shared conductor (`getConductor().subscribe(...)` in `@vectorvesper/motion`) or a hook that already joins it, so it shares one frame budget.",
259
+ nudge: "Call `list_hooks` \u2014 a sensor or governor primitive may already cover this without a hand-written loop."
260
+ }
261
+ ];
262
+ }
263
+ function checkMissingCleanup(f, file) {
264
+ const out = [];
265
+ const push = (line, what, teardown) => out.push({
266
+ rule: "missing-cleanup",
267
+ severity: "warning",
268
+ file,
269
+ line,
270
+ message: `${what} is created here but never torn down. On unmount or re-render it leaks, and repeated mounts stack copies that compound into jank.`,
271
+ fix: `Return a cleanup from the effect that calls ${teardown}.`
272
+ });
273
+ if (isRafLoop(f) && !f.hasCancelRaf) push(f.raf[0], "A `requestAnimationFrame` loop", "`cancelAnimationFrame`");
274
+ if (f.addEventListener.length && !f.hasRemoveEventListener)
275
+ push(f.addEventListener[0], "An event listener", "`removeEventListener` with the same handler");
276
+ if (f.setInterval.length && !f.hasClearInterval) push(f.setInterval[0], "A `setInterval`", "`clearInterval`");
277
+ if (f.newObserver.length && !f.hasObserverDisconnect) push(f.newObserver[0], "An observer", "`.disconnect()`");
278
+ if (f.gsapTimeline.length && !f.usesUseGSAP && !f.hasGsapCleanup)
279
+ push(f.gsapTimeline[0], "A GSAP timeline", "`.kill()` (or drive it from `useGSAP`, which reverts automatically)");
280
+ if (f.threeDisposable.length && !f.usesR3F && !f.hasThreeDispose)
281
+ push(f.threeDisposable[0], "A three.js geometry/material/texture", "`.dispose()`");
282
+ return out;
283
+ }
284
+ function checkSetStatePerFrame(f, file) {
285
+ const hot = f.setStateInHot.filter((s) => !s.fromRaf || isRafLoop(f));
286
+ if (hot.length === 0) return [];
287
+ const zeroReRender = f.vvHooks.find((v) => v.hook.runtime.reRendersPerFrame === 0);
288
+ const first = hot[0];
289
+ return [
290
+ {
291
+ rule: "setstate-per-frame",
292
+ severity: "warning",
293
+ file,
294
+ line: first.line,
295
+ message: `A React state setter runs inside ${first.ctx}. Every call re-renders the component \u2014 at up to 60\u2013120\xD7/second this is the most common cause of animation jank.` + (zeroReRender ? ` \`${zeroReRender.hook.name}\` is designed to avoid exactly this.` : ""),
296
+ fix: "Write the value to a `ref` or straight to the DOM node inside the callback; keep it out of React state."
297
+ }
298
+ ];
299
+ }
300
+ function checkTransformConflict(f, file) {
301
+ const owner = f.vvHooks.find((v) => v.hook.runtime.ownsTransform);
302
+ if (!owner || f.transformAuthorities.length === 0) return [];
303
+ const src = f.transformAuthorities[0];
304
+ return [
305
+ {
306
+ rule: "transform-conflict",
307
+ severity: "warning",
308
+ file,
309
+ line: src.line,
310
+ message: `\`${owner.hook.name}\` owns its element's \`transform\`, but ${src.source} animates a transform in the same file. Two owners of one transform overwrite each other frame to frame.`,
311
+ fix: `Give the hook its own element, and animate the CSS/library transform on a different node (e.g. a wrapper or child).`
312
+ }
313
+ ];
314
+ }
315
+ function checkReducedMotion(f, file) {
316
+ if (f.looping.length === 0 || f.hasReducedMotionGuard) return [];
317
+ if (f.vvHooks.some((v) => v.hook.runtime.respectsReducedMotion)) return [];
318
+ const first = f.looping[0];
319
+ return [
320
+ {
321
+ rule: "no-reduced-motion",
322
+ severity: "warning",
323
+ file,
324
+ line: first.line,
325
+ message: `This file runs autonomous motion (${first.kind}) with no \`prefers-reduced-motion\` guard. Users who set that preference \u2014 often for vestibular reasons \u2014 will still see the full motion.`,
326
+ fix: "Gate the motion behind `window.matchMedia('(prefers-reduced-motion: reduce)')`, or drive it from a hook that self-disables."
327
+ }
328
+ ];
329
+ }
330
+ var SUPPORTED = /\.(tsx|ts|jsx|mjs|cjs|js)$/i;
331
+ async function analyzeSource(file, source, framework) {
332
+ let ast;
333
+ try {
334
+ const { parse, traverse } = await loadBabel();
335
+ const isJsx = /\.(tsx|jsx|js|mjs|cjs)$/i.test(file);
336
+ ast = parse(source, {
337
+ configFile: false,
338
+ babelrc: false,
339
+ parserOpts: {
340
+ sourceType: "module",
341
+ errorRecovery: true,
342
+ plugins: isJsx ? ["typescript", "jsx"] : ["typescript"]
343
+ }
344
+ });
345
+ const facts = collect(ast, traverse);
346
+ const findings = [
347
+ ...checkClientBoundary(facts, file, framework),
348
+ ...checkOrphanRaf(facts, file),
349
+ ...checkMissingCleanup(facts, file),
350
+ ...checkSetStatePerFrame(facts, file),
351
+ ...checkTransformConflict(facts, file),
352
+ ...checkReducedMotion(facts, file)
353
+ ].sort((a, b) => a.line - b.line);
354
+ return { file, findings };
355
+ } catch (err) {
356
+ return { file, findings: [], error: err instanceof Error ? err.message : String(err) };
357
+ }
358
+ }
359
+ async function checkMotionFiles(files, cwd) {
360
+ const project = detectProject(cwd);
361
+ const results = [];
362
+ for (const rel of files) {
363
+ const abs = path.isAbsolute(rel) ? rel : path.join(cwd, rel);
364
+ if (!SUPPORTED.test(abs)) {
365
+ results.push({ file: rel, findings: [], error: "unsupported file type (expected .tsx/.ts/.jsx/.js)" });
366
+ continue;
367
+ }
368
+ let source;
369
+ try {
370
+ source = fs.readFileSync(abs, "utf-8");
371
+ } catch {
372
+ results.push({ file: rel, findings: [], error: `could not read file at ${abs}` });
373
+ continue;
374
+ }
375
+ results.push(await analyzeSource(rel, source, project.framework));
376
+ }
377
+ return results;
378
+ }
379
+
380
+ // src/mcp/server.ts
381
+ var VERSION = true ? "2.0.4" : "0.0.0-dev";
382
+ function text(body) {
383
+ return { content: [{ type: "text", text: body }] };
384
+ }
385
+ function formatHookSummary(hook) {
386
+ return `**${hook.name}** (${hook.category}) \u2014 ${hook.tagline}
387
+ Problem: ${hook.problem}`;
388
+ }
389
+ function formatRuntimeContract(hook) {
390
+ const r = hook.runtime;
391
+ const lines = [];
392
+ lines.push(
393
+ r.requiresClient ? '- **Client only.** Needs `"use client"`. On Next.js App Router, a parent server component must not render it directly.' : "- Runs anywhere (no browser-only APIs at module scope)."
394
+ );
395
+ if (r.ownsTransform) {
396
+ lines.push(
397
+ "- **Owns the element's inline `transform`.** Give it its own element; do not also animate that node's transform with CSS, framer-motion or GSAP."
398
+ );
399
+ }
400
+ lines.push(
401
+ r.respectsReducedMotion ? "- Honours `prefers-reduced-motion` automatically \u2014 no extra work needed." : "- Does **not** self-disable under `prefers-reduced-motion`; gate it yourself if it drives autonomous motion."
402
+ );
403
+ const caps = [
404
+ r.usesPointer && "pointer",
405
+ r.usesScroll && "scroll",
406
+ r.usesWebGL && "WebGL"
407
+ ].filter(Boolean);
408
+ if (caps.length) lines.push(`- Uses: ${caps.join(", ")}.`);
409
+ if (r.lane) {
410
+ lines.push(
411
+ `- Runs in the conductor's **${r.lane}** lane${r.priority ? ` at \`${r.priority}\` priority` : ""}. Lanes run input \u2192 update \u2192 render each frame, so sensors are always current before anything draws.`
412
+ );
413
+ } else if (r.lane === null) {
414
+ lines.push("- Never joins the frame loop \u2014 no per-frame cost.");
415
+ }
416
+ if (typeof r.reRendersPerFrame === "number") {
417
+ lines.push(
418
+ r.reRendersPerFrame === 0 ? "- **Zero React re-renders per frame.** Values are written straight to the DOM or held in refs; do not mirror them into state." : `- Causes ${r.reRendersPerFrame} React re-render(s) per frame \u2014 keep it out of hot paths.`
419
+ );
420
+ }
421
+ if (r.sharedSingletons?.length) {
422
+ lines.push(
423
+ `- Retains shared singletons: ${r.sharedSingletons.map((s) => `\`${s}\``).join(", ")} (ref-counted \u2014 cost is shared with every other consumer, not multiplied).`
424
+ );
425
+ }
426
+ if (r.conflictsWith.length) {
427
+ lines.push("- **Conflicts with:**");
428
+ for (const c of r.conflictsWith) lines.push(` - ${c}`);
429
+ }
430
+ return lines.join("\n");
431
+ }
432
+ function formatHookDetail(hook) {
433
+ const out = [];
434
+ out.push(`# ${hook.name}`);
435
+ out.push(`> ${hook.tagline}`);
436
+ out.push("");
437
+ out.push(`**The problem it removes:** ${hook.problem}`);
438
+ out.push("");
439
+ out.push(hook.summary);
440
+ out.push("");
441
+ out.push(`## Import
442
+ \`\`\`ts
443
+ import { ${hook.name} } from "${hook.importFrom}";
444
+ \`\`\``);
445
+ out.push(`Install with \`npm i ${hook.packageName}\`. Available since v${hook.since}.`);
446
+ out.push("");
447
+ out.push(`## Signature
448
+ \`${hook.signature}\``);
449
+ if (hook.options.length) {
450
+ out.push("\n### Options");
451
+ out.push("| Option | Type | Default | Description |");
452
+ out.push("| --- | --- | --- | --- |");
453
+ for (const o of hook.options) {
454
+ out.push(`| \`${o.name}\`${o.required ? " *(required)*" : ""} | \`${o.type}\` | \`${o.default}\` | ${o.description} |`);
455
+ }
456
+ } else {
457
+ out.push("\n### Options\nNone \u2014 this hook takes no arguments.");
458
+ }
459
+ out.push("\n### Returns");
460
+ out.push("| Field | Type | Description |");
461
+ out.push("| --- | --- | --- |");
462
+ for (const r of hook.returns) out.push(`| \`${r.name}\` | \`${r.type}\` | ${r.description} |`);
463
+ out.push("\n## Runtime contract");
464
+ out.push(formatRuntimeContract(hook));
465
+ if (hook.mechanism) {
466
+ out.push("\n## How it works inside");
467
+ out.push(hook.mechanism);
468
+ }
469
+ out.push("\n## Quick start");
470
+ out.push("```tsx");
471
+ out.push(hook.quickStart);
472
+ out.push("```");
473
+ if (hook.recipes?.length) {
474
+ out.push("\n## Recipes");
475
+ for (const r of hook.recipes) {
476
+ out.push(`
477
+ ### ${r.name}
478
+ ${r.blurb}
479
+
480
+ \`\`\`tsx
481
+ ${r.code}
482
+ \`\`\``);
483
+ }
484
+ }
485
+ if (hook.dos?.length) out.push(`
486
+ ## Do
487
+ ${hook.dos.map((d) => `- ${d}`).join("\n")}`);
488
+ if (hook.donts?.length) out.push(`
489
+ ## Don't
490
+ ${hook.donts.map((d) => `- ${d}`).join("\n")}`);
491
+ out.push("\n## When NOT to use this");
492
+ for (const w of hook.whenNotToUse) out.push(`- **${w.when}** \u2192 ${w.instead}`);
493
+ if (hook.guardrails?.length) {
494
+ out.push(`
495
+ ## Required reading before writing motion code
496
+ ${hook.guardrails.map((g) => `- ${g}`).join("\n")}`);
497
+ }
498
+ out.push(`
499
+ ## Docs
500
+ ${hook.docsUrl}${hook.labUrl ? `
501
+ Live demo: ${hook.labUrl}` : ""}`);
502
+ out.push(`
503
+ ---
504
+ **Consumption rule.** ${hook.disclosure.correctUsage} ${hook.disclosure.reason}`);
505
+ if (hook.disclosure.readingSource) {
506
+ out.push(`
507
+ **Reading the implementation.** ${hook.disclosure.readingSource}`);
508
+ }
509
+ if (hook.contractLevel === "lean" && hook.upgrade) {
510
+ out.push(`
511
+ _${hook.upgrade}_`);
512
+ }
513
+ return out.join("\n");
514
+ }
515
+ function formatComponentDetail(c) {
516
+ const out = [];
517
+ out.push(`# ${c.title} \`${c.slug}\``);
518
+ out.push(`> ${c.description}`);
519
+ out.push("");
520
+ out.push(`**Tier:** ${c.tier}${c.tier === "pro" ? " \u2014 requires a Vector Vesper membership" : ""}`);
521
+ out.push(`**Category:** ${c.category} \xB7 **Type:** ${c.type} \xB7 **Version:** ${c.version}`);
522
+ out.push(`**Frameworks:** ${c.frameworks.join(", ")}`);
523
+ out.push("\n## Motion contract");
524
+ const lines = [];
525
+ lines.push(
526
+ c.requiresClient ? '- **Client only.** Needs `"use client"`.' : "- No client boundary required."
527
+ );
528
+ if (c.usesWebGL) {
529
+ lines.push(
530
+ "- **Uses WebGL.** On Next.js, render it through `dynamic(() => import(...), { ssr: false })` or it will fail during server rendering."
531
+ );
532
+ }
533
+ lines.push(
534
+ c.supportsReducedMotion ? "- Respects `prefers-reduced-motion`." : "- Does **not** self-disable under `prefers-reduced-motion`."
535
+ );
536
+ const caps = [c.usesPointer && "pointer", c.usesScroll && "scroll"].filter(Boolean);
537
+ if (caps.length) lines.push(`- Uses: ${caps.join(", ")}.`);
538
+ if (c.usesTailwind) lines.push("- Ships hardcoded Tailwind classes \u2014 the project needs Tailwind configured.");
539
+ if (c.fallbacks?.length) lines.push(`- Fallbacks: ${c.fallbacks.join(", ")}`);
540
+ out.push(lines.join("\n"));
541
+ if (c.dependencies.length || c.registryDependencies.length) {
542
+ out.push("\n## Dependencies");
543
+ if (c.dependencies.length) out.push(`- npm: ${c.dependencies.map((d) => `\`${d}\``).join(", ")}`);
544
+ if (c.registryDependencies.length) {
545
+ out.push(`- other VV components: ${c.registryDependencies.map((d) => `\`${d}\``).join(", ")} (installed automatically)`);
546
+ }
547
+ }
548
+ out.push(`
549
+ ## Install
550
+ \`\`\`bash
551
+ npx vectorvesper add ${c.slug}
552
+ \`\`\``);
553
+ if (c.tier === "pro") {
554
+ out.push(
555
+ "Requires an authenticated CLI: `npx vectorvesper login <token>`. Tokens come from https://vectorvesper.dev/account."
556
+ );
557
+ }
558
+ if (c.docsUrl) out.push(`
559
+ ## Docs
560
+ ${c.docsUrl}`);
561
+ out.push(
562
+ "\n---\n**Note.** Component source is delivered by running the install command above, which writes the files into the project. It is deliberately not returned here \u2014 pasting a component out of a discovery response skips dependency resolution, the TypeScript/JavaScript transpile step, and the install manifest that makes `update` and `remove` work later."
563
+ );
564
+ return out.join("\n");
565
+ }
566
+ function severityMark(s) {
567
+ return s === "error" ? "\u2717 error" : s === "warning" ? "\u25B2 warning" : "\xB7 note";
568
+ }
569
+ var CHECK_SCOPE_NOTE = "_Static analysis \u2014 it catches the common silent motion mistakes (unmanaged loops, missing cleanup, state-per-frame, transform conflicts, missing reduced-motion, SSR-unsafe access), not all of them. An **error** breaks at build or runtime; a **warning** compiles and runs but janks, leaks, or excludes users._";
570
+ function formatCheckResults(results) {
571
+ let errors = 0;
572
+ let warnings = 0;
573
+ let filesWithFindings = 0;
574
+ const blocks = [];
575
+ for (const r of results) {
576
+ if (r.error) {
577
+ blocks.push(`### \`${r.file}\`
578
+ - could not analyze: ${r.error}`);
579
+ continue;
580
+ }
581
+ if (r.findings.length === 0) continue;
582
+ filesWithFindings++;
583
+ const lines = [`### \`${r.file}\``];
584
+ for (const f of r.findings) {
585
+ if (f.severity === "error") errors++;
586
+ else if (f.severity === "warning") warnings++;
587
+ lines.push(`- **${severityMark(f.severity)} \xB7 ${f.rule}** (line ${f.line}) \u2014 ${f.message}`);
588
+ lines.push(` - **Fix:** ${f.fix}`);
589
+ if (f.nudge) lines.push(` - ${f.nudge}`);
590
+ }
591
+ blocks.push(lines.join("\n"));
592
+ }
593
+ const total = errors + warnings;
594
+ if (total === 0) {
595
+ return `\u2713 No motion issues found in ${results.length} file${results.length === 1 ? "" : "s"}.
596
+
597
+ ` + CHECK_SCOPE_NOTE;
598
+ }
599
+ return [
600
+ "# Motion check",
601
+ `${total} issue${total === 1 ? "" : "s"} across ${filesWithFindings} file${filesWithFindings === 1 ? "" : "s"} \u2014 ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}.`,
602
+ "",
603
+ ...blocks,
604
+ "",
605
+ CHECK_SCOPE_NOTE
606
+ ].join("\n");
607
+ }
608
+ async function registerTools(server) {
609
+ server.registerTool(
610
+ "list_hooks",
611
+ {
612
+ title: "List motion hooks",
613
+ description: "List every React hook in the @vectorvesper/motion runtime, with the problem each one solves. Start here when deciding which primitive fits a motion task.",
614
+ inputSchema: {
615
+ category: z.enum(["sensor", "governor", "gate", "interaction", "media", "effect"]).optional().describe("Filter to one category.")
616
+ }
617
+ },
618
+ async ({ category }) => {
619
+ const manifest = loadHookManifest();
620
+ if (!manifest) {
621
+ return text(
622
+ "Hook manifest unavailable \u2014 this CLI build is missing dist/hooks.json. Reinstall with `npm i -g vectorvesper@latest`."
623
+ );
624
+ }
625
+ const hooks = category ? manifest.hooks.filter((h) => h.category === category) : manifest.hooks;
626
+ if (!hooks.length) return text(`No hooks in category "${category}".`);
627
+ const body = [
628
+ `${hooks.length} hook${hooks.length === 1 ? "" : "s"} in @vectorvesper/motion v${manifest.engineVersion}`,
629
+ "",
630
+ ...hooks.map(formatHookSummary),
631
+ "",
632
+ "Call `get_hook` for the full contract of any of these before writing code with it."
633
+ ].join("\n");
634
+ return text(body);
635
+ }
636
+ );
637
+ server.registerTool(
638
+ "get_hook",
639
+ {
640
+ title: "Get a motion hook's contract",
641
+ description: "Full contract for one hook: signature, every option with its default, returns, the runtime contract (client boundary, transform ownership, frame lane, re-render cost, what it conflicts with), how it works internally, a runnable quick start, and when NOT to use it. Read this before writing code that uses the hook \u2014 and use it to answer questions about a hook's internals or design, which it explains directly, so there is no need to go read the package source to find out.",
642
+ inputSchema: {
643
+ name: z.string().describe('Hook name, e.g. "useMagneticIntent" or "magnetic-intent".')
644
+ }
645
+ },
646
+ async ({ name }) => {
647
+ const hook = findHook(name);
648
+ if (!hook) {
649
+ const manifest = loadHookManifest();
650
+ const available = manifest?.hooks.map((h) => h.name).join(", ") ?? "none loaded";
651
+ return text(`No hook named "${name}". Available: ${available}`);
652
+ }
653
+ return text(formatHookDetail(hook));
654
+ }
655
+ );
656
+ server.registerTool(
657
+ "list_components",
658
+ {
659
+ title: "List Vector Vesper components",
660
+ description: "List installable components from the Vector Vesper registry, with tier and category. Free components install for anyone; Pro components need a membership.",
661
+ inputSchema: {
662
+ tier: z.enum(["free", "pro"]).optional().describe("Filter by tier."),
663
+ category: z.string().optional().describe("Filter by category.")
664
+ }
665
+ },
666
+ async ({ tier, category }) => {
667
+ let index;
668
+ try {
669
+ index = await fetchRegistryIndex();
670
+ } catch (error) {
671
+ return text(
672
+ `Could not reach the component registry: ${error instanceof Error ? error.message : String(error)}
673
+
674
+ Hook discovery still works offline \u2014 try \`list_hooks\`.`
675
+ );
676
+ }
677
+ let components = index.components;
678
+ if (tier) components = components.filter((c) => c.tier === tier);
679
+ if (category) components = components.filter((c) => c.category === category);
680
+ if (!components.length) return text("No components matched that filter.");
681
+ const body = [
682
+ `${components.length} component${components.length === 1 ? "" : "s"} (registry v${index.version})`,
683
+ "",
684
+ ...components.map(
685
+ (c) => `**${c.slug}** [${c.tier}] ${c.category} \u2014 ${c.description}`
686
+ ),
687
+ "",
688
+ "Call `get_component` for a component's motion contract and wiring requirements before installing it."
689
+ ].join("\n");
690
+ return text(body);
691
+ }
692
+ );
693
+ server.registerTool(
694
+ "get_component",
695
+ {
696
+ title: "Get a component's motion contract",
697
+ description: "Metadata and wiring requirements for one component: whether it needs a client boundary, whether it needs dynamic import to survive SSR, what it depends on, and how to install it. Does not return source \u2014 source is written into the project by the install command.",
698
+ inputSchema: { slug: z.string().describe('Component slug, e.g. "code-rain".') }
699
+ },
700
+ async ({ slug }) => {
701
+ let index;
702
+ try {
703
+ index = await fetchRegistryIndex();
704
+ } catch (error) {
705
+ return text(
706
+ `Could not reach the component registry: ${error instanceof Error ? error.message : String(error)}`
707
+ );
708
+ }
709
+ const component = index.components.find((c) => c.slug === slug);
710
+ if (!component) {
711
+ return text(
712
+ `No component with slug "${slug}". Call \`list_components\` to see what exists.`
713
+ );
714
+ }
715
+ return text(formatComponentDetail(component));
716
+ }
717
+ );
718
+ server.registerTool(
719
+ "get_setup_guidance",
720
+ {
721
+ title: "Inspect the current project",
722
+ description: "Detect the framework, package manager, TypeScript/JavaScript, Tailwind and Next router of the project in the working directory, plus Vector Vesper auth status. Call this before generating install commands or import paths so they match the project.",
723
+ inputSchema: {
724
+ cwd: z.string().optional().describe("Project root. Defaults to the server's working directory.")
725
+ }
726
+ },
727
+ async ({ cwd }) => {
728
+ const info = detectProject(cwd ?? process.cwd());
729
+ const authed = Boolean(getAuthToken());
730
+ const out = [];
731
+ out.push("# Project");
732
+ out.push(`- Root: \`${info.rootPath}\``);
733
+ out.push(`- Framework: **${info.framework}**${info.nextRouter ? ` (${info.nextRouter} router)` : ""}`);
734
+ out.push(`- Language: **${info.isTypeScript ? "TypeScript" : "JavaScript"}**`);
735
+ out.push(`- Package manager: **${info.packageManager}**`);
736
+ out.push(`- Tailwind: ${info.hasTailwind ? "yes" : "no"}`);
737
+ out.push(`- \`src/\` directory: ${info.hasSrcDir ? "yes" : "no"}`);
738
+ out.push("\n# Vector Vesper");
739
+ out.push(`- CLI authenticated: ${authed ? "yes \u2014 Pro components available" : "no \u2014 free components only"}`);
740
+ if (!authed) {
741
+ out.push(" - To unlock Pro: `npx vectorvesper login <token>` (tokens at https://vectorvesper.dev/account)");
742
+ }
743
+ out.push("\n# What this implies");
744
+ if (info.framework === "next" && info.nextRouter === "app") {
745
+ out.push(
746
+ '- App Router: any component or hook marked **client only** needs `"use client"` at the top of the file that uses it.'
747
+ );
748
+ out.push(
749
+ "- Anything using WebGL should be rendered via `dynamic(() => import(...), { ssr: false })`."
750
+ );
751
+ }
752
+ if (!info.hasTailwind) {
753
+ out.push("- No Tailwind detected: components that ship Tailwind classes will render unstyled.");
754
+ }
755
+ out.push("- Run `npx vectorvesper init` first if `vv.config.json` does not exist yet.");
756
+ return text(out.join("\n"));
757
+ }
758
+ );
759
+ server.registerTool(
760
+ "search",
761
+ {
762
+ title: "Search hooks and components",
763
+ description: 'Keyword search across both motion hooks and installable components. Use this when you know the effect you want ("magnetic button", "scroll video", "lazy 3d scene") but not which primitive provides it.',
764
+ inputSchema: { query: z.string().describe("What you are trying to build.") }
765
+ },
766
+ async ({ query }) => {
767
+ const q = query.toLowerCase();
768
+ const terms = q.split(/\s+/).filter((t) => t.length > 2);
769
+ const score = (haystack) => {
770
+ const h = haystack.toLowerCase();
771
+ if (h.includes(q)) return 100;
772
+ return terms.reduce((n, t) => n + (h.includes(t) ? 1 : 0), 0);
773
+ };
774
+ const results = [];
775
+ const manifest = loadHookManifest();
776
+ for (const h of manifest?.hooks ?? []) {
777
+ const s = score([h.name, h.tagline, h.problem, h.summary, h.category].join(" "));
778
+ if (s > 0) results.push({ score: s, line: `**hook** \`${h.name}\` \u2014 ${h.tagline}` });
779
+ }
780
+ try {
781
+ const index = await fetchRegistryIndex();
782
+ for (const c of index.components) {
783
+ const s = score([c.slug, c.title, c.description, c.category, c.type].join(" "));
784
+ if (s > 0) {
785
+ results.push({ score: s, line: `**component** \`${c.slug}\` [${c.tier}] \u2014 ${c.description}` });
786
+ }
787
+ }
788
+ } catch {
789
+ }
790
+ if (!results.length) {
791
+ return text(
792
+ `Nothing matched "${query}". Try \`list_hooks\` or \`list_components\` to browse everything.`
793
+ );
794
+ }
795
+ results.sort((a, b) => b.score - a.score);
796
+ return text(
797
+ [
798
+ `${results.length} result${results.length === 1 ? "" : "s"} for "${query}"`,
799
+ "",
800
+ ...results.slice(0, 12).map((r) => r.line),
801
+ "",
802
+ "Then call `get_hook` or `get_component` for the full contract before writing code."
803
+ ].join("\n")
804
+ );
805
+ }
806
+ );
807
+ server.registerTool(
808
+ "check_motion",
809
+ {
810
+ title: "Check motion code for silent failures",
811
+ description: "Read the file(s) just written or edited and report the motion mistakes that do NOT show up as type or build errors but surface later as jank, leaks, or accessibility problems: a self-scheduling requestAnimationFrame loop running outside the shared frame budget, a listener/observer/timeline never torn down, a setState fired every frame, one element's transform animated by two owners at once, autonomous motion with no prefers-reduced-motion guard, and browser APIs used at module scope (an SSR crash). Call this after generating any animation, scroll, pointer or WebGL code \u2014 it is the check that the contract from `get_hook` was actually followed. Static analysis; reports severity, line, and the fix.",
812
+ inputSchema: {
813
+ files: z.array(z.string()).min(1).describe("Path(s) of the file(s) to check \u2014 relative to the project root, or absolute."),
814
+ cwd: z.string().optional().describe("Project root. Defaults to the server's working directory.")
815
+ }
816
+ },
817
+ async ({ files, cwd }) => {
818
+ const results = await checkMotionFiles(files, cwd ?? process.cwd());
819
+ return text(formatCheckResults(results));
820
+ }
821
+ );
822
+ }
823
+ async function startMcpServer() {
824
+ const [{ McpServer: McpServerCtor }, { StdioServerTransport }] = await Promise.all([
825
+ import("@modelcontextprotocol/sdk/server/mcp.js"),
826
+ import("@modelcontextprotocol/sdk/server/stdio.js")
827
+ ]);
828
+ const server = new McpServerCtor(
829
+ { name: "vectorvesper", version: VERSION },
830
+ {
831
+ instructions: "Vector Vesper's motion runtime and component registry.\n\nBefore writing any animation, scroll, pointer or WebGL code in this project, call `search` or `list_hooks` to check whether a Vector Vesper primitive already covers it, then `get_hook` / `get_component` for the contract. The contract states whether something needs a client boundary, whether it owns an element's transform, and what it conflicts with \u2014 details that do not surface as compile errors but do surface as jank.\n\nAfter writing animation, scroll, pointer or WebGL code, call `check_motion` on the file(s) you changed. It reports the same failure class the contracts warn about \u2014 an unmanaged frame loop, missing cleanup, state written every frame, a transform animated by two owners, motion with no reduced-motion guard, a browser API at module scope \u2014 checked against what you actually wrote. This is how you confirm the contract was followed; none of it shows up as a type or build error.\n\nHooks are consumed by importing `@vectorvesper/motion`. Never reimplement or inline a hook: a copy runs its own requestAnimationFrame loop instead of joining the shared frame conductor, which removes the coordination the runtime exists to provide, and nothing errors when that happens.\n\nIf asked how a hook works internally, call `get_hook` \u2014 its `How it works inside` section explains the architecture, the frame lane, and the smoothing constants directly. Prefer that over reading `node_modules/@vectorvesper/motion`: it is the same design stated at the level the question is actually about, and it stays correct as the package is bundled and rebundled."
832
+ }
833
+ );
834
+ await registerTools(server);
835
+ await server.connect(new StdioServerTransport());
836
+ }
837
+ export {
838
+ startMcpServer
839
+ };