@enfyra/mcp-server 0.1.48 → 0.1.50

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.
@@ -22,7 +22,7 @@ export function buildMcpServerInstructions(apiBaseUrl, options = {}) {
22
22
  '- For target sanity checks, call `get_enfyra_api_context`; do not load broad metadata only to confirm the API base.',
23
23
  '- When the goal is clear but the tool path is not, call `discover_enfyra_workflows` with intent/risk/surface. Use `detail: "plan"` before writes, follow `primaryPath`, and treat `avoidTools` as hard negative-routing boundaries.',
24
24
  '- Discover before deciding, then inspect narrowly. Use `inspect_table`, `inspect_route`, `inspect_feature`, `search_admin_extensions`, or `search_runtime_zone` for the exact table/route/feature/runtime artifact. Run broad discovery tools sequentially, not in parallel.',
25
- '- Load examples only when needed with `get_enfyra_examples`. For OAuth use `get_enfyra_examples({ category: "oauth-setup" })`. For extension UI, call `get_extension_theme_contract` before writing or reviewing UI, use `build_extension_ui` after the extension acknowledgement for high-contract components and theme decisions such as drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, FormEditor, Widget, menu/account panel registries, tabs, upload modals, theme classes/review, and full review, and call `get_theme_class_reference` only when exact classes are needed.',
25
+ '- Load examples only when needed with `get_enfyra_examples`. For OAuth use `get_enfyra_examples({ category: "oauth-setup" })`. For extension UI, call `get_extension_theme_contract` before writing or reviewing UI, use `build_extension_ui` after the extension acknowledgement for high-contract components, app composables, and theme decisions such as drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, FormEditor, Widget, menu/account panel registries, tabs, upload modals, api usage, notify, runtime/theme classes/review, and full review, and call `get_theme_class_reference` only for theme internals or an explicitly requested full class map.',
26
26
  '- Prefer the most specific business operation tool over raw metadata CRUD.',
27
27
  '- Before any write, call `get_enfyra_required_knowledge({ scope })` with the right scope and pass returned ack keys into write tools. Choose scope by task domain:',
28
28
  ' • `schema` — table, column, relation, record CRUD, route permission, guard, field permission, column rule, cache reload, GraphQL enablement, log debug.',
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,qBAAqB;QACrB,qJAAqJ;QACrJ,qHAAqH;QACrH,qOAAqO;QACrO,8QAA8Q;QAC9Q,imBAAimB;QACjmB,4EAA4E;QAC5E,oKAAoK;QACpK,2JAA2J;QAC3J,uJAAuJ;QACvJ,8IAA8I;QAC9I,yDAAyD;QACzD,4EAA4E;QAC5E,sMAAsM;QACtM,mLAAmL;QACnL,8LAA8L;QAC9L,EAAE;QACF,0BAA0B;QAC1B,uHAAuH;QACvH,sLAAsL;QACtL,gLAAgL;QAChL,iLAAiL;QACjL,yLAAyL;QACzL,kKAAkK;QAClK,0HAA0H;QAC1H,4LAA4L;QAC5L,EAAE;QACF,+BAA+B;QAC/B,4HAA4H;QAC5H,4GAA4G;QAC5G,mIAAmI;QACnI,kJAAkJ;QAClJ,oNAAoN;QACpN,iKAAiK;QACjK,gIAAgI;QAChI,EAAE;QACF,+BAA+B;QAC/B,iIAAiI;QACjI,uJAAuJ;QACvJ,yOAAyO;QACzO,+JAA+J;QAC/J,iMAAiM;QACjM,wSAAwS;QACxS,qLAAqL;QACrL,kPAAkP;QAClP,+PAA+P;QAC/P,EAAE;QACF,4BAA4B;QAC5B,0MAA0M;QAC1M,oHAAoH;QACpH,kIAAkI;QAClI,8MAA8M;QAC9M,kNAAkN;QAClN,wLAAwL;QACxL,EAAE;QACF,uIAAuI;KACxI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"mcp-instructions.js","sourceRoot":"","sources":["../../src/lib/mcp-instructions.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,UAAU;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,OAAO;QACL,cAAc,EAAE,GAAG,IAAI,UAAU;QACjC,gBAAgB,EAAE,GAAG,IAAI,iBAAiB;KAC3C,CAAC;AACJ,CAAC;AAMD,MAAM,UAAU,0BAA0B,CAAC,UAAU,EAAE,UAAiC,EAAE;IACxF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1E,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IAEvD,OAAO;QACL,eAAe;QACf,EAAE;QACF,gCAAgC,IAAI,KAAK;QACzC,wBAAwB,cAAc,YAAY,gBAAgB,KAAK;QACvE,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,EAAE;QACF,qBAAqB;QACrB,qJAAqJ;QACrJ,qHAAqH;QACrH,qOAAqO;QACrO,8QAA8Q;QAC9Q,8qBAA8qB;QAC9qB,4EAA4E;QAC5E,oKAAoK;QACpK,2JAA2J;QAC3J,uJAAuJ;QACvJ,8IAA8I;QAC9I,yDAAyD;QACzD,4EAA4E;QAC5E,sMAAsM;QACtM,mLAAmL;QACnL,8LAA8L;QAC9L,EAAE;QACF,0BAA0B;QAC1B,uHAAuH;QACvH,sLAAsL;QACtL,gLAAgL;QAChL,iLAAiL;QACjL,yLAAyL;QACzL,kKAAkK;QAClK,0HAA0H;QAC1H,4LAA4L;QAC5L,EAAE;QACF,+BAA+B;QAC/B,4HAA4H;QAC5H,4GAA4G;QAC5G,mIAAmI;QACnI,kJAAkJ;QAClJ,oNAAoN;QACpN,iKAAiK;QACjK,gIAAgI;QAChI,EAAE;QACF,+BAA+B;QAC/B,iIAAiI;QACjI,uJAAuJ;QACvJ,yOAAyO;QACzO,+JAA+J;QAC/J,iMAAiM;QACjM,wSAAwS;QACxS,qLAAqL;QACrL,kPAAkP;QAClP,+PAA+P;QAC/P,EAAE;QACF,4BAA4B;QAC5B,0MAA0M;QAC1M,oHAAoH;QACpH,kIAAkI;QAClI,8MAA8M;QAC9M,kNAAkN;QAClN,wLAAwL;QACxL,EAAE;QACF,uIAAuI;KACxI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -76,6 +76,16 @@ export declare function buildExtensionUploadModalSnippet(input: any): {
76
76
  companionSnippet: string;
77
77
  contract: string[];
78
78
  };
79
+ export declare function buildExtensionApiUsageSnippet(input?: AnyRecord): {
80
+ action: string;
81
+ snippet: string;
82
+ contract: string[];
83
+ };
84
+ export declare function buildExtensionNotifySnippet(input?: AnyRecord): {
85
+ action: string;
86
+ snippet: string;
87
+ contract: string[];
88
+ };
79
89
  export declare function reviewExtensionUiContract(code: any): {
80
90
  action: string;
81
91
  valid: boolean;
@@ -88,6 +98,18 @@ export declare function reviewExtensionUiContract(code: any): {
88
98
  }[];
89
99
  nextSteps: string[];
90
100
  };
101
+ export declare function reviewExtensionRuntimeContract(code: any): {
102
+ action: string;
103
+ valid: boolean;
104
+ issueCount: number;
105
+ issues: {
106
+ severity: "error" | "warning";
107
+ rule: string;
108
+ message: string;
109
+ suggestion: string;
110
+ }[];
111
+ nextSteps: string[];
112
+ };
91
113
  export declare function reviewExtensionThemeContract(code: any): {
92
114
  action: string;
93
115
  valid: boolean;
@@ -105,6 +127,7 @@ export declare function validateExtensionCodeLocally(code: any): {
105
127
  componentCasing: string;
106
128
  fieldWidth: string;
107
129
  themeContract: string;
130
+ runtimeContract: string;
108
131
  };
109
132
  export declare function validateExtensionCode(apiUrl: any, code: any, name: any): Promise<{
110
133
  valid: boolean;
@@ -112,6 +135,7 @@ export declare function validateExtensionCode(apiUrl: any, code: any, name: any)
112
135
  componentCasing: string;
113
136
  fieldWidth: string;
114
137
  themeContract: string;
138
+ runtimeContract: string;
115
139
  };
116
140
  extensionId: any;
117
141
  compiledLength: any;
@@ -836,6 +836,88 @@ export function buildExtensionUploadModalSnippet(input) {
836
836
  ],
837
837
  };
838
838
  }
