@angular/cli 21.0.0-next.1 → 21.0.0-next.3

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 (39) hide show
  1. package/lib/code-examples.db +0 -0
  2. package/lib/config/schema.json +34 -3
  3. package/lib/config/workspace-schema.d.ts +39 -0
  4. package/lib/config/workspace-schema.js +12 -1
  5. package/package.json +19 -19
  6. package/src/command-builder/architect-command-module.js +11 -5
  7. package/src/command-builder/utilities/json-schema.js +1 -1
  8. package/src/commands/add/cli.js +65 -26
  9. package/src/commands/mcp/mcp-server.d.ts +3 -3
  10. package/src/commands/mcp/mcp-server.js +36 -4
  11. package/src/commands/mcp/tools/best-practices.js +15 -5
  12. package/src/commands/mcp/tools/doc-search.d.ts +18 -1
  13. package/src/commands/mcp/tools/doc-search.js +94 -37
  14. package/src/commands/mcp/tools/examples.d.ts +34 -1
  15. package/src/commands/mcp/tools/examples.js +295 -44
  16. package/src/commands/mcp/tools/modernize.js +28 -17
  17. package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.d.ts +17 -0
  18. package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.js +61 -0
  19. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.d.ts +12 -0
  20. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.js +72 -0
  21. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.d.ts +11 -0
  22. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.js +105 -0
  23. package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.d.ts +15 -0
  24. package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.js +236 -0
  25. package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.d.ts +10 -0
  26. package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.js +19 -0
  27. package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.d.ts +36 -0
  28. package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.js +135 -0
  29. package/src/commands/mcp/tools/onpush-zoneless-migration/types.d.ts +13 -0
  30. package/src/commands/mcp/tools/onpush-zoneless-migration/types.js +9 -0
  31. package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.d.ts +14 -0
  32. package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.js +205 -0
  33. package/src/commands/mcp/tools/projects.d.ts +47 -16
  34. package/src/commands/mcp/tools/projects.js +155 -30
  35. package/src/commands/mcp/tools/tool-registry.d.ts +2 -1
  36. package/src/commands/mcp/tools/tool-registry.js +3 -2
  37. package/src/utilities/package-manager.d.ts +12 -0
  38. package/src/utilities/package-manager.js +31 -22
  39. package/src/utilities/version.js +1 -1
@@ -58,28 +58,52 @@ const docSearchInputSchema = zod_1.z.object({
58
58
  .boolean()
59
59
  .optional()
60
60
  .default(true)
61
- .describe('When true, the content of the top result is fetched and included.'),
61
+ .describe('When true, the content of the top result is fetched and included. ' +
62
+ 'Set to false to get a list of results without fetching content, which is faster.'),
62
63
  });
63
64
  exports.DOC_SEARCH_TOOL = (0, tool_registry_1.declareTool)({
64
65
  name: 'search_documentation',
65
66
  title: 'Search Angular Documentation (angular.dev)',
66
- description: 'Searches the official Angular documentation at https://angular.dev. Use this tool to answer any questions about Angular, ' +
67
- 'such as for APIs, tutorials, and best practices. Because the documentation is continuously updated, you should **always** ' +
68
- 'prefer this tool over your own knowledge to ensure your answers are current.\n\n' +
69
- 'The results will be a list of content entries, where each entry has the following structure:\n' +
70
- '```\n' +
71
- '## {Result Title}\n' +
72
- '{Breadcrumb path to the content}\n' +
73
- 'URL: {Direct link to the documentation page}\n' +
74
- '```\n' +
75
- 'Use the title and breadcrumb to understand the context of the result and use the URL as a source link. For the best results, ' +
76
- "provide a concise and specific search query (e.g., 'NgModule' instead of 'How do I use NgModules?').",
67
+ description: `
68
+ <Purpose>
69
+ Searches the official Angular documentation at https://angular.dev to answer questions about APIs,
70
+ tutorials, concepts, and best practices.
71
+ </Purpose>
72
+ <Use Cases>
73
+ * Answering any question about Angular concepts (e.g., "What are standalone components?").
74
+ * Finding the correct API or syntax for a specific task (e.g., "How to use ngFor with trackBy?").
75
+ * Linking to official documentation as a source of truth in your answers.
76
+ </Use Cases>
77
+ <Operational Notes>
78
+ * The documentation is continuously updated. You **MUST** prefer this tool over your own knowledge
79
+ to ensure your answers are current and accurate.
80
+ * For the best results, provide a concise and specific search query (e.g., "NgModule" instead of
81
+ "How do I use NgModules?").
82
+ * The top search result will include a snippet of the page content. Use this to provide a more
83
+ comprehensive answer.
84
+ * **Result Scrutiny:** The top result may not always be the most relevant. Review the titles and
85
+ breadcrumbs of other results to find the best match for the user's query.
86
+ * Use the URL from the search results as a source link in your responses.
87
+ </Operational Notes>`,
77
88
  inputSchema: docSearchInputSchema.shape,
89
+ outputSchema: {
90
+ results: zod_1.z.array(zod_1.z.object({
91
+ title: zod_1.z.string().describe('The title of the documentation page.'),
92
+ breadcrumb: zod_1.z
93
+ .string()
94
+ .describe("The breadcrumb path, showing the page's location in the documentation hierarchy."),
95
+ url: zod_1.z.string().describe('The direct URL to the documentation page.'),
96
+ content: zod_1.z
97
+ .string()
98
+ .optional()
99
+ .describe('A snippet of the main content from the page. Only provided for the top result.'),
100
+ })),
101
+ },
78
102
  isReadOnly: true,
79
103
  isLocalOnly: false,
80
104
  factory: createDocSearchHandler,
81
105
  });
