pluginParsers.ts ×47

Frontier kind: Code frontier

unlabeled · c_b369b914d738

1120 tests · 13711 LOC · 52 files · introduces 0 tests · 410 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
47 ranges410 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1066 ranges13711 lines · 52 files · Browse complete extent
All tests (intent)
1120 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 410 introduced LOC across 47 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentPlugins/common/pluginParsers.ts 410 introduced LOC · 47 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- pluginParsers.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { parse as parseJSONC } from '../../../base/common/json.js';
7 > import { cloneAndChange, equals as objectEquals } from '../../../base/common/objects.js';
8 > import { isAbsolute } from '../../../base/common/path.js';
9 > import { basename, extname, isEqualOrParent, joinPath, normalizePath, isEqual as isURLEquals, dirname } from '../../../base/common/resources.js';
10 > import { escapeRegExpCharacters } from '../../../base/common/strings.js';
11 > import { hasKey, Mutable } from '../../../base/common/types.js';
12 > import { URI } from '../../../base/common/uri.js';
13 > import { IFileService } from '../../files/common/files.js';
14 > import { parseFrontMatter } from '../../../base/common/yaml.js';
15 > import { IMcpRemoteServerConfiguration, IMcpServerConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../mcp/common/mcpPlatformTypes.js';
16 > import { CustomizationType, McpServerStatus, type AgentCustomization, type HookCustomization, type McpServerCustomization, type RuleCustomization, type SkillCustomization } from '../../agentHost/common/state/protocol/state.js';
17 > import { DEFAULT_MCP_APP } from '../../agentHost/common/state/protocol/mcpAppDefaults.js';
18 > import { customizationId } from '../../agentHost/common/state/sessionState.js';
19 > import { readAgentPluginManifest } from './agentPluginParser.js';
20 >
21 > // ---------------------------------------------------------------------------
22 > // Types
23 > // ---------------------------------------------------------------------------
24 >
25 > /** A single hook command to execute. Platform resolution happens at conversion time. */
26 > export interface IParsedHookCommand {
27 > /** Cross-platform default command. */
28 > readonly command?: string;
29 > /** Windows-specific command. */
30 > readonly windows?: string;
31 > /** Linux-specific command. */
32 > readonly linux?: string;
33 > /** macOS-specific command. */
34 > readonly osx?: string;
35 > /** Working directory. */
36 > readonly cwd?: URI;
37 > /** Environment variables. */
38 > readonly env?: Record<string, string>;
39 > /** Timeout in seconds. */
40 > readonly timeout?: number;
41 > /** URI of the file this hook was defined in. */
42 > readonly sourceUri?: URI;
43 > }
44 >
45 > export namespace IParsedHookCommand {
46 > export function isEquals(a: IParsedHookCommand | undefined, b: IParsedHookCommand | undefined): boolean {
47 if (a === b) {
48 return true;
60 && isURLEquals(a.sourceUri, b.sourceUri);
61 }
63 >
64 > /** A group of hooks for a single lifecycle event. */
65 > export interface IParsedHookGroup {
66 > /** Canonical hook type identifier (e.g. `'SessionStart'`, `'PreToolUse'`). */
67 > readonly type: string;
68 > /** The commands to execute for this hook type. */
69 > readonly commands: readonly IParsedHookCommand[];
70 > /** URI where this hook is defined. */
71 > readonly uri: URI;
72 > /** Original key as it appears in the hook file. */
73 > readonly originalId: string;
74 > /**
75 > * Protocol-level projection of this hook group as a child customization.
76 > * Multiple groups parsed from the same file share the same `customization.id`
77 > * so consumers can dedupe by id when collecting customizations.
78 > */
79 > readonly customization: HookCustomization;
80 > }
81 >
82 > export interface IMcpServerDefinition {
83 > readonly name: string;
84 > readonly configuration: IMcpServerConfiguration;
85 > readonly uri: URI;
86 > /** Protocol-level projection of this MCP server as a child customization. */
87 > readonly customization: McpServerCustomization;
88 > }
89 >
90 > /** A named resource (skill, agent, command, or instruction) within a plugin. */
91 > export interface INamedPluginResource {
92 > readonly uri: URI;
93 > readonly name: string;
94 > /**
95 > * Optional short description, populated for resources whose readers
96 > * parse it from the file's YAML frontmatter (e.g. agents).
97 > */
98 > readonly description?: string;
99 > }
100 >
101 > /** A parsed agent paired with its protocol-level child customization. */
102 > export interface IParsedAgent extends INamedPluginResource {
103 > readonly customization: AgentCustomization;
104 > }
105 >
106 > /** A parsed skill paired with its protocol-level child customization. */
107 > export interface IParsedSkill extends INamedPluginResource {
108 > readonly customization: SkillCustomization;
109 > }
110 >
111 > /** A parsed rule (instruction) paired with its protocol-level child customization. */
112 > export interface IParsedRule extends INamedPluginResource {
113 > readonly customization: RuleCustomization;
114 > }
115 >
116 > /** The result of parsing a single plugin directory. */
117 > export interface IParsedPlugin {
118 > readonly format: PluginFormat;
119 > readonly hooks: readonly IParsedHookGroup[];
120 > readonly mcpServers: readonly IMcpServerDefinition[];
121 > readonly skills: readonly IParsedSkill[];
122 > readonly agents: readonly IParsedAgent[];
123 > readonly instructions: readonly IParsedRule[];
124 > }
125 >
126 > // ---------------------------------------------------------------------------
127 > // Plugin format detection
128 > // ---------------------------------------------------------------------------
129 >
130 > export const enum PluginFormat {
131 > Copilot,
132 > Claude,
133 > OpenPlugin,
134 > AgentPlugin,
135 > }
136 >
137 > export interface IPluginFormatConfig {
138 > readonly format: PluginFormat;
139 > readonly manifestPath: string;
140 > readonly hookConfigPath: string;
141 > readonly componentPaths?: Readonly<Partial<Record<PluginComponent, string | false>>>;
142 > readonly requiresManifest?: boolean;
143 > readonly pluginRootTokens: readonly string[];
144 > readonly pluginRootEnvVars: readonly string[];
145 > /** Parses hooks from a JSON object using the format's conventions. */
146 > parseHooks(hookUri: URI, json: unknown, pluginUri: URI, workspaceRoot: URI | undefined, userHome: URI): IParsedHookGroup[];
147 > }
148 >
149 > export type PluginComponent = 'commands' | 'skills' | 'agents' | 'rules' | 'hooks' | 'mcpServers';
150 >
151 > const COPILOT_FORMAT: IPluginFormatConfig = {
152 > format: PluginFormat.Copilot,
153 > manifestPath: 'plugin.json',
154 > hookConfigPath: 'hooks.json',
155 > pluginRootTokens: ['${PLUGIN_ROOT}', '${CLAUDE_PLUGIN_ROOT}'],
156 > pluginRootEnvVars: ['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT'],
157 > parseHooks(hookUri, json, _pluginUri, workspaceRoot, userHome) {
158 return parseHooksJson(hookUri, json, workspaceRoot, userHome);
159 },
160 > }; pluginParsers.ts
161 >
162 > const CLAUDE_FORMAT: IPluginFormatConfig = {
163 > format: PluginFormat.Claude,
164 > manifestPath: '.claude-plugin/plugin.json',
165 > hookConfigPath: 'hooks/hooks.json',
166 > pluginRootTokens: ['${PLUGIN_ROOT}', '${CLAUDE_PLUGIN_ROOT}'],
167 > pluginRootEnvVars: ['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT'],
168 > parseHooks(hookUri, json, pluginUri, workspaceRoot, userHome) {
169 return interpolateHookPluginRoot(hookUri, json, pluginUri, workspaceRoot, userHome, '${CLAUDE_PLUGIN_ROOT}', 'CLAUDE_PLUGIN_ROOT');
170 },
171 > }; pluginParsers.ts
172 >
173 > const OPEN_PLUGIN_FORMAT: IPluginFormatConfig = {
174 > format: PluginFormat.OpenPlugin,
175 > manifestPath: '.plugin/plugin.json',
176 > hookConfigPath: 'hooks/hooks.json',
177 > pluginRootTokens: ['${PLUGIN_ROOT}', '${CLAUDE_PLUGIN_ROOT}'],
178 > pluginRootEnvVars: ['PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT'],
179 > parseHooks(hookUri, json, pluginUri, workspaceRoot, userHome) {
180 return interpolateHookPluginRoot(hookUri, json, pluginUri, workspaceRoot, userHome, '${PLUGIN_ROOT}', 'PLUGIN_ROOT');
181 },
182 > }; pluginParsers.ts
183 >
184 > const AGENT_PLUGIN_FORMAT: IPluginFormatConfig = {
185 > format: PluginFormat.AgentPlugin,
186 > manifestPath: 'plugin.json',
187 > hookConfigPath: '',
188 > componentPaths: {
189 > commands: false,
190 > skills: 'skills',
191 > agents: false,
192 > rules: false,
193 > hooks: false,
194 > mcpServers: 'mcp.json',
195 > },
196 > requiresManifest: true,
197 > pluginRootTokens: [],
198 > pluginRootEnvVars: [],
199 > parseHooks() {
200 return [];
201 },
202 > }; pluginParsers.ts
203 >
204 export async function detectPluginFormat(pluginUri: URI, fileService: IFileService): Promise<IPluginFormatConfig> {
205 if (await readAgentPluginManifest(pluginUri, fileService)) {
217 return COPILOT_FORMAT;
218 }
220 export async function readPluginManifest(pluginUri: URI, format: IPluginFormatConfig, fileService: IFileService): Promise<Record<string, unknown> | undefined> {
221 if (format.format === PluginFormat.AgentPlugin) {
226 return json && typeof json === 'object' && !Array.isArray(json) ? json as Record<string, unknown> : undefined;
227 }
229 > export function getPluginManifestComponent(format: IPluginFormatConfig, component: PluginComponent, manifest: Record<string, unknown> | undefined): unknown {
230 return format.componentPaths && Object.hasOwn(format.componentPaths, component) ? undefined : manifest?.[component];
231 }
233 > export function resolvePluginComponentDirs(
234 pluginUri: URI,
235 format: IPluginFormatConfig,
252 );
253 }
255 > // ---------------------------------------------------------------------------
256 > // Child customization helpers
257 > // ---------------------------------------------------------------------------
258 >
259 > /**
260 > * Mints a child-customization id from a source uri plus an optional opaque
261 > * disambiguator. Used when multiple customizations are declared inline in
262 > * a single file (e.g. two MCP servers in one `.mcp.json`, or two hook
263 > * lifecycle groups in one hook file).
264 > *
265 > * Percent-encodes any pre-existing `#` in the URI before appending the
266 > * disambiguating fragment so the resulting id can never collide with a
267 > * URI that happens to already contain a matching fragment.
268 > */
269 function buildChildId(uri: URI, disambiguator?: string): string {
270 const base = customizationId(uri.toString());
274 return `${base.replace(/#/g, '%23')}#${disambiguator}`;
275 }
277 function makeAgentCustomization(resource: INamedPluginResource): AgentCustomization {
278 const uri = resource.uri.toString();
285 };
286 }
288 function makeSkillCustomization(resource: INamedPluginResource): SkillCustomization {
289 const uri = resource.uri.toString();
296 };
297 }
299 function makeRuleCustomization(resource: INamedPluginResource): RuleCustomization {
300 const uri = resource.uri.toString();
307 };
308 }
310 function makeHookCustomization(hookUri: URI): HookCustomization {
311 return {
316 };
317 }
319 > /**
320 > * Builds the protocol {@link McpServerCustomization} for an MCP server
321 > * declared at `definitionUri` (the manifest / settings / `.mcp.json` file
322 > * the server is defined in). The id is disambiguated by server `name` so
323 > * multiple servers declared in one file get distinct ids, and the entry
324 > * carries {@link DEFAULT_MCP_APP} so MCP App support is advertised
325 > * consistently with every other MCP customization.
326 > *
327 > * The seed state is {@link McpServerStatus.Stopped}: a declared-but-not-yet
328 > * connected server has not been started by any SDK, so it must not claim to
329 > * be {@link McpServerStatus.Starting}. The live state is enriched from the
330 > * SDK's reported status once a session materializes.
331 > */
332 > export function makeMcpServerCustomization(definitionUri: URI, name: string): McpServerCustomization {
333 return {
334 type: CustomizationType.McpServer,
341 };
342 }
344 > // ---------------------------------------------------------------------------
345 > // Component path config
346 > // ---------------------------------------------------------------------------
347 >
348 > export interface IComponentPathConfig {
349 > readonly paths: readonly string[];
350 > readonly exclusive: boolean;
351 > }
352 >
353 > const emptyComponentPathConfig: IComponentPathConfig = { paths: [], exclusive: false };
354 >
355 > /**
356 > * Parses a manifest component path field into a normalized config.
357 > * Supports `undefined`, `string`, `string[]`, and `{ paths: string[], exclusive?: boolean }`.
358 > */
359 > export function parseComponentPathConfig(raw: unknown): IComponentPathConfig {
360 if (raw === undefined || raw === null) {
361 return emptyComponentPathConfig;
389 return emptyComponentPathConfig;
390 }
392 > /**
393 > * Resolves the directories to scan for a given component type, combining
394 > * the default directory with any custom paths from the manifest config.
395 > * Paths that resolve outside the boundary are silently ignored.
396 > * @param boundaryUri The outermost directory that resolved paths must stay within. Defaults to {@link pluginUri}.
397 > */
398 > export function resolveComponentDirs(pluginUri: URI, defaultDir: string, config: IComponentPathConfig, boundaryUri?: URI): readonly URI[] {
399 const boundary = (boundaryUri && isEqualOrParent(pluginUri, boundaryUri)) ? boundaryUri : pluginUri;
400 const dirs: URI[] = [];
410 return dirs;
411 }
413 > // ---------------------------------------------------------------------------
414 > // MCP server helpers
415 > // ---------------------------------------------------------------------------
416 >
417 > /**
418 > * Extracts the MCP server map from a raw JSON value. Accepts both the
419 > * wrapped format `{ mcpServers: { … } }` and the flat format.
420 > */
421 > export function resolveMcpServersMap(raw: unknown): Record<string, unknown> | undefined {
422 if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
423 return undefined;
428 : obj;
429 }
431 > /**
432 > * Normalizes a raw JSON value into a typed MCP server configuration.
433 > */
434 > export function normalizeMcpServerConfiguration(rawConfig: unknown): IMcpServerConfiguration | undefined {
435 if (!rawConfig || typeof rawConfig !== 'object') {
436 return undefined;
477 return undefined;
478 }
480 > /**
481 > * Characters in a file path that require shell quoting to prevent
482 > * word splitting or interpretation by common shells.
483 > */
484 > const shellUnsafeChars = /[\s&|<>()^;!`"']/;
485 >
486 > /**
487 > * Replaces a plugin-root token in a shell command string with the
488 > * given fsPath, shell-quoting if the path contains special characters.
489 > */
490 > export function shellQuotePluginRootInCommand(command: string, fsPath: string, token: string) {
491 if (!command.includes(token)) {
492 return command;
511 });
512 }
514 > /**
515 > * Replaces plugin-root token references in MCP server definition string fields
516 > * with the plugin root filesystem path.
517 > */
518 > export function interpolateMcpPluginRoot(
519 def: IMcpServerDefinition,
520 fsPath: string,
562 return { name: def.name, configuration: interpolated, uri: def.uri, customization: def.customization };
563 }
565 > /**
566 > * Regex matching bare `${VAR_NAME}` references (uppercase only) that are NOT
567 > * using VS Code's `${env:VAR}` colon-delimited syntax.
568 > */
569 > const BARE_ENV_VAR_RE = /\$\{(?![A-Za-z]+:)([A-Z_][A-Z0-9_]*)\}/g;
570 >
571 > /**
572 > * Converts bare `${VAR}` environment-variable references to VS Code `${env:VAR}` syntax.
573 > */
574 > export function convertBareEnvVarsToVsCodeSyntax(
575 def: IMcpServerDefinition,
576 ): IMcpServerDefinition {
586 });
587 }
589 > // ---------------------------------------------------------------------------
590 > // Hook parsing helpers
591 > // ---------------------------------------------------------------------------
592 >
593 > /**
594 > * Maps known hook type identifiers from all formats (VS Code PascalCase,
595 > * Copilot CLI camelCase, Claude PascalCase) to canonical identifiers.
596 > */
597 > const HOOK_TYPE_MAP: Record<string, string> = {
598 > // PascalCase (VS Code / Claude)
599 > 'SessionStart': 'SessionStart',
600 > 'SessionEnd': 'SessionEnd',
601 > 'UserPromptSubmit': 'UserPromptSubmit',
602 > 'PreToolUse': 'PreToolUse',
603 > 'PostToolUse': 'PostToolUse',
604 > 'PreCompact': 'PreCompact',
605 > 'SubagentStart': 'SubagentStart',
606 > 'SubagentStop': 'SubagentStop',
607 > 'Stop': 'Stop',
608 > 'ErrorOccurred': 'ErrorOccurred',
609 > // camelCase (GitHub Copilot CLI)
610 > 'sessionStart': 'SessionStart',
611 > 'sessionEnd': 'SessionEnd',
612 > 'userPromptSubmitted': 'UserPromptSubmit',
613 > 'preToolUse': 'PreToolUse',
614 > 'postToolUse': 'PostToolUse',
615 > 'agentStop': 'Stop',
616 > 'subagentStop': 'SubagentStop',
617 > 'errorOccurred': 'ErrorOccurred',
618 > };
619 >
620 > /**
621 > * Normalizes a raw hook command object, validating structure and mapping
622 > * legacy `bash`/`powershell` fields to platform-specific overrides.
623 > */
624 function normalizeHookCommand(raw: Record<string, unknown>): IParsedHookCommand | undefined {
625 // Allow omitted type (Claude compatibility) — treat as 'command'
656 };
657 }
659 > /**
660 > * Resolves a raw hook command JSON object into a {@link IParsedHookCommand},
661 > * normalizing fields and resolving the working directory.
662 > */
663 function resolveHookCommand(raw: Record<string, unknown>, workspaceRoot: URI | undefined, userHome: URI): IParsedHookCommand | undefined {
664 const normalized = normalizeHookCommand(raw);
683 return { ...normalized, cwd: cwdUri };
684 }
686 > /**
687 > * Extracts hook commands from an item that may be a direct command object
688 > * or a nested structure with a `matcher` (Claude format).
689 > */
690 function extractHookCommands(item: unknown, workspaceRoot: URI | undefined, userHome: URI): IParsedHookCommand[] {
691 if (!item || typeof item !== 'object') {
717 return commands;
718 }
720 > /**
721 > * Parses hooks from a JSON object (any supported format).
722 > *
723 > * Handles Claude's `disableAllHooks` short-circuit, the `HOOK_TYPE_MAP`
724 > * canonicalization, and the nested `{ matcher, hooks: [...] }` command
725 > * form. Returns one {@link IParsedHookGroup} per recognized lifecycle
726 > * event; all groups parsed from the same file share a single
727 > * {@link IParsedHookGroup.customization} (keyed on `hookUri`), so callers
728 > * that only need the file-level customization can read it off any group.
729 > */
730 > export function parseHooksJson(
731 hookUri: URI,
732 json: unknown,
777 return result;
778 }
780 > /**
781 > * Applies plugin-root token interpolation to hook commands for
782 > * Claude and OpenPlugin formats.
783 > */
784 > export function interpolateHookPluginRoot(
785 hookUri: URI,
786 json: unknown,
834 return parseHooksJson(hookUri, cloneAndChange(json, replacer), workspaceRoot, userHome);
835 }
837 > // ---------------------------------------------------------------------------
838 > // Filesystem helpers
839 > // ---------------------------------------------------------------------------
840 >
841 export async function readJsonFile(uri: URI, fileService: IFileService): Promise<unknown | undefined> {
842 try {
847 }
848 }
850 export async function pathExists(resource: URI, fileService: IFileService): Promise<boolean> {
851 try {
856 }
857 }
859 > // ---------------------------------------------------------------------------
860 > // Component readers
861 > // ---------------------------------------------------------------------------
862 >
863 > const COMMAND_FILE_SUFFIX = '.md';
864 > const RULE_FILE_SUFFIX = '.mdc';
865 > const INSTRUCTION_FILE_SUFFIX = '.instructions.md';
866 >
867 export async function readSkills(
868 pluginRoot: URI,
931 return skills;
932 }
934 export async function readPluginSkills(pluginRoot: URI, dirs: readonly URI[], format: IPluginFormatConfig, fileService: IFileService): Promise<readonly INamedPluginResource[]> {
935 return readSkills(pluginRoot, dirs, fileService, format.format === PluginFormat.AgentPlugin
937 : undefined);
938 }
940 async function isResolvedWithin(root: URI, resource: URI, fileService: IFileService): Promise<boolean> {
941 try {
949 }
950 }
952 export async function readMarkdownComponents(dirs: readonly URI[], fileService: IFileService): Promise<readonly INamedPluginResource[]> {
953 const seen = new Set<string>();
989 return items;
990 }
992 function getInstructionFileName(resource: URI): string | undefined {
993 const fileName = basename(resource);
1001 return undefined;
1002 }
1004 > /**
1005 > * Reads rule/instruction files from plugin `rules` component directories.
1006 > *
1007 > * Open Plugins rules are conventionally `.mdc` files. We also accept
1008 > * `.instructions.md` for compatibility with VS Code-discovered instructions
1009 > * bundled as synthetic plugins.
1010 > */
1011 export async function readInstructionComponents(dirs: readonly URI[], fileService: IFileService): Promise<readonly INamedPluginResource[]> {
1012 const seen = new Set<string>();
1054 return items;
1055 }
1057 > /**
1058 > * Reads `.md` files in agent directories and enriches each entry with
1059 > * the optional `name` / `description` from YAML frontmatter. Falls back
1060 > * to the file-derived name when frontmatter is missing or unreadable.
1061 > */
1062 export async function readAgentComponents(dirs: readonly URI[], fileService: IFileService): Promise<readonly INamedPluginResource[]> {
1063 const files = await readMarkdownComponents(dirs, fileService);
1090 return result;
1091 }
1093 export async function parseAgentFile(uri: URI, fileService: IFileService): Promise<{ name: string; description?: string; userInvocable?: boolean }> {
1094 // Use regex to strip the trailing `.agent.md` or .md before parsing, so we can fall back to a cleaner name if frontmatter is missing or broken.
1105 }
1106 }
1108 export async function parseSkillFile(uri: URI, fileService: IFileService): Promise<{ name: string; description?: string; userInvokable?: boolean }> {
1109 try {
1118 }
1119 }
1121 export async function parseRuleFile(uri: URI, fileService: IFileService): Promise<{ name: string; description?: string; globs?: string[]; alwaysApply?: boolean }> {
1122 const nameFromFile = basename(uri).replace(/(\.instructions)?\.md$/i, '');
1133 }
1134 }
1136 async function readHooks(
1137 pluginUri: URI,
1152 return [];
1153 }
1155 async function readMcpServers(
1156 pluginUri: URI,
1173 return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
1174 }
1176 export async function readPluginMcpServers(
1177 pluginUri: URI,
1182 return readMcpServers(pluginUri, paths, format, fileService);
1183 }
1185 > export function parseMcpServerDefinitionMap(
1186 definitionURI: URI,
1187 raw: unknown,
1219 return definitions;
1220 }
1222 > // ---------------------------------------------------------------------------
1223 > // Top-level parse function
1224 > // ---------------------------------------------------------------------------
1225 >
1226 > /**
1227 > * Parses a plugin directory to extract hooks, MCP servers, skills, agents,
1228 > * and instructions.
1229 > * This is the main entry point for the agent host to discover plugin contents.
1230 > */
1231 export async function parsePlugin(
1232 pluginUri: URI,
1292 };
1293 }
1295 > /** Pairs an agent {@link INamedPluginResource} with its protocol-level {@link AgentCustomization}. */
1296 > export function toParsedAgent(resource: INamedPluginResource): IParsedAgent {
1297 return { ...resource, customization: makeAgentCustomization(resource) };
1298 }
1300 > /** Pairs a skill {@link INamedPluginResource} with its protocol-level {@link SkillCustomization}. */
1301 > export function toParsedSkill(resource: INamedPluginResource): IParsedSkill {
1302 return { ...resource, customization: makeSkillCustomization(resource) };
1303 }
1305 function toParsedRule(resource: INamedPluginResource): IParsedRule {
1306 return { ...resource, customization: makeRuleCustomization(resource) };