vectorvesper 2.4.1 → 2.5.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/dist/hooks.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "1.0.0",
3
3
  "engineVersion": "4.0.0",
4
- "generatedAt": "2026-08-28T08:18:25.103Z",
4
+ "generatedAt": "2026-08-29T13:09:19.680Z",
5
5
  "contractLevel": "full",
6
6
  "hooks": [
7
7
  {
@@ -2804,8 +2804,8 @@
2804
2804
  "practices/react-motion-rules.md",
2805
2805
  "practices/adaptive-performance.md"
2806
2806
  ],
2807
- "docsUrl": "https://vectorvesper.dev/runtime/frame-conductor",
2808
- "labUrl": "https://vectorvesper.dev/runtime/frame-conductor",
2807
+ "docsUrl": "https://vectorvesper.dev/runtime/tick",
2808
+ "labUrl": "https://vectorvesper.dev/runtime/tick",
2809
2809
  "disclosure": {
2810
2810
  "exposeImplementation": false,
2811
2811
  "reason": "Reproducing this hook's implementation makes the consumer run a private requestAnimationFrame loop instead of joining the shared FrameConductor. That silently removes the coordination, scheduling and frame-budget governance the runtime exists to provide, and nothing errors when it happens. Copying is a correctness bug, not just a licensing question.",
package/dist/index.js CHANGED
@@ -1122,7 +1122,7 @@ Remove ${slug}
1122
1122
  import fs7 from "fs";
1123
1123
  import path7 from "path";
1124
1124
  import pc8 from "picocolors";
1125
- var CLI_VERSION = true ? "2.4.1" : "0.0.0-dev";
1125
+ var CLI_VERSION = true ? "2.5.0" : "0.0.0-dev";
1126
1126
  async function infoCommand() {
1127
1127
  console.log(pc8.bold(pc8.cyan("\nVector Vesper Diagnostics\n")));
1128
1128
  const projectInfo = detectProject();
@@ -1420,7 +1420,7 @@ async function whoamiCommand() {
1420
1420
  }
1421
1421
 
1422
1422
  // src/index.ts
1423
- var version = true ? "2.4.1" : "0.0.0-dev";
1423
+ var version = true ? "2.5.0" : "0.0.0-dev";
1424
1424
  var program = new Command();
1425
1425
  program.name("vv").description("Vector Vesper CLI \u2014 add visual components to your React project").version(version).option("--verbose", "Show detailed debug output").hook("preAction", (thisCommand) => {
1426
1426
  const opts = thisCommand.opts();
@@ -1462,7 +1462,7 @@ program.command("mcp [action] [client]").description(
1462
1462
  "Run as an MCP server (stdio) for AI coding agents. `vv mcp install` wires it into your editor; `vv mcp status` checks the setup."
1463
1463
  ).option("--stdio", "Force server mode even from an interactive terminal").option("-g, --global", "Write user-level config instead of project-level (install)").action(
1464
1464
  async (action, client, options) => {
1465
- const { mcpCommand } = await import("./mcp-FP5FICSI.js");
1465
+ const { mcpCommand } = await import("./mcp-QHRDHAME.js");
1466
1466
  await mcpCommand(action, { ...options, client });
1467
1467
  }
1468
1468
  );
@@ -12,7 +12,7 @@ import pc from "picocolors";
12
12
  import fs from "fs";
13
13
  import os from "os";
14
14
  import path from "path";
15
- var CLIENT_IDS = ["claude", "cursor", "antigravity", "opencode"];
15
+ var CLIENT_IDS = ["claude", "cursor", "windsurf", "antigravity", "opencode"];
16
16
  var SERVER_NAME = "vectorvesper";
17
17
  var ARGS = ["-y", "vectorvesper@latest", "mcp", "--stdio"];
18
18
  function home(...parts) {
@@ -32,6 +32,14 @@ var CLIENTS = {
32
32
  globalPath: home(".cursor", "mcp.json"),
33
33
  dialect: "mcpServers"
34
34
  },
35
+ windsurf: {
36
+ label: "Windsurf",
37
+ // Windsurf reads one user-level file; there is no workspace scope, so a
38
+ // --global flag would have nothing to switch to.
39
+ projectPath: null,
40
+ globalPath: home(".codeium", "windsurf", "mcp_config.json"),
41
+ dialect: "mcpServers"
42
+ },
35
43
  antigravity: {
36
44
  label: "Antigravity",
37
45
  // Workspace scope lives under .agents/; the global file is shared by the
@@ -138,7 +146,7 @@ function detectClients(cwd = process.cwd()) {
138
146
  }
139
147
 
140
148
  // src/commands/mcp.ts
141
- var VERSION = true ? "2.4.1" : "0.0.0-dev";
149
+ var VERSION = true ? "2.5.0" : "0.0.0-dev";
142
150
  var CONFIG_SNIPPET = `{
143
151
  "mcpServers": {
144
152
  "vectorvesper": {
@@ -285,7 +293,7 @@ ${pc.red("\u2717")} Unknown argument "${action}" for \`vv mcp\`.
285
293
  return;
286
294
  }
287
295
  protectStdout();
288
- const { startMcpServer } = await import("./server-AYBXJAAO.js");
296
+ const { startMcpServer } = await import("./server-ENS44LZV.js");
289
297
  try {
290
298
  await startMcpServer();
291
299
  } catch (error) {
@@ -38,6 +38,7 @@ var VV_ACCESSORS = /* @__PURE__ */ new Set([
38
38
  "getFramePressure",
39
39
  "getRendererHealth"
40
40
  ]);
41
+ var CONTEXT_EVENTS = /* @__PURE__ */ new Set(["webglcontextlost", "webglcontextrestored"]);
41
42
  var TEARDOWN_RETURNING = /* @__PURE__ */ new Set(["subscribe", "retain"]);
42
43
  var LAYOUT_READ_PROPS = /* @__PURE__ */ new Set([
43
44
  "offsetWidth",
@@ -84,10 +85,26 @@ function collectReactSetters(ast, traverse) {
84
85
  });
85
86
  return names;
86
87
  }
87
- function accessorName(callee) {
88
+ function accessorName(calleePath) {
89
+ const callee = calleePath?.node ?? calleePath;
88
90
  const obj = callee?.object;
89
- if (obj?.type !== "CallExpression" && obj?.type !== "OptionalCallExpression") return void 0;
90
- return obj.callee?.type === "Identifier" ? obj.callee.name : void 0;
91
+ if (!obj) return void 0;
92
+ if (obj.type === "CallExpression" || obj.type === "OptionalCallExpression") {
93
+ return obj.callee?.type === "Identifier" ? obj.callee.name : void 0;
94
+ }
95
+ if (obj.type === "Identifier" && typeof calleePath?.get === "function") {
96
+ const binding = calleePath.scope?.getBinding(obj.name);
97
+ const init = binding?.path?.node?.init;
98
+ if (init && (init.type === "CallExpression" || init.type === "OptionalCallExpression")) {
99
+ return init.callee?.type === "Identifier" ? init.callee.name : void 0;
100
+ }
101
+ }
102
+ return void 0;
103
+ }
104
+ function unsubscribeBinding(callPath) {
105
+ const declarator = callPath?.parentPath;
106
+ if (declarator?.node?.type !== "VariableDeclarator") return void 0;
107
+ return declarator.node.id?.type === "Identifier" ? declarator.node.id.name : void 0;
91
108
  }
92
109
  function asProgram(cbNode) {
93
110
  if (!cbNode) return null;
@@ -129,6 +146,7 @@ function collect(ast, traverse) {
129
146
  laneViolations: [],
130
147
  orphanSubscriptions: [],
131
148
  canvases: [],
149
+ rawGlContexts: [],
132
150
  handlesContextLoss: false,
133
151
  gatingObserver: []
134
152
  };
@@ -150,21 +168,72 @@ function collect(ast, traverse) {
150
168
  if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) return init;
151
169
  return null;
152
170
  };
153
- const scanHotCallback = (cbNode, ctx, fromRaf) => {
171
+ const scanHotCallback = (cbNode, ctx, fromRaf, unsubName) => {
154
172
  const file = asProgram(cbNode);
155
173
  if (!file) return;
156
- traverse(file, {
157
- CallExpression(p) {
158
- if (isStateSetterCall(p.node.callee, reactSetters)) {
159
- f.setStateInHot.push({ line: cbNode.loc?.start.line ?? 0, ctx, fromRaf });
174
+ const isTerminal = (setterPath) => {
175
+ if (!unsubName) return false;
176
+ let node = setterPath;
177
+ while (node) {
178
+ if (node.node?.type === "BlockStatement") {
179
+ const parent = node.parentPath?.node?.type;
180
+ if (parent === "ArrowFunctionExpression" || parent === "FunctionExpression" || parent === "FunctionDeclaration") {
181
+ return false;
182
+ }
183
+ let found = false;
184
+ node.traverse({
185
+ CallExpression(inner) {
186
+ if (inner.node.callee?.type === "Identifier" && inner.node.callee.name === unsubName) {
187
+ found = true;
188
+ }
189
+ }
190
+ });
191
+ if (found) return true;
160
192
  }
161
- },
162
- OptionalCallExpression(p) {
163
- if (isStateSetterCall(p.node.callee, reactSetters)) {
164
- f.setStateInHot.push({ line: cbNode.loc?.start.line ?? 0, ctx, fromRaf });
193
+ node = node.parentPath;
194
+ }
195
+ return false;
196
+ };
197
+ const isLatched = (setterPath) => {
198
+ let node = setterPath;
199
+ while (node) {
200
+ const parent = node.parentPath;
201
+ if (parent?.node?.type === "IfStatement" && parent.node.test) {
202
+ const guards = /* @__PURE__ */ new Set();
203
+ const collect2 = (n) => {
204
+ if (!n || typeof n.type !== "string") return;
205
+ if (n.type === "Identifier") guards.add(n.name);
206
+ for (const key of Object.keys(n)) {
207
+ const v = n[key];
208
+ if (Array.isArray(v)) v.forEach(collect2);
209
+ else if (v && typeof v.type === "string") collect2(v);
210
+ }
211
+ };
212
+ collect2(parent.node.test);
213
+ let latched = false;
214
+ const check = (inner) => {
215
+ const target = inner.node.left ?? inner.node.argument;
216
+ if (target?.type === "Identifier" && guards.has(target.name)) latched = true;
217
+ };
218
+ if (typeof node.traverse === "function") {
219
+ node.traverse({ AssignmentExpression: check, UpdateExpression: check });
220
+ }
221
+ if (latched) return true;
165
222
  }
223
+ node = parent;
166
224
  }
167
- });
225
+ return false;
226
+ };
227
+ const onCall = (p) => {
228
+ if (!isStateSetterCall(p.node.callee, reactSetters)) return;
229
+ f.setStateInHot.push({
230
+ line: cbNode.loc?.start.line ?? 0,
231
+ ctx,
232
+ fromRaf,
233
+ gated: isTerminal(p) || isLatched(p)
234
+ });
235
+ };
236
+ traverse(file, { CallExpression: onCall, OptionalCallExpression: onCall });
168
237
  };
169
238
  const containsStateSetter = (cbNode) => {
170
239
  const file = asProgram(cbNode);
@@ -225,7 +294,13 @@ function collect(ast, traverse) {
225
294
  f.frameCallbacks.push({ line, lane, via });
226
295
  const cb = resolveFunction(argPaths[1]);
227
296
  if (!cb) return;
228
- scanHotCallback(cb, lane ? `a \`${via}("${lane}")\` frame callback` : `a \`${via}\` frame callback`, false);
297
+ const unsubName = unsubscribeBinding(p);
298
+ scanHotCallback(
299
+ cb,
300
+ lane ? `a \`${via}("${lane}")\` frame callback` : `a \`${via}\` frame callback`,
301
+ false,
302
+ unsubName
303
+ );
229
304
  if (lane === "input" || lane === "update" || lane === "render") scanLaneDiscipline(cb, lane, via);
230
305
  };
231
306
  const handleCall = (p) => {
@@ -252,7 +327,9 @@ function collect(ast, traverse) {
252
327
  f.hasClearInterval = true;
253
328
  break;
254
329
  case "addEventListener": {
255
- f.addEventListener.push(line);
330
+ const evNode = node.arguments?.[0];
331
+ const evName = evNode?.type === "StringLiteral" ? evNode.value : null;
332
+ if (!evName || !CONTEXT_EVENTS.has(evName)) f.addEventListener.push(line);
256
333
  const ev = node.arguments?.[0];
257
334
  if (ev?.type === "StringLiteral" && HOT_EVENTS.has(ev.value)) {
258
335
  scanHotCallback(resolveFunction(argPaths[1]), `a "${ev.value}" listener`, false);
@@ -271,6 +348,13 @@ function collect(ast, traverse) {
271
348
  case "matchMedia":
272
349
  f.hasReducedMotionGuard = true;
273
350
  break;
351
+ case "getContext": {
352
+ const kind = node.arguments?.[0];
353
+ if (kind?.type === "StringLiteral" && /^(webgl2?|experimental-webgl)$/.test(kind.value)) {
354
+ f.rawGlContexts.push(line);
355
+ }
356
+ break;
357
+ }
274
358
  case "kill":
275
359
  case "revert":
276
360
  f.hasGsapCleanup = true;
@@ -280,7 +364,7 @@ function collect(ast, traverse) {
280
364
  break;
281
365
  case "subscribe":
282
366
  case "retain": {
283
- const accessor = accessorName(node.callee);
367
+ const accessor = accessorName(typeof p.get === "function" ? p.get("callee") : node.callee);
284
368
  if (!accessor || !VV_ACCESSORS.has(accessor)) break;
285
369
  if (name === "subscribe" && accessor === "getConductor") {
286
370
  recordFrameCallback(p, argPaths, "getConductor().subscribe");
@@ -327,7 +411,9 @@ function collect(ast, traverse) {
327
411
  const v = p.node.value;
328
412
  if (v.includes("prefers-reduced-motion")) f.hasReducedMotionGuard = true;
329
413
  if (v === "webglcontextlost" || v === "webglcontextrestored") f.handlesContextLoss = true;
330
- if (/\binfinite\b/.test(v)) f.looping.push({ line: p.node.loc?.start.line ?? 0, kind: "CSS `infinite` animation" });
414
+ if (/\binfinite\b/.test(v) && /\b\d+(?:\.\d+)?m?s\b/.test(v)) {
415
+ f.looping.push({ line: p.node.loc?.start.line ?? 0, kind: "CSS `infinite` animation" });
416
+ }
331
417
  if (/\btransition\b|\banimation\b/.test(v) && /\btransform\b|\ball\b/.test(v)) {
332
418
  f.transformAuthorities.push({ line: p.node.loc?.start.line ?? 0, source: "a CSS transition/animation" });
333
419
  }
@@ -499,7 +585,7 @@ function checkMissingCleanup(f, file) {
499
585
  return out;
500
586
  }
501
587
  function checkSetStatePerFrame(f, file) {
502
- const hot = f.setStateInHot.filter((s) => !s.fromRaf || isRafLoop(f));
588
+ const hot = f.setStateInHot.filter((s) => (!s.fromRaf || isRafLoop(f)) && !s.gated);
503
589
  if (hot.length === 0) return [];
504
590
  const zeroReRender = f.vvHooks.find((v) => v.hook.runtime.reRendersPerFrame === 0);
505
591
  const first = hot[0];
@@ -583,7 +669,9 @@ function checkRenderQualityProps(f, file) {
583
669
  return out;
584
670
  }
585
671
  function checkContextLoss(f, file) {
586
- if (!f.usesR3F || f.canvases.length === 0) return [];
672
+ const r3fCanvas = f.usesR3F && f.canvases.length > 0;
673
+ const rawContext = f.rawGlContexts.length > 0;
674
+ if (!r3fCanvas && !rawContext) return [];
587
675
  const handled = f.handlesContextLoss || f.vvImports.has("useRenderQuality") || f.vvImports.has("getRendererHealth");
588
676
  if (handled) return [];
589
677
  return [
@@ -591,15 +679,16 @@ function checkContextLoss(f, file) {
591
679
  rule: "webgl-context-loss",
592
680
  severity: "warning",
593
681
  file,
594
- line: f.canvases[0].line,
682
+ line: r3fCanvas ? f.canvases[0].line : f.rawGlContexts[0],
595
683
  message: "Nothing in this file notices a lost graphics context. When the browser takes one away the canvas goes black permanently and nothing is logged, so it reads as a rendering bug rather than a recoverable event.",
596
- fix: "Spread `useRenderQuality`'s props onto the canvas and key it on a scene gate's `generation` \u2014 the `onCreated` it returns attaches the listener and reports the loss, and the moving key rebuilds the tree against a fresh context. Failing that, attach a `webglcontextlost` listener that calls `preventDefault()` (without it some drivers refuse a replacement context outright) and report to `getRendererHealth()`.",
684
+ fix: (r3fCanvas ? "Spread `useRenderQuality`'s props onto the canvas and key it on a scene gate's `generation` \u2014 the `onCreated` it returns attaches the listener and reports the loss, and the moving key rebuilds the tree against a fresh context. Failing that, attach a " : "You created this context by hand, so the adapter does not apply. Attach a ") + "`webglcontextlost` listener that calls `preventDefault()` (without it some drivers refuse a replacement context outright) and report to `getRendererHealth()`.",
597
685
  nudge: 'Call `get_pattern("lazy-3d-section")` for the whole wiring.'
598
686
  }
599
687
  ];
600
688
  }
601
689
  function checkHandRolledGate(f, file) {
602
- if (!f.usesR3F || f.canvases.length === 0) return [];
690
+ const ownsGpu = f.usesR3F && f.canvases.length > 0 || f.rawGlContexts.length > 0;
691
+ if (!ownsGpu) return [];
603
692
  if (f.gatingObserver.length === 0 || f.vvImports.has("useSceneGate")) return [];
604
693
  return [
605
694
  {
@@ -886,7 +975,7 @@ function formatPlan({ building, using }) {
886
975
  }
887
976
 
888
977
  // src/mcp/server.ts
889
- var VERSION = true ? "2.4.1" : "0.0.0-dev";
978
+ var VERSION = true ? "2.5.0" : "0.0.0-dev";
890
979
  function text(body) {
891
980
  return { content: [{ type: "text", text: body }] };
892
981
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vectorvesper",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "Motion runtime and WebGL/R3F component CLI for React, with an MCP server that tells your coding agent which primitive to use, how to wire it, and when not to.",
5
5
  "type": "module",
6
6
  "license": "MIT",