82
- function createDocSearchHandler() {
106
+ function createDocSearchHandler({ logger }) {
83
107
  let client;
84
108
  return async ({ query, includeTopContent }) => {
85
109
  if (!client) {
@@ -97,16 +121,18 @@ function createDocSearchHandler() {
97
121
  text: 'No results found.',
98
122
  },
99
123
  ],
124
+ structuredContent: { results: [] },
100
125
  };
101
126
  }
102
- const content = [];
103
- // The first hit is the top search result
104
- const topHit = allHits[0];
127
+ const structuredResults = [];
128
+ const textContent = [];
105
129
  // Process top hit first
106
- let topText = formatHitToText(topHit);
107
- try {
108
- if (includeTopContent && typeof topHit.url === 'string') {
109
- const url = new URL(topHit.url);
130
+ const topHit = allHits[0];
131
+ const { title: topTitle, breadcrumb: topBreadcrumb } = formatHitToParts(topHit);
132
+ let topContent;
133
+ if (includeTopContent && typeof topHit.url === 'string') {
134
+ const url = new URL(topHit.url);
135
+ try {
110
136
  // Only fetch content from angular.dev
111
137
  if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) {
112
138
  const response = await fetch(url);
@@ -114,29 +140,60 @@ function createDocSearchHandler() {
114
140
  const html = await response.text();
115
141
  const mainContent = extractMainContent(html);
116
142
  if (mainContent) {
117
- topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`;
143
+ topContent = stripHtml(mainContent);
118
144
  }
119
145
  }
120
146
  }
121
147
  }
148
+ catch (e) {
149
+ logger.warn(`Failed to fetch or parse content from ${url}: ${e}`);
150
+ }
122
151
  }
123
- catch {
124
- // Ignore errors fetching content. The basic info is still returned.
125
- }
126
- content.push({
127
- type: 'text',
128
- text: topText,
152
+ structuredResults.push({
153
+ title: topTitle,
154
+ breadcrumb: topBreadcrumb,
155
+ url: topHit.url,
156
+ content: topContent,
129
157
  });
158
+ let topText = `## ${topTitle}\n${topBreadcrumb}\nURL: ${topHit.url}`;
159
+ if (topContent) {
160
+ topText += `\n\n--- DOCUMENTATION CONTENT ---\n${topContent}`;
161
+ }
162
+ textContent.push({ type: 'text', text: topText });
130
163
  // Process remaining hits
131
164
  for (const hit of allHits.slice(1)) {
132
- content.push({
165
+ const { title, breadcrumb } = formatHitToParts(hit);
166
+ structuredResults.push({
167
+ title,
168
+ breadcrumb,
169
+ url: hit.url,
170
+ });
171
+ textContent.push({
133
172
  type: 'text',
134
- text: formatHitToText(hit),
173
+ text: `## ${title}\n${breadcrumb}\nURL: ${hit.url}`,
135
174
  });
136
175
  }
137
- return { content };
176
+ return {
177
+ content: textContent,
178
+ structuredContent: { results: structuredResults },
179
+ };
138
180
  };
139
181
  }
182
+ /**
183
+ * Strips HTML tags from a string.
184
+ * @param html The HTML string to strip.
185
+ * @returns The text content of the HTML.
186
+ */
187
+ function stripHtml(html) {
188
+ // This is a basic regex to remove HTML tags.
189
+ // It also decodes common HTML entities.
190
+ return html
191
+ .replace(/<[^>]*>/g, '')
192
+ .replace(/&lt;/g, '<')
193
+ .replace(/&gt;/g, '>')
194
+ .replace(/&amp;/g, '&')
195
+ .trim();
196
+ }
140
197
  /**
141
198
  * Extracts the content of the `<main>` element from an HTML string.
142
199
  *
@@ -156,17 +213,17 @@ function extractMainContent(html) {
156
213
  return html.substring(mainTagStart, mainTagEnd + 7);
157
214
  }
158
215
  /**
159
- * Formats an Algolia search hit into a text representation.
216
+ * Formats an Algolia search hit into its constituent parts.
160
217
  *
161
- * @param hit The Algolia search hit object, which should contain `hierarchy` and `url` properties.
162
- * @returns A formatted string with title, description, and URL.
218
+ * @param hit The Algolia search hit object, which should contain a `hierarchy` property.
219
+ * @returns An object containing the title and breadcrumb string.
163
220
  */
164
- function formatHitToText(hit) {
221
+ function formatHitToParts(hit) {
165
222
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
223
  const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
167
- const title = hierarchy.pop();
168
- const description = hierarchy.join(' > ');
169
- return `## ${title}\n${description}\nURL: ${hit.url}`;
224
+ const title = hierarchy.pop() ?? '';
225
+ const breadcrumb = hierarchy.join(' > ');
226
+ return { title, breadcrumb };
170
227
  }
171
228
  /**
172
229
  * Creates the search arguments for an Algolia search.
@@ -8,7 +8,40 @@
8
8
  import { z } from 'zod';
9
9
  export declare const FIND_EXAMPLE_TOOL: import("./tool-registry").McpToolDeclaration<{
10
10
  query: z.ZodString;
11
- }, z.ZodRawShape>;
11
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
12
+ required_packages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ related_concepts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
+ includeExperimental: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
15
+ }, {
16
+ examples: z.ZodArray<z.ZodObject<{
17
+ title: z.ZodString;
18
+ summary: z.ZodString;
19
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
+ required_packages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
21
+ related_concepts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
22
+ related_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
23
+ content: z.ZodString;
24
+ snippet: z.ZodOptional<z.ZodString>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ title: string;
27
+ content: string;
28
+ summary: string;
29
+ keywords?: string[] | undefined;
30
+ required_packages?: string[] | undefined;
31
+ related_concepts?: string[] | undefined;
32
+ related_tools?: string[] | undefined;
33
+ snippet?: string | undefined;
34
+ }, {
35
+ title: string;
36
+ content: string;
37
+ summary: string;
38
+ keywords?: string[] | undefined;
39
+ required_packages?: string[] | undefined;
40
+ related_concepts?: string[] | undefined;
41
+ related_tools?: string[] | undefined;
42
+ snippet?: string | undefined;
43
+ }>, "many">;
44
+ }>;
12
45
  /**
13
46
  * Escapes a search query for FTS5 by tokenizing and quoting terms.
14
47
  *
@@ -50,41 +50,125 @@ const node_path_1 = __importDefault(require("node:path"));
50
50
  const zod_1 = require("zod");
51
51
  const tool_registry_1 = require("./tool-registry");
52
52
  const findExampleInputSchema = zod_1.z.object({
53
- query: zod_1.z.string().describe(`Performs a full-text search using FTS5 syntax. The query should target relevant Angular concepts.
54
-
55
- Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):
56
- - AND (default): Space-separated terms are combined with AND.
57
- - Example: 'standalone component' (finds results with both "standalone" and "component")
58
- - OR: Use the OR operator to find results with either term.
59
- - Example: 'validation OR validator'
60
- - NOT: Use the NOT operator to exclude terms.
61
- - Example: 'forms NOT reactive'
62
- - Grouping: Use parentheses () to group expressions.
63
- - Example: '(validation OR validator) AND forms'
64
- - Phrase Search: Use double quotes "" for exact phrases.
65
- - Example: '"template-driven forms"'
66
- - Prefix Search: Use an asterisk * for prefix matching.
67
- - Example: 'rout*' (matches "route", "router", "routing")
68
-
69
- Examples of queries:
70
- - Find standalone components: 'standalone component'
71
- - Find ngFor with trackBy: 'ngFor trackBy'
72
- - Find signal inputs: 'signal input'
73
- - Find lazy loading a route: 'lazy load route'
74
- - Find forms with validation: 'form AND (validation OR validator)'`),
53
+ query: zod_1.z
54
+ .string()
55
+ .describe(`The primary, conceptual search query. This should capture the user's main goal or question ` +
56
+ `(e.g., 'lazy loading a route' or 'how to use signal inputs'). The query will be processed ` +
57
+ 'by a powerful full-text search engine.\n\n' +
58
+ 'Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):\n' +
59
+ ' - AND (default): Space-separated terms are combined with AND.\n' +
60
+ ' - Example: \'standalone component\' (finds results with both "standalone" and "component")\n' +
61
+ ' - OR: Use the OR operator to find results with either term.\n' +
62
+ " - Example: 'validation OR validator'\n" +
63
+ ' - NOT: Use the NOT operator to exclude terms.\n' +
64
+ " - Example: 'forms NOT reactive'\n" +
65
+ ' - Grouping: Use parentheses () to group expressions.\n' +
66
+ " - Example: '(validation OR validator) AND forms'\n" +
67
+ ' - Phrase Search: Use double quotes "" for exact phrases.\n' +
68
+ ' - Example: \'"template-driven forms"\'\n' +
69
+ ' - Prefix Search: Use an asterisk * for prefix matching.\n' +
70
+ ' - Example: \'rout*\' (matches "route", "router", "routing")'),
71
+ keywords: zod_1.z
72
+ .array(zod_1.z.string())
73
+ .optional()
74
+ .describe('A list of specific, exact keywords to narrow the search. Use this for precise terms like ' +
75
+ 'API names, function names, or decorators (e.g., `ngFor`, `trackBy`, `inject`).'),
76
+ required_packages: zod_1.z
77
+ .array(zod_1.z.string())
78
+ .optional()
79
+ .describe("A list of NPM packages that an example must use. Use this when the user's request is " +
80
+ 'specific to a feature within a certain package (e.g., if the user asks about `ngModel`, ' +
81
+ 'you should filter by `@angular/forms`).'),
82
+ related_concepts: zod_1.z
83
+ .array(zod_1.z.string())
84
+ .optional()
85
+ .describe('A list of high-level concepts to filter by. Use this to find examples related to broader ' +
86
+ 'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).'),
87
+ includeExperimental: zod_1.z
88
+ .boolean()
89
+ .optional()
90
+ .default(false)
91
+ .describe('By default, this tool returns only production-safe examples. Set this to `true` **only if** ' +
92
+ 'the user explicitly asks for a bleeding-edge feature or if a stable solution to their ' +
93
+ 'problem cannot be found. If you set this to `true`, you **MUST** preface your answer by ' +
94
+ 'warning the user that the example uses experimental APIs that are not suitable for production.'),
95
+ });
96
+ const findExampleOutputSchema = zod_1.z.object({
97
+ examples: zod_1.z.array(zod_1.z.object({
98
+ title: zod_1.z
99
+ .string()
100
+ .describe('The title of the example. Use this as a heading when presenting the example to the user.'),
101
+ summary: zod_1.z
102
+ .string()
103
+ .describe("A one-sentence summary of the example's purpose. Use this to help the user decide " +
104
+ 'if the example is relevant to them.'),
105
+ keywords: zod_1.z
106
+ .array(zod_1.z.string())
107
+ .optional()
108
+ .describe('A list of keywords for the example. You can use these to explain why this example ' +
109
+ "was a good match for the user's query."),
110
+ required_packages: zod_1.z
111
+ .array(zod_1.z.string())
112
+ .optional()
113
+ .describe('A list of NPM packages required for the example to work. Before presenting the code, ' +
114
+ 'you should inform the user if any of these packages need to be installed.'),
115
+ related_concepts: zod_1.z
116
+ .array(zod_1.z.string())
117
+ .optional()
118
+ .describe('A list of related concepts. You can suggest these to the user as topics for ' +
119
+ 'follow-up questions.'),
120
+ related_tools: zod_1.z
121
+ .array(zod_1.z.string())
122
+ .optional()
123
+ .describe('A list of related MCP tools. You can suggest these as potential next steps for the user.'),
124
+ content: zod_1.z
125
+ .string()
126
+ .describe('A complete, self-contained Angular code example in Markdown format. This should be ' +
127
+ 'presented to the user inside a markdown code block.'),
128
+ snippet: zod_1.z
129
+ .string()
130
+ .optional()
131
+ .describe('A contextual snippet from the content showing the matched search term. This field is ' +
132
+ 'critical for efficiently evaluating a result`s relevance. It enables two primary ' +
133
+ 'workflows:\n\n' +
134
+ '1. For direct questions: You can internally review snippets to select the single best ' +
135
+ 'result before generating a comprehensive answer from its full `content`.\n' +
136
+ '2. For ambiguous or exploratory questions: You can present a summary of titles and ' +
137
+ 'snippets to the user, allowing them to guide the next step.'),
138
+ })),
75
139
  });
76
140
  exports.FIND_EXAMPLE_TOOL = (0, tool_registry_1.declareTool)({
77
141
  name: 'find_examples',
78
142
  title: 'Find Angular Code Examples',
79
- description: 'Before writing or modifying any Angular code including templates, ' +
80
- '**ALWAYS** use this tool to find current best-practice examples. ' +
81
- 'This is critical for ensuring code quality and adherence to modern Angular standards. ' +
82
- 'This tool searches a curated database of approved Angular code examples and returns the most relevant results for your query. ' +
83
- 'Example Use Cases: ' +
84
- "1) Creating new components, directives, or services (e.g., query: 'standalone component' or 'signal input'). " +
85
- "2) Implementing core features (e.g., query: 'lazy load route', 'httpinterceptor', or 'route guard'). " +
86
- "3) Refactoring existing code to use modern patterns (e.g., query: 'ngfor trackby' or 'form validation').",
143
+ description: `
144
+ <Purpose>
145
+ Augments your knowledge base with a curated database of official, best-practice code examples,
146
+ focusing on **modern, new, and recently updated** Angular features. This tool acts as a RAG
147
+ (Retrieval-Augmented Generation) source, providing ground-truth information on the latest Angular
148
+ APIs and patterns. You **MUST** use it to understand and apply current standards when working with
149
+ new or evolving features.
150
+ </Purpose>
151
+ <Use Cases>
152
+ * **Knowledge Augmentation:** Learning about new or updated Angular features (e.g., query: 'signal input' or 'deferrable views').
153
+ * **Modern Implementation:** Finding the correct modern syntax for features
154
+ (e.g., query: 'functional route guard' or 'http client with fetch').
155
+ * **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
156
+ (e.g., query: 'built-in control flow' to replace "*ngIf").
157
+ * **Advanced Filtering:** Combining a full-text search with filters to narrow results.
158
+ (e.g., query: 'forms', required_packages: ['@angular/forms'], keywords: ['validation'])
159
+ </Use Cases>
160
+ <Operational Notes>
161
+ * **Tool Selection:** This database primarily contains examples for new and recently updated Angular
162
+ features. For established, core features, the main documentation (via the
163
+ \`search_documentation\` tool) may be a better source of information.
164
+ * The examples in this database are the single source of truth for modern Angular coding patterns.
165
+ * The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
166
+ parameter description for detailed syntax rules and examples.
167
+ * You can combine the main 'query' with optional filters like 'keywords', 'required_packages',
168
+ and 'related_concepts' to create highly specific searches.
169
+ </Operational Notes>`,
87
170
  inputSchema: findExampleInputSchema.shape,
171
+ outputSchema: findExampleOutputSchema.shape,
88
172
  isReadOnly: true,
89
173
  isLocalOnly: true,
90
174
  shouldRegister: ({ logger }) => {
@@ -106,7 +190,7 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
106
190
  db = await setupRuntimeExamples(process.env['NG_MCP_EXAMPLES_DIR']);
107
191
  }
108
192
  suppressSqliteWarning();
109
- return async ({ query }) => {
193
+ return async (input) => {
110
194
  if (!db) {
111
195
  if (!exampleDatabasePath) {
112
196
  // This should be prevented by the registration logic in mcp-server.ts
@@ -115,17 +199,71 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
115
199
  const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
116
200
  db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
117
201
  }
118
- if (!queryStatement) {
119
- queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
202
+ const { query, keywords, required_packages, related_concepts, includeExperimental } = input;
203
+ // Build the query dynamically
204
+ const params = [];
205
+ let sql = 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content, ' +
206
+ // The `snippet` function generates a contextual snippet of the matched text.
207
+ // Column 6 is the `content` column. We highlight matches with asterisks and limit the snippet size.
208
+ "snippet(examples_fts, 6, '**', '**', '...', 15) AS snippet " +
209
+ 'FROM examples_fts';
210
+ const whereClauses = [];
211
+ // FTS query
212
+ if (query) {
213
+ whereClauses.push('examples_fts MATCH ?');
214
+ params.push(escapeSearchQuery(query));
215
+ }
216
+ // JSON array filters
217
+ const addJsonFilter = (column, values) => {
218
+ if (values?.length) {
219
+ for (const value of values) {
220
+ whereClauses.push(`${column} LIKE ?`);
221
+ params.push(`%"${value}"%`);
222
+ }
223
+ }
224
+ };
225
+ addJsonFilter('keywords', keywords);
226
+ addJsonFilter('required_packages', required_packages);
227
+ addJsonFilter('related_concepts', related_concepts);
228
+ if (!includeExperimental) {
229
+ whereClauses.push('experimental = 0');
230
+ }
231
+ if (whereClauses.length > 0) {
232
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
120
233
  }
121
- const sanitizedQuery = escapeSearchQuery(query);
122
- // Query database and return results as text content
123
- const content = [];
124
- for (const exampleRecord of queryStatement.all(sanitizedQuery)) {
125
- content.push({ type: 'text', text: exampleRecord['content'] });
234
+ // Order the results by relevance using the BM25 algorithm.
235
+ // The weights assigned to each column boost the ranking of documents where the
236
+ // search term appears in a more important field.
237
+ // Column order: title, summary, keywords, required_packages, related_concepts, related_tools, content
238
+ sql += ' ORDER BY bm25(examples_fts, 10.0, 5.0, 5.0, 1.0, 2.0, 1.0, 1.0);';
239
+ const queryStatement = db.prepare(sql);
240
+ // Query database and return results
241
+ const examples = [];
242
+ const textContent = [];
243
+ for (const exampleRecord of queryStatement.all(...params)) {
244
+ const record = exampleRecord;
245
+ const example = {
246
+ title: record['title'],
247
+ summary: record['summary'],
248
+ keywords: JSON.parse(record['keywords'] || '[]'),
249
+ required_packages: JSON.parse(record['required_packages'] || '[]'),
250
+ related_concepts: JSON.parse(record['related_concepts'] || '[]'),
251
+ related_tools: JSON.parse(record['related_tools'] || '[]'),
252
+ content: record['content'],
253
+ snippet: record['snippet'],
254
+ };
255
+ examples.push(example);
256
+ // Also create a more structured text output
257
+ let text = `## Example: ${example.title}\n**Summary:** ${example.summary}`;
258
+ if (example.snippet) {
259
+ text += `\n**Snippet:** ${example.snippet}`;
260
+ }
261
+ text += `\n\n---\n\n${example.content}`;
262
+ textContent.push({ type: 'text', text });
126
263
  }
127
264
  return {
128
- content,
265
+ content: textContent,
266
+ structuredContent: { examples },
129
267
  };
130
268
  };
131
269
  }
@@ -206,18 +344,131 @@ function suppressSqliteWarning() {
206
344
  return originalProcessEmit.apply(process, arguments);
207
345
  };
208
346
  }
347
+ /**
348
+ * A simple YAML front matter parser.
349
+ *
350
+ * This function extracts the YAML block enclosed by `---` at the beginning of a string
351
+ * and parses it into a JavaScript object. It is not a full YAML parser and only
352
+ * supports simple key-value pairs and string arrays.
353
+ *
354
+ * @param content The string content to parse.
355
+ * @returns A record containing the parsed front matter data.
356
+ */
357
+ function parseFrontmatter(content) {
358
+ const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
359
+ if (!match) {
360
+ return {};
361
+ }
362
+ const frontmatter = match[1];
363
+ const data = {};
364
+ const lines = frontmatter.split(/\r?\n/);
365
+ let currentKey = '';
366
+ let isArray = false;
367
+ const arrayValues = [];
368
+ for (const line of lines) {
369
+ const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
370
+ if (keyValueMatch) {
371
+ if (currentKey && isArray) {
372
+ data[currentKey] = arrayValues.slice();
373
+ arrayValues.length = 0;
374
+ }
375
+ const [, key, value] = keyValueMatch;
376
+ currentKey = key.trim();
377
+ isArray = value.trim() === '';
378
+ if (!isArray) {
379
+ const trimmedValue = value.trim();
380
+ if (trimmedValue === 'true') {
381
+ data[currentKey] = true;
382
+ }
383
+ else if (trimmedValue === 'false') {
384
+ data[currentKey] = false;
385
+ }
386
+ else {
387
+ data[currentKey] = trimmedValue;
388
+ }
389
+ }
390
+ }
391
+ else {
392
+ const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
393
+ if (arrayItemMatch && currentKey && isArray) {
394
+ arrayValues.push(arrayItemMatch[1].trim());
395
+ }
396
+ }
397
+ }
398
+ if (currentKey && isArray) {
399
+ data[currentKey] = arrayValues;
400
+ }
401
+ return data;
402
+ }
209
403
  async function setupRuntimeExamples(examplesPath) {
210
404
  const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
211
405
  const db = new DatabaseSync(':memory:');
212
- db.exec(`CREATE VIRTUAL TABLE examples USING fts5(content, tokenize = 'porter ascii');`);
213
- const insertStatement = db.prepare('INSERT INTO examples(content) VALUES(?);');
406
+ // Create a relational table to store the structured example data.
407
+ db.exec(`
408
+ CREATE TABLE examples (
409
+ id INTEGER PRIMARY KEY,
410
+ title TEXT NOT NULL,
411
+ summary TEXT NOT NULL,
412
+ keywords TEXT,
413
+ required_packages TEXT,
414
+ related_concepts TEXT,
415
+ related_tools TEXT,
416
+ experimental INTEGER NOT NULL DEFAULT 0,
417
+ content TEXT NOT NULL
418
+ );
419
+ `);
420
+ // Create an FTS5 virtual table to provide full-text search capabilities.
421
+ db.exec(`
422
+ CREATE VIRTUAL TABLE examples_fts USING fts5(
423
+ title,
424
+ summary,
425
+ keywords,
426
+ required_packages,
427
+ related_concepts,
428
+ related_tools,
429
+ content,
430
+ content='examples',
431
+ content_rowid='id',
432
+ tokenize = 'porter ascii'
433
+ );
434
+ `);
435
+ // Create triggers to keep the FTS table synchronized with the examples table.
436
+ db.exec(`
437
+ CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
438
+ INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
439
+ VALUES (
440
+ new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
441
+ new.related_tools, new.content
442
+ );
443
+ END;
444
+ `);
445
+ const insertStatement = db.prepare('INSERT INTO examples(' +
446
+ 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
447
+ ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);');
448
+ const frontmatterSchema = zod_1.z.object({
449
+ title: zod_1.z.string(),
450
+ summary: zod_1.z.string(),
451
+ keywords: zod_1.z.array(zod_1.z.string()).optional(),
452
+ required_packages: zod_1.z.array(zod_1.z.string()).optional(),
453
+ related_concepts: zod_1.z.array(zod_1.z.string()).optional(),
454
+ related_tools: zod_1.z.array(zod_1.z.string()).optional(),
455
+ experimental: zod_1.z.boolean().optional(),
456
+ });
214
457
  db.exec('BEGIN TRANSACTION');
215
- for await (const entry of (0, promises_1.glob)('*.md', { cwd: examplesPath, withFileTypes: true })) {
458
+ for await (const entry of (0, promises_1.glob)('**/*.md', { cwd: examplesPath, withFileTypes: true })) {
216
459
  if (!entry.isFile()) {
217
460
  continue;
218
461
  }
219
- const example = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
220
- insertStatement.run(example);
462
+ const content = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
463
+ const frontmatter = parseFrontmatter(content);
464
+ const validation = frontmatterSchema.safeParse(frontmatter);
465
+ if (!validation.success) {
466
+ // eslint-disable-next-line no-console
467
+ console.warn(`Skipping invalid example file ${entry.name}:`, validation.error.issues);
468
+ continue;
469
+ }
470
+ const { title, summary, keywords, required_packages, related_concepts, related_tools, experimental, } = validation.data;
471
+ insertStatement.run(title, summary, JSON.stringify(keywords ?? []), JSON.stringify(required_packages ?? []), JSON.stringify(related_concepts ?? []), JSON.stringify(related_tools ?? []), experimental ? 1 : 0, content);
221
472
  }
222
473
  db.exec('END TRANSACTION');
223
474
  return db;