vectorvesper 2.4.0 → 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-28T05:06:16.709Z",
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.0" : "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.0" : "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-UEQ2Y6FU.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.0" : "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-H4XNBFKQ.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",
@@ -65,15 +66,45 @@ function calleeName(callee) {
65
66
  }
66
67
  return void 0;
67
68
  }
68
- function isStateSetterCall(callee) {
69
+ function isStateSetterCall(callee, reactSetters) {
69
70
  if (!callee || callee.type !== "Identifier") return false;
70
- const name = callee.name;
71
- return /^set[A-Z]/.test(name) && name !== "setInterval" && name !== "setTimeout";
71
+ return reactSetters.has(callee.name);
72
72
  }
73
- function accessorName(callee) {
73
+ function collectReactSetters(ast, traverse) {
74
+ const names = /* @__PURE__ */ new Set();
75
+ traverse(ast, {
76
+ VariableDeclarator(p) {
77
+ const { id, init } = p.node;
78
+ if (id?.type !== "ArrayPattern") return;
79
+ const callee = init?.callee;
80
+ const hook = callee?.type === "Identifier" ? callee.name : callee?.property?.name;
81
+ if (hook !== "useState" && hook !== "useReducer") return;
82
+ const second = id.elements?.[1];
83
+ if (second?.type === "Identifier") names.add(second.name);
84
+ }
85
+ });
86
+ return names;
87
+ }
88
+ function accessorName(calleePath) {
89
+ const callee = calleePath?.node ?? calleePath;
74
90
  const obj = callee?.object;
75
- if (obj?.type !== "CallExpression" && obj?.type !== "OptionalCallExpression") return void 0;
76
- 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;
77
108
  }
78
109
  function asProgram(cbNode) {
79
110
  if (!cbNode) return null;
@@ -115,9 +146,11 @@ function collect(ast, traverse) {
115
146
  laneViolations: [],
116
147
  orphanSubscriptions: [],
117
148
  canvases: [],
149
+ rawGlContexts: [],
118
150
  handlesContextLoss: false,
119
151
  gatingObserver: []
120
152
  };
153
+ const reactSetters = collectReactSetters(ast, traverse);
121
154
  const manifest = loadHookManifest();
122
155
  const hookByName = /* @__PURE__ */ new Map();
123
156
  for (const h of manifest?.hooks ?? []) hookByName.set(h.name, h);
@@ -135,28 +168,79 @@ function collect(ast, traverse) {
135
168
  if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) return init;
136
169
  return null;
137
170
  };
138
- const scanHotCallback = (cbNode, ctx, fromRaf) => {
171
+ const scanHotCallback = (cbNode, ctx, fromRaf, unsubName) => {
139
172
  const file = asProgram(cbNode);
140
173
  if (!file) return;
141
- traverse(file, {
142
- CallExpression(p) {
143
- if (isStateSetterCall(p.node.callee)) {
144
- 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;
145
192
  }
146
- },
147
- OptionalCallExpression(p) {
148
- if (isStateSetterCall(p.node.callee)) {
149
- 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;
150
222
  }
223
+ node = parent;
151
224
  }
152
- });
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 });
153
237
  };
154
238
  const containsStateSetter = (cbNode) => {
155
239
  const file = asProgram(cbNode);
156
240
  if (!file) return false;
157
241
  let found = false;
158
242
  const visit = (p) => {
159
- if (isStateSetterCall(p.node.callee)) found = true;
243
+ if (isStateSetterCall(p.node.callee, reactSetters)) found = true;
160
244
  };
161
245
  traverse(file, { CallExpression: visit, OptionalCallExpression: visit });
162
246
  return found;
@@ -210,7 +294,13 @@ function collect(ast, traverse) {
210
294
  f.frameCallbacks.push({ line, lane, via });
211
295
  const cb = resolveFunction(argPaths[1]);
212
296
  if (!cb) return;
213
- 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
+ );
214
304
  if (lane === "input" || lane === "update" || lane === "render") scanLaneDiscipline(cb, lane, via);
215
305
  };
216
306
  const handleCall = (p) => {
@@ -237,7 +327,9 @@ function collect(ast, traverse) {
237
327
  f.hasClearInterval = true;
238
328
  break;
239
329
  case "addEventListener": {
240
- 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);
241
333
  const ev = node.arguments?.[0];
242
334
  if (ev?.type === "StringLiteral" && HOT_EVENTS.has(ev.value)) {
243
335
  scanHotCallback(resolveFunction(argPaths[1]), `a "${ev.value}" listener`, false);
@@ -256,6 +348,13 @@ function collect(ast, traverse) {
256
348
  case "matchMedia":
257
349
  f.hasReducedMotionGuard = true;
258
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
+ }
259
358
  case "kill":
260
359
  case "revert":
261
360
  f.hasGsapCleanup = true;
@@ -265,7 +364,7 @@ function collect(ast, traverse) {
265
364
  break;
266
365
  case "subscribe":
267
366
  case "retain": {
268
- const accessor = accessorName(node.callee);
367
+ const accessor = accessorName(typeof p.get === "function" ? p.get("callee") : node.callee);
269
368
  if (!accessor || !VV_ACCESSORS.has(accessor)) break;
270
369
  if (name === "subscribe" && accessor === "getConductor") {
271
370
  recordFrameCallback(p, argPaths, "getConductor().subscribe");
@@ -312,7 +411,9 @@ function collect(ast, traverse) {
312
411
  const v = p.node.value;
313
412
  if (v.includes("prefers-reduced-motion")) f.hasReducedMotionGuard = true;
314
413
  if (v === "webglcontextlost" || v === "webglcontextrestored") f.handlesContextLoss = true;
315
- 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
+ }
316
417
  if (/\btransition\b|\banimation\b/.test(v) && /\btransform\b|\ball\b/.test(v)) {
317
418
  f.transformAuthorities.push({ line: p.node.loc?.start.line ?? 0, source: "a CSS transition/animation" });
318
419
  }
@@ -484,7 +585,7 @@ function checkMissingCleanup(f, file) {
484
585
  return out;
485
586
  }
486
587
  function checkSetStatePerFrame(f, file) {
487
- const hot = f.setStateInHot.filter((s) => !s.fromRaf || isRafLoop(f));
588
+ const hot = f.setStateInHot.filter((s) => (!s.fromRaf || isRafLoop(f)) && !s.gated);
488
589
  if (hot.length === 0) return [];
489
590
  const zeroReRender = f.vvHooks.find((v) => v.hook.runtime.reRendersPerFrame === 0);
490
591
  const first = hot[0];
@@ -568,7 +669,9 @@ function checkRenderQualityProps(f, file) {
568
669
  return out;
569
670
  }
570
671
  function checkContextLoss(f, file) {
571
- 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 [];
572
675
  const handled = f.handlesContextLoss || f.vvImports.has("useRenderQuality") || f.vvImports.has("getRendererHealth");
573
676
  if (handled) return [];
574
677
  return [
@@ -576,15 +679,16 @@ function checkContextLoss(f, file) {
576
679
  rule: "webgl-context-loss",
577
680
  severity: "warning",
578
681
  file,
579
- line: f.canvases[0].line,
682
+ line: r3fCanvas ? f.canvases[0].line : f.rawGlContexts[0],
580
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.",
581
- 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()`.",
582
685
  nudge: 'Call `get_pattern("lazy-3d-section")` for the whole wiring.'
583
686
  }
584
687
  ];
585
688
  }
586
689
  function checkHandRolledGate(f, file) {
587
- 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 [];
588
692
  if (f.gatingObserver.length === 0 || f.vvImports.has("useSceneGate")) return [];
589
693
  return [
590
694
  {
@@ -871,7 +975,7 @@ function formatPlan({ building, using }) {
871
975
  }
872
976
 
873
977
  // src/mcp/server.ts
874
- var VERSION = true ? "2.4.0" : "0.0.0-dev";
978
+ var VERSION = true ? "2.5.0" : "0.0.0-dev";
875
979
  function text(body) {
876
980
  return { content: [{ type: "text", text: body }] };
877
981
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vectorvesper",
3
- "version": "2.4.0",
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",