@angular/cli
Version:
CLI tool for Angular
206 lines (205 loc) • 8.39 kB
JavaScript
;
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOC_SEARCH_TOOL = void 0;
const node_crypto_1 = require("node:crypto");
const zod_1 = require("zod");
const constants_1 = require("../constants");
const tool_registry_1 = require("./tool-registry");
const ALGOLIA_APP_ID = 'L1XWT2UJ7F';
// https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key
// This is a search only, rate limited key. It is sent within the URL of the query request.
// This is not the actual key.
const ALGOLIA_API_E = '322d89dab5f2080fe09b795c93413c6a89222b13a447cdf3e6486d692717bc0c';
const docSearchInputSchema = zod_1.z.object({
query: zod_1.z
.string()
.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.'),
});
exports.DOC_SEARCH_TOOL = (0, tool_registry_1.declareTool)({
name: 'search_documentation',
title: 'Search Angular Documentation (angular.dev)',
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?').",
inputSchema: docSearchInputSchema.shape,
isReadOnly: true,
isLocalOnly: false,
factory: createDocSearchHandler,
});
function createDocSearchHandler() {
let client;
return async ({ query, includeTopContent }) => {
if (!client) {
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 { searchClient } = await Promise.resolve().then(() => __importStar(require('algoliasearch')));
client = searchClient(ALGOLIA_APP_ID, dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'));
}
const { results } = await client.search(createSearchArguments(query));
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 = extractMainContent(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: formatHitToText(hit),
});
}
return { content };
};
}
/**
* Extracts the content of the `<main>` element from an HTML string.
*
* @param html The HTML content of a page.
* @returns The content of the `<main>` element, or `undefined` if not found.
*/
function extractMainContent(html) {
const mainTagStart = html.indexOf('<main');
if (mainTagStart === -1) {
return undefined;
}
const mainTagEnd = html.lastIndexOf('</main>');
if (mainTagEnd <= mainTagStart) {
return undefined;
}
// Add 7 to include '</main>'
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.
*
* The arguments are based on the search implementation in `adev`.
*
* @param query The search query string.
* @returns The search arguments for the Algolia client.
*/
function createSearchArguments(query) {
// Search arguments are based on adev's search service:
// https://github.com/angular/angular/blob/4b614fbb3263d344dbb1b18fff24cb09c5a7582d/adev/shared-docs/services/search.service.ts#L58
return [
{
// TODO: Consider major version specific indices once available
indexName: 'angular_v17',
params: {
query,
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
],
hitsPerPage: 10,
},
type: 'default',
},
];
}