rafters 0.0.82 → 0.0.83

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,11 @@ 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({})
28392
28431
  });
28393
28432
  var RegistryIndexSchema = external_exports.object({
28394
28433
  name: external_exports.string(),
@@ -28476,6 +28515,46 @@ var RegistryClient = class {
28476
28515
  }
28477
28516
  throw new Error(`"${name}" not found in registry (component, primitive, composite, or rule)`);
28478
28517
  }
28518
+ /**
28519
+ * Fetch every registry item in one call, for the whole-catalog load the MCP
28520
+ * intel graph needs at startup.
28521
+ *
28522
+ * Prefers a single bulk round-trip against `/registry/items.json`. A registry
28523
+ * that does not serve that route -- an older deploy, or a third-party
28524
+ * `registryUrl` (which is user-configurable, see ConfigWiringSchema) -- returns
28525
+ * a non-2xx, and this falls back to looping `fetchItem` over `fetchIndex()`'s
28526
+ * name list, retaining today's per-item cost so no workspace's `describe` ever
28527
+ * hard-fails on a registry that predates the bulk endpoint.
28528
+ */
28529
+ async fetchAllItems() {
28530
+ const response = await fetch(`${this.baseUrl}/registry/items.json`);
28531
+ if (response.ok) {
28532
+ return external_exports.array(RegistryItemSchema).parse(await response.json());
28533
+ }
28534
+ return this.fetchAllItemsByIndex();
28535
+ }
28536
+ /**
28537
+ * The fallback whole-catalog load: one `fetchItem` per name in the index,
28538
+ * across every describe-relevant kind. Deduped so a name appearing in more
28539
+ * than one index list is fetched once.
28540
+ */
28541
+ async fetchAllItemsByIndex() {
28542
+ const index = await this.fetchIndex();
28543
+ const names = [
28544
+ ...index.components,
28545
+ ...index.primitives,
28546
+ ...index.composites,
28547
+ ...index.substrate
28548
+ ];
28549
+ const seen = /* @__PURE__ */ new Set();
28550
+ const items = [];
28551
+ for (const name of names) {
28552
+ if (seen.has(name)) continue;
28553
+ seen.add(name);
28554
+ items.push(await this.fetchItem(name));
28555
+ }
28556
+ return items;
28557
+ }
28479
28558
  /**
28480
28559
  * List all available components.
28481
28560
  */
@@ -30874,8 +30953,8 @@ function readVersion() {
30874
30953
  var VERSION = readVersion();
30875
30954
 
30876
30955
  // src/mcp/tools.ts
30877
- import { readFile as readFile6 } from "fs/promises";
30878
- import { join as join12 } from "path";
30956
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
30957
+ import { isAbsolute as isAbsolute3, join as join12 } from "path";
30879
30958
 
30880
30959
  // ../composites/src/built-in-rules/email.ts
30881
30960
  var email3 = external_exports.string().email();
@@ -31025,9 +31104,6 @@ function register3(composite) {
31025
31104
  }
31026
31105
  composites.set(id, composite);
31027
31106
  }
31028
- function get2(id) {
31029
- return composites.get(id);
31030
- }
31031
31107
  function getAll2() {
31032
31108
  return Array.from(composites.values());
31033
31109
  }
@@ -31238,7 +31314,323 @@ function resolveWorkspace(workspaces, defaultWorkspace, name) {
31238
31314
  return defaultWorkspace;
31239
31315
  }
31240
31316
 
31317
+ // src/mcp/graph.ts
31318
+ function assembleGraph(items) {
31319
+ const nodes = /* @__PURE__ */ new Map();
31320
+ for (const item of items) {
31321
+ const kind = kindOf(item.type);
31322
+ if (kind === null) continue;
31323
+ const src = item.intelligence;
31324
+ const intel = {
31325
+ dos: src?.usagePatterns?.dos ?? [],
31326
+ nevers: src?.usagePatterns?.nevers ?? []
31327
+ };
31328
+ if (src?.cognitiveLoad !== void 0) intel.cognitiveLoad = src.cognitiveLoad;
31329
+ if (src?.semanticMeaning !== void 0) intel.semanticMeaning = src.semanticMeaning;
31330
+ if (src?.accessibility !== void 0) intel.accessibility = src.accessibility;
31331
+ if (src?.attentionEconomics !== void 0) intel.attentionEconomics = src.attentionEconomics;
31332
+ if (src?.trustBuilding !== void 0) intel.trustBuilding = src.trustBuilding;
31333
+ nodes.set(item.name, {
31334
+ id: item.name,
31335
+ kind,
31336
+ intel,
31337
+ facets: item.facets ?? {},
31338
+ composesWith: item.composites
31339
+ });
31340
+ }
31341
+ for (const node of nodes.values()) {
31342
+ for (const target of node.composesWith) {
31343
+ if (!nodes.has(target)) {
31344
+ throw new Error(`graph: node "${node.id}" composesWith unknown id "${target}"`);
31345
+ }
31346
+ }
31347
+ }
31348
+ return { nodes };
31349
+ }
31350
+ function kindOf(type2) {
31351
+ if (type2 === "composite") return "composite";
31352
+ if (type2 === "ui" || type2 === "primitive") return "component";
31353
+ return null;
31354
+ }
31355
+ function describe(addr, graph, target) {
31356
+ const parts = addr === "" ? [] : addr.split(".");
31357
+ if (parts.length === 0) {
31358
+ return {
31359
+ kinds: [
31360
+ { addr: "components", type: "edge" },
31361
+ { addr: "composites", type: "edge" }
31362
+ ],
31363
+ nodeCount: graph.nodes.size
31364
+ };
31365
+ }
31366
+ const [head, ...rest] = parts;
31367
+ if (head === void 0) return { error: `cannot resolve: ${addr}` };
31368
+ if (head === "components" || head === "composites") {
31369
+ const kind = head === "components" ? "component" : "composite";
31370
+ const roster = [];
31371
+ for (const node2 of graph.nodes.values()) {
31372
+ if (node2.kind === kind) roster.push({ id: node2.id });
31373
+ }
31374
+ return roster;
31375
+ }
31376
+ const node = graph.nodes.get(head);
31377
+ if (!node) return { error: `unknown node: ${head}` };
31378
+ if (rest.length === 0) return layer0(node, target);
31379
+ if (rest.length === 1 && rest[0] === "composesWith") {
31380
+ return resolveEdges(node, graph, target);
31381
+ }
31382
+ if (rest[0] === "props") {
31383
+ const facet = target === void 0 ? void 0 : node.facets[target];
31384
+ const propName = rest[1];
31385
+ const prop = facet && propName !== void 0 ? facet.props[propName] : void 0;
31386
+ if (!prop) return { error: `cannot resolve: ${addr}` };
31387
+ if (rest.length === 2) return toAgentProp(prop);
31388
+ return { error: `cannot resolve: ${addr}` };
31389
+ }
31390
+ return { error: `cannot resolve: ${addr}` };
31391
+ }
31392
+ function toAgentProp(prop) {
31393
+ if (prop.type === "grammar") {
31394
+ const { vocab: _vocab, ...agentView } = prop;
31395
+ return agentView;
31396
+ }
31397
+ return prop;
31398
+ }
31399
+ function layer0(node, target) {
31400
+ const facet = target === void 0 ? void 0 : node.facets[target];
31401
+ const children = [];
31402
+ if (facet) {
31403
+ for (const [name, prop] of Object.entries(facet.props)) {
31404
+ const child = { addr: `${node.id}.props.${name}`, type: prop.type };
31405
+ if (prop.type === "deprecated") child.deprecatedFor = prop.deprecatedFor;
31406
+ children.push(child);
31407
+ }
31408
+ }
31409
+ if (node.composesWith.length > 0) {
31410
+ children.push({ addr: `${node.id}.composesWith`, type: "edge" });
31411
+ }
31412
+ const result = { id: node.id, kind: node.kind, intel: node.intel, children };
31413
+ if (facet) {
31414
+ result.snippet = facet.snippet;
31415
+ if (facet.slots !== void 0) result.slots = facet.slots;
31416
+ if (facet.events !== void 0) result.events = facet.events;
31417
+ }
31418
+ return result;
31419
+ }
31420
+ function resolveEdges(node, graph, target) {
31421
+ const seen = /* @__PURE__ */ new Set();
31422
+ const results = [];
31423
+ for (const edgeId of node.composesWith) {
31424
+ if (seen.has(edgeId)) continue;
31425
+ seen.add(edgeId);
31426
+ const edgeNode = graph.nodes.get(edgeId);
31427
+ if (edgeNode) results.push(layer0(edgeNode, target));
31428
+ }
31429
+ return results;
31430
+ }
31431
+
31432
+ // src/mcp/intent.ts
31433
+ var INTENT_TAGS = {
31434
+ modal: ["attention", "blocking", "above-all", "focus-trap"],
31435
+ alert: ["attention", "inline", "passive"],
31436
+ tooltip: ["hint", "passive", "inline", "hover"]
31437
+ };
31438
+ var INTENT_AXES = [
31439
+ {
31440
+ tag: "above-all",
31441
+ keywords: [
31442
+ "above everything",
31443
+ "above all",
31444
+ "on top of everything",
31445
+ "sits on top",
31446
+ "on top",
31447
+ "over everything",
31448
+ "over all",
31449
+ "topmost",
31450
+ "highest layer"
31451
+ ]
31452
+ },
31453
+ {
31454
+ tag: "blocking",
31455
+ keywords: [
31456
+ "above everything",
31457
+ "on top of everything",
31458
+ "over everything",
31459
+ "block",
31460
+ "blocking",
31461
+ "must dismiss",
31462
+ "interrupt",
31463
+ "take over",
31464
+ "stops everything",
31465
+ "requires a response",
31466
+ "can't ignore",
31467
+ "cannot ignore"
31468
+ ]
31469
+ },
31470
+ {
31471
+ tag: "inline",
31472
+ keywords: ["inline", "in place", "in the flow", "within the page", "non-blocking"]
31473
+ },
31474
+ {
31475
+ tag: "passive",
31476
+ keywords: ["passive", "non-intrusive", "subtle", "quietly", "in the background"]
31477
+ },
31478
+ {
31479
+ tag: "attention",
31480
+ keywords: ["attention", "notice", "get noticed", "important message"]
31481
+ },
31482
+ {
31483
+ tag: "hint",
31484
+ keywords: ["hint", "a tip", "explain", "extra info", "more info"]
31485
+ },
31486
+ {
31487
+ tag: "hover",
31488
+ keywords: ["hover", "on hover", "mouse over"]
31489
+ }
31490
+ ];
31491
+ function isNaturalLanguageQuery(input) {
31492
+ return input.includes(" ");
31493
+ }
31494
+ function matchIntent(query, graph) {
31495
+ const normalized = query.toLowerCase();
31496
+ const matchedTags = collectMatchedTags(normalized);
31497
+ if (matchedTags.size === 0) return noMatch();
31498
+ const winner = selectUse(matchedTags, graph);
31499
+ if (winner === void 0) return noMatch();
31500
+ const winningTags = winner.tags.filter((tag) => matchedTags.has(tag));
31501
+ const decisive = new Set(winningTags);
31502
+ const nearMiss = selectNearMiss(winner, decisive, matchedTags, graph);
31503
+ if (nearMiss === void 0) return noMatch();
31504
+ const use = describeNode(winner.id, graph);
31505
+ const not = describeNode(nearMiss.id, graph);
31506
+ if (use === void 0 || not === void 0) return noMatch();
31507
+ const divergingTags = nearMiss.tags.filter((tag) => !winner.tags.includes(tag));
31508
+ const because = `${winner.id} is ${winningTags.join("/")}; ${nearMiss.id} is ${divergingTags.join("/")}`;
31509
+ return { use, not, because };
31510
+ }
31511
+ function collectMatchedTags(normalizedQuery) {
31512
+ const matched = /* @__PURE__ */ new Set();
31513
+ for (const axis of INTENT_AXES) {
31514
+ for (const keyword of axis.keywords) {
31515
+ if (normalizedQuery.includes(keyword)) {
31516
+ matched.add(axis.tag);
31517
+ break;
31518
+ }
31519
+ }
31520
+ }
31521
+ return matched;
31522
+ }
31523
+ function selectUse(matchedTags, graph) {
31524
+ let best;
31525
+ let bestScore = 0;
31526
+ for (const [id, tags] of Object.entries(INTENT_TAGS)) {
31527
+ if (!graph.nodes.has(id)) continue;
31528
+ const score = tags.reduce((sum, tag) => matchedTags.has(tag) ? sum + 1 : sum, 0);
31529
+ if (score > bestScore) {
31530
+ bestScore = score;
31531
+ best = { id, tags };
31532
+ }
31533
+ }
31534
+ return best;
31535
+ }
31536
+ function selectNearMiss(use, decisive, matchedTags, graph) {
31537
+ let best;
31538
+ let bestOverlap = 0;
31539
+ for (const [id, tags] of Object.entries(INTENT_TAGS)) {
31540
+ if (id === use.id || !graph.nodes.has(id)) continue;
31541
+ if (tags.some((tag) => decisive.has(tag))) continue;
31542
+ const overlap = tags.filter((tag) => use.tags.includes(tag));
31543
+ const sharesOutsideMatched = overlap.some((tag) => !matchedTags.has(tag));
31544
+ if (!sharesOutsideMatched) continue;
31545
+ if (overlap.length > bestOverlap) {
31546
+ bestOverlap = overlap.length;
31547
+ best = { id, tags };
31548
+ }
31549
+ }
31550
+ return best;
31551
+ }
31552
+ function describeNode(id, graph) {
31553
+ const result = describe(id, graph);
31554
+ if (!Array.isArray(result) && "children" in result) return result;
31555
+ return void 0;
31556
+ }
31557
+ function noMatch() {
31558
+ return { note: "no route matched; describe(components) or describe(composites) to browse" };
31559
+ }
31560
+
31561
+ // src/mcp/overlay.ts
31562
+ function buildInstalledSet(config2) {
31563
+ return {
31564
+ components: new Set(config2.installed?.components ?? []),
31565
+ composites: new Set(config2.installed?.composites ?? [])
31566
+ };
31567
+ }
31568
+ function describeWithOverlay(addr, graph, ctx) {
31569
+ const result = describe(addr, graph, ctx.target);
31570
+ if (addr === "components" || addr === "composites") {
31571
+ const set3 = addr === "components" ? ctx.installed.components : ctx.installed.composites;
31572
+ const roster = result;
31573
+ return roster.map((entry) => ({ id: entry.id, presence: presenceOf(set3, entry.id) }));
31574
+ }
31575
+ if (addr !== "" && !addr.includes(".") && isNodeResult(result)) {
31576
+ const set3 = result.kind === "component" ? ctx.installed.components : ctx.installed.composites;
31577
+ return {
31578
+ ...result,
31579
+ presence: presenceOf(set3, result.id),
31580
+ target: ctx.target,
31581
+ rendersForTarget: ctx.target !== void 0 && graph.nodes.get(result.id)?.facets[ctx.target] !== void 0
31582
+ };
31583
+ }
31584
+ return result;
31585
+ }
31586
+ function presenceOf(set3, id) {
31587
+ return set3.has(id) ? "installed" : "available";
31588
+ }
31589
+ function isNodeResult(result) {
31590
+ return typeof result === "object" && result !== null && !Array.isArray(result) && "id" in result && "kind" in result && "children" in result;
31591
+ }
31592
+
31241
31593
  // src/mcp/tools.ts
31594
+ function isSafeRelPath(p2) {
31595
+ if (isAbsolute3(p2)) return false;
31596
+ return !p2.split(/[/\\]/).includes("..");
31597
+ }
31598
+ var SafePathFieldSchema = PathFieldSchema.refine(
31599
+ (field) => {
31600
+ const entries = typeof field === "string" ? [field] : field;
31601
+ return entries.every((e) => isSafeRelPath(typeof e === "string" ? e : e.path));
31602
+ },
31603
+ { message: "path must be relative and stay inside the workspace (no absolute or `..` paths)" }
31604
+ );
31605
+ var ConfigWiringSchema = external_exports.object({
31606
+ // Closed set -- excludes 'unknown', which is a detection sentinel, not a
31607
+ // valid target a caller may set.
31608
+ framework: external_exports.enum(["next", "vite", "remix", "react-router", "astro", "wc", "vanilla"]),
31609
+ // Must be a valid http(s) URL. (Blocking private/link-local/metadata hosts
31610
+ // is tracked as a follow-up -- see the registryUrl SSRF issue.)
31611
+ registryUrl: external_exports.string().url().refine((u) => /^https?:$/.test(new URL(u).protocol), {
31612
+ message: "registryUrl must be an http(s) URL"
31613
+ }),
31614
+ // Closed set derived from the framework (see ComponentTarget in detect.ts).
31615
+ componentTarget: external_exports.enum(["react", "astro", "vue", "svelte", "wc"]),
31616
+ source: external_exports.string().min(1),
31617
+ cssPath: external_exports.union([external_exports.string(), external_exports.null()]).refine((v) => v === null || isSafeRelPath(v), {
31618
+ message: "cssPath must be relative and stay inside the workspace"
31619
+ }),
31620
+ componentsPath: SafePathFieldSchema,
31621
+ primitivesPath: SafePathFieldSchema,
31622
+ compositesPath: SafePathFieldSchema,
31623
+ rulesPath: SafePathFieldSchema,
31624
+ exports: external_exports.object({
31625
+ tailwind: external_exports.boolean(),
31626
+ typescript: external_exports.boolean(),
31627
+ dtcg: external_exports.boolean(),
31628
+ compiled: external_exports.boolean(),
31629
+ documentation: external_exports.boolean()
31630
+ }).partial()
31631
+ }).partial().strict();
31632
+ var STUDIO_OWNED_KEYS = ["intent", "darkMode", "fonts"];
31633
+ var ADD_MANAGED_KEYS = ["installed"];
31242
31634
  var WORKSPACE_PARAM = {
31243
31635
  workspace: {
31244
31636
  type: "string",
@@ -31248,16 +31640,76 @@ var WORKSPACE_PARAM = {
31248
31640
  var TOOL_DEFINITIONS = [
31249
31641
  {
31250
31642
  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.",
31643
+ 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
31644
  inputSchema: {
31253
31645
  type: "object",
31254
- properties: {},
31646
+ properties: {
31647
+ ...WORKSPACE_PARAM,
31648
+ framework: { type: "string", description: 'Target framework (e.g., "react", "astro")' },
31649
+ registryUrl: {
31650
+ type: "string",
31651
+ description: "Registry to install from / query. Point at your own internal registry."
31652
+ },
31653
+ componentTarget: { type: "string", description: "Where installed components are written" },
31654
+ source: { type: "string", description: "Design system this project was imported from" },
31655
+ cssPath: { type: ["string", "null"], description: "Path to the main CSS file, or null" },
31656
+ componentsPath: {
31657
+ oneOf: [{ type: "string" }, { type: "array" }],
31658
+ description: "Folder(s) to read/write components. String or array of entries."
31659
+ },
31660
+ primitivesPath: {
31661
+ oneOf: [{ type: "string" }, { type: "array" }],
31662
+ description: "Folder(s) to read/write primitives. String or array of entries."
31663
+ },
31664
+ compositesPath: {
31665
+ oneOf: [{ type: "string" }, { type: "array" }],
31666
+ description: "Folder(s) to read/write composites. String or array of entries."
31667
+ },
31668
+ rulesPath: {
31669
+ oneOf: [{ type: "string" }, { type: "array" }],
31670
+ description: "Folder(s) to read/write rules. String or array of entries."
31671
+ },
31672
+ exports: {
31673
+ type: "object",
31674
+ description: "Which output formats to emit (tailwind, typescript, dtcg, compiled, documentation)."
31675
+ }
31676
+ },
31255
31677
  required: []
31256
31678
  }
31257
31679
  },
31680
+ {
31681
+ name: "rafters_describe",
31682
+ 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. 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.',
31683
+ inputSchema: {
31684
+ type: "object",
31685
+ properties: {
31686
+ ...WORKSPACE_PARAM,
31687
+ address: {
31688
+ type: "string",
31689
+ description: 'A dot-address ("button.props.variant") or a natural-language question.'
31690
+ }
31691
+ },
31692
+ required: ["address"]
31693
+ }
31694
+ },
31695
+ {
31696
+ name: "rafters_generate",
31697
+ 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.",
31698
+ inputSchema: {
31699
+ type: "object",
31700
+ properties: {
31701
+ ...WORKSPACE_PARAM,
31702
+ intent: { type: "string", description: "What to generate" }
31703
+ },
31704
+ required: ["intent"]
31705
+ }
31706
+ },
31707
+ // Deprecated aliases -- kept for one minor release, then removed (tracked as a
31708
+ // follow-up). Input schemas unchanged; every response carries a `deprecated`
31709
+ // field pointing at rafters_describe.
31258
31710
  {
31259
31711
  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.",
31712
+ 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
31713
  inputSchema: {
31262
31714
  type: "object",
31263
31715
  properties: {
@@ -31271,7 +31723,7 @@ var TOOL_DEFINITIONS = [
31271
31723
  },
31272
31724
  {
31273
31725
  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.',
31726
+ 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
31727
  inputSchema: {
31276
31728
  type: "object",
31277
31729
  properties: {
@@ -31290,7 +31742,7 @@ var TOOL_DEFINITIONS = [
31290
31742
  },
31291
31743
  {
31292
31744
  name: "rafters_component",
31293
- description: "Get component intelligence: cognitive load, accessibility, do/never guidance, variants, sizes.",
31745
+ description: "[DEPRECATED -- use rafters_describe] Get component intelligence: cognitive load, accessibility, do/never guidance, variants, sizes.",
31294
31746
  inputSchema: {
31295
31747
  type: "object",
31296
31748
  properties: {
@@ -31304,6 +31756,7 @@ var TOOL_DEFINITIONS = [
31304
31756
  }
31305
31757
  }
31306
31758
  ];
31759
+ var DEPRECATED_MSG = "use rafters_describe instead";
31307
31760
  var RaftersToolHandler = class {
31308
31761
  workspaces;
31309
31762
  defaultWorkspace;
@@ -31311,32 +31764,52 @@ var RaftersToolHandler = class {
31311
31764
  compositesLoadedFor = /* @__PURE__ */ new Set();
31312
31765
  /** Tracks built-in composite loading separately (loaded once globally). */
31313
31766
  builtInCompositesLoaded = false;
31314
- constructor(workspaces, defaultWorkspace) {
31767
+ /**
31768
+ * Registry clients keyed by base URL, so each workspace's `registryUrl` gets
31769
+ * its own fetch cache. Workspaces without a configured URL share the default
31770
+ * singleton (which points at the public registry).
31771
+ */
31772
+ registryClients = /* @__PURE__ */ new Map();
31773
+ /**
31774
+ * Per-workspace-root cache: the intel graph, built once, lazily, on the first
31775
+ * describe/generate call that touches that root. The graph is universal and
31776
+ * target-free (a node carries all its per-target facets); the workspace's
31777
+ * target is applied per query by the overlay, not baked into the cache. Same
31778
+ * per-workspace caching shape as `compositesLoadedFor`/`registryClients`. A
31779
+ * failed build is never inserted, so the next call retries rather than
31780
+ * permanently wedging that workspace.
31781
+ */
31782
+ graphsByWorkspace = /* @__PURE__ */ new Map();
31783
+ /**
31784
+ * The whole-catalog item source `ensureGraph` builds from. Defaults to the
31785
+ * workspace's registry client `fetchAllItems()` (bulk endpoint with per-item
31786
+ * fallback). Injectable so the dispatch can be unit-tested against a fixture
31787
+ * catalog without a network round-trip.
31788
+ */
31789
+ itemsSource;
31790
+ constructor(workspaces, defaultWorkspace, itemsSource) {
31315
31791
  this.workspaces = workspaces;
31316
31792
  this.defaultWorkspace = defaultWorkspace;
31793
+ this.itemsSource = itemsSource ?? (async (ws) => (await this.registryClientFor(ws)).fetchAllItems());
31317
31794
  }
31318
31795
  async handleToolCall(name, args) {
31319
31796
  switch (name) {
31320
31797
  case "rafters_workspaces":
31321
- return this.handleWorkspaces();
31798
+ return this.handleWorkspaces(args);
31799
+ case "rafters_describe":
31800
+ return this.handleDescribe(args.address, args.workspace);
31801
+ case "rafters_generate":
31802
+ return this.handleGenerate(args.intent, args.workspace);
31322
31803
  case "rafters_composite":
31323
31804
  return this.handleComposite(args);
31324
31805
  case "rafters_pattern":
31325
31806
  return this.handlePattern(args);
31326
31807
  case "rafters_component":
31327
- return this.handleComponent(args.name);
31808
+ return this.handleComponent(args.name, args.workspace);
31328
31809
  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
- };
31810
+ return this.errorResult(`Unknown tool: ${name}`, {
31811
+ suggestion: "Available tools: rafters_workspaces, rafters_describe, rafters_generate. Deprecated: rafters_composite, rafters_pattern, rafters_component."
31812
+ });
31340
31813
  }
31341
31814
  }
31342
31815
  /**
@@ -31353,42 +31826,84 @@ var RaftersToolHandler = class {
31353
31826
  * Use this when a tool requires a workspace and the agent didn't pick one.
31354
31827
  */
31355
31828
  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
- };
31829
+ return this.errorResult("workspace parameter required", {
31830
+ suggestion: "Multiple workspaces are available. Pass `workspace` with one of the names below.",
31831
+ workspaces: this.workspaces.map((w) => ({ name: w.name, root: w.root }))
31832
+ });
31371
31833
  }
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
- };
31834
+ /** Wrap a JSON-serialisable payload as a text tool result. */
31835
+ jsonResult(payload) {
31836
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
31837
+ }
31838
+ /** Wrap an error string (with optional extra fields) as a tool result. */
31839
+ errorResult(error47, extra) {
31840
+ return this.jsonResult({ error: error47, ...extra });
31841
+ }
31842
+ /**
31843
+ * List workspaces, or -- when any wiring field is present -- update the
31844
+ * target workspace's config. `workspace` selects which; every other key is
31845
+ * treated as a wiring patch. No wiring keys means the list query.
31846
+ */
31847
+ async handleWorkspaces(args) {
31848
+ const { workspace, ...patch } = args;
31849
+ if (Object.keys(patch).length === 0) {
31850
+ return this.jsonResult({
31851
+ workspaces: this.workspaces.map((w) => ({
31852
+ name: w.name,
31853
+ root: w.root,
31854
+ isDefault: w.name === this.defaultWorkspace?.name
31855
+ })),
31856
+ defaultWorkspace: this.defaultWorkspace?.name ?? null
31857
+ });
31858
+ }
31859
+ return this.updateWorkspaceConfig(workspace, patch);
31860
+ }
31861
+ /**
31862
+ * Write a WIRING patch to a workspace's config.rafters.json. Rejects the
31863
+ * designer-owned keys (intent, darkMode, fonts -> Studio) and add-managed
31864
+ * keys (installed -> `rafters add`) with a pointer to the right surface, so
31865
+ * this path structurally cannot remove designer choice. Only the fields in
31866
+ * the patch change; everything else in the config is preserved.
31867
+ */
31868
+ async updateWorkspaceConfig(workspaceName, patch) {
31869
+ const resolved = this.resolve(workspaceName);
31870
+ if (!resolved) {
31871
+ return this.workspaceRequiredError();
31872
+ }
31873
+ const studioKeys = STUDIO_OWNED_KEYS.filter((k) => k in patch);
31874
+ if (studioKeys.length > 0) {
31875
+ return this.errorResult(
31876
+ `${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.`
31877
+ );
31878
+ }
31879
+ const addKeys = ADD_MANAGED_KEYS.filter((k) => k in patch);
31880
+ if (addKeys.length > 0) {
31881
+ return this.errorResult(`${addKeys.join(", ")} is managed by \`rafters add\`, not the MCP.`);
31882
+ }
31883
+ const result = ConfigWiringSchema.safeParse(patch);
31884
+ if (!result.success) {
31885
+ return this.errorResult(`invalid wiring patch: ${result.error.message}`);
31886
+ }
31887
+ if (Object.keys(result.data).length === 0) {
31888
+ return this.errorResult("no writable wiring fields provided");
31889
+ }
31890
+ const config2 = await this.readConfig(resolved.root);
31891
+ if (!config2) {
31892
+ return this.errorResult(
31893
+ `no config at ${getRaftersPaths(resolved.root).config} -- run \`rafters init\` first`
31894
+ );
31895
+ }
31896
+ const updated = { ...config2, ...result.data };
31897
+ if (result.data.exports !== void 0) {
31898
+ updated.exports = { ...config2.exports, ...result.data.exports };
31899
+ }
31900
+ try {
31901
+ await this.writeConfig(resolved.root, updated);
31902
+ } catch (err) {
31903
+ const message = err instanceof Error ? err.message : "Unknown error";
31904
+ return this.errorResult(`failed to write config: ${message}`);
31905
+ }
31906
+ return this.jsonResult({ ok: true, workspace: resolved.name, updated: result.data });
31392
31907
  }
31393
31908
  /**
31394
31909
  * Recursively discover composites under the given directories via the shared
@@ -31417,6 +31932,138 @@ var RaftersToolHandler = class {
31417
31932
  this.compositesLoadedFor.add(workspace.root);
31418
31933
  }
31419
31934
  }
31935
+ /**
31936
+ * Read and migrate a workspace's `.rafters/config.rafters.json`. Returns null
31937
+ * when the file is absent or unparseable. The single config read the MCP
31938
+ * tools share -- composite paths and the registry URL both come through here.
31939
+ */
31940
+ async readConfig(workspaceRoot) {
31941
+ const paths = getRaftersPaths(workspaceRoot);
31942
+ try {
31943
+ return migrateConfig(
31944
+ JSON.parse(await readFile6(paths.config, "utf-8"))
31945
+ );
31946
+ } catch {
31947
+ return null;
31948
+ }
31949
+ }
31950
+ /** Persist a workspace's config back to `.rafters/config.rafters.json`. */
31951
+ async writeConfig(workspaceRoot, config2) {
31952
+ const paths = getRaftersPaths(workspaceRoot);
31953
+ await writeFile4(paths.config, `${JSON.stringify(config2, null, 2)}
31954
+ `);
31955
+ }
31956
+ /**
31957
+ * Resolve the registry client for a workspace, honoring its configured
31958
+ * `registryUrl`. Workspaces with no config, or no `registryUrl`, share the
31959
+ * default singleton. Clients are cached per URL so each keeps its fetch cache.
31960
+ */
31961
+ async registryClientFor(workspace) {
31962
+ if (!workspace) return registryClient;
31963
+ const url3 = (await this.readConfig(workspace.root))?.registryUrl;
31964
+ if (!url3) return registryClient;
31965
+ let client = this.registryClients.get(url3);
31966
+ if (!client) {
31967
+ client = new RegistryClient(url3);
31968
+ this.registryClients.set(url3, client);
31969
+ }
31970
+ return client;
31971
+ }
31972
+ /**
31973
+ * Build (once, lazily) and cache the intel graph for a workspace root.
31974
+ * `assembleGraph` (#2072/#2090) consumes the whole-catalog `RegistryItem[]` and
31975
+ * carries each node's complete intelligence and all per-target facets onto the
31976
+ * universal graph. Throws when the catalog can't be loaded (both the bulk
31977
+ * endpoint and the per-item fallback failed) or the graph is structurally
31978
+ * broken (a dangling `composesWith` edge -- #2072's deliberate fail-fast); the
31979
+ * caller converts that into a structured error result. A failed build is never
31980
+ * cached, so the next call retries.
31981
+ */
31982
+ async ensureGraph(workspace) {
31983
+ const key = workspace?.root ?? "";
31984
+ const cached2 = this.graphsByWorkspace.get(key);
31985
+ if (cached2) return cached2;
31986
+ const graph = assembleGraph(await this.itemsSource(workspace));
31987
+ this.graphsByWorkspace.set(key, graph);
31988
+ return graph;
31989
+ }
31990
+ /**
31991
+ * Resolve a workspace's overlay context: the configured `componentTarget`
31992
+ * (echoed as-is, `undefined` in degraded mode) and its installed set. Per the
31993
+ * integration note on #2074, `installed.primitives` folds into the components
31994
+ * set -- a `primitive`-kind item maps to graph kind `component`, so without the
31995
+ * fold every installed primitive would misreport as `available`. Built here
31996
+ * rather than by mutating `buildInstalledSet`'s output, leaving overlay.ts
31997
+ * untouched.
31998
+ *
31999
+ * `componentTarget` comes off unvalidated on-disk config (`readConfig` is a raw
32000
+ * `JSON.parse`), so it is run through `ComponentTargetSchema` here rather than
32001
+ * trusted as a `ComponentTarget` -- a stale or typo'd value falls back to
32002
+ * degraded mode (`undefined` target) instead of silently forcing
32003
+ * `rendersForTarget` false for every node.
32004
+ */
32005
+ async overlayContext(workspace) {
32006
+ const config2 = workspace ? await this.readConfig(workspace.root) : null;
32007
+ const base = buildInstalledSet(config2 ?? {});
32008
+ const components = /* @__PURE__ */ new Set([...base.components, ...config2?.installed?.primitives ?? []]);
32009
+ const parsedTarget = ComponentTargetSchema.safeParse(config2?.componentTarget);
32010
+ return {
32011
+ target: parsedTarget.success ? parsedTarget.data : void 0,
32012
+ installed: { components, composites: base.composites }
32013
+ };
32014
+ }
32015
+ /**
32016
+ * The `rafters_describe` dispatcher -- the one seam that composes #2072/#2074/
32017
+ * #2075. A natural-language question routes through the intent door
32018
+ * (`matchIntent`); a dot-address resolves through the workspace overlay
32019
+ * (`describeWithOverlay`, which delegates to #2072's `describe`).
32020
+ */
32021
+ async handleDescribe(address, workspaceName) {
32022
+ const resolved = this.resolve(workspaceName);
32023
+ if (workspaceName && !resolved) {
32024
+ return this.workspaceRequiredError();
32025
+ }
32026
+ let graph;
32027
+ try {
32028
+ graph = await this.ensureGraph(resolved);
32029
+ } catch (err) {
32030
+ const message = err instanceof Error ? err.message : "Unknown error";
32031
+ return this.errorResult(`failed to build intel graph: ${message}`);
32032
+ }
32033
+ if (isNaturalLanguageQuery(address)) {
32034
+ return this.jsonResult(matchIntent(address, graph));
32035
+ }
32036
+ const ctx = await this.overlayContext(resolved);
32037
+ return this.jsonResult(describeWithOverlay(address, graph, ctx));
32038
+ }
32039
+ /**
32040
+ * The `rafters_generate` stub (Issue E). Registered now so the surface shape
32041
+ * (name, input schema) stabilizes before the generator lands; it never
32042
+ * attempts real composition. Deliberately does NOT build the graph: nothing
32043
+ * here reads it, and a build failure must not turn an honest stub into an
32044
+ * error result.
32045
+ */
32046
+ async handleGenerate(intent, workspaceName) {
32047
+ const resolved = this.resolve(workspaceName);
32048
+ if (workspaceName && !resolved) {
32049
+ return this.workspaceRequiredError();
32050
+ }
32051
+ return this.jsonResult({
32052
+ implemented: false,
32053
+ note: `generate is a spike -- see issue E; not yet implemented (requested: ${intent})`
32054
+ });
32055
+ }
32056
+ /**
32057
+ * Stamp the deprecated marker as a top-level field. Objects gain a sibling
32058
+ * `deprecated` key; the rare array/leaf result is wrapped so the marker still
32059
+ * rides at top level.
32060
+ */
32061
+ withDeprecated(payload) {
32062
+ if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
32063
+ return { ...payload, deprecated: DEPRECATED_MSG };
32064
+ }
32065
+ return { result: payload, deprecated: DEPRECATED_MSG };
32066
+ }
31420
32067
  /**
31421
32068
  * Resolve the set of folders to scan for composite manifests in a workspace.
31422
32069
  * Reads `.rafters/config.rafters.json` and applies the workspace's
@@ -31426,13 +32073,7 @@ var RaftersToolHandler = class {
31426
32073
  */
31427
32074
  async compositeReadRoots(workspaceRoot) {
31428
32075
  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
- }
32076
+ const config2 = await this.readConfig(workspaceRoot);
31436
32077
  if (!config2?.compositesPath) {
31437
32078
  return [join12(paths.root, "composites")];
31438
32079
  }
@@ -31440,16 +32081,16 @@ var RaftersToolHandler = class {
31440
32081
  }
31441
32082
  async handleComposite(args) {
31442
32083
  const { id, query, category, workspace } = args;
32084
+ if (id) {
32085
+ return this.describeById(id, workspace);
32086
+ }
31443
32087
  const resolved = this.resolve(workspace);
31444
32088
  if (workspace && !resolved) {
31445
32089
  return this.workspaceRequiredError();
31446
32090
  }
31447
32091
  await this.ensureCompositesLoaded(resolved);
31448
32092
  let composites2;
31449
- if (id) {
31450
- const c4 = get2(id);
31451
- composites2 = c4 ? [c4] : [];
31452
- } else if (query) {
32093
+ if (query) {
31453
32094
  composites2 = search(query);
31454
32095
  } else if (category) {
31455
32096
  composites2 = getByCategory(category);
@@ -31474,9 +32115,7 @@ var RaftersToolHandler = class {
31474
32115
  // are listed -- the array is empty when nothing is constrained.
31475
32116
  blockRules: c4.blocks.filter((b2) => b2.rules && b2.rules.length > 0).map((b2) => ({ id: b2.id, type: b2.type, rules: b2.rules }))
31476
32117
  }));
31477
- return {
31478
- content: [{ type: "text", text: JSON.stringify({ composites: result }, null, 2) }]
31479
- };
32118
+ return this.jsonResult({ composites: result, deprecated: DEPRECATED_MSG });
31480
32119
  }
31481
32120
  async handlePattern(args) {
31482
32121
  const { solves, query, workspace } = args;
@@ -31503,17 +32142,10 @@ var RaftersToolHandler = class {
31503
32142
  id: c4.manifest.id,
31504
32143
  solves: c4.manifest.solves
31505
32144
  }));
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
- };
32145
+ return this.errorResult("No patterns found matching query", {
32146
+ available,
32147
+ deprecated: DEPRECATED_MSG
32148
+ });
31517
32149
  }
31518
32150
  const patterns = composites2.map((c4) => ({
31519
32151
  id: c4.manifest.id,
@@ -31523,51 +32155,38 @@ var RaftersToolHandler = class {
31523
32155
  cognitiveLoad: c4.manifest.cognitiveLoad,
31524
32156
  usagePatterns: c4.manifest.usagePatterns
31525
32157
  }));
31526
- return {
31527
- content: [{ type: "text", text: JSON.stringify({ patterns }, null, 2) }]
31528
- };
32158
+ return this.jsonResult({ patterns, deprecated: DEPRECATED_MSG });
31529
32159
  }
31530
- async handleComponent(componentName) {
32160
+ /**
32161
+ * DEPRECATED alias for `rafters_describe`. A component resolves by id through
32162
+ * the overlay exactly as `describe(<id>)` does (#2072's resolver has no
32163
+ * separate component/composite address form -- `describe(<id>)` resolves either
32164
+ * kind), so this is a direct, lossless forward with the deprecated marker added.
32165
+ */
32166
+ async handleComponent(componentName, workspaceName) {
32167
+ return this.describeById(componentName, workspaceName);
32168
+ }
32169
+ /**
32170
+ * Shared by the deprecated `rafters_component` and `rafters_composite({id})`
32171
+ * paths: resolve one id through the overlay, stamp the deprecated marker.
32172
+ */
32173
+ async describeById(id, workspaceName) {
32174
+ const resolved = this.resolve(workspaceName);
32175
+ if (workspaceName && !resolved) {
32176
+ return this.workspaceRequiredError();
32177
+ }
32178
+ let graph;
31531
32179
  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
- };
32180
+ graph = await this.ensureGraph(resolved);
31560
32181
  } catch (err) {
31561
32182
  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
- };
32183
+ return this.errorResult(`failed to build intel graph: ${message}`, {
32184
+ deprecated: DEPRECATED_MSG
32185
+ });
31570
32186
  }
32187
+ const ctx = await this.overlayContext(resolved);
32188
+ const out = describeWithOverlay(id, graph, ctx);
32189
+ return this.jsonResult(this.withDeprecated(out));
31571
32190
  }
31572
32191
  };
31573
32192
 
@@ -31644,8 +32263,8 @@ import { existsSync as existsSync8 } from "fs";
31644
32263
  import { resolve as resolve6 } from "path";
31645
32264
 
31646
32265
  // ../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";
32266
+ import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
32267
+ import { isAbsolute as isAbsolute4, join as join14 } from "path";
31649
32268
  var REGISTRY_PLUGINS3 = [scalePlugin, contrastPlugin, statePlugin, invertPlugin];
31650
32269
  var STUDIO_REASON_DEFAULT = "studio interactive edit";
31651
32270
  var TokenResponseSchema = external_exports.object({
@@ -31706,7 +32325,7 @@ async function readRaftersConfig() {
31706
32325
  }
31707
32326
  function validateFontsPath(path) {
31708
32327
  if (path === null || path === void 0) return null;
31709
- if (isAbsolute3(path)) return "fonts path must be relative";
32328
+ if (isAbsolute4(path)) return "fonts path must be relative";
31710
32329
  return null;
31711
32330
  }
31712
32331
  function validateIntent(intent) {
@@ -31725,6 +32344,17 @@ var SetTokenMessageSchema = external_exports.object({
31725
32344
  persist: external_exports.boolean().optional(),
31726
32345
  reason: external_exports.string().optional()
31727
32346
  });
32347
+ var ConfigPatchSchema = external_exports.object({
32348
+ intent: external_exports.string().min(1).optional(),
32349
+ darkMode: external_exports.enum(["class", "media"]).optional(),
32350
+ fonts: FontsConfigSchema.optional()
32351
+ }).refine((patch) => Object.keys(patch).length > 0, {
32352
+ message: "patch must set at least one of: intent, darkMode, fonts"
32353
+ });
32354
+ function ridOf(rawData) {
32355
+ const rid = rawData?.__rid;
32356
+ return typeof rid === "string" ? rid : void 0;
32357
+ }
31728
32358
  var TokenPatchSchema = TokenSchema.pick({
31729
32359
  value: true,
31730
32360
  trustLevel: true,
@@ -32235,83 +32865,60 @@ function studioApiPlugin() {
32235
32865
  client.send("rafters:token-updated", { ok: false, error: String(error47) });
32236
32866
  }
32237
32867
  });
32238
- server.ws.on("rafters:get-config", async (_rawData, client) => {
32868
+ server.ws.on("rafters:get-config", async (rawData, client) => {
32869
+ const __rid = ridOf(rawData);
32870
+ const reply = (msg) => client.send("rafters:config", { ...msg, __rid });
32239
32871
  const config2 = await readRaftersConfig();
32240
32872
  if (!config2) {
32241
- client.send("rafters:config", {
32242
- ok: false,
32243
- error: `config not found at ${configPath}`
32244
- });
32873
+ reply({ ok: false, error: `config not found at ${configPath}` });
32245
32874
  return;
32246
32875
  }
32247
- client.send("rafters:config", { ok: true, config: config2 });
32876
+ reply({ ok: true, config: config2 });
32248
32877
  });
32249
- server.ws.on("rafters:set-intent", async (rawData, client) => {
32250
- const parsed = external_exports.object({ intent: external_exports.string().min(1) }).safeParse(rawData);
32878
+ server.ws.on("rafters:set-config", async (rawData, client) => {
32879
+ const __rid = ridOf(rawData);
32880
+ const reply = (msg) => client.send("rafters:config", { ...msg, __rid });
32881
+ const parsed = ConfigPatchSchema.safeParse(rawData);
32251
32882
  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
- });
32883
+ reply({ ok: false, error: `Invalid patch: ${parsed.error.message}` });
32270
32884
  return;
32271
32885
  }
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;
32886
+ const patch = parsed.data;
32887
+ if (patch.intent !== void 0) {
32888
+ const intentError = validateIntent(patch.intent);
32889
+ if (intentError) {
32890
+ reply({ ok: false, error: intentError });
32891
+ return;
32892
+ }
32289
32893
  }
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;
32894
+ if (patch.fonts?.path !== void 0) {
32895
+ const pathError = validateFontsPath(patch.fonts.path);
32896
+ if (pathError) {
32897
+ reply({ ok: false, error: pathError });
32898
+ return;
32899
+ }
32295
32900
  }
32296
32901
  const config2 = await readRaftersConfig();
32297
32902
  if (!config2) {
32298
- client.send("rafters:fonts-updated", {
32299
- ok: false,
32300
- error: `config not found at ${configPath}`
32301
- });
32903
+ reply({ ok: false, error: `config not found at ${configPath}` });
32302
32904
  return;
32303
32905
  }
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 };
32906
+ const updated = { ...config2 };
32907
+ if (patch.intent !== void 0) updated.intent = patch.intent;
32908
+ if (patch.darkMode !== void 0) updated.darkMode = patch.darkMode;
32909
+ if (patch.fonts !== void 0) {
32910
+ const merged = { ...config2.fonts };
32911
+ if (patch.fonts.path !== void 0) merged.path = patch.fonts.path;
32912
+ if (patch.fonts.imports !== void 0) merged.imports = patch.fonts.imports;
32913
+ updated.fonts = merged;
32914
+ }
32309
32915
  try {
32310
- await writeFile4(configPath, JSON.stringify(updated, null, 2));
32311
- client.send("rafters:fonts-updated", { ok: true, fonts });
32916
+ await writeFile5(configPath, `${JSON.stringify(updated, null, 2)}
32917
+ `);
32918
+ reply({ ok: true, config: updated });
32312
32919
  } catch (error47) {
32313
- console.log(`[rafters] Fonts update failed: ${error47}`);
32314
- client.send("rafters:fonts-updated", { ok: false, error: String(error47) });
32920
+ console.log(`[rafters] Config update failed: ${error47}`);
32921
+ reply({ ok: false, error: String(error47) });
32315
32922
  }
32316
32923
  });
32317
32924
  server.middlewares.use(async (req, res, next) => {