rafters 0.0.82 → 0.0.84

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/index.js CHANGED
@@ -28380,6 +28380,41 @@ var RegistryItemIntelligenceSchema = external_exports.object({
28380
28380
  nevers: external_exports.array(external_exports.string()).default([])
28381
28381
  }).optional()
28382
28382
  });
28383
+ var ComponentTargetSchema = external_exports.enum(["react", "astro", "vue", "svelte", "wc"]);
28384
+ var PropFieldSchema = external_exports.discriminatedUnion("type", [
28385
+ external_exports.object({
28386
+ type: external_exports.literal("enum"),
28387
+ values: external_exports.array(external_exports.string()),
28388
+ // verbatim literal union members -- never "string"
28389
+ default: external_exports.string().optional(),
28390
+ required: external_exports.boolean().optional(),
28391
+ constraint: external_exports.object({
28392
+ when: external_exports.object({ prop: external_exports.string(), matches: external_exports.string() }),
28393
+ requires: external_exports.object({ prop: external_exports.string() })
28394
+ }).optional()
28395
+ }),
28396
+ external_exports.object({
28397
+ // Matches #2072's PropNode 'grammar' arm exactly, so graph.ts's
28398
+ // describe(<id>.props.<name>.vocab) drill has real shape data.
28399
+ type: external_exports.literal("grammar"),
28400
+ grammar: external_exports.array(external_exports.string()).min(1),
28401
+ // grammar shape tokens, e.g. ['word', 'word/alpha']
28402
+ vocab: external_exports.string(),
28403
+ // drillable addr, e.g. 'container.props.fill.vocab' -- never inlined
28404
+ onInvalid: external_exports.literal("silent-noop"),
28405
+ default: external_exports.string().optional()
28406
+ }),
28407
+ external_exports.object({
28408
+ type: external_exports.literal("deprecated"),
28409
+ deprecatedFor: external_exports.string()
28410
+ })
28411
+ ]);
28412
+ var FacetSchema = external_exports.object({
28413
+ props: external_exports.record(external_exports.string(), PropFieldSchema),
28414
+ slots: external_exports.array(external_exports.string()).optional(),
28415
+ events: external_exports.array(external_exports.string()).optional(),
28416
+ snippet: external_exports.string()
28417
+ });
28383
28418
  var RegistryItemSchema = external_exports.object({
28384
28419
  name: external_exports.string(),
28385
28420
  type: RegistryItemTypeSchema,
@@ -28388,7 +28423,12 @@ var RegistryItemSchema = external_exports.object({
28388
28423
  files: external_exports.array(RegistryFileSchema),
28389
28424
  rules: external_exports.array(external_exports.string()).default([]),
28390
28425
  composites: external_exports.array(external_exports.string()).default([]),
28391
- intelligence: RegistryItemIntelligenceSchema.optional()
28426
+ intelligence: RegistryItemIntelligenceSchema.optional(),
28427
+ // Per-target facets. zod v4's `z.record(enum, ...)` demands EVERY enum key be
28428
+ // present; a component built for only some targets must parse, so this is a
28429
+ // partial record (only the built targets appear).
28430
+ facets: external_exports.partialRecord(ComponentTargetSchema, FacetSchema).default({}),
28431
+ parent: external_exports.string().optional()
28392
28432
  });
28393
28433
  var RegistryIndexSchema = external_exports.object({
28394
28434
  name: external_exports.string(),
@@ -28476,6 +28516,46 @@ var RegistryClient = class {
28476
28516
  }
28477
28517
  throw new Error(`"${name}" not found in registry (component, primitive, composite, or rule)`);
28478
28518
  }
28519
+ /**
28520
+ * Fetch every registry item in one call, for the whole-catalog load the MCP
28521
+ * intel graph needs at startup.
28522
+ *
28523
+ * Prefers a single bulk round-trip against `/registry/items.json`. A registry
28524
+ * that does not serve that route -- an older deploy, or a third-party
28525
+ * `registryUrl` (which is user-configurable, see ConfigWiringSchema) -- returns
28526
+ * a non-2xx, and this falls back to looping `fetchItem` over `fetchIndex()`'s
28527
+ * name list, retaining today's per-item cost so no workspace's `describe` ever
28528
+ * hard-fails on a registry that predates the bulk endpoint.
28529
+ */
28530
+ async fetchAllItems() {
28531
+ const response = await fetch(`${this.baseUrl}/registry/items.json`);
28532
+ if (response.ok) {
28533
+ return external_exports.array(RegistryItemSchema).parse(await response.json());
28534
+ }
28535
+ return this.fetchAllItemsByIndex();
28536
+ }
28537
+ /**
28538
+ * The fallback whole-catalog load: one `fetchItem` per name in the index,
28539
+ * across every describe-relevant kind. Deduped so a name appearing in more
28540
+ * than one index list is fetched once.
28541
+ */
28542
+ async fetchAllItemsByIndex() {
28543
+ const index = await this.fetchIndex();
28544
+ const names = [
28545
+ ...index.components,
28546
+ ...index.primitives,
28547
+ ...index.composites,
28548
+ ...index.substrate
28549
+ ];
28550
+ const seen = /* @__PURE__ */ new Set();
28551
+ const items = [];
28552
+ for (const name of names) {
28553
+ if (seen.has(name)) continue;
28554
+ seen.add(name);
28555
+ items.push(await this.fetchItem(name));
28556
+ }
28557
+ return items;
28558
+ }
28479
28559
  /**
28480
28560
  * List all available components.
28481
28561
  */
@@ -30874,8 +30954,8 @@ function readVersion() {
30874
30954
  var VERSION = readVersion();
30875
30955
 
30876
30956
  // src/mcp/tools.ts
30877
- import { readFile as readFile6 } from "fs/promises";
30878
- import { join as join12 } from "path";
30957
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
30958
+ import { isAbsolute as isAbsolute3, join as join12 } from "path";
30879
30959
 
30880
30960
  // ../composites/src/built-in-rules/email.ts
30881
30961
  var email3 = external_exports.string().email();
@@ -31025,9 +31105,6 @@ function register3(composite) {
31025
31105
  }
31026
31106
  composites.set(id, composite);
31027
31107
  }
31028
- function get2(id) {
31029
- return composites.get(id);
31030
- }
31031
31108
  function getAll2() {
31032
31109
  return Array.from(composites.values());
31033
31110
  }
@@ -31238,7 +31315,427 @@ function resolveWorkspace(workspaces, defaultWorkspace, name) {
31238
31315
  return defaultWorkspace;
31239
31316
  }
31240
31317
 
31318
+ // src/mcp/graph.ts
31319
+ var EXPAND_OP = "*";
31320
+ var PROBE_OP = "?";
31321
+ function assembleGraph(items) {
31322
+ const nodes = /* @__PURE__ */ new Map();
31323
+ for (const item of items) {
31324
+ const kind = kindOf(item.type);
31325
+ if (kind === null) continue;
31326
+ const src = item.intelligence;
31327
+ const intel = {
31328
+ dos: src?.usagePatterns?.dos ?? [],
31329
+ nevers: src?.usagePatterns?.nevers ?? []
31330
+ };
31331
+ if (src?.cognitiveLoad !== void 0) intel.cognitiveLoad = src.cognitiveLoad;
31332
+ if (src?.semanticMeaning !== void 0) intel.semanticMeaning = src.semanticMeaning;
31333
+ if (src?.accessibility !== void 0) intel.accessibility = src.accessibility;
31334
+ if (src?.attentionEconomics !== void 0) intel.attentionEconomics = src.attentionEconomics;
31335
+ if (src?.trustBuilding !== void 0) intel.trustBuilding = src.trustBuilding;
31336
+ const gn = {
31337
+ id: item.name,
31338
+ kind,
31339
+ intel,
31340
+ facets: item.facets ?? {},
31341
+ composesWith: item.composites,
31342
+ parts: []
31343
+ };
31344
+ if (item.parent !== void 0) gn.parent = item.parent;
31345
+ nodes.set(item.name, gn);
31346
+ }
31347
+ for (const node of nodes.values()) {
31348
+ if (node.parent === void 0) continue;
31349
+ if (node.parent === node.id) {
31350
+ throw new Error(`graph: node "${node.id}" parent references itself`);
31351
+ }
31352
+ if (!nodes.has(node.parent)) {
31353
+ throw new Error(`graph: node "${node.id}" parent names unknown id "${node.parent}"`);
31354
+ }
31355
+ }
31356
+ for (const node of nodes.values()) {
31357
+ if (node.parent === void 0) continue;
31358
+ const visited = /* @__PURE__ */ new Set();
31359
+ visited.add(node.id);
31360
+ let current = node.parent;
31361
+ while (current !== void 0) {
31362
+ if (visited.has(current)) {
31363
+ throw new Error(
31364
+ `graph: node "${node.id}" has a circular parent chain through "${current}"`
31365
+ );
31366
+ }
31367
+ visited.add(current);
31368
+ current = nodes.get(current)?.parent;
31369
+ }
31370
+ }
31371
+ for (const node of nodes.values()) {
31372
+ if (node.parent === void 0) continue;
31373
+ const parentNode = nodes.get(node.parent);
31374
+ if (parentNode) parentNode.parts.push(node.id);
31375
+ }
31376
+ for (const node of nodes.values()) {
31377
+ for (const target of node.composesWith) {
31378
+ if (!nodes.has(target)) {
31379
+ throw new Error(`graph: node "${node.id}" composesWith unknown id "${target}"`);
31380
+ }
31381
+ }
31382
+ }
31383
+ return { nodes };
31384
+ }
31385
+ function kindOf(type2) {
31386
+ if (type2 === "composite") return "composite";
31387
+ if (type2 === "ui" || type2 === "primitive") return "component";
31388
+ return null;
31389
+ }
31390
+ function describe(addr, graph, target) {
31391
+ const parts = addr === "" ? [] : addr.split(".");
31392
+ if (parts.length === 0) {
31393
+ return {
31394
+ kinds: [
31395
+ { addr: "components", type: "edge" },
31396
+ { addr: "composites", type: "edge" }
31397
+ ],
31398
+ nodeCount: graph.nodes.size
31399
+ };
31400
+ }
31401
+ const [head, ...rest] = parts;
31402
+ if (head === void 0) return { error: `cannot resolve: ${addr}` };
31403
+ if (head === "components" || head === "composites") {
31404
+ if (rest.length > 0) return { error: `cannot expand: ${addr}` };
31405
+ const kind = head === "components" ? "component" : "composite";
31406
+ const roster = [];
31407
+ for (const node2 of graph.nodes.values()) {
31408
+ if (node2.kind === kind) roster.push({ id: node2.id });
31409
+ }
31410
+ return roster;
31411
+ }
31412
+ if (parts[parts.length - 1] === PROBE_OP) {
31413
+ const baseAddr = parts.slice(0, -1).join(".");
31414
+ const result = describe(baseAddr, graph, target);
31415
+ if (typeof result === "object" && result !== null && "error" in result) return null;
31416
+ return result;
31417
+ }
31418
+ if (head === EXPAND_OP || head === PROBE_OP) {
31419
+ return { error: `cannot expand at root: use 'components' or 'composites'` };
31420
+ }
31421
+ const node = graph.nodes.get(head);
31422
+ if (!node) return { error: `unknown node: ${head}` };
31423
+ if (rest.length === 0) return layer0(node, graph, target);
31424
+ if (rest[rest.length - 1] === EXPAND_OP) {
31425
+ const prefix = rest.slice(0, -1);
31426
+ if (prefix.length === 0) return expandNode(node, target);
31427
+ if (prefix.length === 1 && prefix[0] === "props") return expandProps(node, target);
31428
+ return { error: `cannot expand: ${addr}` };
31429
+ }
31430
+ if (rest.length === 1 && rest[0] === "composesWith") {
31431
+ return resolveEdges(node, graph, target);
31432
+ }
31433
+ if (rest[0] === "props") {
31434
+ const facet = target === void 0 ? void 0 : node.facets[target];
31435
+ const propName = rest[1];
31436
+ const prop = facet && propName !== void 0 ? facet.props[propName] : void 0;
31437
+ if (!prop) return { error: `cannot resolve: ${addr}` };
31438
+ if (rest.length === 2) return toAgentProp(prop);
31439
+ return { error: `cannot resolve: ${addr}` };
31440
+ }
31441
+ return { error: `cannot resolve: ${addr}` };
31442
+ }
31443
+ function toAgentProp(prop) {
31444
+ if (prop.type === "grammar") {
31445
+ const { vocab: _vocab, ...agentView } = prop;
31446
+ return agentView;
31447
+ }
31448
+ return prop;
31449
+ }
31450
+ function layer0(node, graph, target) {
31451
+ const facet = target === void 0 ? void 0 : node.facets[target];
31452
+ const children = [];
31453
+ if (facet) {
31454
+ for (const [name, prop] of Object.entries(facet.props)) {
31455
+ const child = { addr: `${node.id}.props.${name}`, type: prop.type };
31456
+ if (prop.type === "deprecated") child.deprecatedFor = prop.deprecatedFor;
31457
+ children.push(child);
31458
+ }
31459
+ }
31460
+ if (node.composesWith.length > 0) {
31461
+ children.push({ addr: `${node.id}.composesWith`, type: "edge" });
31462
+ }
31463
+ for (const partId of node.parts) {
31464
+ children.push({ addr: partId, type: "part" });
31465
+ }
31466
+ const result = { id: node.id, kind: node.kind, intel: node.intel, children };
31467
+ if (facet) {
31468
+ result.snippet = facet.snippet;
31469
+ if (facet.slots !== void 0) result.slots = facet.slots;
31470
+ if (facet.events !== void 0) result.events = facet.events;
31471
+ }
31472
+ if (node.parent !== void 0) {
31473
+ result.parent = node.parent;
31474
+ const parentNode = graph.nodes.get(node.parent);
31475
+ if (parentNode) {
31476
+ const siblings = parentNode.parts.filter((id) => id !== node.id);
31477
+ if (siblings.length > 0) result.siblings = siblings;
31478
+ }
31479
+ }
31480
+ return result;
31481
+ }
31482
+ function resolveEdges(node, graph, target) {
31483
+ const seen = /* @__PURE__ */ new Set();
31484
+ const results = [];
31485
+ for (const edgeId of node.composesWith) {
31486
+ if (seen.has(edgeId)) continue;
31487
+ seen.add(edgeId);
31488
+ const edgeNode = graph.nodes.get(edgeId);
31489
+ if (edgeNode) results.push(layer0(edgeNode, graph, target));
31490
+ }
31491
+ return results;
31492
+ }
31493
+ function expandProps(node, target) {
31494
+ const facet = target === void 0 ? void 0 : node.facets[target];
31495
+ const props = {};
31496
+ if (facet) {
31497
+ for (const [name, prop] of Object.entries(facet.props)) {
31498
+ props[name] = toAgentProp(prop);
31499
+ }
31500
+ }
31501
+ return { expanded: true, props };
31502
+ }
31503
+ function expandNode(node, target) {
31504
+ const facet = target === void 0 ? void 0 : node.facets[target];
31505
+ const props = {};
31506
+ if (facet) {
31507
+ for (const [name, prop] of Object.entries(facet.props)) {
31508
+ props[name] = toAgentProp(prop);
31509
+ }
31510
+ }
31511
+ const result = {
31512
+ id: node.id,
31513
+ kind: node.kind,
31514
+ intel: node.intel,
31515
+ props
31516
+ };
31517
+ if (node.composesWith.length > 0) result.composesWith = node.composesWith;
31518
+ if (node.parts.length > 0) result.parts = node.parts;
31519
+ if (facet) {
31520
+ result.snippet = facet.snippet;
31521
+ if (facet.slots !== void 0) result.slots = facet.slots;
31522
+ if (facet.events !== void 0) result.events = facet.events;
31523
+ }
31524
+ return result;
31525
+ }
31526
+
31527
+ // src/mcp/intent.ts
31528
+ var INTENT_TAGS = {
31529
+ modal: ["attention", "blocking", "above-all", "focus-trap"],
31530
+ alert: ["attention", "inline", "passive"],
31531
+ tooltip: ["hint", "passive", "inline", "hover"]
31532
+ };
31533
+ var INTENT_AXES = [
31534
+ {
31535
+ tag: "above-all",
31536
+ keywords: [
31537
+ "above everything",
31538
+ "above all",
31539
+ "on top of everything",
31540
+ "sits on top",
31541
+ "on top",
31542
+ "over everything",
31543
+ "over all",
31544
+ "topmost",
31545
+ "highest layer"
31546
+ ]
31547
+ },
31548
+ {
31549
+ tag: "blocking",
31550
+ keywords: [
31551
+ "above everything",
31552
+ "on top of everything",
31553
+ "over everything",
31554
+ "block",
31555
+ "blocking",
31556
+ "must dismiss",
31557
+ "interrupt",
31558
+ "take over",
31559
+ "stops everything",
31560
+ "requires a response",
31561
+ "can't ignore",
31562
+ "cannot ignore"
31563
+ ]
31564
+ },
31565
+ {
31566
+ tag: "inline",
31567
+ keywords: ["inline", "in place", "in the flow", "within the page", "non-blocking"]
31568
+ },
31569
+ {
31570
+ tag: "passive",
31571
+ keywords: ["passive", "non-intrusive", "subtle", "quietly", "in the background"]
31572
+ },
31573
+ {
31574
+ tag: "attention",
31575
+ keywords: ["attention", "notice", "get noticed", "important message"]
31576
+ },
31577
+ {
31578
+ tag: "hint",
31579
+ keywords: ["hint", "a tip", "explain", "extra info", "more info"]
31580
+ },
31581
+ {
31582
+ tag: "hover",
31583
+ keywords: ["hover", "on hover", "mouse over"]
31584
+ }
31585
+ ];
31586
+ function isNaturalLanguageQuery(input) {
31587
+ return input.includes(" ");
31588
+ }
31589
+ function matchIntent(query, graph) {
31590
+ const normalized = query.toLowerCase();
31591
+ const matchedTags = collectMatchedTags(normalized);
31592
+ if (matchedTags.size === 0) return noMatch();
31593
+ const winner = selectUse(matchedTags, graph);
31594
+ if (winner === void 0) return noMatch();
31595
+ const winningTags = winner.tags.filter((tag) => matchedTags.has(tag));
31596
+ const decisive = new Set(winningTags);
31597
+ const nearMiss = selectNearMiss(winner, decisive, matchedTags, graph);
31598
+ if (nearMiss === void 0) return noMatch();
31599
+ const use = describeNode(winner.id, graph);
31600
+ const not = describeNode(nearMiss.id, graph);
31601
+ if (use === void 0 || not === void 0) return noMatch();
31602
+ const divergingTags = nearMiss.tags.filter((tag) => !winner.tags.includes(tag));
31603
+ const because = `${winner.id} is ${winningTags.join("/")}; ${nearMiss.id} is ${divergingTags.join("/")}`;
31604
+ return { use, not, because };
31605
+ }
31606
+ function collectMatchedTags(normalizedQuery) {
31607
+ const matched = /* @__PURE__ */ new Set();
31608
+ for (const axis of INTENT_AXES) {
31609
+ for (const keyword of axis.keywords) {
31610
+ if (normalizedQuery.includes(keyword)) {
31611
+ matched.add(axis.tag);
31612
+ break;
31613
+ }
31614
+ }
31615
+ }
31616
+ return matched;
31617
+ }
31618
+ function selectUse(matchedTags, graph) {
31619
+ let best;
31620
+ let bestScore = 0;
31621
+ for (const [id, tags] of Object.entries(INTENT_TAGS)) {
31622
+ if (!graph.nodes.has(id)) continue;
31623
+ const score = tags.reduce((sum, tag) => matchedTags.has(tag) ? sum + 1 : sum, 0);
31624
+ if (score > bestScore) {
31625
+ bestScore = score;
31626
+ best = { id, tags };
31627
+ }
31628
+ }
31629
+ return best;
31630
+ }
31631
+ function selectNearMiss(use, decisive, matchedTags, graph) {
31632
+ let best;
31633
+ let bestOverlap = 0;
31634
+ for (const [id, tags] of Object.entries(INTENT_TAGS)) {
31635
+ if (id === use.id || !graph.nodes.has(id)) continue;
31636
+ if (tags.some((tag) => decisive.has(tag))) continue;
31637
+ const overlap = tags.filter((tag) => use.tags.includes(tag));
31638
+ const sharesOutsideMatched = overlap.some((tag) => !matchedTags.has(tag));
31639
+ if (!sharesOutsideMatched) continue;
31640
+ if (overlap.length > bestOverlap) {
31641
+ bestOverlap = overlap.length;
31642
+ best = { id, tags };
31643
+ }
31644
+ }
31645
+ return best;
31646
+ }
31647
+ function describeNode(id, graph) {
31648
+ const result = describe(id, graph);
31649
+ if (result !== null && !Array.isArray(result) && "children" in result) return result;
31650
+ return void 0;
31651
+ }
31652
+ function noMatch() {
31653
+ return { note: "no route matched; describe(components) or describe(composites) to browse" };
31654
+ }
31655
+
31656
+ // src/mcp/overlay.ts
31657
+ function buildInstalledSet(config2) {
31658
+ return {
31659
+ components: new Set(config2.installed?.components ?? []),
31660
+ composites: new Set(config2.installed?.composites ?? [])
31661
+ };
31662
+ }
31663
+ function describeWithOverlay(addr, graph, ctx) {
31664
+ const result = describe(addr, graph, ctx.target);
31665
+ if (addr === "components" || addr === "composites") {
31666
+ const set3 = addr === "components" ? ctx.installed.components : ctx.installed.composites;
31667
+ const roster = result;
31668
+ return roster.map((entry) => ({ id: entry.id, presence: presenceOf(set3, entry.id) }));
31669
+ }
31670
+ if (addr !== "" && !addr.includes(".") && isNodeResult(result)) {
31671
+ return { ...result, ...stampOf(result.id, result.kind, graph, ctx) };
31672
+ }
31673
+ if (isExpandedNodeResult(result)) {
31674
+ return { ...result, ...stampOf(result.id, result.kind, graph, ctx) };
31675
+ }
31676
+ return result;
31677
+ }
31678
+ function presenceOf(set3, id) {
31679
+ return set3.has(id) ? "installed" : "available";
31680
+ }
31681
+ function stampOf(id, kind, graph, ctx) {
31682
+ const set3 = kind === "component" ? ctx.installed.components : ctx.installed.composites;
31683
+ return {
31684
+ presence: presenceOf(set3, id),
31685
+ target: ctx.target,
31686
+ // rendersForTarget reads the node's own facets (on the universal graph):
31687
+ // does a facet for the resolved target exist?
31688
+ rendersForTarget: ctx.target !== void 0 && graph.nodes.get(id)?.facets[ctx.target] !== void 0
31689
+ };
31690
+ }
31691
+ function isNodeResult(result) {
31692
+ return typeof result === "object" && result !== null && !Array.isArray(result) && "id" in result && "kind" in result && "children" in result;
31693
+ }
31694
+ function isExpandedNodeResult(result) {
31695
+ return typeof result === "object" && result !== null && !Array.isArray(result) && "id" in result && "kind" in result && "props" in result && !("children" in result);
31696
+ }
31697
+
31241
31698
  // src/mcp/tools.ts
31699
+ function isSafeRelPath(p2) {
31700
+ if (isAbsolute3(p2)) return false;
31701
+ return !p2.split(/[/\\]/).includes("..");
31702
+ }
31703
+ var SafePathFieldSchema = PathFieldSchema.refine(
31704
+ (field) => {
31705
+ const entries = typeof field === "string" ? [field] : field;
31706
+ return entries.every((e) => isSafeRelPath(typeof e === "string" ? e : e.path));
31707
+ },
31708
+ { message: "path must be relative and stay inside the workspace (no absolute or `..` paths)" }
31709
+ );
31710
+ var ConfigWiringSchema = external_exports.object({
31711
+ // Closed set -- excludes 'unknown', which is a detection sentinel, not a
31712
+ // valid target a caller may set.
31713
+ framework: external_exports.enum(["next", "vite", "remix", "react-router", "astro", "wc", "vanilla"]),
31714
+ // Must be a valid http(s) URL. (Blocking private/link-local/metadata hosts
31715
+ // is tracked as a follow-up -- see the registryUrl SSRF issue.)
31716
+ registryUrl: external_exports.string().url().refine((u) => /^https?:$/.test(new URL(u).protocol), {
31717
+ message: "registryUrl must be an http(s) URL"
31718
+ }),
31719
+ // Closed set derived from the framework (see ComponentTarget in detect.ts).
31720
+ componentTarget: external_exports.enum(["react", "astro", "vue", "svelte", "wc"]),
31721
+ source: external_exports.string().min(1),
31722
+ cssPath: external_exports.union([external_exports.string(), external_exports.null()]).refine((v) => v === null || isSafeRelPath(v), {
31723
+ message: "cssPath must be relative and stay inside the workspace"
31724
+ }),
31725
+ componentsPath: SafePathFieldSchema,
31726
+ primitivesPath: SafePathFieldSchema,
31727
+ compositesPath: SafePathFieldSchema,
31728
+ rulesPath: SafePathFieldSchema,
31729
+ exports: external_exports.object({
31730
+ tailwind: external_exports.boolean(),
31731
+ typescript: external_exports.boolean(),
31732
+ dtcg: external_exports.boolean(),
31733
+ compiled: external_exports.boolean(),
31734
+ documentation: external_exports.boolean()
31735
+ }).partial()
31736
+ }).partial().strict();
31737
+ var STUDIO_OWNED_KEYS = ["intent", "darkMode", "fonts"];
31738
+ var ADD_MANAGED_KEYS = ["installed"];
31242
31739
  var WORKSPACE_PARAM = {
31243
31740
  workspace: {
31244
31741
  type: "string",
@@ -31248,16 +31745,76 @@ var WORKSPACE_PARAM = {
31248
31745
  var TOOL_DEFINITIONS = [
31249
31746
  {
31250
31747
  name: "rafters_workspaces",
31251
- description: "List rafters workspaces visible to this MCP session. Returns name, path, and which one is the default for unscoped tool calls. Call this first when the project might be a monorepo.",
31748
+ description: "List rafters workspaces, or update a workspace's WIRING config. Called with no arguments (or just `workspace`): returns each workspace name, path, and which is the default for unscoped tool calls -- call this first when the project might be a monorepo. Called with any wiring field: updates that workspace's .rafters/config.rafters.json -- framework, registryUrl, componentTarget, source, cssPath, and the path fields (componentsPath, primitivesPath, compositesPath, rulesPath) and exports; only fields you pass change. Cannot set designer decisions (intent, darkMode, fonts) -- those are set in Studio -- and cannot set installed -- that is managed by `rafters add`.",
31252
31749
  inputSchema: {
31253
31750
  type: "object",
31254
- properties: {},
31751
+ properties: {
31752
+ ...WORKSPACE_PARAM,
31753
+ framework: { type: "string", description: 'Target framework (e.g., "react", "astro")' },
31754
+ registryUrl: {
31755
+ type: "string",
31756
+ description: "Registry to install from / query. Point at your own internal registry."
31757
+ },
31758
+ componentTarget: { type: "string", description: "Where installed components are written" },
31759
+ source: { type: "string", description: "Design system this project was imported from" },
31760
+ cssPath: { type: ["string", "null"], description: "Path to the main CSS file, or null" },
31761
+ componentsPath: {
31762
+ oneOf: [{ type: "string" }, { type: "array" }],
31763
+ description: "Folder(s) to read/write components. String or array of entries."
31764
+ },
31765
+ primitivesPath: {
31766
+ oneOf: [{ type: "string" }, { type: "array" }],
31767
+ description: "Folder(s) to read/write primitives. String or array of entries."
31768
+ },
31769
+ compositesPath: {
31770
+ oneOf: [{ type: "string" }, { type: "array" }],
31771
+ description: "Folder(s) to read/write composites. String or array of entries."
31772
+ },
31773
+ rulesPath: {
31774
+ oneOf: [{ type: "string" }, { type: "array" }],
31775
+ description: "Folder(s) to read/write rules. String or array of entries."
31776
+ },
31777
+ exports: {
31778
+ type: "object",
31779
+ description: "Which output formats to emit (tailwind, typescript, dtcg, compiled, documentation)."
31780
+ }
31781
+ },
31255
31782
  required: []
31256
31783
  }
31257
31784
  },
31785
+ {
31786
+ name: "rafters_describe",
31787
+ description: 'Recursively introspect the component/composite intel graph. describe() returns the installed surface; describe(components)/describe(composites) list the kind roster; describe(button) returns a node -- intel plus type-marked, drillable children; describe(button.props.fill) drills into a prop; describe(button.props.fill.vocab) returns the real token values. describe(button.*) expands all props inline in one call (no more drill-per-prop round trips); describe(button.props.fill.?) probes safely (null on miss, not an error). A natural-language question (e.g. "what do I use when it needs to be above everything") routes to the best-matching node plus a near-miss counter-example instead of an address.',
31788
+ inputSchema: {
31789
+ type: "object",
31790
+ properties: {
31791
+ ...WORKSPACE_PARAM,
31792
+ address: {
31793
+ type: "string",
31794
+ description: 'A dot-address ("button.props.variant") or a natural-language question.'
31795
+ }
31796
+ },
31797
+ required: ["address"]
31798
+ }
31799
+ },
31800
+ {
31801
+ name: "rafters_generate",
31802
+ description: "STUB (Issue E). Will produce a rafters-correct composition with visible placeholders for app-specific fields/actions/copy. Currently returns a structured not-implemented result.",
31803
+ inputSchema: {
31804
+ type: "object",
31805
+ properties: {
31806
+ ...WORKSPACE_PARAM,
31807
+ intent: { type: "string", description: "What to generate" }
31808
+ },
31809
+ required: ["intent"]
31810
+ }
31811
+ },
31812
+ // Deprecated aliases -- kept for one minor release, then removed (tracked as a
31813
+ // follow-up). Input schemas unchanged; every response carries a `deprecated`
31814
+ // field pointing at rafters_describe.
31258
31815
  {
31259
31816
  name: "rafters_composite",
31260
- description: "Query composites by ID, search term, or category. Returns designer intent (solves, appliesWhen, do/never), I/O rules for chaining, and block structure.",
31817
+ description: "[DEPRECATED -- use rafters_describe] Query composites by ID, search term, or category. Returns designer intent (solves, appliesWhen, do/never), I/O rules for chaining, and block structure.",
31261
31818
  inputSchema: {
31262
31819
  type: "object",
31263
31820
  properties: {
@@ -31271,7 +31828,7 @@ var TOOL_DEFINITIONS = [
31271
31828
  },
31272
31829
  {
31273
31830
  name: "rafters_pattern",
31274
- description: 'Get design pattern guidance by querying composites. Search by what the pattern solves (e.g., "authentication", "data entry", "navigation") to get do/never rules, cognitive load, and designer intent.',
31831
+ description: '[DEPRECATED -- use rafters_describe] Get design pattern guidance by querying composites. Search by what the pattern solves (e.g., "authentication", "data entry", "navigation") to get do/never rules, cognitive load, and designer intent.',
31275
31832
  inputSchema: {
31276
31833
  type: "object",
31277
31834
  properties: {
@@ -31290,7 +31847,7 @@ var TOOL_DEFINITIONS = [
31290
31847
  },
31291
31848
  {
31292
31849
  name: "rafters_component",
31293
- description: "Get component intelligence: cognitive load, accessibility, do/never guidance, variants, sizes.",
31850
+ description: "[DEPRECATED -- use rafters_describe] Get component intelligence: cognitive load, accessibility, do/never guidance, variants, sizes.",
31294
31851
  inputSchema: {
31295
31852
  type: "object",
31296
31853
  properties: {
@@ -31304,6 +31861,7 @@ var TOOL_DEFINITIONS = [
31304
31861
  }
31305
31862
  }
31306
31863
  ];
31864
+ var DEPRECATED_MSG = "use rafters_describe instead";
31307
31865
  var RaftersToolHandler = class {
31308
31866
  workspaces;
31309
31867
  defaultWorkspace;
@@ -31311,32 +31869,52 @@ var RaftersToolHandler = class {
31311
31869
  compositesLoadedFor = /* @__PURE__ */ new Set();
31312
31870
  /** Tracks built-in composite loading separately (loaded once globally). */
31313
31871
  builtInCompositesLoaded = false;
31314
- constructor(workspaces, defaultWorkspace) {
31872
+ /**
31873
+ * Registry clients keyed by base URL, so each workspace's `registryUrl` gets
31874
+ * its own fetch cache. Workspaces without a configured URL share the default
31875
+ * singleton (which points at the public registry).
31876
+ */
31877
+ registryClients = /* @__PURE__ */ new Map();
31878
+ /**
31879
+ * Per-workspace-root cache: the intel graph, built once, lazily, on the first
31880
+ * describe/generate call that touches that root. The graph is universal and
31881
+ * target-free (a node carries all its per-target facets); the workspace's
31882
+ * target is applied per query by the overlay, not baked into the cache. Same
31883
+ * per-workspace caching shape as `compositesLoadedFor`/`registryClients`. A
31884
+ * failed build is never inserted, so the next call retries rather than
31885
+ * permanently wedging that workspace.
31886
+ */
31887
+ graphsByWorkspace = /* @__PURE__ */ new Map();
31888
+ /**
31889
+ * The whole-catalog item source `ensureGraph` builds from. Defaults to the
31890
+ * workspace's registry client `fetchAllItems()` (bulk endpoint with per-item
31891
+ * fallback). Injectable so the dispatch can be unit-tested against a fixture
31892
+ * catalog without a network round-trip.
31893
+ */
31894
+ itemsSource;
31895
+ constructor(workspaces, defaultWorkspace, itemsSource) {
31315
31896
  this.workspaces = workspaces;
31316
31897
  this.defaultWorkspace = defaultWorkspace;
31898
+ this.itemsSource = itemsSource ?? (async (ws) => (await this.registryClientFor(ws)).fetchAllItems());
31317
31899
  }
31318
31900
  async handleToolCall(name, args) {
31319
31901
  switch (name) {
31320
31902
  case "rafters_workspaces":
31321
- return this.handleWorkspaces();
31903
+ return this.handleWorkspaces(args);
31904
+ case "rafters_describe":
31905
+ return this.handleDescribe(args.address, args.workspace);
31906
+ case "rafters_generate":
31907
+ return this.handleGenerate(args.intent, args.workspace);
31322
31908
  case "rafters_composite":
31323
31909
  return this.handleComposite(args);
31324
31910
  case "rafters_pattern":
31325
31911
  return this.handlePattern(args);
31326
31912
  case "rafters_component":
31327
- return this.handleComponent(args.name);
31913
+ return this.handleComponent(args.name, args.workspace);
31328
31914
  default:
31329
- return {
31330
- content: [
31331
- {
31332
- type: "text",
31333
- text: JSON.stringify({
31334
- error: `Unknown tool: ${name}`,
31335
- suggestion: "Available tools: rafters_workspaces, rafters_composite, rafters_pattern, rafters_component."
31336
- })
31337
- }
31338
- ]
31339
- };
31915
+ return this.errorResult(`Unknown tool: ${name}`, {
31916
+ suggestion: "Available tools: rafters_workspaces, rafters_describe, rafters_generate. Deprecated: rafters_composite, rafters_pattern, rafters_component."
31917
+ });
31340
31918
  }
31341
31919
  }
31342
31920
  /**
@@ -31353,42 +31931,84 @@ var RaftersToolHandler = class {
31353
31931
  * Use this when a tool requires a workspace and the agent didn't pick one.
31354
31932
  */
31355
31933
  workspaceRequiredError() {
31356
- return {
31357
- content: [
31358
- {
31359
- type: "text",
31360
- text: JSON.stringify({
31361
- error: "workspace parameter required",
31362
- suggestion: "Multiple workspaces are available. Pass `workspace` with one of the names below.",
31363
- workspaces: this.workspaces.map((w) => ({
31364
- name: w.name,
31365
- root: w.root
31366
- }))
31367
- })
31368
- }
31369
- ]
31370
- };
31934
+ return this.errorResult("workspace parameter required", {
31935
+ suggestion: "Multiple workspaces are available. Pass `workspace` with one of the names below.",
31936
+ workspaces: this.workspaces.map((w) => ({ name: w.name, root: w.root }))
31937
+ });
31371
31938
  }
31372
- async handleWorkspaces() {
31373
- return {
31374
- content: [
31375
- {
31376
- type: "text",
31377
- text: JSON.stringify(
31378
- {
31379
- workspaces: this.workspaces.map((w) => ({
31380
- name: w.name,
31381
- root: w.root,
31382
- isDefault: w.name === this.defaultWorkspace?.name
31383
- })),
31384
- defaultWorkspace: this.defaultWorkspace?.name ?? null
31385
- },
31386
- null,
31387
- 2
31388
- )
31389
- }
31390
- ]
31391
- };
31939
+ /** Wrap a JSON-serialisable payload as a text tool result. */
31940
+ jsonResult(payload) {
31941
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
31942
+ }
31943
+ /** Wrap an error string (with optional extra fields) as a tool result. */
31944
+ errorResult(error47, extra) {
31945
+ return this.jsonResult({ error: error47, ...extra });
31946
+ }
31947
+ /**
31948
+ * List workspaces, or -- when any wiring field is present -- update the
31949
+ * target workspace's config. `workspace` selects which; every other key is
31950
+ * treated as a wiring patch. No wiring keys means the list query.
31951
+ */
31952
+ async handleWorkspaces(args) {
31953
+ const { workspace, ...patch } = args;
31954
+ if (Object.keys(patch).length === 0) {
31955
+ return this.jsonResult({
31956
+ workspaces: this.workspaces.map((w) => ({
31957
+ name: w.name,
31958
+ root: w.root,
31959
+ isDefault: w.name === this.defaultWorkspace?.name
31960
+ })),
31961
+ defaultWorkspace: this.defaultWorkspace?.name ?? null
31962
+ });
31963
+ }
31964
+ return this.updateWorkspaceConfig(workspace, patch);
31965
+ }
31966
+ /**
31967
+ * Write a WIRING patch to a workspace's config.rafters.json. Rejects the
31968
+ * designer-owned keys (intent, darkMode, fonts -> Studio) and add-managed
31969
+ * keys (installed -> `rafters add`) with a pointer to the right surface, so
31970
+ * this path structurally cannot remove designer choice. Only the fields in
31971
+ * the patch change; everything else in the config is preserved.
31972
+ */
31973
+ async updateWorkspaceConfig(workspaceName, patch) {
31974
+ const resolved = this.resolve(workspaceName);
31975
+ if (!resolved) {
31976
+ return this.workspaceRequiredError();
31977
+ }
31978
+ const studioKeys = STUDIO_OWNED_KEYS.filter((k) => k in patch);
31979
+ if (studioKeys.length > 0) {
31980
+ return this.errorResult(
31981
+ `${studioKeys.join(", ")} ${studioKeys.length > 1 ? "are" : "is"} a designer decision owned by Rafters Studio, not the MCP. Set ${studioKeys.length > 1 ? "them" : "it"} in Studio.`
31982
+ );
31983
+ }
31984
+ const addKeys = ADD_MANAGED_KEYS.filter((k) => k in patch);
31985
+ if (addKeys.length > 0) {
31986
+ return this.errorResult(`${addKeys.join(", ")} is managed by \`rafters add\`, not the MCP.`);
31987
+ }
31988
+ const result = ConfigWiringSchema.safeParse(patch);
31989
+ if (!result.success) {
31990
+ return this.errorResult(`invalid wiring patch: ${result.error.message}`);
31991
+ }
31992
+ if (Object.keys(result.data).length === 0) {
31993
+ return this.errorResult("no writable wiring fields provided");
31994
+ }
31995
+ const config2 = await this.readConfig(resolved.root);
31996
+ if (!config2) {
31997
+ return this.errorResult(
31998
+ `no config at ${getRaftersPaths(resolved.root).config} -- run \`rafters init\` first`
31999
+ );
32000
+ }
32001
+ const updated = { ...config2, ...result.data };
32002
+ if (result.data.exports !== void 0) {
32003
+ updated.exports = { ...config2.exports, ...result.data.exports };
32004
+ }
32005
+ try {
32006
+ await this.writeConfig(resolved.root, updated);
32007
+ } catch (err) {
32008
+ const message = err instanceof Error ? err.message : "Unknown error";
32009
+ return this.errorResult(`failed to write config: ${message}`);
32010
+ }
32011
+ return this.jsonResult({ ok: true, workspace: resolved.name, updated: result.data });
31392
32012
  }
31393
32013
  /**
31394
32014
  * Recursively discover composites under the given directories via the shared
@@ -31417,6 +32037,138 @@ var RaftersToolHandler = class {
31417
32037
  this.compositesLoadedFor.add(workspace.root);
31418
32038
  }
31419
32039
  }
32040
+ /**
32041
+ * Read and migrate a workspace's `.rafters/config.rafters.json`. Returns null
32042
+ * when the file is absent or unparseable. The single config read the MCP
32043
+ * tools share -- composite paths and the registry URL both come through here.
32044
+ */
32045
+ async readConfig(workspaceRoot) {
32046
+ const paths = getRaftersPaths(workspaceRoot);
32047
+ try {
32048
+ return migrateConfig(
32049
+ JSON.parse(await readFile6(paths.config, "utf-8"))
32050
+ );
32051
+ } catch {
32052
+ return null;
32053
+ }
32054
+ }
32055
+ /** Persist a workspace's config back to `.rafters/config.rafters.json`. */
32056
+ async writeConfig(workspaceRoot, config2) {
32057
+ const paths = getRaftersPaths(workspaceRoot);
32058
+ await writeFile4(paths.config, `${JSON.stringify(config2, null, 2)}
32059
+ `);
32060
+ }
32061
+ /**
32062
+ * Resolve the registry client for a workspace, honoring its configured
32063
+ * `registryUrl`. Workspaces with no config, or no `registryUrl`, share the
32064
+ * default singleton. Clients are cached per URL so each keeps its fetch cache.
32065
+ */
32066
+ async registryClientFor(workspace) {
32067
+ if (!workspace) return registryClient;
32068
+ const url3 = (await this.readConfig(workspace.root))?.registryUrl;
32069
+ if (!url3) return registryClient;
32070
+ let client = this.registryClients.get(url3);
32071
+ if (!client) {
32072
+ client = new RegistryClient(url3);
32073
+ this.registryClients.set(url3, client);
32074
+ }
32075
+ return client;
32076
+ }
32077
+ /**
32078
+ * Build (once, lazily) and cache the intel graph for a workspace root.
32079
+ * `assembleGraph` (#2072/#2090) consumes the whole-catalog `RegistryItem[]` and
32080
+ * carries each node's complete intelligence and all per-target facets onto the
32081
+ * universal graph. Throws when the catalog can't be loaded (both the bulk
32082
+ * endpoint and the per-item fallback failed) or the graph is structurally
32083
+ * broken (a dangling `composesWith` edge -- #2072's deliberate fail-fast); the
32084
+ * caller converts that into a structured error result. A failed build is never
32085
+ * cached, so the next call retries.
32086
+ */
32087
+ async ensureGraph(workspace) {
32088
+ const key = workspace?.root ?? "";
32089
+ const cached2 = this.graphsByWorkspace.get(key);
32090
+ if (cached2) return cached2;
32091
+ const graph = assembleGraph(await this.itemsSource(workspace));
32092
+ this.graphsByWorkspace.set(key, graph);
32093
+ return graph;
32094
+ }
32095
+ /**
32096
+ * Resolve a workspace's overlay context: the configured `componentTarget`
32097
+ * (echoed as-is, `undefined` in degraded mode) and its installed set. Per the
32098
+ * integration note on #2074, `installed.primitives` folds into the components
32099
+ * set -- a `primitive`-kind item maps to graph kind `component`, so without the
32100
+ * fold every installed primitive would misreport as `available`. Built here
32101
+ * rather than by mutating `buildInstalledSet`'s output, leaving overlay.ts
32102
+ * untouched.
32103
+ *
32104
+ * `componentTarget` comes off unvalidated on-disk config (`readConfig` is a raw
32105
+ * `JSON.parse`), so it is run through `ComponentTargetSchema` here rather than
32106
+ * trusted as a `ComponentTarget` -- a stale or typo'd value falls back to
32107
+ * degraded mode (`undefined` target) instead of silently forcing
32108
+ * `rendersForTarget` false for every node.
32109
+ */
32110
+ async overlayContext(workspace) {
32111
+ const config2 = workspace ? await this.readConfig(workspace.root) : null;
32112
+ const base = buildInstalledSet(config2 ?? {});
32113
+ const components = /* @__PURE__ */ new Set([...base.components, ...config2?.installed?.primitives ?? []]);
32114
+ const parsedTarget = ComponentTargetSchema.safeParse(config2?.componentTarget);
32115
+ return {
32116
+ target: parsedTarget.success ? parsedTarget.data : void 0,
32117
+ installed: { components, composites: base.composites }
32118
+ };
32119
+ }
32120
+ /**
32121
+ * The `rafters_describe` dispatcher -- the one seam that composes #2072/#2074/
32122
+ * #2075. A natural-language question routes through the intent door
32123
+ * (`matchIntent`); a dot-address resolves through the workspace overlay
32124
+ * (`describeWithOverlay`, which delegates to #2072's `describe`).
32125
+ */
32126
+ async handleDescribe(address, workspaceName) {
32127
+ const resolved = this.resolve(workspaceName);
32128
+ if (workspaceName && !resolved) {
32129
+ return this.workspaceRequiredError();
32130
+ }
32131
+ let graph;
32132
+ try {
32133
+ graph = await this.ensureGraph(resolved);
32134
+ } catch (err) {
32135
+ const message = err instanceof Error ? err.message : "Unknown error";
32136
+ return this.errorResult(`failed to build intel graph: ${message}`);
32137
+ }
32138
+ if (isNaturalLanguageQuery(address)) {
32139
+ return this.jsonResult(matchIntent(address, graph));
32140
+ }
32141
+ const ctx = await this.overlayContext(resolved);
32142
+ return this.jsonResult(describeWithOverlay(address, graph, ctx));
32143
+ }
32144
+ /**
32145
+ * The `rafters_generate` stub (Issue E). Registered now so the surface shape
32146
+ * (name, input schema) stabilizes before the generator lands; it never
32147
+ * attempts real composition. Deliberately does NOT build the graph: nothing
32148
+ * here reads it, and a build failure must not turn an honest stub into an
32149
+ * error result.
32150
+ */
32151
+ async handleGenerate(intent, workspaceName) {
32152
+ const resolved = this.resolve(workspaceName);
32153
+ if (workspaceName && !resolved) {
32154
+ return this.workspaceRequiredError();
32155
+ }
32156
+ return this.jsonResult({
32157
+ implemented: false,
32158
+ note: `generate is a spike -- see issue E; not yet implemented (requested: ${intent})`
32159
+ });
32160
+ }
32161
+ /**
32162
+ * Stamp the deprecated marker as a top-level field. Objects gain a sibling
32163
+ * `deprecated` key; the rare array/leaf result is wrapped so the marker still
32164
+ * rides at top level.
32165
+ */
32166
+ withDeprecated(payload) {
32167
+ if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
32168
+ return { ...payload, deprecated: DEPRECATED_MSG };
32169
+ }
32170
+ return { result: payload, deprecated: DEPRECATED_MSG };
32171
+ }
31420
32172
  /**
31421
32173
  * Resolve the set of folders to scan for composite manifests in a workspace.
31422
32174
  * Reads `.rafters/config.rafters.json` and applies the workspace's
@@ -31426,13 +32178,7 @@ var RaftersToolHandler = class {
31426
32178
  */
31427
32179
  async compositeReadRoots(workspaceRoot) {
31428
32180
  const paths = getRaftersPaths(workspaceRoot);
31429
- let config2 = null;
31430
- try {
31431
- config2 = migrateConfig(
31432
- JSON.parse(await readFile6(paths.config, "utf-8"))
31433
- );
31434
- } catch {
31435
- }
32181
+ const config2 = await this.readConfig(workspaceRoot);
31436
32182
  if (!config2?.compositesPath) {
31437
32183
  return [join12(paths.root, "composites")];
31438
32184
  }
@@ -31440,16 +32186,16 @@ var RaftersToolHandler = class {
31440
32186
  }
31441
32187
  async handleComposite(args) {
31442
32188
  const { id, query, category, workspace } = args;
32189
+ if (id) {
32190
+ return this.describeById(id, workspace);
32191
+ }
31443
32192
  const resolved = this.resolve(workspace);
31444
32193
  if (workspace && !resolved) {
31445
32194
  return this.workspaceRequiredError();
31446
32195
  }
31447
32196
  await this.ensureCompositesLoaded(resolved);
31448
32197
  let composites2;
31449
- if (id) {
31450
- const c4 = get2(id);
31451
- composites2 = c4 ? [c4] : [];
31452
- } else if (query) {
32198
+ if (query) {
31453
32199
  composites2 = search(query);
31454
32200
  } else if (category) {
31455
32201
  composites2 = getByCategory(category);
@@ -31474,9 +32220,7 @@ var RaftersToolHandler = class {
31474
32220
  // are listed -- the array is empty when nothing is constrained.
31475
32221
  blockRules: c4.blocks.filter((b2) => b2.rules && b2.rules.length > 0).map((b2) => ({ id: b2.id, type: b2.type, rules: b2.rules }))
31476
32222
  }));
31477
- return {
31478
- content: [{ type: "text", text: JSON.stringify({ composites: result }, null, 2) }]
31479
- };
32223
+ return this.jsonResult({ composites: result, deprecated: DEPRECATED_MSG });
31480
32224
  }
31481
32225
  async handlePattern(args) {
31482
32226
  const { solves, query, workspace } = args;
@@ -31503,17 +32247,10 @@ var RaftersToolHandler = class {
31503
32247
  id: c4.manifest.id,
31504
32248
  solves: c4.manifest.solves
31505
32249
  }));
31506
- return {
31507
- content: [
31508
- {
31509
- type: "text",
31510
- text: JSON.stringify({
31511
- error: "No patterns found matching query",
31512
- available
31513
- })
31514
- }
31515
- ]
31516
- };
32250
+ return this.errorResult("No patterns found matching query", {
32251
+ available,
32252
+ deprecated: DEPRECATED_MSG
32253
+ });
31517
32254
  }
31518
32255
  const patterns = composites2.map((c4) => ({
31519
32256
  id: c4.manifest.id,
@@ -31523,51 +32260,38 @@ var RaftersToolHandler = class {
31523
32260
  cognitiveLoad: c4.manifest.cognitiveLoad,
31524
32261
  usagePatterns: c4.manifest.usagePatterns
31525
32262
  }));
31526
- return {
31527
- content: [{ type: "text", text: JSON.stringify({ patterns }, null, 2) }]
31528
- };
32263
+ return this.jsonResult({ patterns, deprecated: DEPRECATED_MSG });
32264
+ }
32265
+ /**
32266
+ * DEPRECATED alias for `rafters_describe`. A component resolves by id through
32267
+ * the overlay exactly as `describe(<id>)` does (#2072's resolver has no
32268
+ * separate component/composite address form -- `describe(<id>)` resolves either
32269
+ * kind), so this is a direct, lossless forward with the deprecated marker added.
32270
+ */
32271
+ async handleComponent(componentName, workspaceName) {
32272
+ return this.describeById(componentName, workspaceName);
31529
32273
  }
31530
- async handleComponent(componentName) {
32274
+ /**
32275
+ * Shared by the deprecated `rafters_component` and `rafters_composite({id})`
32276
+ * paths: resolve one id through the overlay, stamp the deprecated marker.
32277
+ */
32278
+ async describeById(id, workspaceName) {
32279
+ const resolved = this.resolve(workspaceName);
32280
+ if (workspaceName && !resolved) {
32281
+ return this.workspaceRequiredError();
32282
+ }
32283
+ let graph;
31531
32284
  try {
31532
- const item = await registryClient.fetchComponent(componentName);
31533
- return {
31534
- content: [
31535
- {
31536
- type: "text",
31537
- text: JSON.stringify(
31538
- {
31539
- name: item.name,
31540
- type: item.type,
31541
- description: item.description,
31542
- primitives: item.primitives,
31543
- rules: item.rules,
31544
- composites: item.composites,
31545
- files: item.files,
31546
- // The intelligence field carries the WHY of the component:
31547
- // cognitive load, accessibility, do/never, semantic meaning.
31548
- // Extracted from JSDoc by the registry generator and present
31549
- // on every component JSON. Previously stripped by the schema
31550
- // (not declared) and not referenced by the handler -- the
31551
- // tool's whole reason for existing went missing somewhere.
31552
- intelligence: item.intelligence
31553
- },
31554
- null,
31555
- 2
31556
- )
31557
- }
31558
- ]
31559
- };
32285
+ graph = await this.ensureGraph(resolved);
31560
32286
  } catch (err) {
31561
32287
  const message = err instanceof Error ? err.message : "Unknown error";
31562
- return {
31563
- content: [
31564
- {
31565
- type: "text",
31566
- text: JSON.stringify({ error: message })
31567
- }
31568
- ]
31569
- };
32288
+ return this.errorResult(`failed to build intel graph: ${message}`, {
32289
+ deprecated: DEPRECATED_MSG
32290
+ });
31570
32291
  }
32292
+ const ctx = await this.overlayContext(resolved);
32293
+ const out = describeWithOverlay(id, graph, ctx);
32294
+ return this.jsonResult(this.withDeprecated(out));
31571
32295
  }
31572
32296
  };
31573
32297
 
@@ -31644,8 +32368,8 @@ import { existsSync as existsSync8 } from "fs";
31644
32368
  import { resolve as resolve6 } from "path";
31645
32369
 
31646
32370
  // ../studio/src/api/vite-plugin.ts
31647
- import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
31648
- import { isAbsolute as isAbsolute3, join as join14 } from "path";
32371
+ import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
32372
+ import { isAbsolute as isAbsolute4, join as join14 } from "path";
31649
32373
  var REGISTRY_PLUGINS3 = [scalePlugin, contrastPlugin, statePlugin, invertPlugin];
31650
32374
  var STUDIO_REASON_DEFAULT = "studio interactive edit";
31651
32375
  var TokenResponseSchema = external_exports.object({
@@ -31706,7 +32430,7 @@ async function readRaftersConfig() {
31706
32430
  }
31707
32431
  function validateFontsPath(path) {
31708
32432
  if (path === null || path === void 0) return null;
31709
- if (isAbsolute3(path)) return "fonts path must be relative";
32433
+ if (isAbsolute4(path)) return "fonts path must be relative";
31710
32434
  return null;
31711
32435
  }
31712
32436
  function validateIntent(intent) {
@@ -31725,6 +32449,17 @@ var SetTokenMessageSchema = external_exports.object({
31725
32449
  persist: external_exports.boolean().optional(),
31726
32450
  reason: external_exports.string().optional()
31727
32451
  });
32452
+ var ConfigPatchSchema = external_exports.object({
32453
+ intent: external_exports.string().min(1).optional(),
32454
+ darkMode: external_exports.enum(["class", "media"]).optional(),
32455
+ fonts: FontsConfigSchema.optional()
32456
+ }).refine((patch) => Object.keys(patch).length > 0, {
32457
+ message: "patch must set at least one of: intent, darkMode, fonts"
32458
+ });
32459
+ function ridOf(rawData) {
32460
+ const rid = rawData?.__rid;
32461
+ return typeof rid === "string" ? rid : void 0;
32462
+ }
31728
32463
  var TokenPatchSchema = TokenSchema.pick({
31729
32464
  value: true,
31730
32465
  trustLevel: true,
@@ -32235,83 +32970,60 @@ function studioApiPlugin() {
32235
32970
  client.send("rafters:token-updated", { ok: false, error: String(error47) });
32236
32971
  }
32237
32972
  });
32238
- server.ws.on("rafters:get-config", async (_rawData, client) => {
32973
+ server.ws.on("rafters:get-config", async (rawData, client) => {
32974
+ const __rid = ridOf(rawData);
32975
+ const reply = (msg) => client.send("rafters:config", { ...msg, __rid });
32239
32976
  const config2 = await readRaftersConfig();
32240
32977
  if (!config2) {
32241
- client.send("rafters:config", {
32242
- ok: false,
32243
- error: `config not found at ${configPath}`
32244
- });
32978
+ reply({ ok: false, error: `config not found at ${configPath}` });
32245
32979
  return;
32246
32980
  }
32247
- client.send("rafters:config", { ok: true, config: config2 });
32981
+ reply({ ok: true, config: config2 });
32248
32982
  });
32249
- server.ws.on("rafters:set-intent", async (rawData, client) => {
32250
- const parsed = external_exports.object({ intent: external_exports.string().min(1) }).safeParse(rawData);
32983
+ server.ws.on("rafters:set-config", async (rawData, client) => {
32984
+ const __rid = ridOf(rawData);
32985
+ const reply = (msg) => client.send("rafters:config", { ...msg, __rid });
32986
+ const parsed = ConfigPatchSchema.safeParse(rawData);
32251
32987
  if (!parsed.success) {
32252
- client.send("rafters:intent-updated", {
32253
- ok: false,
32254
- error: `Invalid message: ${parsed.error.message}`
32255
- });
32256
- return;
32257
- }
32258
- const { intent } = parsed.data;
32259
- const intentError = validateIntent(intent);
32260
- if (intentError) {
32261
- client.send("rafters:intent-updated", { ok: false, error: intentError });
32262
- return;
32263
- }
32264
- const config2 = await readRaftersConfig();
32265
- if (!config2) {
32266
- client.send("rafters:intent-updated", {
32267
- ok: false,
32268
- error: `config not found at ${configPath}`
32269
- });
32988
+ reply({ ok: false, error: `Invalid patch: ${parsed.error.message}` });
32270
32989
  return;
32271
32990
  }
32272
- const updated = { ...config2, intent };
32273
- try {
32274
- await writeFile4(configPath, JSON.stringify(updated, null, 2));
32275
- client.send("rafters:intent-updated", { ok: true, intent });
32276
- } catch (error47) {
32277
- console.log(`[rafters] Intent update failed: ${error47}`);
32278
- client.send("rafters:intent-updated", { ok: false, error: String(error47) });
32279
- }
32280
- });
32281
- server.ws.on("rafters:set-fonts", async (rawData, client) => {
32282
- const parsed = FontsConfigSchema.safeParse(rawData);
32283
- if (!parsed.success) {
32284
- client.send("rafters:fonts-updated", {
32285
- ok: false,
32286
- error: `Invalid message: ${parsed.error.message}`
32287
- });
32288
- return;
32991
+ const patch = parsed.data;
32992
+ if (patch.intent !== void 0) {
32993
+ const intentError = validateIntent(patch.intent);
32994
+ if (intentError) {
32995
+ reply({ ok: false, error: intentError });
32996
+ return;
32997
+ }
32289
32998
  }
32290
- const fontsData = parsed.data;
32291
- const pathError = validateFontsPath(fontsData.path);
32292
- if (pathError) {
32293
- client.send("rafters:fonts-updated", { ok: false, error: pathError });
32294
- return;
32999
+ if (patch.fonts?.path !== void 0) {
33000
+ const pathError = validateFontsPath(patch.fonts.path);
33001
+ if (pathError) {
33002
+ reply({ ok: false, error: pathError });
33003
+ return;
33004
+ }
32295
33005
  }
32296
33006
  const config2 = await readRaftersConfig();
32297
33007
  if (!config2) {
32298
- client.send("rafters:fonts-updated", {
32299
- ok: false,
32300
- error: `config not found at ${configPath}`
32301
- });
33008
+ reply({ ok: false, error: `config not found at ${configPath}` });
32302
33009
  return;
32303
33010
  }
32304
- const existing = config2.fonts ?? {};
32305
- const fonts = { ...existing };
32306
- if (fontsData.path !== void 0) fonts.path = fontsData.path;
32307
- if (fontsData.imports !== void 0) fonts.imports = fontsData.imports;
32308
- const updated = { ...config2, fonts };
33011
+ const updated = { ...config2 };
33012
+ if (patch.intent !== void 0) updated.intent = patch.intent;
33013
+ if (patch.darkMode !== void 0) updated.darkMode = patch.darkMode;
33014
+ if (patch.fonts !== void 0) {
33015
+ const merged = { ...config2.fonts };
33016
+ if (patch.fonts.path !== void 0) merged.path = patch.fonts.path;
33017
+ if (patch.fonts.imports !== void 0) merged.imports = patch.fonts.imports;
33018
+ updated.fonts = merged;
33019
+ }
32309
33020
  try {
32310
- await writeFile4(configPath, JSON.stringify(updated, null, 2));
32311
- client.send("rafters:fonts-updated", { ok: true, fonts });
33021
+ await writeFile5(configPath, `${JSON.stringify(updated, null, 2)}
33022
+ `);
33023
+ reply({ ok: true, config: updated });
32312
33024
  } catch (error47) {
32313
- console.log(`[rafters] Fonts update failed: ${error47}`);
32314
- client.send("rafters:fonts-updated", { ok: false, error: String(error47) });
33025
+ console.log(`[rafters] Config update failed: ${error47}`);
33026
+ reply({ ok: false, error: String(error47) });
32315
33027
  }
32316
33028
  });
32317
33029
  server.middlewares.use(async (req, res, next) => {