nodality 1.3.15 → 1.3.16

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.
Files changed (218) hide show
  1. package/bin/install-skill.mjs +3 -3
  2. package/bin/mcp-server.mjs +46 -1
  3. package/bin/nodality.mjs +103 -0
  4. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  5. package/dist/animator.esm.js.LICENSE.txt +1 -1
  6. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  7. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  8. package/dist/base.cjs.js.LICENSE.txt +1 -1
  9. package/dist/base.esm.js.LICENSE.txt +1 -1
  10. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  11. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  12. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  13. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  14. package/dist/bundle.umd.js +1 -1
  15. package/dist/bundle.umd.js.LICENSE.txt +1 -1
  16. package/dist/button.cjs.js.LICENSE.txt +1 -1
  17. package/dist/button.esm.js.LICENSE.txt +1 -1
  18. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  19. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  20. package/dist/center.cjs.js.LICENSE.txt +1 -1
  21. package/dist/center.esm.js.LICENSE.txt +1 -1
  22. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  23. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  24. package/dist/code.cjs.js.LICENSE.txt +1 -1
  25. package/dist/code.esm.js.LICENSE.txt +1 -1
  26. package/dist/container.cjs.js.LICENSE.txt +1 -1
  27. package/dist/container.esm.js.LICENSE.txt +1 -1
  28. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  29. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  30. package/dist/designer.cjs.js +1 -1
  31. package/dist/designer.cjs.js.LICENSE.txt +1 -1
  32. package/dist/designer.esm.js +1 -1
  33. package/dist/designer.esm.js.LICENSE.txt +1 -1
  34. package/dist/element-mapper.cjs.js +1 -1
  35. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  36. package/dist/element-mapper.esm.js +1 -1
  37. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  38. package/dist/finalresult.esm.js +1 -1
  39. package/dist/finalresult.esm.js.LICENSE.txt +1 -1
  40. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  41. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  42. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  43. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  44. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  45. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  46. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  47. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  48. package/dist/free.cjs.js.LICENSE.txt +1 -1
  49. package/dist/free.esm.js.LICENSE.txt +1 -1
  50. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  51. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  52. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  53. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  54. package/dist/image.cjs.js.LICENSE.txt +1 -1
  55. package/dist/image.esm.js.LICENSE.txt +1 -1
  56. package/dist/index.cjs.js +1 -1
  57. package/dist/index.cjs.js.LICENSE.txt +1 -1
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.esm.js +1 -1
  60. package/dist/index.esm.js.LICENSE.txt +1 -1
  61. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  62. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  63. package/dist/link-getter.cjs.js +1 -1
  64. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  65. package/dist/link-getter.esm.js +1 -1
  66. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  67. package/dist/link.cjs.js +1 -1
  68. package/dist/link.cjs.js.LICENSE.txt +1 -1
  69. package/dist/link.esm.js +1 -1
  70. package/dist/link.esm.js.LICENSE.txt +1 -1
  71. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  72. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  73. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  74. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  75. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  76. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  77. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  78. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  79. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  80. package/dist/picker.esm.js.LICENSE.txt +1 -1
  81. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  82. package/dist/progress.esm.js.LICENSE.txt +1 -1
  83. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  84. package/dist/radio.esm.js.LICENSE.txt +1 -1
  85. package/dist/range.cjs.js.LICENSE.txt +1 -1
  86. package/dist/range.esm.js.LICENSE.txt +1 -1
  87. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  88. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  89. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  90. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  91. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  92. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  93. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  94. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  95. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  96. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  97. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  98. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  99. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  100. package/dist/stack.esm.js.LICENSE.txt +1 -1
  101. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  102. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  103. package/dist/table.cjs.js.LICENSE.txt +1 -1
  104. package/dist/table.esm.js.LICENSE.txt +1 -1
  105. package/dist/text-field.cjs.js +1 -1
  106. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  107. package/dist/text-field.esm.js +1 -1
  108. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  109. package/dist/text.cjs.js.LICENSE.txt +1 -1
  110. package/dist/text.esm.js.LICENSE.txt +1 -1
  111. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  112. package/dist/theme.esm.js.LICENSE.txt +1 -1
  113. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  114. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  115. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  116. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  117. package/dist/video.cjs.js.LICENSE.txt +1 -1
  118. package/dist/video.esm.js.LICENSE.txt +1 -1
  119. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  120. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  121. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  122. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  123. package/layout/animator.js +1 -1
  124. package/layout/audio.js +1 -1
  125. package/layout/audionew.js +1 -1
  126. package/layout/base.js +1 -1
  127. package/layout/beta-desktop-bar.js +1 -1
  128. package/layout/beta-mobile-bar.js +1 -1
  129. package/layout/button.js +1 -1
  130. package/layout/center.js +1 -1
  131. package/layout/checkbox.js +1 -1
  132. package/layout/circle.js +1 -1
  133. package/layout/code.js +1 -1
  134. package/layout/container.js +1 -1
  135. package/layout/dropdown-2025.js +1 -1
  136. package/layout/flex-card.js +1 -1
  137. package/layout/flex-grid.js +1 -1
  138. package/layout/flex-row.js +1 -1
  139. package/layout/form-components/custom.js +1 -1
  140. package/layout/form-components/data-list.js +1 -1
  141. package/layout/form-components/floating-input.js +1 -1
  142. package/layout/form-components/form-all.js +1 -1
  143. package/layout/form-components/form.js +9 -2
  144. package/layout/form-components/image-picker.js +1 -1
  145. package/layout/form-components/picker.js +1 -1
  146. package/layout/form-components/radio.js +1 -1
  147. package/layout/form-components/radiogroup.js +1 -1
  148. package/layout/form-components/range.js +1 -1
  149. package/layout/free.js +1 -1
  150. package/layout/grid-overlay.js +1 -1
  151. package/layout/grid-switcher.js +1 -1
  152. package/layout/grid.js +1 -1
  153. package/layout/horizontal-scroller.js +1 -1
  154. package/layout/image.js +1 -1
  155. package/layout/index.js +1 -1
  156. package/layout/link.js +6 -3
  157. package/layout/list.js +1 -1
  158. package/layout/meta-adder.js +1 -1
  159. package/layout/modal-2025.js +1 -1
  160. package/layout/morph.js +1 -1
  161. package/layout/multiswitcher.js +1 -1
  162. package/layout/nav-bar.js +1 -1
  163. package/layout/nav-factor/custom-div.js +1 -1
  164. package/layout/new-nav-bar.js +1 -1
  165. package/layout/polygon.js +1 -1
  166. package/layout/prerender-site.js +1 -1
  167. package/layout/prerender.js +1 -1
  168. package/layout/progress.js +1 -1
  169. package/layout/row.js +1 -1
  170. package/layout/scroll-video.js +1 -1
  171. package/layout/shop.js +1 -1
  172. package/layout/side-bar.js +1 -1
  173. package/layout/side-nav-bar.js +1 -1
  174. package/layout/simple-bar.js +1 -1
  175. package/layout/slider-2025.js +1 -1
  176. package/layout/spacer.js +1 -1
  177. package/layout/stack.js +1 -1
  178. package/layout/svg.js +1 -1
  179. package/layout/switcher.js +1 -1
  180. package/layout/table.js +1 -1
  181. package/layout/text-field.js +4 -2
  182. package/layout/text.js +1 -1
  183. package/layout/ulist.js +1 -1
  184. package/layout/video.js +1 -1
  185. package/layout/wrap.js +1 -1
  186. package/layout/zoom-card.js +1 -1
  187. package/lib/agent-surface.js +1 -1
  188. package/lib/card-getter.js +1 -1
  189. package/lib/check-page.js +37 -5
  190. package/lib/codegen.js +1 -1
  191. package/lib/data.js +1 -1
  192. package/lib/designer.js +1 -1
  193. package/lib/element-mapper.js +3 -1
  194. package/lib/element-params.generated.js +3 -3
  195. package/lib/grid-geometry.js +1 -1
  196. package/lib/keyframe-animation.js +1 -1
  197. package/lib/known-inert.js +85 -0
  198. package/lib/link-getter.js +1 -1
  199. package/lib/morph-node.js +1 -1
  200. package/lib/normalize-spec.js +1 -1
  201. package/lib/parse-html.js +1 -1
  202. package/lib/raster-inspect.js +1 -1
  203. package/lib/raster-ops.js +1 -1
  204. package/lib/raster-presets.js +1 -1
  205. package/lib/resolve-refs.js +1 -1
  206. package/lib/scroll-video.js +1 -1
  207. package/lib/seo.js +1 -1
  208. package/lib/stacker.js +1 -1
  209. package/lib/suggest.js +1 -1
  210. package/lib/theme.js +1 -1
  211. package/lib/transform-anim.js +1 -1
  212. package/lib/transition.js +1 -1
  213. package/lib/validate-nodes.js +14 -1
  214. package/lib/webmcp-adapter.js +1 -1
  215. package/package.json +1 -1
  216. package/schema.json +2620 -957
  217. package/scripts/generate-schema.mjs +7 -0
  218. package/skills/nodality/SKILL.md +11 -1