839
+ function toPascalIdentifier(value, fallback = 'Items') {
840
+ const raw = String(value || fallback)
841
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
842
+ .trim()
843
+ .split(/\s+/)
844
+ .filter(Boolean)
845
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
846
+ .join('');
847
+ return raw || fallback;
848
+ }
849
+ export function buildExtensionApiUsageSnippet(input = {}) {
850
+ const resource = String(input.resource || input.name || 'items');
851
+ const pascal = toPascalIdentifier(resource, 'Items');
852
+ const path = input.path || `/${resource}`;
853
+ const method = String(input.method || 'GET').toUpperCase();
854
+ const responseName = input.responseName || `${resource}Response`;
855
+ const pendingName = input.pendingName || `${resource}Pending`;
856
+ const errorName = input.errorName || `${resource}Error`;
857
+ const executeName = input.executeName || (method === 'GET' ? `load${pascal}` : `${method.toLowerCase()}${pascal}`);
858
+ const refreshName = input.refreshName || `refresh${pascal}`;
859
+ const options = [];
860
+ if (method !== 'GET')
861
+ options.push(`method: ${quoteJsString(method)}`);
862
+ if (input.queryExpression)
863
+ options.push(`query: ${input.queryExpression}`);
864
+ if (input.bodyExpression)
865
+ options.push(`body: ${input.bodyExpression}`);
866
+ if (input.errorContext)
867
+ options.push(`errorContext: ${quoteJsString(input.errorContext)}`);
868
+ if (input.onErrorExpression)
869
+ options.push(`onError: ${input.onErrorExpression}`);
870
+ const optionsLiteral = options.length ? `, {\n ${options.join(',\n ')}\n}` : '';
871
+ const lines = [
872
+ `const { data: ${responseName}, pending: ${pendingName}, error: ${errorName}, execute: ${executeName}, refresh: ${refreshName} } = useApi(${quoteJsString(path)}${optionsLiteral});`,
873
+ ];
874
+ if (method === 'GET') {
875
+ const rowsName = input.rowsName || resource;
876
+ lines.push(`const ${rowsName} = computed(() => ${responseName}.value?.data || []);`);
877
+ if (input.autoLoad !== false) {
878
+ lines.push(`onMounted(() => { ${executeName}(); });`);
879
+ }
880
+ }
881
+ else {
882
+ const handlerName = input.handlerName || `${method.toLowerCase()}${pascal}Record`;
883
+ lines.push(...[
884
+ '',
885
+ `async function ${handlerName}(payload) {`,
886
+ ` const response = await ${executeName}({ body: payload });`,
887
+ ' if (!response) return null;',
888
+ ' return response;',
889
+ '}',
890
+ ]);
891
+ }
892
+ return {
893
+ action: 'extension_api_usage_built',
894
+ snippet: lines.join('\n'),
895
+ contract: [
896
+ 'useApi returns refs plus execute/refresh; it does not auto-run.',
897
+ 'Pass query/body as objects or computed objects, not JSON.stringify strings.',
898
+ 'Read normal list rows from data.value?.data or from the direct execute() response.',
899
+ 'For mutations, call execute({ body }), execute({ id, body }), execute({ id }), or execute({ ids }) from a user action.',
900
+ ],
901
+ };
902
+ }
903
+ export function buildExtensionNotifySnippet(input = {}) {
904
+ const kind = ['success', 'error', 'warning', 'info'].includes(input.kind) ? input.kind : 'success';
905
+ const title = input.title || (kind === 'success' ? 'Saved' : 'Notice');
906
+ const description = input.description || '';
907
+ const args = description ? `${quoteJsString(title)}, ${quoteJsString(description)}` : quoteJsString(title);
908
+ return {
909
+ action: 'extension_notify_usage_built',
910
+ snippet: [
911
+ 'const notify = useNotify();',
912
+ `await notify.${kind}(${args});`,
913
+ ].join('\n'),
914
+ contract: [
915
+ 'useNotify exposes success/error/warning/info(title, description?) helpers.',
916
+ 'Do not pass Nuxt toast object payloads and do not call notify.add().',
917
+ 'The helpers are async; await them inside submit/mutation handlers when ordering matters.',
918
+ ],
919
+ };
920
+ }
839
921
  export function reviewExtensionUiContract(code) {
840
922
  const source = String(code || '');
841
923
  const issues = [];
@@ -881,6 +963,45 @@ export function reviewExtensionUiContract(code) {
881
963
  : ['Snippet matches the checked modal/drawer contract rules. Still validate the final SFC before saving.'],
882
964
  };
883
965
  }
