@enfyra/mcp-server 0.1.49 → 0.1.51
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/lib/mcp-instructions.js +1 -1
- package/dist/lib/mcp-instructions.js.map +1 -1
- package/dist/lib/platform-operation-tools.d.ts +24 -0
- package/dist/lib/platform-operation-tools.js +154 -19
- package/dist/lib/platform-operation-tools.js.map +1 -1
- package/dist/lib/required-knowledge.js +6 -9
- package/dist/lib/required-knowledge.js.map +1 -1
- package/dist/lib/tool-routing.js +2 -2
- package/dist/lib/tool-routing.js.map +1 -1
- package/package.json +2 -2
|
@@ -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 for theme internals or an explicitly requested full class map.',
|
|
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,
|
|
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,42 @@ 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 (/\buseApi\s*\(/.test(source) && !/\bexecute\s*:/.test(source) && !/\brefresh\s*:/.test(source) && !/\.\s*(?:execute|refresh)\s*\(/.test(source)) {
|
|
983
|
+
push('warning', 'use-api-no-execute-alias', 'useApi() appears without an execute/refresh alias or call.', 'Call useApi() as a top-level setup composable, then call or await execute()/refresh() from onMounted, watchers, or user actions when the request should run.');
|
|
984
|
+
}
|
|
985
|
+
if (/\buseNotify\s*\(/.test(source) && !/\bnotify\.(?:success|error|warning|info)\s*\(/.test(source) && !/\b(?:success|error|warning|info)\s*:\s*\w+/.test(source)) {
|
|
986
|
+
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.');
|
|
987
|
+
}
|
|
988
|
+
return issues;
|
|
989
|
+
}
|
|
990
|
+
export function reviewExtensionRuntimeContract(code) {
|
|
991
|
+
const issues = collectExtensionRuntimeIssues(code);
|
|
992
|
+
return {
|
|
993
|
+
action: 'extension_runtime_contract_reviewed',
|
|
994
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
995
|
+
issueCount: issues.length,
|
|
996
|
+
issues,
|
|
997
|
+
nextSteps: issues.length
|
|
998
|
+
? ['Use build_extension_ui kind=api_usage or kind=notify for known-good snippets, then patch/update the extension.']
|
|
999
|
+
: ['Snippet matches the checked runtime composable/package rules. Still validate the final SFC before saving.'],
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
884
1002
|
const THEME_CLASS_INTENTS = {
|
|
885
1003
|
neutral_surface: {
|
|
886
1004
|
classes: 'eapp-surface-card eapp-radius-panel border eapp-divider',
|
|
@@ -1088,6 +1206,18 @@ export function buildExtensionUiSnippet(kind, input = {}) {
|
|
|
1088
1206
|
case 'upload_modal':
|
|
1089
1207
|
result = buildExtensionUploadModalSnippet(input);
|
|
1090
1208
|
break;
|
|
1209
|
+
case 'api_usage':
|
|
1210
|
+
result = buildExtensionApiUsageSnippet(input);
|
|
1211
|
+
break;
|
|
1212
|
+
case 'notify':
|
|
1213
|
+
result = buildExtensionNotifySnippet(input);
|
|
1214
|
+
break;
|
|
1215
|
+
case 'runtime_review':
|
|
1216
|
+
if (!input?.code) {
|
|
1217
|
+
throw new Error('build_extension_ui kind=runtime_review requires input.code.');
|
|
1218
|
+
}
|
|
1219
|
+
result = reviewExtensionRuntimeContract(input.code);
|
|
1220
|
+
break;
|
|
1091
1221
|
case 'theme_classes':
|
|
1092
1222
|
result = buildExtensionThemeClasses(input);
|
|
1093
1223
|
break;
|
|
@@ -1103,12 +1233,14 @@ export function buildExtensionUiSnippet(kind, input = {}) {
|
|
|
1103
1233
|
}
|
|
1104
1234
|
const uiReview = reviewExtensionUiContract(input.code);
|
|
1105
1235
|
const themeReview = reviewExtensionThemeContract(input.code);
|
|
1236
|
+
const runtimeReview = reviewExtensionRuntimeContract(input.code);
|
|
1106
1237
|
result = {
|
|
1107
|
-
action: '
|
|
1108
|
-
valid: uiReview.valid && themeReview.valid,
|
|
1109
|
-
issueCount: uiReview.issueCount + themeReview.issueCount,
|
|
1238
|
+
action: 'extension_ui_theme_runtime_contract_reviewed',
|
|
1239
|
+
valid: uiReview.valid && themeReview.valid && runtimeReview.valid,
|
|
1240
|
+
issueCount: uiReview.issueCount + themeReview.issueCount + runtimeReview.issueCount,
|
|
1110
1241
|
ui: uiReview,
|
|
1111
1242
|
theme: themeReview,
|
|
1243
|
+
runtime: runtimeReview,
|
|
1112
1244
|
};
|
|
1113
1245
|
break;
|
|
1114
1246
|
default:
|
|
@@ -1167,19 +1299,16 @@ function getExtensionThemeContract() {
|
|
|
1167
1299
|
'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>.',
|
|
1168
1300
|
'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.',
|
|
1169
1301
|
'Inputs and textareas should not add hover movement or decorative hover states; focus, invalid, disabled, and loading states must be explicit.',
|
|
1170
|
-
'For drawers, modals, page shell headers/actions, permission gates, empty states, resource lists, form editors, widgets, menu/account panel registries, tabs,
|
|
1171
|
-
'Use build_extension_ui kind=theme_classes for theme classes by intent, and kind=theme_review or
|
|
1302
|
+
'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.',
|
|
1303
|
+
'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.',
|
|
1172
1304
|
'Extension validation rejects UInput, UTextarea, USelect, USelectMenu, UInputMenu, UInputNumber, UInputTags, UInputTime, and UInputDate without class="w-full" unless marked data-compact or data-inline.',
|
|
1173
1305
|
'Use UBadge or token-backed badge spans for status. Keep badges legible in both themes with tokenized background, text, and border.',
|
|
1174
1306
|
],
|
|
1175
1307
|
appComposables: [
|
|
1176
|
-
'useApi(
|
|
1177
|
-
'
|
|
1178
|
-
'
|
|
1179
|
-
'
|
|
1180
|
-
'For mutations, set method in useApi options and call execute({ body }), execute({ id, body }), execute({ id }), or execute({ ids }) as appropriate.',
|
|
1181
|
-
'Use errorContext for readable default failures. Return true from onError only when the component fully handles the error locally.',
|
|
1182
|
-
'useNotify() provides async success/error/warning/info(title, description?) methods plus dismiss/clear. Do not use Nuxt toast object payloads with it.',
|
|
1308
|
+
'Call useApi() as a top-level setup composable. It returns data/error/pending/status refs plus execute/refresh; call or await execute()/refresh() from onMounted, watchers, or user actions when the request should run.',
|
|
1309
|
+
'Do not write useNotify shapes from memory. Use build_extension_ui kind=notify for known-good notification snippets. Use build_extension_ui kind=api_usage only when a generated fetch/mutation scaffold is useful.',
|
|
1310
|
+
'Use build_extension_ui kind=runtime_review or kind=review before saving extension code that includes useApi, useNotify, getPackages, or package loading.',
|
|
1311
|
+
'validate_extension_code and extension write tools reject static imports, useToast/useNotify.add misuse, and JSON.stringify useApi options.',
|
|
1183
1312
|
],
|
|
1184
1313
|
shellComponentContracts: {
|
|
1185
1314
|
CommonDrawer: [
|
|
@@ -1400,7 +1529,12 @@ export function validateExtensionCodeLocally(code) {
|
|
|
1400
1529
|
if (firstThemeError) {
|
|
1401
1530
|
throw new Error(`Invalid extension theme contract: ${firstThemeError.message} Rule: ${firstThemeError.rule}. ${firstThemeError.suggestion}`);
|
|
1402
1531
|
}
|
|
1403
|
-
|
|
1532
|
+
const runtimeReview = reviewExtensionRuntimeContract(code);
|
|
1533
|
+
const firstRuntimeError = runtimeReview.issues.find((issue) => issue.severity === 'error');
|
|
1534
|
+
if (firstRuntimeError) {
|
|
1535
|
+
throw new Error(`Invalid extension runtime contract: ${firstRuntimeError.message} Rule: ${firstRuntimeError.rule}. ${firstRuntimeError.suggestion}`);
|
|
1536
|
+
}
|
|
1537
|
+
return { componentCasing: 'passed', fieldWidth: 'passed', themeContract: 'passed', runtimeContract: 'passed' };
|
|
1404
1538
|
}
|
|
1405
1539
|
export async function validateExtensionCode(apiUrl, code, name) {
|
|
1406
1540
|
const localChecks = validateExtensionCodeLocally(code);
|
|
@@ -2587,10 +2721,8 @@ async function runExtensionWorkflow(apiUrl, opts) {
|
|
|
2587
2721
|
nextSteps: latestState.nextSteps,
|
|
2588
2722
|
guidance: [
|
|
2589
2723
|
'Call get_extension_theme_contract before generating or reviewing extension UI.',
|
|
2590
|
-
'
|
|
2591
|
-
'
|
|
2592
|
-
'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.',
|
|
2593
|
-
'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.',
|
|
2724
|
+
'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.',
|
|
2725
|
+
'Use build_extension_ui kind=api_usage, notify, theme_classes, runtime_review, theme_review, or review instead of hand-writing those contracts from memory.',
|
|
2594
2726
|
'Extension validation rejects common field controls without class="w-full" unless intentionally marked data-compact or data-inline.',
|
|
2595
2727
|
'PermissionGate renders the permitted slot directly and is UX-only; backend permissions and owner checks remain authoritative.',
|
|
2596
2728
|
'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.',
|
|
@@ -2700,7 +2832,7 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2700
2832
|
server.tool('build_extension_ui', [
|
|
2701
2833
|
'Lazy gateway for Enfyra admin extension UI builders.',
|
|
2702
2834
|
'Use this after get_enfyra_required_knowledge(scope="extension") when a high-contract extension UI snippet is needed.',
|
|
2703
|
-
'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.',
|
|
2835
|
+
'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.',
|
|
2704
2836
|
].join(' '), {
|
|
2705
2837
|
kind: z.enum([
|
|
2706
2838
|
'drawer',
|
|
@@ -2715,11 +2847,14 @@ export function registerPlatformOperationTools(server, ENFYRA_API_URL) {
|
|
|
2715
2847
|
'account_panel_item',
|
|
2716
2848
|
'tabs',
|
|
2717
2849
|
'upload_modal',
|
|
2850
|
+
'api_usage',
|
|
2851
|
+
'notify',
|
|
2852
|
+
'runtime_review',
|
|
2718
2853
|
'theme_classes',
|
|
2719
2854
|
'theme_review',
|
|
2720
2855
|
'review',
|
|
2721
2856
|
]).describe('Which extension UI contract builder/reviewer to run.'),
|
|
2722
|
-
input: z.record(z.any()).optional().default({}).describe('Builder input object. For kind=theme_classes, pass { intent }. For kind=theme_review/review, pass { code }.'),
|
|
2857
|
+
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 }.'),
|
|
2723
2858
|
extensionKnowledgeAckKey: extensionKnowledgeAckParam(z),
|
|
2724
2859
|
}, async ({ kind, input, extensionKnowledgeAckKey }) => {
|
|
2725
2860
|
assertExtensionKnowledgeAck(extensionKnowledgeAckKey);
|