You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 4-6.RSVP
Socket
Book a DemoInstallSign in
Socket

@angular/cli

Package Overview
Dependencies
Maintainers
2
Versions
942
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@angular/cli - npm Package Compare versions

Comparing version

to
20.2.0-next.1

src/commands/mcp/tools/best-practices.d.ts

32

package.json
{
"name": "@angular/cli",
"version": "20.2.0-next.0",
"version": "20.2.0-next.1",
"description": "CLI tool for Angular",

@@ -28,11 +28,11 @@ "main": "lib/cli/index.js",

"dependencies": {
"@angular-devkit/architect": "0.2002.0-next.0",
"@angular-devkit/core": "20.2.0-next.0",
"@angular-devkit/schematics": "20.2.0-next.0",
"@inquirer/prompts": "7.6.0",
"@angular-devkit/architect": "0.2002.0-next.1",
"@angular-devkit/core": "20.2.0-next.1",
"@angular-devkit/schematics": "20.2.0-next.1",
"@inquirer/prompts": "7.7.1",
"@listr2/prompt-adapter-inquirer": "3.0.1",
"@modelcontextprotocol/sdk": "1.15.1",
"@schematics/angular": "20.2.0-next.0",
"@modelcontextprotocol/sdk": "1.16.0",
"@schematics/angular": "20.2.0-next.1",
"@yarnpkg/lockfile": "1.1.0",
"algoliasearch": "5.32.0",
"algoliasearch": "5.34.0",
"ini": "5.0.0",

@@ -52,10 +52,10 @@ "jsonc-parser": "3.3.1",

"packageGroup": {
"@angular/cli": "20.2.0-next.0",
"@angular/build": "20.2.0-next.0",
"@angular/ssr": "20.2.0-next.0",
"@angular-devkit/architect": "0.2002.0-next.0",
"@angular-devkit/build-angular": "20.2.0-next.0",
"@angular-devkit/build-webpack": "0.2002.0-next.0",
"@angular-devkit/core": "20.2.0-next.0",
"@angular-devkit/schematics": "20.2.0-next.0"
"@angular/cli": "20.2.0-next.1",
"@angular/build": "20.2.0-next.1",
"@angular/ssr": "20.2.0-next.1",
"@angular-devkit/architect": "0.2002.0-next.1",
"@angular-devkit/build-angular": "20.2.0-next.1",
"@angular-devkit/build-webpack": "0.2002.0-next.1",
"@angular-devkit/core": "20.2.0-next.1",
"@angular-devkit/schematics": "20.2.0-next.1"
}

@@ -62,0 +62,0 @@ },

@@ -22,16 +22,19 @@ "use strict";

const positional = groups[usageInstance.getPositionalGroupName()];
const seen = new Set();
const hidden = new Set(hiddenOptions);
const normalizeOptions = [];
const allAliases = new Set([...Object.values(aliases).flat()]);
// Reverted order of https://github.com/yargs/yargs/blob/971e351705f0fbc5566c6ed1dfd707fa65e11c0d/lib/usage.ts#L419-L424
for (const [names, type] of [
[number, 'number'],
[array, 'array'],
[string, 'string'],
[boolean, 'boolean'],
[number, 'number'],
]) {
for (const name of names) {
if (allAliases.has(name) || hidden.has(name)) {
if (allAliases.has(name) || hidden.has(name) || seen.has(name)) {
// Ignore hidden, aliases and already visited option.
continue;
}
seen.add(name);
const positionalIndex = positional?.indexOf(name) ?? -1;

@@ -38,0 +41,0 @@ const alias = aliases[name];

@@ -28,7 +28,11 @@ "use strict";

case 'update':
logs.push(`${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
logs.push(
// TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution.
`${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
files.add(eventPath);
break;
case 'create':
logs.push(`${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
logs.push(
// TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution.
`${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
files.add(eventPath);

@@ -35,0 +39,0 @@ break;

@@ -17,5 +17,6 @@ "use strict";

const node_path_1 = __importDefault(require("node:path"));
const zod_1 = require("zod");
const version_1 = require("../../utilities/version");
const best_practices_1 = require("./tools/best-practices");
const doc_search_1 = require("./tools/doc-search");
const projects_1 = require("./tools/projects");
async function createMcpServer(context) {

@@ -41,70 +42,6 @@ const server = new mcp_js_1.McpServer({

});
server.registerTool('list_projects', {
title: 'List Angular Projects',
description: 'Lists the names of all applications and libraries defined within an Angular workspace. ' +
'It reads the `angular.json` configuration file to identify the projects. ',
annotations: {
readOnlyHint: true,
},
outputSchema: {
projects: zod_1.z.array(zod_1.z.object({
name: zod_1.z
.string()
.describe('The name of the project, as defined in the `angular.json` file.'),
type: zod_1.z
.enum(['application', 'library'])
.optional()
.describe(`The type of the project, either 'application' or 'library'.`),
root: zod_1.z
.string()
.describe('The root directory of the project, relative to the workspace root.'),
sourceRoot: zod_1.z
.string()
.describe(`The root directory of the project's source files, relative to the workspace root.`),
selectorPrefix: zod_1.z
.string()
.optional()
.describe('The prefix to use for component selectors.' +
` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`),
})),
},
}, async () => {
const { workspace } = context;
if (!workspace) {
return {
content: [
{
type: 'text',
text: 'No Angular workspace found.' +
' An `angular.json` file, which marks the root of a workspace,' +
' could not be located in the current directory or any of its parent directories.',
},
],
};
}
const projects = [];
// Convert to output format
for (const [name, project] of workspace.projects.entries()) {
projects.push({
name,
type: project.extensions['projectType'],
root: project.root,
sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
selectorPrefix: project.extensions['prefix'],
});
}
// The structuredContent field is newer and may not be supported by all hosts.
// A text representation of the content is also provided for compatibility.
return {
content: [
{
type: 'text',
text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`,
},
],
structuredContent: { projects },
};
});
(0, best_practices_1.registerBestPracticesTool)(server);
(0, projects_1.registerListProjectsTool)(server, context);
await (0, doc_search_1.registerDocSearchTool)(server);
return server;
}

@@ -63,5 +63,13 @@ "use strict";

title: 'Search Angular Documentation (angular.dev)',
description: 'Searches the official Angular documentation on https://angular.dev.' +
' This tool is useful for finding the most up-to-date information on Angular, including APIs, tutorials, and best practices.' +
' Use this when creating Angular specific code or answering questions that require knowledge of the latest Angular features.',
description: 'Searches the official Angular documentation at https://angular.dev. Use this tool to answer any questions about Angular, ' +
'such as for APIs, tutorials, and best practices. Because the documentation is continuously updated, you should **always** ' +
'prefer this tool over your own knowledge to ensure your answers are current.\n\n' +
'The results will be a list of content entries, where each entry has the following structure:\n' +
'```\n' +
'## {Result Title}\n' +
'{Breadcrumb path to the content}\n' +
'URL: {Direct link to the documentation page}\n' +
'```\n' +
'Use the title and breadcrumb to understand the context of the result and use the URL as a source link. For the best results, ' +
"provide a concise and specific search query (e.g., 'NgModule' instead of 'How do I use NgModules?').",
annotations: {

@@ -73,6 +81,10 @@ readOnlyHint: true,

.string()
.describe('The search query to use when searching the Angular documentation.' +
' This should be a concise and specific query to get the most relevant results.'),
.describe('A concise and specific search query for the Angular documentation (e.g., "NgModule" or "standalone components").'),
includeTopContent: zod_1.z
.boolean()
.optional()
.default(true)
.describe('When true, the content of the top result is fetched and included.'),
},
}, async ({ query }) => {
}, async ({ query, includeTopContent }) => {
if (!client) {

@@ -84,13 +96,48 @@ const dcip = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', (constants_1.k1 + ALGOLIA_APP_ID).padEnd(32, '^'), constants_1.iv).setAuthTag(Buffer.from(constants_1.at, 'base64'));

const { results } = await client.search(createSearchArguments(query));
// Convert results into text content entries instead of stringifying the entire object
const content = results.flatMap((result) => result.hits.map((hit) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
const title = hierarchy.pop();
const description = hierarchy.join(' > ');
const allHits = results.flatMap((result) => result.hits);
if (allHits.length === 0) {
return {
content: [
{
type: 'text',
text: 'No results found.',
},
],
};
}
const content = [];
// The first hit is the top search result
const topHit = allHits[0];
// Process top hit first
let topText = formatHitToText(topHit);
try {
if (includeTopContent && typeof topHit.url === 'string') {
const url = new URL(topHit.url);
// Only fetch content from angular.dev
if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) {
const response = await fetch(url);
if (response.ok) {
const html = await response.text();
const mainContent = extractBodyContent(html);
if (mainContent) {
topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`;
}
}
}
}
}
catch {
// Ignore errors fetching content. The basic info is still returned.
}
content.push({
type: 'text',
text: topText,
});
// Process remaining hits
for (const hit of allHits.slice(1)) {
content.push({
type: 'text',
text: `## ${title}\n${description}\nURL: ${hit.url}`,
};
}));
text: formatHitToText(hit),
});
}
return { content };

@@ -100,2 +147,34 @@ });

/**
* Extracts the content of the `<body>` element from an HTML string.
*
* @param html The HTML content of a page.
* @returns The content of the `<body>` element, or `undefined` if not found.
*/
function extractBodyContent(html) {
// TODO: Use '<main>' element instead of '<body>' when available in angular.dev HTML.
const mainTagStart = html.indexOf('<body');
if (mainTagStart === -1) {
return undefined;
}
const mainTagEnd = html.lastIndexOf('</body>');
if (mainTagEnd <= mainTagStart) {
return undefined;
}
// Add 7 to include '</body>'
return html.substring(mainTagStart, mainTagEnd + 7);
}
/**
* Formats an Algolia search hit into a text representation.
*
* @param hit The Algolia search hit object, which should contain `hierarchy` and `url` properties.
* @returns A formatted string with title, description, and URL.
*/
function formatHitToText(hit) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
const title = hierarchy.pop();
const description = hierarchy.join(' > ');
return `## ${title}\n${description}\nURL: ${hit.url}`;
}
/**
* Creates the search arguments for an Algolia search.

@@ -102,0 +181,0 @@ *

@@ -25,2 +25,2 @@ "use strict";

}
exports.VERSION = new Version('20.2.0-next.0');
exports.VERSION = new Version('20.2.0-next.1');

Sorry, the diff of this file is too big to display