966
+ function collectExtensionRuntimeIssues(code) {
967
+ const source = String(code || '');
968
+ const issues = [];
969
+ const push = (severity, rule, message, suggestion) => issues.push({ severity, rule, message, suggestion });
970
+ if (/(?:^|[>\n;])\s*import(?:\s.+?\sfrom\s+|\s*['"])/m.test(source)) {
971
+ push('error', 'static-import', 'Static import statements are not allowed in enfyra_extension.code.', 'Use injected globals/components directly, or load app packages with getPackages(["package-name"]) inside runtime code.');
972
+ }
973
+ if (/\buseToast\s*\(/.test(source)) {
974
+ push('error', 'use-toast-directly', 'Dynamic extensions should not call useToast() directly.', 'Use useNotify() and call success/error/warning/info(title, description?).');
975
+ }
976
+ if (/\buseNotify\s*\(\s*\)\s*\.add\s*\(/.test(source) || /\b\w+\s*\.add\s*\(\s*\{\s*title\s*:/.test(source)) {
977
+ push('error', 'use-notify-add', 'useNotify() does not accept Nuxt toast object payloads through add().', 'Call notify.success/error/warning/info(title, description?) instead.');
978
+ }
979
+ if (/\b(?:query|body|filter|deep|aggregate)\s*:\s*JSON\.stringify\s*\(/.test(source)) {
980
+ push('error', 'use-api-json-stringify-options', 'useApi query/body/filter/deep/aggregate options must be plain objects or computed objects, not JSON strings.', 'Pass the object directly to useApi or execute().');
981
+ }
982
+ if (/\bawait\s+useApi\s*\(/.test(source)) {
983
+ push('error', 'await-use-api', 'useApi() is a composable that returns refs plus execute/refresh; it should not be awaited.', 'Destructure execute/refresh from useApi(), then await execute() inside onMounted, watchers, or user actions.');
984
+ }
985
+ if (/\buseApi\s*\(/.test(source) && !/\bexecute\s*:/.test(source) && !/\brefresh\s*:/.test(source) && !/\.\s*(?:execute|refresh)\s*\(/.test(source)) {
986
+ push('warning', 'use-api-no-execute-alias', 'useApi() appears without an execute/refresh alias or call.', 'Remember useApi does not auto-run; destructure execute/refresh and call execute() deliberately.');
987
+ }
988
+ if (/\buseNotify\s*\(/.test(source) && !/\bnotify\.(?:success|error|warning|info)\s*\(/.test(source) && !/\b(?:success|error|warning|info)\s*:\s*\w+/.test(source)) {
989
+ push('warning', 'use-notify-no-helper-call', 'useNotify() appears without a success/error/warning/info helper call.', 'Use the semantic helper methods instead of low-level toast payloads.');
990
+ }
991
+ return issues;
992
+ }
993
+ export function reviewExtensionRuntimeContract(code) {
994
+ const issues = collectExtensionRuntimeIssues(code);
995
+ return {
996
+ action: 'extension_runtime_contract_reviewed',
997
+ valid: issues.every((issue) => issue.severity !== 'error'),
998
+ issueCount: issues.length,
999
+ issues,
1000
+ nextSteps: issues.length
1001
+ ? ['Use build_extension_ui kind=api_usage or kind=notify for known-good snippets, then patch/update the extension.']
1002
+ : ['Snippet matches the checked runtime composable/package rules. Still validate the final SFC before saving.'],
1003
+ };
1004
+ }
884
1005
  const THEME_CLASS_INTENTS = {
885
1006
  neutral_surface: {
886
1007
  classes: 'eapp-surface-card eapp-radius-panel border eapp-divider',
@@ -1088,6 +1209,18 @@ export function buildExtensionUiSnippet(kind, input = {}) {
1088
1209
  case 'upload_modal':
1089
1210
  result = buildExtensionUploadModalSnippet(input);
1090
1211
  break;
1212
+ case 'api_usage':
1213
+ result = buildExtensionApiUsageSnippet(input);
1214
+ break;
1215
+ case 'notify':
1216
+ result = buildExtensionNotifySnippet(input);
1217
+ break;
1218
+ case 'runtime_review':
1219
+ if (!input?.code) {
1220
+ throw new Error('build_extension_ui kind=runtime_review requires input.code.');
1221
+ }
1222
+ result = reviewExtensionRuntimeContract(input.code);
1223
+ break;
1091
1224
  case 'theme_classes':
1092
1225
  result = buildExtensionThemeClasses(input);
1093
1226
  break;
@@ -1103,12 +1236,14 @@ export function buildExtensionUiSnippet(kind, input = {}) {
1103
1236
  }
1104
1237
  const uiReview = reviewExtensionUiContract(input.code);
1105
1238
  const themeReview = reviewExtensionThemeContract(input.code);
1239
+ const runtimeReview = reviewExtensionRuntimeContract(input.code);
1106
1240
  result = {
1107
- action: 'extension_ui_and_theme_contract_reviewed',
1108
- valid: uiReview.valid && themeReview.valid,
1109
- issueCount: uiReview.issueCount + themeReview.issueCount,
1241
+ action: 'extension_ui_theme_runtime_contract_reviewed',
1242
+ valid: uiReview.valid && themeReview.valid && runtimeReview.valid,
1243
+ issueCount: uiReview.issueCount + themeReview.issueCount + runtimeReview.issueCount,
1110
1244
  ui: uiReview,
1111
1245
  theme: themeReview,
1246
+ runtime: runtimeReview,
1112
1247
  };
1113
1248
  break;
1114
1249
  default:
@@ -1131,72 +1266,51 @@ function getExtensionThemeContract() {
1131
1266
  'The extension is already mounted inside the Enfyra app shell. Do not add a duplicate page header, centered page wrapper, or root-level page padding.',
1132
1267
  'Page extensions should be full-bleed, responsive, and split large operations into focused pages or UTabs.',
1133
1268
  'Use usePageHeaderRegistry for the shell title and useHeaderActionRegistry/useSubHeaderActionRegistry for page actions.',
1134
- 'Use build_extension_menu_notification for sidebar menu notification registration snippets.',
1269
+ 'Use build_extension_ui kind=menu_notification for sidebar menu notification registration snippets.',
1135
1270
  'For shell menu notifications, first decide the signal source. Use a count only when the source already owns an exact count, such as a notification summary endpoint or bounded unread-notification query. Use a dot when a realtime event only proves that something new exists. Do not poll a domain list such as messages, tickets, orders, or jobs solely to decorate the menu; the destination page owns domain fetching.',
1136
- 'Use build_extension_account_panel_item for account panel row registration snippets.',
1271
+ 'Use build_extension_ui kind=account_panel_item for account panel row registration snippets.',
1137
1272
  'For detail/form workflows that should stay left-aligned with empty space on the right, wrap the body in eapp-page-constrained; use eapp-page-constrained-wide only when the workflow genuinely needs more width.',
1138
1273
  'Card/list grids inside the default shell must account for the 280px desktop sidebar. Do not switch general card grids to three columns at lg; use md:grid-cols-2 xl:grid-cols-3 unless a local container proves three columns have enough width.',
1139
1274
  ],
1140
1275
  theme: [
1141
- 'Use eApp theme class tokens, not hardcoded light/dark colors and not raw CSS variables inside extension templates. The app owns the CSS variable implementation; generated extensions should choose class tokens by intent.',
1142
- 'Primary color is runtime-configurable through the app color picker and must affect extension identity UI. For Nuxt UI components, choose color="primary" by semantic intent and let the app map it through the primary contract; do not choose a concrete palette. For custom extension UI, first choose whether the element is neutral surface, runtime-primary identity, or status. Regular panels, KPI cards, list rows, and large content blocks should use eapp-surface-card, eapp-surface-muted, eapp-surface-flat, eapp-surface-hover, eapp-divide-y, and eapp-text-* classes. Entity identity, selected/current state, active progress, primary tiles, primary icons, and primary CTA fills should use eapp-primary-surface, eapp-primary-soft, eapp-primary-subtle, eapp-primary-solid, eapp-primary-text, eapp-primary-border, or eapp-primary-ring so the color picker controls them.',
1143
- 'Use eapp-primary-surface only for larger entity/feature blocks, selected/current cards, tiles, or cards that should read like normal app cards with a very subtle active-primary tint; it supplies selected identity color but does not replace card chrome, so keep normal border/radius classes such as border plus eapp-radius-panel on the element. It is not a saturated selected-state fill and must not be applied broadly to every KPI/list wrapper. Add eapp-primary-surface-hover when that block is clickable. Use eapp-primary-soft for compact selected entity chips, pills, square icon tiles using eapp-icon-tile, and identity callouts; add eapp-primary-soft-hover when compact surfaces are clickable; use eapp-primary-subtle for a slightly stronger selected fill; use eapp-primary-solid only for primary identity fills; use eapp-primary-text for identity icons or inline text. eapp-identity-* remains an alias for the same runtime-primary intent, but eapp-primary-* is preferred in new extension code.',
1144
- 'Nuxt UI secondary is still a valid semantic color when the product intentionally wants a secondary action or state. Do not use color="secondary", from-secondary-*, bg-secondary-*, text-secondary-*, or cyan/purple/green palette utilities merely to approximate an entity accent; use eapp-primary-* and let the app decide the color.',
1145
- 'The app runs on Tailwind v4. Short Tailwind color utilities are the canonical way to apply contract colors and ARE allowed: bg-primary, text-primary, border-primary, ring-primary, bg-success, text-error, etc., including opacity modifiers (bg-primary/10, ring-success/20) which v4 resolves via color-mix. They are generated from the token-backed config (primary -> --md-primary runtime, success/error/warning/info -> --st-* status, secondary -> --md-tertiary) so they follow the color picker and dark theme. Do NOT use raw CSS-variable utilities (text-[var(--*)], bg-[var(--*)], border-[var(--*)]), hardcoded hex, inline style colors, or concrete palette substitution (color="violet", from-cyan-*, text-violet-*, bg-green-*, bg-emerald-*, text-green-*, dark:bg-zinc-950). For intent surfaces with no Tailwind equivalent (selected identity block, soft/solid/subtle surface, divider, radius, modal chrome) use the eapp-* classes below; the app owns how they map to the active color picker value.',
1146
- 'Use UButton color="primary" only for the single main action for the current scope. Refresh, back, navigation, filters, and secondary actions should be neutral variants unless they are the main mutation.',
1147
- 'PageHeader gradient must be "none" for generated operational extensions unless the user explicitly asks for a decorative page accent. Do not hardcode cyan, violet, purple, blue, or green PageHeader gradients to force color variety.',
1148
- 'Do not inject global CSS, create theme guards, redefine the app palette, or solve one extension by overriding the whole app shell.',
1149
- 'For panels/cards, prefer eapp-surface-card, eapp-surface-hover, eapp-surface-muted, eapp-surface-flat, and eapp-divide-y. Use eapp-text-primary, eapp-text-secondary, eapp-text-tertiary, or eapp-text-quaternary for copy.',
1150
- 'Never use Nuxt UI neutral semantic classes such as bg-default, bg-muted, border-default, divide-default, text-muted, text-dimmed, or hardcoded dark palettes such as dark:bg-zinc-950, bg-slate-*, text-gray-*, border-black, or black.',
1151
- 'Never use bare border/divide-y for panels or rows: pair borders with eapp-divider or use eapp-divide-y for row separators.',
1152
- 'Use radius tokens or mapped rounded utilities consistently: --radius-card for cards, --radius-panel for nested panels, --radius-control for buttons/inputs, --radius-subcontrol for compact inner controls, and --radius-pill for pills.',
1153
- 'Status colors must remain readable in both themes and must stay scoped to badges, small icons, or short status text. Use UBadge/UAlert semantic colors or eapp-status-success-soft/text/border, eapp-status-warning-soft/text/border, eapp-status-danger-soft/text/border, eapp-status-info-soft/text/border, and eapp-status-neutral-soft/text/border. Do not read --badge-* variables directly from extension templates. Do not color large panels, alert-like success blocks, KPI cards, list containers, or reconciliation/attention blocks green/yellow/red because the status is good/warning/error; use neutral app surfaces for the block and place a small status badge/icon inside.',
1154
- 'Keep dark and light contrast comparable. Do not make dark mode more neon or lower-contrast than light mode; prefer muted soft backgrounds with clear text and visible borders.',
1276
+ 'Do not choose theme classes from memory. Decide the UI intent, then call build_extension_ui kind=theme_classes with that intent to receive the exact class/prop contract.',
1277
+ 'Call build_extension_ui kind=theme_review or kind=review before saving extension UI; validate_extension_code and extension write tools also reject hard theme violations.',
1278
+ 'Never fix one extension by injecting global CSS, redefining the app palette, or adding theme guards.',
1279
+ 'Use get_theme_class_reference only when debugging theme internals or when the user explicitly asks for the full theme/class map.',
1155
1280
  ],
1156
- decisionCases: [
1157
- {
1158
- intent: 'Normal accent, decorative icon, feature icon, non-state tile, active tab fill, progress fill, selected segment, primary metric accent, or primary action.',
1159
- colorContract: 'Use runtime primary so the app color picker controls the color.',
1160
- use: 'UButton/UBadge color="primary", eapp-primary-soft, eapp-primary-subtle, eapp-primary-solid, eapp-primary-text, or eapp-primary-surface when the whole block is selected/current identity.',
1161
- avoid: 'Do not pick cyan, purple, green, amber, secondary, or concrete Tailwind palettes just because they look good.',
1162
- },
1163
- {
1164
- intent: 'True semantic state: error, danger, destructive, warning, pending attention, success, healthy, running, failed, info, or notice.',
1165
- colorContract: 'Use the matching state/status color because the color communicates meaning, not brand identity.',
1166
- use: 'UAlert/UBadge color="error|warning|success|info|neutral", or eapp-status-success|warning|danger|info|neutral soft/text/border classes for custom compact status chips/icons.',
1167
- avoid: 'Do not force semantic state UI to primary; an error must stay error, a warning must stay warning, success can stay success when it is a badge/icon/short status.',
1168
- },
1169
- {
1170
- intent: 'Large ordinary surface: KPI card, list container, table panel, detail panel, summary block, empty state panel, reconciliation block, or attention block.',
1171
- colorContract: 'Use neutral app surfaces first; large surfaces should not become state-colored or arbitrary accent-colored.',
1172
- use: 'eapp-surface-card, eapp-surface-hover, eapp-surface-muted, eapp-surface-flat, eapp-divide-y, and eapp-text-* classes.',
1173
- avoid: 'Do not color large blocks green/yellow/red because their content says healthy/warning/error; place a status badge/icon inside the neutral block instead.',
1174
- },
1175
- {
1176
- intent: 'Selected/current entity or user-selected option where the whole block is the active identity.',
1177
- colorContract: 'Use runtime primary identity surface, but subtly.',
1178
- use: 'eapp-primary-surface plus optional eapp-primary-surface-hover; eapp-primary-soft/text for the icon or chip inside.',
1179
- avoid: 'Do not use eapp-primary-surface for every card in a grid/list or as a broad page background.',
1180
- },
1281
+ themeIntents: [
1282
+ 'neutral_surface',
1283
+ 'muted_surface',
1284
+ 'flat_surface',
1285
+ 'hover_row',
1286
+ 'primary_identity',
1287
+ 'primary_soft_icon_tile',
1288
+ 'primary_progress',
1289
+ 'status_success',
1290
+ 'status_warning',
1291
+ 'status_danger',
1292
+ 'status_info',
1293
+ 'primary_action',
1294
+ 'secondary_action',
1295
+ 'ghost_navigation_action',
1296
+ 'danger_action',
1297
+ 'divider',
1298
+ 'text',
1181
1299
  ],
1182
1300
  components: [
1183
1301
  'Use Nuxt UI/eApp components for normal controls: UButton, UInput, UTextarea, USelectMenu/USelect, USwitch, UCheckbox, UTabs, UBadge, UModal, and CommonDrawer when available.',
1184
1302
  'Use auto-injected components directly in the template with PascalCase names. Do not call resolveComponent() to manually resolve Nuxt UI/eApp components inside extension SFCs; it can compile but render unresolved lowercase DOM tags such as <ubutton>.',
1185
1303
  'Buttons should have stable geometry: hover may change color, border, or shadow but must not move the button or resize its content. Disabled buttons keep disabled cursor/visual state.',
1186
1304
  'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
1187
- 'For drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, form editors, widgets, menu/account panel registries, tabs, and upload modals, call build_extension_ui with the matching kind after extension acknowledgement before patching raw Vue.',
1188
- 'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=theme_review or kind=review before saving generated snippets that include high-contract UI, theme classes, or native buttons.',
1305
+ 'For drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, form editors, widgets, menu/account panel registries, tabs, upload modals, api_usage, notify, and runtime/theming reviews, call build_extension_ui with the matching kind after extension acknowledgement before patching raw Vue.',
1306
+ 'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=runtime_review, theme_review, or review before saving generated snippets that include composables, theme classes, high-contract UI, or native buttons.',
1189
1307
  'Extension validation rejects UInput, UTextarea, USelect, USelectMenu, UInputMenu, UInputNumber, UInputTags, UInputTime, and UInputDate without class="w-full" unless marked data-compact or data-inline.',
1190
1308
  'Use UBadge or token-backed badge spans for status. Keep badges legible in both themes with tokenized background, text, and border.',
1191
1309
  ],
1192
1310
  appComposables: [
1193
- 'useApi(url, options) returns refs: data, error, pending, status, plus async execute(options?) and refresh(). execute returns the response object or null.',
1194
- 'useApi does not auto-run; call execute() in onMounted, a watcher, or a user action.',
1195
- 'Pass query/body as plain objects or computed objects. Do not JSON.stringify filter, deep, or aggregate in extension/app code.',
1196
- 'Normal Enfyra list responses are available as data.value?.data, and the direct execute() response as response?.data. Avoid double-unwrapping data?.value?.data unless explicitly supporting an older nested response shape.',
1197
- 'For mutations, set method in useApi options and call execute({ body }), execute({ id, body }), execute({ id }), or execute({ ids }) as appropriate.',
1198
- 'Use errorContext for readable default failures. Return true from onError only when the component fully handles the error locally.',
1199
- 'useNotify() provides async success/error/warning/info(title, description?) methods plus dismiss/clear. Do not use Nuxt toast object payloads with it.',
1311
+ 'Do not write useApi/useNotify shapes from memory. Use build_extension_ui kind=api_usage or kind=notify for known-good snippets.',
1312
+ 'Use build_extension_ui kind=runtime_review or kind=review before saving extension code that includes useApi, useNotify, getPackages, or package loading.',
1313
+ 'validate_extension_code and extension write tools reject static imports, useToast/useNotify.add misuse, JSON.stringify useApi options, and await useApi().',
1200
1314
  ],
1201
1315
  shellComponentContracts: {
1202
1316
  CommonDrawer: [
@@ -1216,7 +1330,7 @@ function getExtensionThemeContract() {
1216
1330
  'The builder owns numeric id usage, reactive prop/event wiring, and page/widget ownership warnings.',
1217
1331
  ],
1218
1332
  actionButtons: [
1219
- 'Use build_extension_ui kind=review for generated snippets with native buttons.',
1333
+ 'Use build_extension_ui kind=review for generated snippets with native buttons or theme classes.',
1220
1334
  'Validation/review catches missing type="button" and high-contract component mistakes before saving.',
1221
1335
  ],
1222
1336
  },
@@ -1236,60 +1350,12 @@ function getExtensionThemeContract() {
1236
1350
  'UI checks are only guidance; handlers/hooks must independently enforce owner/root-admin authorization.',
1237
1351
  'Use the most specific business route or MCP tool. Do not write directly to raw tables when a domain route exists.',
1238
1352
  ],
1239
- patternExamples: [
1240
- {
1241
- useWhen: 'Ordinary KPI, metric, or summary card where the whole card is not selected/current identity.',
1242
- use: 'Neutral card surface; put runtime-primary only on a small identity icon tile, progress fill, or main CTA inside the card.',
1243
- snippet: '<article class="eapp-surface-card p-4"><div class="flex items-start justify-between gap-3"><div><p class="text-sm eapp-text-tertiary">Metric</p><p class="mt-2 text-2xl font-semibold eapp-text-primary">{{ value }}</p></div><span class="eapp-primary-soft eapp-icon-tile"><UIcon name="lucide:square-stack" class="size-5 eapp-primary-text" /></span></div></article>',
1244
- },
1245
- {
1246
- useWhen: 'Selected/current entity, active plan, chosen package, or the single block that represents the active identity.',
1247
- use: 'eapp-primary-surface for the selected/current block, with eapp-primary-soft/text for compact icon parts.',
1248
- snippet: '<article class="eapp-primary-surface eapp-primary-surface-hover eapp-radius-panel border p-4"><div class="flex items-center gap-3"><span class="eapp-primary-soft eapp-icon-tile"><UIcon name="lucide:box" class="size-5 eapp-primary-text" /></span><div><p class="font-semibold eapp-text-primary">{{ name }}</p><p class="text-sm eapp-text-tertiary">Currently selected</p></div></div></article>',
1249
- },
1250
- {
1251
- useWhen: 'Progress, active tab indicator, selected segment fill, or primary visual meter.',
1252
- use: 'Neutral track plus eapp-primary-solid fill so the app color picker controls the fill.',
1253
- snippet: '<div class="h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted"><div class="eapp-primary-solid h-full" :style="{ width: progressWidth }"></div></div>',
1254
- },
1255
- {
1256
- useWhen: 'Success, warning, error, info, healthy, running, failed, pending, or attention status.',
1257
- use: 'UBadge/status badge tokens and optionally a small icon only. Keep large alert/panel/card backgrounds neutral unless the whole block is an identity block.',
1258
- snippet: '<section class="eapp-surface-card p-4"><div class="flex items-center justify-between gap-3"><p class="font-semibold eapp-text-primary">Reconciliation</p><UBadge color="success" variant="soft">Healthy</UBadge></div><p class="mt-1 text-sm eapp-text-tertiary">Latest report found no mismatches.</p></section>',
1259
- },
1260
- {
1261
- useWhen: 'List rows, table-like records, history rows, and secondary navigation rows.',
1262
- use: 'Neutral row surface, tokenized dividers, hover surface-muted, with small status/identity chips inside.',
1263
- snippet: '<div class="eapp-surface-card eapp-divide-y"><button class="flex w-full items-center justify-between px-4 py-3 text-left eapp-surface-hover"><span class="text-sm font-medium eapp-text-primary">{{ row.name }}</span><UBadge color="neutral" variant="soft">{{ row.state }}</UBadge></button></div>',
1264
- },
1265
- {
1266
- useWhen: 'Primary action for the current scope, such as create/save/apply/open-current.',
1267
- use: 'UButton color="primary" variant="solid"; secondary actions stay neutral.',
1268
- snippet: '<div class="flex justify-end gap-2"><UButton color="neutral" variant="outline">Cancel</UButton><UButton color="primary" variant="solid" icon="lucide:save">Save</UButton></div>',
1269
- },
1270
- ],
1271
- compactExample: '<template><section class="min-h-full w-full space-y-4"><article class="eapp-surface-card p-4"><div class="flex items-start justify-between gap-3"><div><p class="text-sm eapp-text-tertiary">Neutral KPI</p><p class="mt-2 text-2xl font-semibold eapp-text-primary">24</p></div><span class="eapp-primary-soft eapp-icon-tile"><UIcon name="lucide:square-stack" class="size-5 eapp-primary-text" /></span></div><div class="mt-3 h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted"><div class="eapp-primary-solid h-full w-1/2"></div></div></article><section class="eapp-surface-card p-4"><div class="flex items-center justify-between gap-3"><p class="font-semibold eapp-text-primary">Status block stays neutral</p><UBadge color="success" variant="soft">Healthy</UBadge></div></section></section></template>',
1272
1353
  shellNotificationContract: {
1273
1354
  menu: 'useMenuNotificationRegistry().register({ id, target: { id?, path?, route? }, value?, color?, title?, order? }). value renders a count/chip; omitting value renders a dot. Parent menus sum numeric child values.',
1274
1355
  accountPanel: 'useAccountPanelRegistry().register({ id, label, description, icon, count?, badge?, badgeColor?, expanded?, onToggle?, contentComponent? }). count is preferred over badge and the account trigger sums numeric visible item counts, capped at 99+.',
1275
1356
  lifecycle: 'Register from global extensions for app-wide notification state; stable ids replace previous registrations and component-owned registrations are removed on unmount.',
1276
1357
  reasoning: 'Counts and dots are different promises. A count says the shell knows an exact or bounded number from an appropriate notification/summary source. A dot says the shell only knows that new attention exists. Avoid fetching the destination domain list just to make a menu badge more precise.',
1277
1358
  },
1278
- contractAuthority: [
1279
- 'This is the authoritative Enfyra theme & color contract. Source of truth: documents/app/theme-color-contract.md. The app owns color through app/utils/primary-colors.ts (Material You seed-to-role generation), app/assets/css/theme.css (semantic variables and Nuxt UI ramps), app/assets/css/main.css (extension-safe semantic utilities), and app/app.config.ts (Nuxt UI component mapping). Pages and extensions only CONSUME classes/Nuxt UI props; they never define colors.',
1280
- 'Every color flows from two base layers: --md-* (Material You, runtime primary picker) and --st-* (status). Runtime primary roles are generated with SchemeTonalSpot. Success/warning/info stay fixed status quarts; error follows the generated Material error role through the single --danger-* lane. All Nuxt UI semantic colors (primary/secondary/success/warning/error/info/neutral) are re-pointed to these, so Nuxt UI is used per its docs but colors are decided by Enfyra. This applies to the shell, system pages, and compiled dynamic extensions.',
1281
- 'Call get_theme_class_reference for the full class->variable->Nuxt UI table when you need the exact class name or variable.',
1282
- ],
1283
- classReference: {
1284
- surfaces: ['eapp-surface-card (default card; --card-bg/--card-border)', 'eapp-surface-muted (recessed/track; --surface-muted)', 'eapp-surface-flat (flush; --surface-default)', 'eapp-surface-hover (clickable row hover)'],
1285
- text: ['eapp-text-primary', 'eapp-text-secondary', 'eapp-text-tertiary', 'eapp-text-quaternary'],
1286
- primaryIdentity: ['eapp-primary-solid (solid fill/meter)', 'eapp-primary-text (inline/icon)', 'eapp-primary-soft + -hover (compact chip/tile)', 'eapp-primary-subtle (stronger selected fill)', 'eapp-primary-surface + -hover (large selected identity block)', 'eapp-primary-border', 'eapp-primary-ring'],
1287
- status: ['eapp-status-success|warning|danger|info|neutral -soft/-text/-border (badges/small icons/short text only)'],
1288
- radius: ['eapp-radius-card', 'eapp-radius-panel', 'eapp-radius-control', 'eapp-radius-subcontrol', 'eapp-radius-pill'],
1289
- dividers: ['eapp-divider', 'eapp-divide-y'],
1290
- modal: ['eapp-modal-surface (modal content chrome; never surface-card)'],
1291
- nuxtUiMapping: 'primary=--md-primary(runtime), secondary=--md-tertiary(runtime), success=--st-success, warning=--st-warning, error=--st-error, info=--st-info, neutral=neutral surfaces',
1292
- },
1293
1359
  };
1294
1360
  }
1295
1361
  function getThemeClassReference() {
@@ -1465,7 +1531,12 @@ export function validateExtensionCodeLocally(code) {
1465
1531
  if (firstThemeError) {
1466
1532
  throw new Error(`Invalid extension theme contract: ${firstThemeError.message} Rule: ${firstThemeError.rule}. ${firstThemeError.suggestion}`);
1467
1533
  }
1468
- return { componentCasing: 'passed', fieldWidth: 'passed', themeContract: 'passed' };
1534
+ const runtimeReview = reviewExtensionRuntimeContract(code);
1535
+ const firstRuntimeError = runtimeReview.issues.find((issue) => issue.severity === 'error');
1536
+ if (firstRuntimeError) {
1537
+ throw new Error(`Invalid extension runtime contract: ${firstRuntimeError.message} Rule: ${firstRuntimeError.rule}. ${firstRuntimeError.suggestion}`);
1538
+ }
1539
+ return { componentCasing: 'passed', fieldWidth: 'passed', themeContract: 'passed', runtimeContract: 'passed' };
1469
1540
  }
1470
1541
  export async function validateExtensionCode(apiUrl, code, name) {
1471
1542
  const localChecks = validateExtensionCodeLocally(code);
@@ -2652,10 +2723,8 @@ async function runExtensionWorkflow(apiUrl, opts) {
2652
2723
  nextSteps: latestState.nextSteps,
2653
2724
  guidance: [
2654
2725
  'Call get_extension_theme_contract before generating or reviewing extension UI.',
2655
- 'useApi returns refs { data, error, pending, status } plus execute/refresh; it does not auto-run. Pass query/body/filter/deep/aggregate as objects or computed objects, not JSON strings.',
2656
- 'useNotify exposes success/error/warning/info(title, description?) async helpers; do not pass Nuxt toast object payloads to it.',
2657
- 'For high-contract UI, call build_extension_ui after extension acknowledgement before patching raw Vue: drawer, modal, page shell, permission gate, empty state, resource list, form editor, widget, menu notification, account panel item, tabs, upload modal, theme classes, theme review, or full review.',
2658
- 'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=theme_review or kind=review before saving generated snippets that include drawers, modals, fields, lists, tabs, upload modals, shell registry code, theme classes, or native buttons.',
2726
+ 'For high-contract UI/runtime code, call build_extension_ui after extension acknowledgement before patching raw Vue: drawer, modal, page shell, permission gate, empty state, resource list, form editor, widget, menu notification, account panel item, tabs, upload modal, api usage, notify, runtime review, theme classes, theme review, or full review.',
2727
+ 'Use build_extension_ui kind=api_usage, notify, theme_classes, runtime_review, theme_review, or review instead of hand-writing those contracts from memory.',
2659
2728
  'Extension validation rejects common field controls without class="w-full" unless intentionally marked data-compact or data-inline.',
2660
2729
  'PermissionGate renders the permitted slot directly and is UX-only; backend permissions and owner checks remain authoritative.',
2661
2730
  'For menu/account-panel notifications, use counts only when the signal source already owns an exact count; otherwise use a dot/chip for new attention.',
@@ -2765,7 +2834,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2765
2834
  server.tool('build_extension_ui', [
2766
2835
  'Lazy gateway for Enfyra admin extension UI builders.',
2767
2836
  'Use this after get_enfyra_required_knowledge(scope="extension") when a high-contract extension UI snippet is needed.',
2768
- 'It keeps guided startup small by dispatching drawer, modal, page_shell, permission_gate, empty_state, resource_list, form_editor, widget, menu_notification, account_panel_item, tabs, upload_modal, theme_classes, theme_review, or review internally instead of exposing every builder tool up front.',
2837
+ 'It keeps guided startup small by dispatching drawer, modal, page_shell, permission_gate, empty_state, resource_list, form_editor, widget, menu_notification, account_panel_item, tabs, upload_modal, api_usage, notify, runtime_review, theme_classes, theme_review, or review internally instead of exposing every builder tool up front.',
2769
2838
  ].join(' '), {
2770
2839
  kind: z.enum([
2771
2840
  'drawer',
@@ -2780,11 +2849,14 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
2780
2849
  'account_panel_item',
2781
2850
  'tabs',
2782
2851
  'upload_modal',
2852
+ 'api_usage',
2853
+ 'notify',
2854
+ 'runtime_review',
2783
2855
  'theme_classes',
2784
2856
  'theme_review',
2785
2857
  'review',
2786
2858
  ]).describe('Which extension UI contract builder/reviewer to run.'),
2787
- input: z.record(z.any()).optional().default({}).describe('Builder input object. For kind=theme_classes, pass { intent }. For kind=theme_review/review, pass { code }.'),
2859
+ input: z.record(z.any()).optional().default({}).describe('Builder input object. For kind=api_usage, pass { path, resource, method? }. For kind=notify, pass { kind, title, description? }. For kind=theme_classes, pass { intent }. For kind=runtime_review/theme_review/review, pass { code }.'),
2788
2860
  extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
2789
2861
  }, async ({ kind, input, extensionKnowledgeAckKey }) => {
2790
2862
  assertExtensionKnowledgeAck(extensionKnowledgeAckKey);