@@ -55,19 +55,19 @@ function place(src, dest, content) {
55
55
  // Merge our server into an existing config without touching anything
56
56
  // else in it. A file that exists but does not parse is left alone and
57
57
  // reported — clobbering a hand-edited config is worse than skipping.
58
- function registerMcp(configPath) {
58
+ export function registerMcp(configPath, entry = MCP_ENTRY) {
59
59
  let config = {};
60
60
  if (fs.existsSync(configPath)) {
61
61
  try {
62
62
  config = JSON.parse(fs.readFileSync(configPath, "utf8"));
63
63
  } catch {
64
64
  log(`SKIPPED ${configPath} — existing file is not valid JSON; add this by hand:`);
65
- log(` "mcpServers": { "nodality": ${JSON.stringify(MCP_ENTRY)} }`);
65
+ log(` "mcpServers": { "nodality": ${JSON.stringify(entry)} }`);
66
66
  return;
67
67
  }
68
68
  }
69
69
  const existed = fs.existsSync(configPath);
70
- config.mcpServers = { ...config.mcpServers, nodality: MCP_ENTRY };
70
+ config.mcpServers = { ...config.mcpServers, nodality: entry };
71
71
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
72
72
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
73
73
  log(`${existed ? "updated " : "installed"} ${configPath} (mcpServers.nodality)`);
@@ -221,6 +221,7 @@ const TOOLS = [
221
221
  inputSchema: {
222
222
  type: "object",
223
223
  properties: {
224
+ viewport: { description: "\"phone\" (default, 390×844) or \"desktop\" (1440×900), or {width, height}. Width-dependent elements render their view for it." },
224
225
  elements: ELEMENTS_SCHEMA,
225
226
  nodes: NODES_SCHEMA,
226
227
  output: {
@@ -286,6 +287,25 @@ const IMPL = {
286
287
  const schema = JSON.parse(await readFile(join(here, "..", "schema.json"), "utf8"));
287
288
  const types = schema.types || {};
288
289
  if (!type) return ok(schema);
290
+ if (types[type] && Array.isArray(types[type].params)) {
291
+ // The type's own parameters first: what its mapper reads by name, then
292
+ // described ones, then the rest — inherited through components. A
293
+ // composite lists 150+, and the dozen that matter were alphabetised
294
+ // among them.
295
+ const t = types[type];
296
+ const rank = (p) => (p.via === "mapper" ? 0 : p.description ? 1 : 2);
297
+ const params = [...t.params].sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
298
+ return ok({
299
+ type, ...t,
300
+ summary: {
301
+ total: params.length,
302
+ readByType: params.filter((p) => p.via === "mapper").length,
303
+ throughComponents: params.filter((p) => p.via !== "mapper").length,
304
+ described: params.filter((p) => p.description).length,
305
+ },
306
+ params,
307
+ });
308
+ }
289
309
  if (!types[type]) {
290
310
  // Same report shape as everything else, so an agent parses one thing.
291
311
  return ok({
@@ -312,7 +332,7 @@ const IMPL = {
312
332
  return ok(parseReport(String(html ?? ""), dom.window.document));
313
333
  },
314
334
 
315
- preview: async ({ elements, nodes, output }) => {
335
+ preview: async ({ elements, nodes, output, viewport }) => {
316
336
  const N = nodes || [];
317
337
  const report = validateNodes(N, elements);
318
338
  if (!report.ok) return reportAsError(report);
@@ -338,10 +358,23 @@ const IMPL = {
338
358
  `<title>Nodality preview</title></head><body>` +
339
359
  `<div id="mount"></div></body></html>`, "utf8");
340
360
 
361
+ // The prerenderer emulates a phone by default, and anything chosen by
362
+ // width (a nav, a multiswitcher) renders as its phone view. "desktop"
363
+ // shows the other side.
364
+ const size = viewport === "desktop" ? { width: 1440, height: 900 }
365
+ : (viewport && typeof viewport === "object" ? viewport : { width: 390, height: 844 });
366
+ // Ids that rendered as an empty box, recorded while the DOM exists.
367
+ const empty = [];
368
+ const ids = [];
369
+ const collect = (list) => { for (const el of Array.isArray(list) ? list : []) {
370
+ if (el && typeof el === "object") { if (typeof el.id === "string") ids.push(el.id); collect(el.children); }
371
+ } };
372
+ collect(elements);
341
373
  await prerender({
342
374
  template,
343
375
  mount: "#mount",
344
376
  output: out,
377
+ viewport: size,
345
378
  // `build` is handed the jsdom window and is expected to render
346
379
  // into it. The Designer is imported HERE, inside the callback,
347
380
  // rather than at module scope: prerender installs the DOM globals
@@ -351,6 +384,13 @@ const IMPL = {
351
384
  build: async () => {
352
385
  const { Des } = await import("../lib/designer.js");
353
386
  new Des().nodes(N).add(elements).set({ mount: "#mount", code: false });
387
+ for (const id of ids) {
388
+ const node = document.querySelector(`[id="${id}"]`);
389
+ if (!node) { empty.push({ id, why: "not in the output" }); continue; }
390
+ const hasContent = node.children.length > 0 || (node.textContent || "").trim() !== ""
391
+ || /^(IMG|VIDEO|AUDIO|INPUT|SELECT|TEXTAREA|CANVAS|IFRAME|HR|BR|SVG)$/i.test(node.tagName);
392
+ if (!hasContent) empty.push({ id, why: "rendered empty" });
393
+ }
354
394
  },
355
395
  });
356
396
 
@@ -381,7 +421,12 @@ const IMPL = {
381
421
 
382
422
  return ok({
383
423
  path: out, bytes,
424
+ viewport: size,
384
425
  ...(surface ? { surface } : {}),
426
+ // An element that rendered nothing is said, not left to look like a
427
+ // finished page: a 181-byte preview of a nav was the only sign that
428
+ // its view is chosen at run time.
429
+ ...(empty.length ? { empty, emptyNote: "These ids rendered no content at this viewport. A runtime choice (multiswitcher, morph state), content that loads in the browser (commerce), or an element given nothing to show. Try viewport: \"desktop\", or open the file in a browser." } : {}),
385
430
  // Said plainly, because an agent cannot see the result and would
386
431
  // otherwise report a shader that never ran as working.
387
432
  note: "Prerendered DOM only. Raster effects and morph transitions " +
package/bin/nodality.mjs CHANGED
@@ -34,6 +34,60 @@ import path from "node:path";
34
34
  import { prerenderSite } from "../layout/prerender-site.js";
35
35
  import { prerender } from "../layout/prerender.js";
36
36
 
37
+ // ─── link-local: test an unreleased library in a real site ─────────
38
+ //
39
+ // Pre-release testing used to be a manual copy of dist/, lib/, layout/ and the
40
+ // schema into the site's node_modules — easy to get wrong, and invisible once
41
+ // done: nothing said the site was running something other than the release.
42
+ // This does the copy, leaves a marker every other command warns about, and
43
+ // undoes itself from the lockfile.
44
+ async function linkLocal(rest) {
45
+ const fs = await import("node:fs");
46
+ const path = await import("node:path");
47
+ const { execSync } = await import("node:child_process");
48
+ const target = path.join(process.cwd(), "node_modules", "nodality");
49
+ const marker = path.join(target, ".linked-local.json");
50
+
51
+ if (rest.includes("--undo")) {
52
+ if (!fs.existsSync(marker)) { console.log("[nodality] link-local: nothing to undo — the installed copy is a release"); return; }
53
+ fs.rmSync(target, { recursive: true, force: true });
54
+ execSync("npm install --no-audit --no-fund", { stdio: "inherit" });
55
+ const v = JSON.parse(fs.readFileSync(path.join(target, "package.json"), "utf8")).version;
56
+ console.log(`[nodality] link-local: restored the released nodality ${v}. Rebuild your bundle (e.g. npm run build or nodality stage).`);
57
+ return;
58
+ }
59
+
60
+ const fromArg = rest.find((a) => a.startsWith("--from="));
61
+ if (!fromArg) {
62
+ console.error("[nodality] link-local: --from=<path to the nodality repository> is required (or --undo)");
63
+ process.exit(1);
64
+ }
65
+ const from = path.resolve(fromArg.slice("--from=".length));
66
+ const pkg = path.join(from, "package.json");
67
+ if (!fs.existsSync(pkg) || JSON.parse(fs.readFileSync(pkg, "utf8")).name !== "nodality") {
68
+ console.error(`[nodality] link-local: ${from} is not the nodality repository`);
69
+ process.exit(1);
70
+ }
71
+ if (!fs.existsSync(target)) {
72
+ console.error("[nodality] link-local: install nodality in this project first (npm i nodality)");
73
+ process.exit(1);
74
+ }
75
+ if (!rest.includes("--no-build")) {
76
+ console.log(`[nodality] link-local: building ${from}…`);
77
+ execSync("npm run build", { cwd: from, stdio: "inherit" });
78
+ }
79
+ for (const part of ["dist", "lib", "layout", "bin", "schema.json"]) {
80
+ const src = path.join(from, part);
81
+ if (!fs.existsSync(src)) continue;
82
+ fs.rmSync(path.join(target, part), { recursive: true, force: true });
83
+ fs.cpSync(src, path.join(target, part), { recursive: true });
84
+ }
85
+ const version = JSON.parse(fs.readFileSync(pkg, "utf8")).version;
86
+ fs.writeFileSync(marker, JSON.stringify({ from, builtAt: new Date().toISOString(), baseVersion: version }, null, 2));
87
+ console.log(`[nodality] link-local: this project now runs the LOCAL build from ${from} (on top of ${version}).`);
88
+ console.log("[nodality] Rebuild your bundle (e.g. npm run build or nodality stage). Every nodality command will warn until: npx nodality link-local --undo");
89
+ }
90
+
37
91
  // ─── Usage ──────────────────────────────────────────────────────
38
92
 
39
93
  function showUsage() {
@@ -55,6 +109,14 @@ function showUsage() {
55
109
  # { "mcpServers": { "nodality": {
56
110
  # "command": "npx",
57
111
  # "args": ["nodality", "mcp"] } } }
112
+ nodality link-local --from=<repo> [--no-build] # Build a local nodality checkout and run
113
+ # this project on it, for testing before a
114
+ # release; every command warns while linked.
115
+ nodality link-local --undo # Back to the released version (lockfile).
116
+ nodality mcp --register [--root=<dir>] # Write that entry into <dir>/.mcp.json —
117
+ # the folder agent sessions start in
118
+ # (default: here). From a parent folder
119
+ # it runs this project's installed copy.
58
120
  nodality skill [flags] # Install the /nodality skill into agent
59
121
  # IDEs. No flags: interactive (or full
60
122
  # default install when not a TTY).
@@ -869,6 +931,20 @@ async function main() {
869
931
  }
870
932
 
871
933
  const [command, ...rest] = process.argv.slice(2);
934
+
935
+ // A site running a locally built library says so on every command, so a
936
+ // pre-release copy is never mistaken for the release (see link-local).
937
+ {
938
+ const fs = await import("node:fs");
939
+ const path = await import("node:path");
940
+ const marker = path.join(process.cwd(), "node_modules", "nodality", ".linked-local.json");
941
+ if (command !== "link-local" && fs.existsSync(marker)) {
942
+ try {
943
+ const m = JSON.parse(fs.readFileSync(marker, "utf8"));
944
+ console.warn(`[nodality] ⚠ using a LOCAL build of nodality from ${m.from} (built ${m.builtAt}). Undo: npx nodality link-local --undo`);
945
+ } catch {}
946
+ }
947
+ }
872
948
  if (!command || command === "help" || command === "--help" || command === "-h") {
873
949
  showUsage();
874
950
  } else if (command === "prerender") {
@@ -887,6 +963,33 @@ async function main() {
887
963
  } else if (command === "skill") {
888
964
  const { runSkillInstall } = await import("./install-skill.mjs");
889
965
  await runSkillInstall(rest);
966
+ } else if (command === "link-local") {
967
+ await linkLocal(rest);
968
+ } else if (command === "mcp" && rest.includes("--register")) {
969
+ // Register the server where agent sessions START, which is not always
970
+ // the project. A session opened one folder up never saw the project's
971
+ // own .mcp.json, and the agent drove the server by hand instead. From a
972
+ // parent root the entry runs npx with --prefix, so it is still the
973
+ // project's installed nodality that answers, not whatever npm resolves.
974
+ const path = await import("node:path");
975
+ const { registerMcp } = await import("./install-skill.mjs");
976
+ const rootArg = rest.find((a) => a.startsWith("--root="));
977
+ // Real paths on both sides: through a symlink (macOS /var is
978
+ // /private/var) the two spellings of one folder looked unrelated and a
979
+ // correct root was refused.
980
+ const fsMod = await import("node:fs");
981
+ const real = (p) => { try { return fsMod.realpathSync(p); } catch { return path.resolve(p); } };
982
+ const root = real(path.resolve(rootArg ? rootArg.slice("--root=".length) : process.cwd()));
983
+ const project = real(process.cwd());
984
+ const rel = path.relative(root, project);
985
+ if (rel.startsWith("..")) {
986
+ console.error(`[nodality] mcp --register: --root must be this project or a folder above it (${root})`);
987
+ process.exit(1);
988
+ }
989
+ const entry = rel === "" ? { command: "npx", args: ["nodality", "mcp"] }
990
+ : { command: "npx", args: ["--prefix", rel, "nodality", "mcp"] };
991
+ registerMcp(path.join(root, ".mcp.json"), entry);
992
+ console.log(`[nodality] start sessions in ${root} — the nodality tools are registered there`);
890
993
  } else if (command === "mcp") {
891
994
  // Lazily imported so that prerender/compile/stage pay nothing for a
892
995
  // subcommand they never touch, and so a broken morph core cannot
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.14
2
+ * nodality v1.3.15
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */