@linktr.ee/arbor-mcp 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +488 -63
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
10
14
  };
11
15
  var __export = (target, all) => {
12
16
  for (var name in all)
@@ -33,7 +37,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
37
  var require_rules = __commonJS({
34
38
  "../../docs/content/ux-writing/rules.json"(exports, module) {
35
39
  module.exports = {
36
- $schema: "./types.schema.json",
37
40
  version: "1.0.0",
38
41
  description: "Static lint rules for UX writing. Consumable by Lambda, Figma plugin, Claude skill, and CI scripts.",
39
42
  rules: {
@@ -160,6 +163,86 @@ var require_rules = __commonJS({
160
163
  pattern: "are you sure",
161
164
  reason: "Patronizing. State consequences clearly instead.",
162
165
  suggestion: "Replace with clear consequence statement: 'This will delete all data'"
166
+ },
167
+ {
168
+ pattern: "\\btier\\b",
169
+ matchType: "regex",
170
+ label: "tier",
171
+ reason: 'Linktree plans are called "plans", not "tiers". "Tier" is internal language.',
172
+ suggestion: 'Use "plan" instead of "tier".'
173
+ },
174
+ {
175
+ pattern: "\\bPRO\\b",
176
+ matchType: "regex",
177
+ caseSensitive: true,
178
+ label: "PRO",
179
+ reason: 'Plan names use title case: "Pro", not "PRO".',
180
+ suggestion: 'Write the plan name as "Pro".'
181
+ },
182
+ {
183
+ pattern: "\\b(?:your|my|their|our|the|a|an|this|that)\\s+profiles?\\b(?!\\s+(?:photo|image|picture|avatar|url|link))",
184
+ matchType: "regex",
185
+ label: "profile",
186
+ reason: `Don't call a Linktree a generic "profile".`,
187
+ suggestion: 'Refer to it as your "Linktree" (the proper noun "Linktree Profile" is fine).'
188
+ },
189
+ {
190
+ pattern: "\\bpackage(s)?\\b",
191
+ matchType: "regex",
192
+ label: "package",
193
+ reason: 'Linktree plans are called "plans", not "packages".',
194
+ suggestion: 'Use "plan" instead of "package".'
195
+ },
196
+ {
197
+ pattern: "\\bbucket(s)?\\b",
198
+ matchType: "regex",
199
+ label: "bucket",
200
+ reason: 'Linktree plans are called "plans", not "buckets".',
201
+ suggestion: 'Use "plan" instead of "bucket".'
202
+ },
203
+ {
204
+ pattern: "\\busers?\\b(?!\\s*-?\\s*(?:friendly|name|names|generated|facing|flow|flows|journey|journeys|persona|personas|research|testing|experience|interface|story|stories))",
205
+ matchType: "regex",
206
+ label: "users",
207
+ reason: 'People who use Linktree are "Linkers", not "users".',
208
+ suggestion: 'Use "Linkers" \u2014 or just say "you".'
209
+ },
210
+ {
211
+ pattern: "\\bLink Tree\\b",
212
+ matchType: "regex",
213
+ label: "Link Tree",
214
+ reason: 'Linktree is one word: "Linktree", not "Link Tree".',
215
+ suggestion: 'Write it as "Linktree".'
216
+ },
217
+ {
218
+ pattern: "\\bLinkTree\\b",
219
+ matchType: "regex",
220
+ caseSensitive: true,
221
+ label: "LinkTree",
222
+ reason: 'Linktree capitalizes only the L: "Linktree", not "LinkTree".',
223
+ suggestion: 'Write it as "Linktree".'
224
+ },
225
+ {
226
+ pattern: "\\b(linkinbio|link-in-bio|bio link)\\b",
227
+ matchType: "regex",
228
+ label: "link-in-bio",
229
+ reason: `It's "link in bio" \u2014 three words, no hyphen.`,
230
+ suggestion: 'Use "link in bio" (not "linkinbio", "link-in-bio" or "bio link").'
231
+ },
232
+ {
233
+ pattern: "subscription",
234
+ reason: 'Linktree billing is a "plan", not a "subscription".',
235
+ suggestion: 'Use "plan". (Keep "subscribe"/"subscriber" for the visitor notification feature.)'
236
+ },
237
+ {
238
+ pattern: "utilize",
239
+ reason: "Formal and stiff. Plain language is friendlier.",
240
+ suggestion: 'Use "use" instead of "utilize".'
241
+ },
242
+ {
243
+ pattern: "facilitate",
244
+ reason: "Formal and stiff. Plain language is friendlier.",
245
+ suggestion: 'Use "help" instead of "facilitate".'
163
246
  }
164
247
  ]
165
248
  },
@@ -462,7 +545,7 @@ var require_patterns = __commonJS({
462
545
  examples: {
463
546
  do: [
464
547
  "Update required. This version is no longer supported. Update now to continue.",
465
- "Subscription expired. Your account is paused. Renew subscription to restore access.",
548
+ "Plan expired. Your account is paused. Renew your plan to restore access.",
466
549
  "Verification needed. Confirm your email to access features. Check your inbox."
467
550
  ],
468
551
  dont: [
@@ -511,7 +594,7 @@ var require_patterns = __commonJS({
511
594
  do: [
512
595
  "Changes saved",
513
596
  "Email sent",
514
- "Profile updated",
597
+ "Linktree updated",
515
598
  "You're all set",
516
599
  "You're officially a Pro!"
517
600
  ],
@@ -915,6 +998,7 @@ var require_engine = __commonJS({
915
998
  var contentTypesData = require_content_types();
916
999
  var rules = rulesData.rules;
917
1000
  var scoring = rulesData.scoring;
1001
+ var MIN_CONTENT_TYPE_CONFIDENCE = 30;
918
1002
  var passiveVoiceRegex = new RegExp(
919
1003
  rules["passive-voice"].check.pattern,
920
1004
  "gi"
@@ -931,6 +1015,15 @@ var require_engine = __commonJS({
931
1015
  }
932
1016
  compiledTextPatterns.set(typeId, regexes);
933
1017
  }
1018
+ var compiledBannedPhrases = rules["banned-phrases"].phrases.map((phrase) => {
1019
+ if (phrase.matchType !== "regex") return phrase;
1020
+ try {
1021
+ const flags = phrase.caseSensitive ? "" : "i";
1022
+ return { ...phrase, _regex: new RegExp(phrase.pattern, flags) };
1023
+ } catch {
1024
+ return { ...phrase, _regex: null };
1025
+ }
1026
+ });
934
1027
  function countSyllables(word) {
935
1028
  const w = word.toLowerCase().replace(/[^a-z]/g, "");
936
1029
  if (w.length <= 3) return 1;
@@ -946,34 +1039,42 @@ var require_engine = __commonJS({
946
1039
  }
947
1040
  function checkSentenceCase(text, issues) {
948
1041
  const words = text.split(/\s+/);
949
- for (let i = 1; i < words.length; i++) {
950
- const word = words[i].replace(/[^a-zA-Z]/g, "");
951
- if (word === "I" || /^[^a-zA-Z]*I['\u2019\u2018]/.test(words[i])) continue;
952
- if (word.length > 0 && word[0] === word[0].toUpperCase() && word[0] !== word[0].toLowerCase() && !properNounAllowlist.has(word)) {
953
- issues.push({
954
- ruleId: "sentence-case",
955
- ruleName: "Sentence case",
956
- severity: "warning",
957
- message: `"${word}" appears capitalized. Use sentence case for UI text.`,
958
- suggestion: `Change to "${word.toLowerCase()}"`
959
- });
960
- break;
1042
+ let sentenceStart = true;
1043
+ for (let i = 0; i < words.length; i++) {
1044
+ const raw = words[i];
1045
+ if (!raw) continue;
1046
+ const endsSentence = /[.!?][)\]"'\u2019\u201d]*$/.test(raw);
1047
+ if (!sentenceStart) {
1048
+ const word = raw.replace(/^[^a-zA-Z]+/, "").replace(/[^a-zA-Z]+$/, "");
1049
+ const isPronounI = word.replace(/[^a-zA-Z]/g, "") === "I" || /^[^a-zA-Z]*I['\u2019\u2018]/.test(raw);
1050
+ if (!isPronounI && word.length > 0 && word[0] === word[0].toUpperCase() && word[0] !== word[0].toLowerCase() && !properNounAllowlist.has(word)) {
1051
+ const sentenceCased = word === word.toUpperCase() ? word.toLowerCase() : word[0].toLowerCase() + word.slice(1);
1052
+ issues.push({
1053
+ ruleId: "sentence-case",
1054
+ ruleName: "Sentence case",
1055
+ severity: "warning",
1056
+ message: `"${word}" appears capitalized. Use sentence case for UI text.`,
1057
+ suggestion: `Change to "${sentenceCased}"`
1058
+ });
1059
+ break;
1060
+ }
961
1061
  }
1062
+ sentenceStart = endsSentence;
962
1063
  }
963
1064
  }
964
1065
  function checkBannedPhrases(text, contentType, issues) {
965
1066
  const lower = text.toLowerCase();
966
- for (const phrase of rules["banned-phrases"].phrases) {
967
- if (lower.includes(phrase.pattern)) {
968
- const severity = contentType === "button" ? rules["banned-phrases"].severityByContext.button : rules["banned-phrases"].severityByContext.other;
969
- issues.push({
970
- ruleId: "banned-phrases",
971
- ruleName: "Banned phrase",
972
- severity,
973
- message: `"${phrase.pattern}" \u2014 ${phrase.reason}`,
974
- suggestion: phrase.suggestion
975
- });
976
- }
1067
+ for (const phrase of compiledBannedPhrases) {
1068
+ const matched = phrase.matchType === "regex" ? phrase._regex !== null && phrase._regex.test(text) : lower.includes(phrase.pattern);
1069
+ if (!matched) continue;
1070
+ const severity = contentType === "button" ? rules["banned-phrases"].severityByContext.button : rules["banned-phrases"].severityByContext.other;
1071
+ issues.push({
1072
+ ruleId: "banned-phrases",
1073
+ ruleName: "Banned phrase",
1074
+ severity,
1075
+ message: `"${phrase.label || phrase.pattern}" \u2014 ${phrase.reason}`,
1076
+ suggestion: phrase.suggestion
1077
+ });
977
1078
  }
978
1079
  }
979
1080
  function checkGenericButtons(text, issues) {
@@ -1113,6 +1214,9 @@ var require_engine = __commonJS({
1113
1214
  bestMatch = { type: typeId, confidence: score };
1114
1215
  }
1115
1216
  }
1217
+ if (!bestMatch || bestMatch.confidence < MIN_CONTENT_TYPE_CONFIDENCE) {
1218
+ return null;
1219
+ }
1116
1220
  return bestMatch;
1117
1221
  }
1118
1222
  function getPattern(contentType) {
@@ -5432,7 +5536,7 @@ ZodNaN.create = (params) => {
5432
5536
  ...processCreateParams(params)
5433
5537
  });
5434
5538
  };
5435
- var BRAND = Symbol("zod_brand");
5539
+ var BRAND = /* @__PURE__ */ Symbol("zod_brand");
5436
5540
  var ZodBranded = class extends ZodType {
5437
5541
  _parse(input) {
5438
5542
  const { ctx } = this._processInputParams(input);
@@ -5930,6 +6034,7 @@ async function querySupernova(resource, opts = {}) {
5930
6034
  const {
5931
6035
  designSystemId,
5932
6036
  versionId,
6037
+ versionSelector,
5933
6038
  proxyUrl = process.env.ARBOR_MCP_SUPERNOVA_PROXY_URL ?? "",
5934
6039
  proxyToken = process.env.ARBOR_MCP_SUPERNOVA_PROXY_TOKEN ?? "",
5935
6040
  fetchImpl = globalThis.fetch,
@@ -5948,12 +6053,13 @@ async function querySupernova(resource, opts = {}) {
5948
6053
  if (parsed.protocol !== "https:") {
5949
6054
  return { ok: false, reason: "not_configured", detail: `proxy URL must be https (got ${parsed.protocol})` };
5950
6055
  }
5951
- const cacheKey = `${resource}|${designSystemId ?? ""}|${versionId ?? ""}`;
6056
+ const cacheKey = `${resource}|${designSystemId ?? ""}|${versionId ?? ""}|${versionSelector ?? ""}`;
5952
6057
  const cached = cache.get(cacheKey);
5953
6058
  if (cached && now() - cached.fetchedAt < CACHE_TTL_MS2) return cached.value;
5954
6059
  const body = { resource };
5955
6060
  if (designSystemId) body.designSystemId = designSystemId;
5956
6061
  if (versionId) body.versionId = versionId;
6062
+ if (versionSelector && !versionId) body.versionSelector = versionSelector;
5957
6063
  const controller = new AbortController();
5958
6064
  const timer = setTimeout(() => controller.abort(), timeoutMs);
5959
6065
  let res;
@@ -5998,15 +6104,124 @@ async function querySupernova(resource, opts = {}) {
5998
6104
  return value;
5999
6105
  }
6000
6106
 
6107
+ // src/lib/resolve-version.ts
6108
+ function extractVersionList(data) {
6109
+ const wrapped = data?.result?.designSystemVersions;
6110
+ const list = Array.isArray(wrapped) ? wrapped : Array.isArray(data) ? data : null;
6111
+ if (list === null) return null;
6112
+ return list.filter((x) => !!x && typeof x === "object");
6113
+ }
6114
+ async function resolveVersionId(version, deps = {}) {
6115
+ const query = deps.query ?? ((r) => querySupernova(r));
6116
+ const result = await query("versions");
6117
+ if (!result.ok) {
6118
+ return { ok: false, reason: "unavailable", detail: `could not load versions (${result.reason})` };
6119
+ }
6120
+ const list = extractVersionList(result.data);
6121
+ if (list === null) {
6122
+ return { ok: false, reason: "unavailable", detail: "proxy returned an unrecognized versions payload" };
6123
+ }
6124
+ const match = list.find((v) => v.isReadonly === true && v.version === version);
6125
+ if (!match) {
6126
+ return { ok: false, reason: "not_found", detail: `no frozen Supernova version "${version}" \u2014 it may not be cut yet (see arbor_list_versions)` };
6127
+ }
6128
+ const versionId = String(match.id ?? "");
6129
+ if (!versionId) {
6130
+ return { ok: false, reason: "unavailable", detail: `frozen version "${version}" has no id` };
6131
+ }
6132
+ const meta = match.meta;
6133
+ const versionName = meta && typeof meta === "object" && typeof meta.name === "string" && meta.name.length > 0 ? meta.name : version;
6134
+ return { ok: true, versionId, versionName };
6135
+ }
6136
+
6137
+ // src/lib/intentions.ts
6138
+ var INTENTIONS_ARTIFACT = "component-intentions.json";
6139
+ var INTENTION_STATUSES = [
6140
+ "shipped",
6141
+ "in-progress",
6142
+ "design-only",
6143
+ "deprecated",
6144
+ "researched-not-shipped"
6145
+ ];
6146
+ var STATUS_SET = new Set(INTENTION_STATUSES);
6147
+ function normalizeComponentKey(name) {
6148
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
6149
+ }
6150
+ function asStringArray(v) {
6151
+ return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
6152
+ }
6153
+ function asDisambiguations(v) {
6154
+ if (!Array.isArray(v)) return [];
6155
+ const out = [];
6156
+ for (const d of v) {
6157
+ if (d && typeof d === "object") {
6158
+ const component = d.component;
6159
+ const distinction = d.distinction;
6160
+ if (typeof component === "string" && typeof distinction === "string") {
6161
+ out.push({ component, distinction });
6162
+ }
6163
+ }
6164
+ }
6165
+ return out;
6166
+ }
6167
+ function coerceEntry(raw) {
6168
+ if (!raw || typeof raw !== "object") return null;
6169
+ const r = raw;
6170
+ const name = typeof r.name === "string" ? r.name.trim() : "";
6171
+ const status = typeof r.status === "string" ? r.status : "";
6172
+ if (!name || !STATUS_SET.has(status)) return null;
6173
+ return {
6174
+ name,
6175
+ status,
6176
+ shippedVersion: typeof r.shippedVersion === "string" ? r.shippedVersion : null,
6177
+ useWhen: asStringArray(r.useWhen),
6178
+ notWhen: asStringArray(r.notWhen),
6179
+ disambiguateFrom: asDisambiguations(r.disambiguateFrom),
6180
+ guideline: typeof r.guideline === "string" ? r.guideline : null,
6181
+ decisions: asStringArray(r.decisions)
6182
+ };
6183
+ }
6184
+ async function loadIntentions(load = loadPublishedJson) {
6185
+ const res = await load(INTENTIONS_ARTIFACT);
6186
+ if (!res.ok) return { ok: false, reason: res.reason };
6187
+ const data = res.data;
6188
+ if (data.degraded === true || !Array.isArray(data.components)) {
6189
+ return { ok: false, reason: "degraded" };
6190
+ }
6191
+ const byKey = /* @__PURE__ */ new Map();
6192
+ for (const raw of data.components) {
6193
+ const entry = coerceEntry(raw);
6194
+ if (entry) byKey.set(normalizeComponentKey(entry.name), entry);
6195
+ }
6196
+ return { ok: true, byKey, generatedAt: typeof data.generatedAt === "string" ? data.generatedAt : null };
6197
+ }
6198
+
6001
6199
  // src/tools/list-components.ts
6002
6200
  var TOOL_NAME3 = "arbor_list_components";
6003
6201
  var TOOL_TITLE3 = "List Arbor components";
6004
6202
  var TOOL_DESCRIPTION3 = "List the components in the Arbor design system (name, id, short description) from Supernova via the Arbor federation proxy. Read-only metadata for discovery; returns top-level components, not Figma variants. Does NOT return token values, source code, or props \u2014 use arbor_get_component for one component, the shadcn registry/Storybook for source, arbor_open_in_playroom for a runnable snippet. Does NOT change anything.";
6005
- var inputSchema3 = external_exports.object({ query: external_exports.string().trim().min(1).max(100).optional().describe("Optional case-insensitive name/description filter") }).strict();
6203
+ var inputSchema3 = external_exports.object({
6204
+ query: external_exports.string().trim().min(1).max(100).optional().describe("Optional case-insensitive name/description filter"),
6205
+ versionSelector: external_exports.enum(["draft", "latest_released"]).optional().describe(
6206
+ "Which design system version to read: 'draft' (default \u2014 the editable working version) or 'latest_released' (the newest frozen release; available only once a release has been cut, otherwise unavailable)"
6207
+ ),
6208
+ version: external_exports.string().trim().min(1).max(50).optional().describe(
6209
+ 'List components as of a specific released version, e.g. "15.1.0" \u2014 resolves to that frozen Supernova version ("what shipped in 15.1.0"). Takes precedence over versionSelector. See arbor_list_versions for the versions that have been cut.'
6210
+ )
6211
+ }).strict();
6006
6212
  var outputSchema3 = external_exports.object({
6007
6213
  source: external_exports.enum(["remote", "unavailable"]),
6008
6214
  total: external_exports.number(),
6009
- components: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), description: external_exports.string().nullable() })),
6215
+ components: external_exports.array(
6216
+ external_exports.object({
6217
+ id: external_exports.string(),
6218
+ name: external_exports.string(),
6219
+ description: external_exports.string().nullable(),
6220
+ // Lifecycle status merged from the component-intentions manifest (DNG-726).
6221
+ // null when the manifest has no entry for this component or could not be loaded.
6222
+ status: external_exports.enum(INTENTION_STATUSES).nullable()
6223
+ })
6224
+ ),
6010
6225
  detail: external_exports.string().nullable()
6011
6226
  });
6012
6227
  var annotations3 = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, destructiveHint: false };
@@ -6025,8 +6240,23 @@ function extractComponents(data) {
6025
6240
  });
6026
6241
  }
6027
6242
  async function handleListComponents(args, deps = {}) {
6028
- const query = deps.query ?? ((r) => querySupernova(r));
6029
- const result = await query("components");
6243
+ const query = deps.query ?? ((r, opts) => querySupernova(r, opts));
6244
+ let queryOpts;
6245
+ if (args.version) {
6246
+ const resolveVersion = deps.resolveVersion ?? ((v) => resolveVersionId(v));
6247
+ const resolved = await resolveVersion(args.version);
6248
+ if (!resolved.ok) {
6249
+ const out2 = { source: "unavailable", total: 0, components: [], detail: resolved.detail };
6250
+ return {
6251
+ content: [{ type: "text", text: `Could not read version ${args.version}: ${resolved.detail}. Treat as unavailable \u2014 do not assume there are none.` }],
6252
+ structuredContent: out2
6253
+ };
6254
+ }
6255
+ queryOpts = { versionId: resolved.versionId };
6256
+ } else {
6257
+ queryOpts = { versionSelector: args.versionSelector };
6258
+ }
6259
+ const result = await query("components", queryOpts);
6030
6260
  let out;
6031
6261
  const extracted = result.ok ? extractComponents(result.data) : null;
6032
6262
  if (!result.ok) {
@@ -6041,7 +6271,13 @@ async function handleListComponents(args, deps = {}) {
6041
6271
  (c) => c.name.toLowerCase().includes(q) || (c.description ? c.description.toLowerCase().includes(q) : false)
6042
6272
  );
6043
6273
  }
6044
- out = { source: "remote", total: components.length, components, detail: null };
6274
+ const intentionsRes = await (deps.loadIntentions ?? (() => loadIntentions()))();
6275
+ const byKey = intentionsRes.ok ? intentionsRes.byKey : null;
6276
+ const withStatus = components.map((c) => ({
6277
+ ...c,
6278
+ status: byKey ? byKey.get(normalizeComponentKey(c.name))?.status ?? null : null
6279
+ }));
6280
+ out = { source: "remote", total: withStatus.length, components: withStatus, detail: null };
6045
6281
  }
6046
6282
  const summary = out.source === "unavailable" ? `Could not load components (${out.detail}). Treat as unavailable \u2014 do not assume there are none.` : `${out.total} component${out.total === 1 ? "" : "s"}${args.query ? ` matching "${args.query}"` : ""}.`;
6047
6283
  return { content: [{ type: "text", text: summary }], structuredContent: out };
@@ -6055,43 +6291,125 @@ var listComponentsTool = {
6055
6291
  // src/tools/get-component.ts
6056
6292
  var TOOL_NAME4 = "arbor_get_component";
6057
6293
  var TOOL_TITLE4 = "Get one Arbor component";
6058
- var TOOL_DESCRIPTION4 = "Get a single Arbor component's metadata by name (case-insensitive) or id from Supernova via the Arbor federation proxy (name, id, description). Read-only; matches top-level components, not Figma variants. Does NOT return token values or rendered source \u2014 use the shadcn registry / Storybook for implementation and arbor_open_in_playroom for a runnable snippet. Does NOT change anything.";
6294
+ var TOOL_DESCRIPTION4 = "Get a single Arbor component's metadata by name (case-insensitive) or id (name, id, description) PLUS its intention overlay \u2014 lifecycle status (shipped | in-progress | design-only | deprecated | researched-not-shipped), when to use it, when NOT to (and what to use instead), and what it is commonly confused with. Intention comes from Arbor's source of truth, so a design-only component absent from the live design data still resolves. Read-only; matches top-level components, not Figma variants. Does NOT return token values or rendered source \u2014 use the shadcn registry / Storybook for implementation and arbor_open_in_playroom for a runnable snippet. Does NOT change anything.";
6059
6295
  var inputSchema4 = external_exports.object({
6060
6296
  name: external_exports.string().trim().min(1).max(100).optional().describe('Component name (case-insensitive), e.g. "Button"'),
6061
- id: external_exports.string().trim().min(1).max(100).optional().describe("Supernova component id")
6297
+ id: external_exports.string().trim().min(1).max(100).optional().describe("Supernova component id"),
6298
+ versionSelector: external_exports.enum(["draft", "latest_released"]).optional().describe(
6299
+ "Which design system version to read: 'draft' (default \u2014 the editable working version) or 'latest_released' (the newest frozen release; available only once a release has been cut, otherwise unavailable)"
6300
+ ),
6301
+ version: external_exports.string().trim().min(1).max(50).optional().describe(
6302
+ 'Read a specific released version, e.g. "15.1.0" \u2014 resolves to that frozen Supernova version and reads the component there ("what shipped in 15.1.0"). Takes precedence over versionSelector. See arbor_list_versions for the versions that have been cut.'
6303
+ )
6062
6304
  }).strict();
6305
+ var disambiguationSchema = external_exports.object({
6306
+ component: external_exports.string().describe("The thing to disambiguate from \u2014 an Arbor component or an external concept (chip, snackbar, \u2026)"),
6307
+ distinction: external_exports.string().describe("How they differ and which to pick when")
6308
+ });
6309
+ var intentionSchema = external_exports.object({
6310
+ status: external_exports.enum(["shipped", "in-progress", "design-only", "deprecated", "researched-not-shipped"]).describe("Lifecycle/availability in the @linktr.ee/arbor package"),
6311
+ shippedVersion: external_exports.string().nullable().describe("The arbor version where this became available, when known"),
6312
+ useWhen: external_exports.array(external_exports.string()).describe("Situations this is the right choice for"),
6313
+ notWhen: external_exports.array(external_exports.string()).describe("When NOT to reach for it, ideally naming the better alternative"),
6314
+ disambiguateFrom: external_exports.array(disambiguationSchema).describe("Things this is commonly confused with"),
6315
+ guideline: external_exports.string().nullable().describe("Repo-relative path to the long-form guideline, when one exists"),
6316
+ decisions: external_exports.array(external_exports.string()).describe("Decision-log ids (DL-NN) that explain this intention")
6317
+ });
6063
6318
  var outputSchema4 = external_exports.object({
6064
- source: external_exports.enum(["remote", "unavailable"]),
6065
- found: external_exports.boolean(),
6319
+ source: external_exports.enum(["remote", "unavailable"]).describe("Live design-data (Supernova) availability"),
6320
+ found: external_exports.boolean().describe("True when a component OR an intention entry matched"),
6066
6321
  component: external_exports.object({ id: external_exports.string(), name: external_exports.string(), description: external_exports.string().nullable() }).nullable(),
6322
+ intention: intentionSchema.nullable().describe("Intention overlay from the source-of-truth manifest; null when no entry matched or it could not be loaded"),
6323
+ intentionSource: external_exports.enum(["remote", "absent", "unavailable"]).describe("'remote' = matched, 'absent' = manifest loaded but no entry, 'unavailable' = manifest could not be loaded"),
6067
6324
  detail: external_exports.string().nullable()
6068
6325
  });
6069
6326
  var annotations4 = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, destructiveHint: false };
6327
+ async function resolveIntention(lookupName, load) {
6328
+ const res = await load();
6329
+ if (!res.ok) return { intention: null, intentionSource: "unavailable" };
6330
+ if (!lookupName) return { intention: null, intentionSource: "absent" };
6331
+ const match = res.byKey.get(normalizeComponentKey(lookupName));
6332
+ if (!match) return { intention: null, intentionSource: "absent" };
6333
+ const intention = {
6334
+ status: match.status,
6335
+ shippedVersion: match.shippedVersion,
6336
+ useWhen: match.useWhen,
6337
+ notWhen: match.notWhen,
6338
+ disambiguateFrom: match.disambiguateFrom.map((d) => ({ component: d.component, distinction: d.distinction })),
6339
+ guideline: match.guideline,
6340
+ decisions: match.decisions
6341
+ };
6342
+ return { intention, intentionSource: "remote" };
6343
+ }
6070
6344
  async function handleGetComponent(args, deps = {}) {
6071
6345
  const name = args.name?.trim();
6072
6346
  const id = args.id?.trim();
6347
+ const loadIntentionsFn = deps.loadIntentions ?? (() => loadIntentions());
6073
6348
  if (!name && !id) {
6074
- const out2 = { source: "remote", found: false, component: null, detail: "provide name or id" };
6349
+ const out2 = {
6350
+ source: "remote",
6351
+ found: false,
6352
+ component: null,
6353
+ intention: null,
6354
+ intentionSource: "absent",
6355
+ detail: "provide name or id"
6356
+ };
6075
6357
  return {
6076
6358
  content: [{ type: "text", text: 'Provide a component name or id \u2014 e.g. {"name":"Button"} or {"id":"35988997"}.' }],
6077
6359
  structuredContent: out2
6078
6360
  };
6079
6361
  }
6080
- const query = deps.query ?? ((r) => querySupernova(r));
6081
- const result = await query("components");
6082
- let out;
6083
- if (!result.ok) {
6084
- out = { source: "unavailable", found: false, component: null, detail: `could not load components (${result.reason})` };
6362
+ const query = deps.query ?? ((r, opts) => querySupernova(r, opts));
6363
+ let supernovaSource = "remote";
6364
+ let component = null;
6365
+ let supernovaDetail = null;
6366
+ let queryOpts = null;
6367
+ if (args.version) {
6368
+ const resolveVersion = deps.resolveVersion ?? ((v) => resolveVersionId(v));
6369
+ const resolved = await resolveVersion(args.version);
6370
+ if (resolved.ok) {
6371
+ queryOpts = { versionId: resolved.versionId };
6372
+ } else {
6373
+ supernovaSource = "unavailable";
6374
+ supernovaDetail = resolved.detail;
6375
+ }
6085
6376
  } else {
6086
- const components = extractComponents(result.data);
6087
- if (components === null) {
6088
- out = { source: "unavailable", found: false, component: null, detail: "proxy returned an unrecognized components payload" };
6377
+ queryOpts = { versionSelector: args.versionSelector };
6378
+ }
6379
+ if (queryOpts) {
6380
+ const result = await query("components", queryOpts);
6381
+ if (!result.ok) {
6382
+ supernovaSource = "unavailable";
6383
+ supernovaDetail = `could not load components (${result.reason})`;
6089
6384
  } else {
6090
- const match = id ? components.find((c) => c.id === id) : components.find((c) => c.name.toLowerCase() === name.toLowerCase());
6091
- out = match ? { source: "remote", found: true, component: match, detail: null } : { source: "remote", found: false, component: null, detail: `no component ${id ? `with id ${id}` : `named "${name}"`}` };
6385
+ const components = extractComponents(result.data);
6386
+ if (components === null) {
6387
+ supernovaSource = "unavailable";
6388
+ supernovaDetail = "proxy returned an unrecognized components payload";
6389
+ } else {
6390
+ const target = name ? normalizeComponentKey(name) : null;
6391
+ const match = id ? components.find((c) => c.id === id) : components.find((c) => normalizeComponentKey(c.name) === target);
6392
+ if (match) component = match;
6393
+ else supernovaDetail = `no component ${id ? `with id ${id}` : `named "${name}"`}`;
6394
+ }
6092
6395
  }
6093
6396
  }
6094
- const summary = out.source === "unavailable" ? `Could not load components (${out.detail}). Treat as unavailable.` : out.found ? `${out.component.name}${out.component.description ? ` \u2014 ${out.component.description}` : " (no description)"}.` : `No Arbor component ${args.id ? `with id ${args.id.trim()}` : `named "${args.name?.trim()}"`} exists.`;
6397
+ const lookupName = component?.name ?? name ?? null;
6398
+ const { intention, intentionSource } = await resolveIntention(lookupName, loadIntentionsFn);
6399
+ const found = component !== null || intention !== null;
6400
+ let detail = supernovaDetail;
6401
+ if (!component && intention) {
6402
+ detail = supernovaSource === "unavailable" ? `live design data unavailable (${supernovaDetail}); intention found \u2014 status "${intention.status}"` : `not in the live design data, but tracked as "${intention.status}" in Arbor's component intentions`;
6403
+ }
6404
+ const out = { source: supernovaSource, found, component, intention, intentionSource, detail };
6405
+ let summary;
6406
+ if (!found) {
6407
+ summary = supernovaSource === "unavailable" ? `Could not load components (${supernovaDetail}). Treat as unavailable.` : `No Arbor component ${id ? `with id ${id}` : `named "${name}"`} exists.`;
6408
+ } else {
6409
+ const head = component ? `${component.name}${component.description ? ` \u2014 ${component.description}` : " (no description)"}.` : `${lookupName} \u2014 ${detail}.`;
6410
+ const intentionLine = intention ? ` Status: ${intention.status}.${intention.useWhen.length ? ` Use when: ${intention.useWhen[0]}` : ""}` : intentionSource === "unavailable" ? " (intention overlay unavailable)" : "";
6411
+ summary = `${head}${intentionLine}`;
6412
+ }
6095
6413
  return { content: [{ type: "text", text: summary }], structuredContent: out };
6096
6414
  }
6097
6415
  var getComponentTool = {
@@ -6103,15 +6421,38 @@ var getComponentTool = {
6103
6421
  // src/tools/list-versions.ts
6104
6422
  var TOOL_NAME5 = "arbor_list_versions";
6105
6423
  var TOOL_TITLE5 = "List Arbor design system versions";
6106
- var TOOL_DESCRIPTION5 = "List the version history of the Arbor design system (id, name, created date, read-only flag) from Supernova via the Arbor federation proxy. Read-only metadata. Does NOT return token values (use the compiled CSS / registry) and does NOT change anything.";
6424
+ var TOOL_DESCRIPTION5 = "List the version history of the Arbor design system from Supernova (id, name, semver, created date, read-only flag, draft / latest-released markers) via the Arbor federation proxy, plus the committed release manifest (npm release \u2194 Supernova version). Read-only metadata. Does NOT return token values (use the compiled CSS / registry) and does NOT change anything.";
6107
6425
  var inputSchema5 = external_exports.object({}).strict();
6426
+ var versionSchema = external_exports.object({
6427
+ id: external_exports.string(),
6428
+ name: external_exports.string(),
6429
+ semver: external_exports.string().nullable().describe('The version string when it parses as semver (X.Y.Z); null for the draft, whose version is a label like "Shared Draft"'),
6430
+ createdAt: external_exports.string().nullable(),
6431
+ readOnly: external_exports.boolean().nullable(),
6432
+ isDraft: external_exports.boolean().nullable().describe("The editable working version (readOnly === false). DNG-594: a draft's version is a label, not null \u2014 so readOnly is the signal."),
6433
+ isLatestReleased: external_exports.boolean().describe("True for the single newest read-only (frozen) version by createdAt")
6434
+ });
6435
+ var manifestReleaseSchema = external_exports.object({
6436
+ releaseId: external_exports.string(),
6437
+ date: external_exports.string().nullable(),
6438
+ frontDoorVersion: external_exports.string().nullable().describe("The @linktr.ee/arbor version for this release (DL-076 front-door number)"),
6439
+ supernovaVersionId: external_exports.string().nullable().describe("The frozen Supernova version id this release was cut as (null until the cutter + stamper run \u2014 later phases)"),
6440
+ supernovaStatus: external_exports.string().nullable()
6441
+ });
6108
6442
  var outputSchema5 = external_exports.object({
6109
- source: external_exports.enum(["remote", "unavailable"]).describe("'remote' = fetched; 'unavailable' = proxy fetch failed"),
6110
- total: external_exports.number().describe("Number of versions returned"),
6111
- versions: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), createdAt: external_exports.string().nullable(), readOnly: external_exports.boolean().nullable() })).describe("Versions, in the order Supernova returned them"),
6443
+ source: external_exports.enum(["remote", "unavailable"]).describe("'remote' = Supernova versions fetched; 'unavailable' = proxy fetch failed"),
6444
+ total: external_exports.number().describe("Number of Supernova versions returned"),
6445
+ versions: external_exports.array(versionSchema).describe("Supernova versions, in the order returned"),
6446
+ manifest: external_exports.object({
6447
+ source: external_exports.enum(["local", "unavailable"]).describe("'local' = release manifest loaded; 'unavailable' = not published yet / degraded"),
6448
+ total: external_exports.number(),
6449
+ releases: external_exports.array(manifestReleaseSchema),
6450
+ detail: external_exports.string().nullable()
6451
+ }).describe("The committed release manifest (releases.json) mapping npm releases \u2194 Supernova versions"),
6112
6452
  detail: external_exports.string().nullable().describe("Degraded reason when source is unavailable")
6113
6453
  });
6114
6454
  var annotations5 = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, destructiveHint: false };
6455
+ var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
6115
6456
  function extractList(data) {
6116
6457
  const wrapped = data?.result?.designSystemVersions;
6117
6458
  const list = Array.isArray(wrapped) ? wrapped : Array.isArray(data) ? data : null;
@@ -6119,26 +6460,86 @@ function extractList(data) {
6119
6460
  return list.filter((x) => !!x && typeof x === "object");
6120
6461
  }
6121
6462
  var str2 = (v) => typeof v === "string" && v.length > 0 ? v : null;
6463
+ var semverOf = (v) => {
6464
+ const s = str2(v);
6465
+ return s && SEMVER_RE.test(s) ? s : null;
6466
+ };
6467
+ var createdAtEpoch = (s) => {
6468
+ const t = new Date(s ?? 0).getTime();
6469
+ return Number.isFinite(t) ? t : 0;
6470
+ };
6471
+ var metaName = (v) => {
6472
+ const meta = v.meta;
6473
+ return meta && typeof meta === "object" ? str2(meta.name) : null;
6474
+ };
6475
+ function latestReleasedId(versions) {
6476
+ const frozen = versions.filter((v) => v.readOnly === true);
6477
+ if (frozen.length === 0) return null;
6478
+ frozen.sort((a, b) => createdAtEpoch(b.createdAt) - createdAtEpoch(a.createdAt));
6479
+ return frozen[0].id || null;
6480
+ }
6481
+ async function buildManifestLane(load) {
6482
+ const res = await load("releases.json");
6483
+ if (!res.ok) {
6484
+ return { source: "unavailable", total: 0, releases: [], detail: `could not load release manifest (${res.reason})` };
6485
+ }
6486
+ const data = res.data;
6487
+ if (data?.degraded === true) {
6488
+ return { source: "unavailable", total: 0, releases: [], detail: "release manifest is a degraded fallback" };
6489
+ }
6490
+ if (!data || !Array.isArray(data.releases)) {
6491
+ return { source: "unavailable", total: 0, releases: [], detail: "unrecognized releases.json payload" };
6492
+ }
6493
+ const releases = data.releases.filter((r) => !!r && typeof r === "object").map((r) => {
6494
+ const sn = r.supernova ?? {};
6495
+ return {
6496
+ releaseId: String(r.releaseId ?? ""),
6497
+ date: str2(r.date),
6498
+ frontDoorVersion: str2(r.frontDoorVersion),
6499
+ supernovaVersionId: str2(sn.versionId),
6500
+ supernovaStatus: str2(sn.status)
6501
+ };
6502
+ });
6503
+ return { source: "local", total: releases.length, releases, detail: null };
6504
+ }
6122
6505
  async function handleListVersions(_args, deps = {}) {
6123
6506
  const query = deps.query ?? ((r) => querySupernova(r));
6507
+ const load = deps.load ?? ((f) => loadPublishedJson(f));
6124
6508
  const result = await query("versions");
6125
- let out;
6126
6509
  const list = result.ok ? extractList(result.data) : null;
6510
+ let source;
6511
+ let versions;
6512
+ let detail;
6127
6513
  if (!result.ok) {
6128
- out = { source: "unavailable", total: 0, versions: [], detail: `could not load versions (${result.reason})` };
6514
+ source = "unavailable";
6515
+ versions = [];
6516
+ detail = `could not load versions (${result.reason})`;
6129
6517
  } else if (list === null) {
6130
- out = { source: "unavailable", total: 0, versions: [], detail: "proxy returned an unrecognized versions payload" };
6518
+ source = "unavailable";
6519
+ versions = [];
6520
+ detail = "proxy returned an unrecognized versions payload";
6131
6521
  } else {
6132
- const versions = list.map((v) => ({
6522
+ const base = list.map((v) => ({
6133
6523
  id: String(v.id ?? ""),
6134
- name: str2(v.version) ?? str2(v.name) ?? "(unnamed)",
6524
+ name: metaName(v) ?? str2(v.version) ?? "(unnamed)",
6525
+ semver: semverOf(v.version),
6135
6526
  createdAt: str2(v.createdAt),
6136
6527
  readOnly: typeof v.isReadonly === "boolean" ? v.isReadonly : null
6137
6528
  }));
6138
- out = { source: "remote", total: versions.length, versions, detail: null };
6139
- }
6140
- const summary = out.source === "unavailable" ? `Could not load design system versions (${out.detail}). Treat as unavailable \u2014 do not assume there are none.` : `${out.total} design system version${out.total === 1 ? "" : "s"}: ${out.versions.map((v) => v.name).join(", ") || "(none)"}.`;
6141
- return { content: [{ type: "text", text: summary }], structuredContent: out };
6529
+ const latest = latestReleasedId(base);
6530
+ versions = base.map((v) => ({
6531
+ ...v,
6532
+ isDraft: v.readOnly === null ? null : v.readOnly === false,
6533
+ isLatestReleased: latest !== null && v.id === latest
6534
+ }));
6535
+ source = "remote";
6536
+ detail = null;
6537
+ }
6538
+ const manifest = await buildManifestLane(load);
6539
+ const out = { source, total: versions.length, versions, manifest, detail };
6540
+ const versionsLine = out.source === "unavailable" ? `Supernova versions unavailable (${out.detail}) \u2014 do not assume there are none.` : `${out.total} Supernova version${out.total === 1 ? "" : "s"}: ${out.versions.map((v) => v.name).join(", ") || "(none)"}.`;
6541
+ const manifestLine = manifest.source === "unavailable" ? `Release manifest unavailable (${manifest.detail}).` : `${manifest.total} manifest release${manifest.total === 1 ? "" : "s"} (front-door: ${manifest.releases.map((r) => r.frontDoorVersion).filter(Boolean).join(", ") || "n/a"}).`;
6542
+ return { content: [{ type: "text", text: `${versionsLine} ${manifestLine}` }], structuredContent: out };
6142
6543
  }
6143
6544
  var listVersionsTool = {
6144
6545
  name: TOOL_NAME5,
@@ -6918,6 +7319,30 @@ var compositions_default = [
6918
7319
  code: `<RadioGroup defaultValue="option-1">
6919
7320
  <RadioButton value="option-1" variant="pill">Option 1</RadioButton>
6920
7321
  <RadioButton value="option-2" variant="pill">Option 2</RadioButton>
7322
+ </RadioGroup>`
7323
+ },
7324
+ // ── Radio ──
7325
+ // Maps to figma-mappings `Radio` (canonical control, node 9232-27291). The bare
7326
+ // 20px control must render inside a RadioGroup; pair each with an accessible
7327
+ // name (aria-label here, or a sibling <label htmlFor> in real usage).
7328
+ {
7329
+ group: "Radio",
7330
+ name: "Radio",
7331
+ code: `<RadioGroup defaultValue="email" aria-label="Notifications">
7332
+ <Radio value="email" aria-label="Email" />
7333
+ <Radio value="sms" aria-label="SMS" />
7334
+ <Radio value="push" aria-label="Push notification" />
7335
+ </RadioGroup>`
7336
+ },
7337
+ // ── RadioItem ──
7338
+ // Maps to figma-mappings `RadioItem` (canonical control + label, node 12360-58265).
7339
+ {
7340
+ group: "RadioItem",
7341
+ name: "RadioItem",
7342
+ code: `<RadioGroup defaultValue="email" aria-label="Notifications">
7343
+ <RadioItem value="email" description="We'll send a confirmation">Email</RadioItem>
7344
+ <RadioItem value="sms">SMS</RadioItem>
7345
+ <RadioItem value="push" disabled>Push notification</RadioItem>
6921
7346
  </RadioGroup>`
6922
7347
  },
6923
7348
  // ── SearchInput ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/arbor-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Model Context Protocol server exposing Arbor design system tools: Playroom snippets, Figma→code sync health, UX-writing checks, decision-log lookup, and federated component/version metadata.",
5
5
  "keywords": [
6
6
  "arbor",
@@ -45,7 +45,7 @@
45
45
  "@modelcontextprotocol/sdk": "^1.29.0"
46
46
  },
47
47
  "devDependencies": {
48
- "esbuild": "^0.25.0",
48
+ "esbuild": "^0.28.1",
49
49
  "tsx": "^4.7.0",
50
50
  "typescript": "^5.7.0",
51
51
  "zod": "3.25.76"