├── .php-cs-fixer.dist.php ├── Dockerfile ├── LICENSE ├── README.md ├── app.php ├── box.json ├── build-binary.sh ├── composer.json ├── context.yaml ├── ctx ├── docs ├── config │ ├── config-system-guide.md │ ├── import-system-guide.md │ └── readers-and-parsers-guide.md ├── document-compilation-guide.md ├── example │ └── config │ │ ├── custom-tools-example.yaml │ │ ├── exclude-config.yaml │ │ ├── gitlab-source-example.yaml │ │ ├── import-with-filters.yaml │ │ ├── prompt-templates.yml │ │ ├── tagged-prompts.yaml │ │ └── tool-with-arguments.yaml └── mcp │ ├── integration_guide.md │ ├── prompts │ ├── implementation-notes.md │ ├── prompt-tagging-and-filtering.md │ └── prompts_guide.md │ └── tools │ ├── http-tools.md │ └── tools_guide.md ├── download-latest.sh ├── json-schema.json ├── phpunit.xml ├── prompts.yaml ├── rector.php ├── src ├── Application │ ├── AppScope.php │ ├── Application.php │ ├── Bootloader │ │ ├── ComposerClientBootloader.php │ │ ├── ConfigLoaderBootloader.php │ │ ├── ConfigurationBootloader.php │ │ ├── ConsoleBootloader.php │ │ ├── ContentRendererBootloader.php │ │ ├── CoreBootloader.php │ │ ├── ExcludeBootloader.php │ │ ├── GitClientBootloader.php │ │ ├── GithubClientBootloader.php │ │ ├── GitlabClientBootloader.php │ │ ├── HttpClientBootloader.php │ │ ├── ImportBootloader.php │ │ ├── LoggerBootloader.php │ │ ├── ModifierBootloader.php │ │ ├── SourceFetcherBootloader.php │ │ └── VariableBootloader.php │ ├── Dispatcher │ │ └── ConsoleDispatcher.php │ ├── ExceptionHandler.php │ ├── FSPath.php │ ├── JsonSchema.php │ ├── Kernel.php │ └── Logger │ │ ├── ConsoleLogger.php │ │ ├── FileLogger.php │ │ ├── FormatterInterface.php │ │ ├── HasPrefixLoggerInterface.php │ │ ├── LogLevel.php │ │ ├── LoggerFactory.php │ │ ├── LoggerPrefix.php │ │ ├── NullLogger.php │ │ └── SimpleFormatter.php ├── Config │ ├── ConfigType.php │ ├── ConfigurationProvider.php │ ├── Exception │ │ ├── ConfigLoaderException.php │ │ └── ReaderException.php │ ├── Exclude │ │ ├── AbstractExclusion.php │ │ ├── ExcludeParserPlugin.php │ │ ├── ExcludeRegistry.php │ │ ├── ExcludeRegistryInterface.php │ │ ├── ExclusionPatternInterface.php │ │ ├── PathExclusion.php │ │ └── PatternExclusion.php │ ├── Import │ │ ├── CircularImportDetector.php │ │ ├── CircularImportDetectorInterface.php │ │ ├── ImportParserPlugin.php │ │ ├── ImportRegistry.php │ │ ├── ImportResolver.php │ │ ├── Merger │ │ │ ├── AbstractConfigMerger.php │ │ │ ├── ConfigMergerInterface.php │ │ │ ├── ConfigMergerProviderInterface.php │ │ │ ├── ConfigMergerRegistry.php │ │ │ └── VariablesConfigMerger.php │ │ ├── PathMatcher.php │ │ ├── PathPrefixer │ │ │ ├── DocumentOutputPathPrefixer.php │ │ │ ├── PathPrefixer.php │ │ │ └── SourcePathPrefixer.php │ │ ├── ResolvedConfig.php │ │ ├── Source │ │ │ ├── AbstractImportSource.php │ │ │ ├── Config │ │ │ │ ├── AbstractSourceConfig.php │ │ │ │ ├── FilterConfig.php │ │ │ │ ├── SourceConfigFactory.php │ │ │ │ └── SourceConfigInterface.php │ │ │ ├── Exception │ │ │ │ └── ImportSourceException.php │ │ │ ├── ImportSourceInterface.php │ │ │ ├── ImportSourceProvider.php │ │ │ ├── ImportedConfig.php │ │ │ ├── Local │ │ │ │ ├── LocalImportSource.php │ │ │ │ └── LocalSourceConfig.php │ │ │ ├── Registry │ │ │ │ └── ImportSourceRegistry.php │ │ │ └── Url │ │ │ │ ├── UrlImportSource.php │ │ │ │ └── UrlSourceConfig.php │ │ └── WildcardPathFinder.php │ ├── Loader │ │ ├── CompositeConfigLoader.php │ │ ├── ConfigLoader.php │ │ ├── ConfigLoaderFactory.php │ │ ├── ConfigLoaderFactoryInterface.php │ │ └── ConfigLoaderInterface.php │ ├── Parser │ │ ├── CompositeConfigParser.php │ │ ├── ConfigParser.php │ │ ├── ConfigParserInterface.php │ │ ├── ConfigParserPluginInterface.php │ │ ├── ParserPluginRegistry.php │ │ └── VariablesParserPlugin.php │ ├── Reader │ │ ├── AbstractReader.php │ │ ├── ConfigReaderRegistry.php │ │ ├── JsonReader.php │ │ ├── PhpReader.php │ │ ├── ReaderInterface.php │ │ ├── StringJsonReader.php │ │ └── YamlReader.php │ ├── Registry │ │ ├── ConfigRegistry.php │ │ ├── ConfigRegistryAccessor.php │ │ └── RegistryInterface.php │ └── context.yaml ├── Console │ ├── BaseCommand.php │ ├── GenerateCommand.php │ ├── InitCommand.php │ ├── Renderer │ │ ├── GenerateCommandRenderer.php │ │ └── Style.php │ ├── SchemaCommand.php │ ├── SelfUpdateCommand.php │ ├── VersionCommand.php │ └── context.yaml ├── Directories.php ├── DirectoriesInterface.php ├── Document │ ├── Compiler │ │ ├── CompiledDocument.php │ │ ├── DocumentCompiler.php │ │ └── Error │ │ │ ├── ErrorCollection.php │ │ │ └── SourceError.php │ ├── Document.php │ ├── DocumentConfigMerger.php │ ├── DocumentRegistry.php │ └── DocumentsParserPlugin.php ├── Lib │ ├── BinaryUpdater │ │ ├── BinaryUpdater.php │ │ ├── Strategy │ │ │ ├── UnixUpdateStrategy.php │ │ │ ├── UpdateStrategyInterface.php │ │ │ └── WindowsUpdateStrategy.php │ │ └── UpdaterFactory.php │ ├── ComposerClient │ │ ├── ComposerClientInterface.php │ │ └── FileSystemComposerClient.php │ ├── Content │ │ ├── Block │ │ │ ├── AbstractBlock.php │ │ │ ├── BlockInterface.php │ │ │ ├── CodeBlock.php │ │ │ ├── CommentBlock.php │ │ │ ├── DescriptionBlock.php │ │ │ ├── SeparatorBlock.php │ │ │ ├── TextBlock.php │ │ │ ├── TitleBlock.php │ │ │ └── TreeViewBlock.php │ │ ├── ContentBlock.php │ │ ├── ContentBuilder.php │ │ ├── ContentBuilderFactory.php │ │ └── Renderer │ │ │ ├── AbstractRenderer.php │ │ │ ├── MarkdownRenderer.php │ │ │ └── RendererInterface.php │ ├── Finder │ │ ├── FinderInterface.php │ │ └── FinderResult.php │ ├── Git │ │ ├── Command.php │ │ ├── CommandsExecutor.php │ │ ├── CommandsExecutorInterface.php │ │ └── Exception │ │ │ ├── GitClientException.php │ │ │ └── GitCommandException.php │ ├── GithubClient │ │ ├── Architecture.php │ │ ├── BinaryNameBuilder.php │ │ ├── GithubClient.php │ │ ├── GithubClientInterface.php │ │ ├── Model │ │ │ ├── GithubRepository.php │ │ │ └── Release.php │ │ ├── Platform.php │ │ └── ReleaseManager.php │ ├── GitlabClient │ │ ├── GitlabClient.php │ │ ├── GitlabClientInterface.php │ │ └── Model │ │ │ └── GitlabRepository.php │ ├── Html │ │ ├── HtmlCleaner.php │ │ ├── HtmlCleanerInterface.php │ │ ├── SelectorContentExtractor.php │ │ └── SelectorContentExtractorInterface.php │ ├── HttpClient │ │ ├── Exception │ │ │ ├── HttpClientNotAvailableException.php │ │ │ ├── HttpException.php │ │ │ └── HttpRequestException.php │ │ ├── HttpClientInterface.php │ │ ├── HttpResponse.php │ │ └── Psr18Client.php │ ├── PathFilter │ │ ├── AbstractFilter.php │ │ ├── ContentsFilter.php │ │ ├── ExcludePathFilter.php │ │ ├── FileHelper.php │ │ ├── FilePatternFilter.php │ │ ├── FilterInterface.php │ │ └── PathFilter.php │ ├── TokenCounter │ │ ├── CharTokenCounter.php │ │ └── TokenCounterInterface.php │ ├── TreeBuilder │ │ ├── DirectorySorter.php │ │ ├── FileTreeBuilder.php │ │ ├── TreeRenderer │ │ │ └── AsciiTreeRenderer.php │ │ ├── TreeRendererInterface.php │ │ └── TreeViewConfig.php │ ├── Variable │ │ ├── CompositeProcessor.php │ │ ├── Provider │ │ │ ├── CompositeVariableProvider.php │ │ │ ├── ConfigVariableProvider.php │ │ │ ├── DotEnvVariableProvider.php │ │ │ ├── PredefinedVariableProvider.php │ │ │ └── VariableProviderInterface.php │ │ ├── VariableReplacementProcessor.php │ │ ├── VariableReplacementProcessorInterface.php │ │ └── VariableResolver.php │ └── context.yaml ├── McpServer │ ├── Action │ │ ├── Prompts │ │ │ ├── FilesystemOperationsAction.php │ │ │ ├── GetPromptAction.php │ │ │ ├── ListPromptsAction.php │ │ │ └── ProjectStructurePromptAction.php │ │ ├── Resources │ │ │ ├── GetDocumentContentResourceAction.php │ │ │ ├── JsonSchemaResourceAction.php │ │ │ └── ListResourcesAction.php │ │ └── Tools │ │ │ ├── Context │ │ │ ├── ContextAction.php │ │ │ ├── ContextGetAction.php │ │ │ └── ContextRequestAction.php │ │ │ ├── Docs │ │ │ └── DocsSearchAction.php │ │ │ ├── ExecuteCustomToolAction.php │ │ │ ├── Filesystem │ │ │ ├── DirectoryListAction.php │ │ │ ├── FileApplyPatchAction.php │ │ │ ├── FileInfoAction.php │ │ │ ├── FileMoveAction.php │ │ │ ├── FileReadAction.php │ │ │ ├── FileRenameAction.php │ │ │ └── FileWriteAction.php │ │ │ ├── ListToolsAction.php │ │ │ └── Prompts │ │ │ ├── GetPromptToolAction.php │ │ │ └── ListPromptsToolAction.php │ ├── Attribute │ │ ├── InputSchema.php │ │ ├── McpItem.php │ │ ├── Prompt.php │ │ ├── Resource.php │ │ └── Tool.php │ ├── Console │ │ └── MCPServerCommand.php │ ├── GUIDELINE.md │ ├── McpConfig.php │ ├── McpServerBootloader.php │ ├── ProjectService │ │ ├── ProjectService.php │ │ ├── ProjectServiceFactory.php │ │ └── ProjectServiceInterface.php │ ├── Projects │ │ ├── Console │ │ │ ├── ProjectAddCommand.php │ │ │ ├── ProjectCommand.php │ │ │ └── ProjectListCommand.php │ │ ├── DTO │ │ │ ├── CurrentProjectDTO.php │ │ │ ├── ProjectDTO.php │ │ │ └── ProjectStateDTO.php │ │ ├── McpProjectsBootloader.php │ │ ├── ProjectService.php │ │ ├── ProjectServiceInterface.php │ │ └── Repository │ │ │ ├── ProjectStateRepository.php │ │ │ └── ProjectStateRepositoryInterface.php │ ├── Prompt │ │ ├── Console │ │ │ └── ListPromptsCommand.php │ │ ├── Exception │ │ │ ├── PromptParsingException.php │ │ │ └── TemplateResolutionException.php │ │ ├── Extension │ │ │ ├── PromptDefinition.php │ │ │ ├── PromptExtension.php │ │ │ ├── PromptExtensionArgument.php │ │ │ ├── PromptExtensionVariableProvider.php │ │ │ └── TemplateResolver.php │ │ ├── Filter │ │ │ ├── FilterStrategy.php │ │ │ ├── PromptFilterFactory.php │ │ │ ├── PromptFilterInterface.php │ │ │ └── Strategy │ │ │ │ ├── CompositePromptFilter.php │ │ │ │ ├── IdPromptFilter.php │ │ │ │ └── TagPromptFilter.php │ │ ├── McpPromptBootloader.php │ │ ├── PromptConfigFactory.php │ │ ├── PromptConfigMerger.php │ │ ├── PromptMessageProcessor.php │ │ ├── PromptParserPlugin.php │ │ ├── PromptProviderInterface.php │ │ ├── PromptRegistry.php │ │ ├── PromptRegistryInterface.php │ │ └── PromptType.php │ ├── Registry │ │ └── McpItemsRegistry.php │ ├── Routing │ │ ├── ActionCaller.php │ │ ├── Attribute │ │ │ ├── Get.php │ │ │ ├── Post.php │ │ │ └── Route.php │ │ ├── Mcp2PsrRequestAdapter.php │ │ ├── McpResponseStrategy.php │ │ └── RouteRegistrar.php │ ├── Server.php │ ├── ServerRunner.php │ ├── ServerRunnerInterface.php │ ├── Tool │ │ ├── Command │ │ │ ├── CommandExecutor.php │ │ │ └── CommandExecutorInterface.php │ │ ├── Config │ │ │ ├── HttpToolRequest.php │ │ │ ├── ToolArg.php │ │ │ ├── ToolCommand.php │ │ │ ├── ToolDefinition.php │ │ │ └── ToolSchema.php │ │ ├── Console │ │ │ ├── ToolListCommand.php │ │ │ └── ToolRunCommand.php │ │ ├── Exception │ │ │ └── ToolExecutionException.php │ │ ├── McpToolBootloader.php │ │ ├── Provider │ │ │ └── ToolArgumentsProvider.php │ │ ├── ToolConfigMerger.php │ │ ├── ToolHandlerFactory.php │ │ ├── ToolParserPlugin.php │ │ ├── ToolProviderInterface.php │ │ ├── ToolRegistry.php │ │ ├── ToolRegistryInterface.php │ │ └── Types │ │ │ ├── AbstractToolHandler.php │ │ │ ├── HttpToolHandler.php │ │ │ ├── RunToolHandler.php │ │ │ └── ToolHandlerInterface.php │ └── context.yaml ├── Modifier │ ├── Alias │ │ ├── AliasesRegistry.php │ │ ├── ModifierAliasesParserPlugin.php │ │ └── ModifierResolver.php │ ├── Modifier.php │ ├── ModifiersApplier.php │ ├── ModifiersApplierInterface.php │ ├── PhpContentFilter │ │ ├── PhpContentFilter.php │ │ └── PhpContentFilterBootloader.php │ ├── PhpDocs │ │ ├── AstDocTransformer.php │ │ └── PhpDocsModifierBootloader.php │ ├── PhpSignature │ │ ├── PhpSignature.php │ │ └── PhpSignatureModifierBootloader.php │ ├── Sanitizer │ │ ├── Rule │ │ │ ├── CommentInsertionRule.php │ │ │ ├── ContextSanitizer.php │ │ │ ├── KeywordRemovalRule.php │ │ │ ├── RegexReplacementRule.php │ │ │ ├── RuleFactory.php │ │ │ └── RuleInterface.php │ │ ├── SanitizerModifier.php │ │ └── SanitizerModifierBootloader.php │ ├── SourceModifierInterface.php │ ├── SourceModifierRegistry.php │ └── context.yaml ├── Source │ ├── BaseSource.php │ ├── Composer │ │ ├── ComposerSource.php │ │ ├── ComposerSourceBootloader.php │ │ ├── ComposerSourceFactory.php │ │ ├── ComposerSourceFetcher.php │ │ ├── Exception │ │ │ └── ComposerNotFoundException.php │ │ ├── Package │ │ │ ├── ComposerPackageCollection.php │ │ │ └── ComposerPackageInfo.php │ │ └── Provider │ │ │ ├── AbstractComposerProvider.php │ │ │ ├── ComposerProviderInterface.php │ │ │ ├── CompositeComposerProvider.php │ │ │ └── LocalComposerProvider.php │ ├── Docs │ │ ├── DocsSource.php │ │ ├── DocsSourceBootloader.php │ │ ├── DocsSourceFactory.php │ │ └── DocsSourceFetcher.php │ ├── Fetcher │ │ ├── FilterableSourceInterface.php │ │ ├── SourceFetcherInterface.php │ │ └── SourceFetcherProvider.php │ ├── File │ │ ├── FileSource.php │ │ ├── FileSourceBootloader.php │ │ ├── FileSourceFactory.php │ │ ├── FileSourceFetcher.php │ │ └── SymfonyFinder.php │ ├── GitDiff │ │ ├── Fetcher │ │ │ ├── CommitRangeParser.php │ │ │ ├── GitSourceFactory.php │ │ │ ├── GitSourceInterface.php │ │ │ └── Source │ │ │ │ ├── AbstractGitSource.php │ │ │ │ ├── CommitGitSource.php │ │ │ │ ├── FileAtCommitGitSource.php │ │ │ │ ├── StagedGitSource.php │ │ │ │ ├── StashGitSource.php │ │ │ │ ├── TimeRangeGitSource.php │ │ │ │ └── UnstagedGitSource.php │ │ ├── GitDiffFinder.php │ │ ├── GitDiffSource.php │ │ ├── GitDiffSourceBootloader.php │ │ ├── GitDiffSourceFactory.php │ │ ├── GitDiffSourceFetcher.php │ │ └── RenderStrategy │ │ │ ├── Config │ │ │ └── RenderConfig.php │ │ │ ├── Enum │ │ │ └── RenderStrategyEnum.php │ │ │ ├── LLMFriendlyRenderStrategy.php │ │ │ ├── RawRenderStrategy.php │ │ │ ├── RenderStrategyFactory.php │ │ │ └── RenderStrategyInterface.php │ ├── Github │ │ ├── GithubFileInfo.php │ │ ├── GithubFinder.php │ │ ├── GithubSource.php │ │ ├── GithubSourceBootloader.php │ │ ├── GithubSourceFactory.php │ │ └── GithubSourceFetcher.php │ ├── Gitlab │ │ ├── Config │ │ │ ├── GitlabServerParserPlugin.php │ │ │ ├── ServerConfig.php │ │ │ └── ServerRegistry.php │ │ ├── GitlabFileInfo.php │ │ ├── GitlabFinder.php │ │ ├── GitlabSource.php │ │ ├── GitlabSourceBootloader.php │ │ ├── GitlabSourceFactory.php │ │ └── GitlabSourceFetcher.php │ ├── MCP │ │ ├── Config │ │ │ ├── McpServerParserPlugin.php │ │ │ ├── ServerConfig.php │ │ │ └── ServerRegistry.php │ │ ├── Connection │ │ │ └── ConnectionManager.php │ │ ├── McpSource.php │ │ ├── McpSourceBootloader.php │ │ ├── McpSourceFactory.php │ │ ├── McpSourceFetcher.php │ │ └── Operations │ │ │ ├── AbstractOperation.php │ │ │ ├── CallToolOperation.php │ │ │ ├── OperationFactory.php │ │ │ ├── OperationInterface.php │ │ │ └── ReadResourceOperation.php │ ├── Registry │ │ ├── AbstractSourceFactory.php │ │ ├── SourceFactoryInterface.php │ │ ├── SourceProviderInterface.php │ │ ├── SourceRegistry.php │ │ ├── SourceRegistryBootloader.php │ │ └── SourceRegistryInterface.php │ ├── SourceInterface.php │ ├── SourceWithModifiers.php │ ├── Text │ │ ├── TextSource.php │ │ ├── TextSourceBootloader.php │ │ ├── TextSourceFactory.php │ │ └── TextSourceFetcher.php │ ├── Tree │ │ ├── TreeSource.php │ │ ├── TreeSourceBootloader.php │ │ ├── TreeSourceFactory.php │ │ └── TreeSourceFetcher.php │ ├── Url │ │ ├── UrlSource.php │ │ ├── UrlSourceBootloader.php │ │ ├── UrlSourceFactory.php │ │ └── UrlSourceFetcher.php │ └── context.yaml └── SourceParserInterface.php └── version.json /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | include(__DIR__ . '/src') 9 | ->include(__DIR__ . '/tests') 10 | ->include(__DIR__ . '/rector.php') 11 | ->build(); 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG COMPOSER_VERSION="2.8.4" 2 | 3 | FROM ghcr.io/context-hub/docker-ctx-binary/bin-builder:latest AS builder 4 | 5 | # Define build arguments for target platform 6 | ARG TARGET_OS="linux" 7 | ARG TARGET_ARCH="x86_64" 8 | ARG VERSION="latest" 9 | 10 | WORKDIR /app 11 | 12 | # Copy source code 13 | COPY . . 14 | 15 | RUN composer install --no-dev --prefer-dist --ignore-platform-reqs 16 | 17 | # Create build directories 18 | RUN mkdir -p .build/phar .build/bin 19 | 20 | # Build PHAR file 21 | RUN /usr/local/bin/box compile -v 22 | 23 | RUN mkdir -p ./buildroot/bin 24 | RUN cp /build-tools/build/bin/micro.sfx ./buildroot/bin 25 | # Combine micro.sfx with the PHAR to create the final binary 26 | RUN /build-tools/static-php-cli/bin/spc micro:combine .build/phar/ctx.phar --output=.build/bin/ctx 27 | RUN chmod +x .build/bin/ctx 28 | 29 | # Copy to output with appropriate naming including version 30 | RUN mkdir -p /.output 31 | RUN cp .build/bin/ctx /.output/ctx 32 | 33 | # Set default entrypoint (without version in name) 34 | ENTRYPOINT ["/.output/ctx"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Pavel Buchnev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/box-project/box/main/res/schema.json", 3 | "compactors": [ 4 | "KevinGH\\Box\\Compactor\\Json", 5 | "KevinGH\\Box\\Compactor\\Php" 6 | ], 7 | "check-requirements": false, 8 | "dump-autoload": false, 9 | "compression": "GZ", 10 | "git": "git", 11 | "directories": [ 12 | "src", 13 | "vendor" 14 | ], 15 | "files": [ 16 | "ctx", 17 | "app.php", 18 | "LICENSE", 19 | "composer.json", 20 | "composer.lock", 21 | "version.json", 22 | "json-schema.json" 23 | ], 24 | "output": ".build/phar/ctx.phar" 25 | } -------------------------------------------------------------------------------- /build-binary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Building Context Generator Docker image..." 5 | docker build -t context-generator . 6 | 7 | rm -rf .output 8 | mkdir -p .output 9 | 10 | echo "Extracting build artifacts..." 11 | CONTAINER_ID=$(docker create context-generator) 12 | docker cp $CONTAINER_ID:/.output/ctx ./.output/ctx 13 | docker rm $CONTAINER_ID 14 | 15 | echo "Build complete! Artifacts available in ./output directory:" 16 | ls -lh ./.output/ 17 | 18 | echo "You can run the executable with:" 19 | echo "./.output/ctx" -------------------------------------------------------------------------------- /context.yaml: -------------------------------------------------------------------------------- 1 | $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' 2 | 3 | import: 4 | - path: src/**/context.yaml 5 | - path: prompts.yaml 6 | 7 | documents: 8 | - description: 'Project structure overview' 9 | outputPath: project-structure.md 10 | overwrite: true 11 | sources: 12 | - type: tree 13 | sourcePaths: 14 | - src 15 | showCharCount: true 16 | showSize: true 17 | 18 | - description: Core Interfaces 19 | outputPath: core/interfaces.md 20 | sources: 21 | - type: file 22 | sourcePaths: src 23 | filePattern: 24 | - '*Interface.php' 25 | showTreeView: true 26 | 27 | - description: "Changes in the Project" 28 | outputPath: "changes.md" 29 | sources: 30 | - type: git_diff 31 | commit: unstaged 32 | -------------------------------------------------------------------------------- /ctx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 2 | 7 | 8 | 9 | src 10 | 11 | 12 | 13 | 14 | tests/src 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]); 14 | 15 | // Register rules for PHP 8.4 migration 16 | $rectorConfig->sets([ 17 | SetList::PHP_83, 18 | LevelSetList::UP_TO_PHP_83, 19 | ]); 20 | 21 | // Skip vendor directories 22 | $rectorConfig->skip([ 23 | __DIR__ . '/vendor', 24 | ]); 25 | }; 26 | -------------------------------------------------------------------------------- /src/Application/AppScope.php: -------------------------------------------------------------------------------- 1 | static fn( 22 | HasPrefixLoggerInterface $logger, 23 | LocalComposerProvider $localProvider, 24 | ) => new CompositeComposerProvider( 25 | logger: $logger->withPrefix('composer-provider'), 26 | localProvider: $localProvider, 27 | ), 28 | 29 | ComposerClientInterface::class => FileSystemComposerClient::class, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Application/Bootloader/ContentRendererBootloader.php: -------------------------------------------------------------------------------- 1 | MarkdownRenderer::class, 19 | ContentBuilderFactory::class => ContentBuilderFactory::class, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/Bootloader/ExcludeBootloader.php: -------------------------------------------------------------------------------- 1 | ExcludeRegistry::class, 21 | ]; 22 | } 23 | 24 | public function boot(ConfigLoaderBootloader $configLoader, ExcludeParserPlugin $excludeParser): void 25 | { 26 | // Register the exclude parser plugin 27 | $configLoader->registerParserPlugin($excludeParser); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Bootloader/GitClientBootloader.php: -------------------------------------------------------------------------------- 1 | CommandsExecutor::class, 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Application/Bootloader/GithubClientBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 28 | HttpClientInterface $httpClient, 29 | EnvironmentInterface $env, 30 | ): GithubClientInterface => new GithubClient( 31 | httpClient: $httpClient, 32 | token: $env->get('GITHUB_TOKEN'), 33 | ), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Application/Bootloader/GitlabClientBootloader.php: -------------------------------------------------------------------------------- 1 | GitlabClient::class, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application/Bootloader/HttpClientBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 20 | Client $httpClient, 21 | HttpFactory $httpMessageFactory, 22 | ) => new Psr18Client($httpClient, $httpMessageFactory, $httpMessageFactory), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Application/Bootloader/ModifierBootloader.php: -------------------------------------------------------------------------------- 1 | SourceModifierRegistry::class, 18 | ]; 19 | } 20 | 21 | public function boot( 22 | ConfigLoaderBootloader $parserRegistry, 23 | ModifierAliasesParserPlugin $plugin, 24 | ): void { 25 | $parserRegistry->registerParserPlugin($plugin); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Application/Bootloader/SourceFetcherBootloader.php: -------------------------------------------------------------------------------- 1 | [] */ 18 | private array $fetchers = []; 19 | 20 | /** 21 | * Register a source fetcher 22 | * @param class-string $fetcher 23 | */ 24 | public function register(string $fetcher): self 25 | { 26 | $this->fetchers[] = $fetcher; 27 | 28 | return $this; 29 | } 30 | 31 | #[\Override] 32 | public function defineSingletons(): array 33 | { 34 | return [ 35 | SourceParserInterface::class => static function ( 36 | Container $container, 37 | SourceFetcherBootloader $bootloader, 38 | ) { 39 | $fetchers = $bootloader->getFetchers(); 40 | return new SourceFetcherProvider( 41 | fetchers: \array_map( 42 | static fn(string $fetcher) => $container->get($fetcher), 43 | $fetchers, 44 | ), 45 | ); 46 | }, 47 | ]; 48 | } 49 | 50 | public function getFetchers(): array 51 | { 52 | return $this->fetchers; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Application/JsonSchema.php: -------------------------------------------------------------------------------- 1 | getProcessors() !== []) { 38 | $this->popProcessor(); 39 | } 40 | 41 | $this->pushProcessor( 42 | (new TagProcessor())->addTags([$prefix]), 43 | ); 44 | 45 | return $this; 46 | } 47 | 48 | public function getPrefix(): string 49 | { 50 | return ''; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Application/Logger/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | $context Additional context data 18 | * 19 | * @return string The formatted message 20 | */ 21 | public function format(string $level, string|\Stringable $message, array $context = []): string; 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/Logger/HasPrefixLoggerInterface.php: -------------------------------------------------------------------------------- 1 | $type->value, 21 | self::cases(), 22 | ); 23 | } 24 | 25 | public static function fromExtension(string $ext): self 26 | { 27 | return match ($ext) { 28 | 'json' => self::Json, 29 | 'yaml', 'yml' => self::Yaml, 30 | 'php' => self::PHP, 31 | default => throw new \ValueError(\sprintf('Unsupported config type: %s', $ext)), 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Config/Exception/ConfigLoaderException.php: -------------------------------------------------------------------------------- 1 | pattern; 25 | } 26 | 27 | /** 28 | * Abstract method to check if a path matches this pattern 29 | */ 30 | abstract public function matches(string $path): bool; 31 | 32 | /** 33 | * Normalize a pattern for consistent comparison 34 | */ 35 | protected function normalizePattern(string $pattern): string 36 | { 37 | $pattern = \preg_replace('#^\./#', '', $pattern); 38 | 39 | // Remove trailing slash 40 | return \rtrim((string) $pattern, '/'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Config/Exclude/ExcludeRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function getPatterns(): array; 28 | } 29 | -------------------------------------------------------------------------------- /src/Config/Exclude/ExclusionPatternInterface.php: -------------------------------------------------------------------------------- 1 | normalizedPattern = $this->normalizePattern($pattern); 20 | } 21 | 22 | /** 23 | * Check if a path matches this exclusion pattern 24 | * 25 | * A path matches if it's exactly the same as the pattern 26 | * or if it's a file within the directory specified by the pattern 27 | */ 28 | public function matches(string $path): bool 29 | { 30 | $normalizedPath = $this->normalizePattern($path); 31 | 32 | return $normalizedPath === $this->normalizedPattern || 33 | \str_contains($normalizedPath, $this->normalizedPattern); 34 | } 35 | 36 | public function jsonSerialize(): array 37 | { 38 | return [ 39 | 'pattern' => $this->pattern, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Config/Exclude/PatternExclusion.php: -------------------------------------------------------------------------------- 1 | matcher = new PathMatcher($pattern); 23 | } 24 | 25 | /** 26 | * Check if a path matches this exclusion pattern 27 | * 28 | * Uses glob pattern matching via PathMatcher 29 | */ 30 | public function matches(string $path): bool 31 | { 32 | return $this->matcher->isMatch($path); 33 | } 34 | 35 | public function jsonSerialize(): array 36 | { 37 | return [ 38 | 'pattern' => $this->pattern, 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Config/Import/CircularImportDetector.php: -------------------------------------------------------------------------------- 1 | Stack of import paths being processed 14 | */ 15 | private array $importStack = []; 16 | 17 | /** 18 | * Check if adding this path would create a circular dependency 19 | */ 20 | public function wouldCreateCircularDependency(string $path): bool 21 | { 22 | return \in_array($path, $this->importStack, true); 23 | } 24 | 25 | /** 26 | * Begin processing an import path 27 | */ 28 | public function beginProcessing(string $path): void 29 | { 30 | if ($this->wouldCreateCircularDependency($path)) { 31 | throw new \RuntimeException( 32 | \sprintf( 33 | 'Circular import detected: %s is already being processed. Import stack: %s', 34 | $path, 35 | \implode(' -> ', $this->importStack), 36 | ), 37 | ); 38 | } 39 | 40 | $this->importStack[] = $path; 41 | } 42 | 43 | /** 44 | * Finish processing an import path 45 | */ 46 | public function endProcessing(string $path): void 47 | { 48 | // Find the path in the stack and remove it and anything after it 49 | $index = \array_search($path, $this->importStack, true); 50 | 51 | if ($index !== false) { 52 | \array_splice($this->importStack, $index); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Config/Import/CircularImportDetectorInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | #[Singleton] 16 | final class ImportRegistry implements RegistryInterface 17 | { 18 | /** @var list */ 19 | private array $imports = []; 20 | 21 | /** 22 | * Register an import in the registry 23 | * @param TImport $import 24 | */ 25 | public function register(SourceConfigInterface $import): self 26 | { 27 | $this->imports[] = $import; 28 | 29 | return $this; 30 | } 31 | 32 | public function getType(): string 33 | { 34 | return 'import'; 35 | } 36 | 37 | public function getItems(): array 38 | { 39 | return $this->imports; 40 | } 41 | 42 | public function jsonSerialize(): array 43 | { 44 | return $this->imports; 45 | } 46 | 47 | public function getIterator(): \Traversable 48 | { 49 | return new \ArrayIterator($this->getItems()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Config/Import/Merger/ConfigMergerInterface.php: -------------------------------------------------------------------------------- 1 | $mainConfig The main configuration to merge into 23 | * @param ImportedConfig $importedConfig The imported configuration to merge from 24 | * @return array The merged configuration 25 | */ 26 | public function merge(array $mainConfig, ImportedConfig $importedConfig): array; 27 | 28 | /** 29 | * Check if this merger supports the given configuration. 30 | * 31 | * @param ImportedConfig $config The configuration to check 32 | * @return bool True if this merger supports the configuration 33 | */ 34 | public function supports(ImportedConfig $config): bool; 35 | } 36 | -------------------------------------------------------------------------------- /src/Config/Import/Merger/ConfigMergerProviderInterface.php: -------------------------------------------------------------------------------- 1 | $mainConfig The main configuration to merge into 15 | * @param ImportedConfig ...$configs The imported configurations to merge from 16 | * @return array The merged configuration 17 | */ 18 | public function mergeConfigurations(array $mainConfig, ImportedConfig ...$configs): array; 19 | } 20 | -------------------------------------------------------------------------------- /src/Config/Import/Merger/VariablesConfigMerger.php: -------------------------------------------------------------------------------- 1 | combinePaths($pathPrefix, $document['outputPath']); 30 | } 31 | } 32 | } 33 | 34 | return $config; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Config/Import/ResolvedConfig.php: -------------------------------------------------------------------------------- 1 | path; 25 | } 26 | 27 | public function getPathPrefix(): ?string 28 | { 29 | return $this->pathPrefix; 30 | } 31 | 32 | public function getSelectiveDocuments(): ?array 33 | { 34 | return $this->selectiveDocuments; 35 | } 36 | 37 | public function getFilter(): ?FilterConfig 38 | { 39 | return $this->filter; 40 | } 41 | 42 | public function getConfigDirectory(): string 43 | { 44 | return \dirname($this->getPath()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Config/Import/Source/Config/FilterConfig.php: -------------------------------------------------------------------------------- 1 | |null $config Raw filter configuration 14 | */ 15 | public function __construct( 16 | private ?array $config = null, 17 | ) {} 18 | 19 | /** 20 | * Creates a FilterConfig from an array. 21 | * 22 | * @param array|null $config The filter configuration 23 | */ 24 | public static function fromArray(?array $config): self 25 | { 26 | if (empty($config)) { 27 | return new self(); 28 | } 29 | 30 | return new self($config); 31 | } 32 | 33 | /** 34 | * Gets the raw filter configuration. 35 | * 36 | * @return array|null The filter configuration 37 | */ 38 | public function getConfig(): ?array 39 | { 40 | return $this->config; 41 | } 42 | 43 | /** 44 | * Checks if the filter configuration is empty. 45 | */ 46 | public function isEmpty(): bool 47 | { 48 | return empty($this->config); 49 | } 50 | 51 | /** 52 | * @return array|null 53 | */ 54 | public function jsonSerialize(): ?array 55 | { 56 | return $this->config; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Config/Import/Source/Config/SourceConfigFactory.php: -------------------------------------------------------------------------------- 1 | LocalSourceConfig::fromArray($config, $basePath), 28 | 'url' => UrlSourceConfig::fromArray($config, $basePath), 29 | default => throw new \InvalidArgumentException("Unsupported source type: {$type}"), 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Config/Import/Source/Config/SourceConfigInterface.php: -------------------------------------------------------------------------------- 1 | The loaded configuration data 30 | * @throws ImportSourceException If loading fails 31 | */ 32 | public function load(SourceConfigInterface $config): array; 33 | 34 | /** 35 | * Get the list of sections that this source can import 36 | * 37 | * @return array List of section names (Empty array means all sections are allowed) 38 | */ 39 | public function allowedSections(): array; 40 | } 41 | -------------------------------------------------------------------------------- /src/Config/Import/Source/ImportedConfig.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final readonly class ImportedConfig implements \ArrayAccess 13 | { 14 | public function __construct( 15 | public SourceConfigInterface $sourceConfig, 16 | public array $config, 17 | public string $path, 18 | public bool $isLocal, 19 | ) {} 20 | 21 | public function offsetExists(mixed $offset): bool 22 | { 23 | return \array_key_exists($offset, $this->config); 24 | } 25 | 26 | public function offsetGet(mixed $offset): mixed 27 | { 28 | return $this->config[$offset] ?? null; 29 | } 30 | 31 | public function offsetSet(mixed $offset, mixed $value): void 32 | { 33 | throw new \RuntimeException('Cannot set value in imported config'); 34 | } 35 | 36 | public function offsetUnset(mixed $offset): void 37 | { 38 | throw new \RuntimeException('Cannot unset value in imported config'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Config/Loader/ConfigLoaderFactoryInterface.php: -------------------------------------------------------------------------------- 1 | */ 15 | private array $parsers; 16 | 17 | /** 18 | * @param ConfigParserInterface ...$parsers The parsers to use 19 | */ 20 | public function __construct( 21 | ConfigParserInterface ...$parsers, 22 | ) { 23 | $this->parsers = $parsers; 24 | } 25 | 26 | public function parse(array $config): ConfigRegistry 27 | { 28 | $registry = new ConfigRegistry(); 29 | 30 | foreach ($this->parsers as $parser) { 31 | $parsedRegistry = $parser->parse($config); 32 | 33 | foreach ($parsedRegistry->all() as $type => $typeRegistry) { 34 | // Only register if not already registered 35 | if (!$registry->has($type)) { 36 | $registry->register($typeRegistry); 37 | } 38 | } 39 | } 40 | 41 | return $registry; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Config/Parser/ConfigParserInterface.php: -------------------------------------------------------------------------------- 1 | $config The configuration data to parse 18 | */ 19 | public function parse(array $config): ConfigRegistry; 20 | } 21 | -------------------------------------------------------------------------------- /src/Config/Parser/ConfigParserPluginInterface.php: -------------------------------------------------------------------------------- 1 | $config The current configuration array 26 | * @param string $rootPath The root path for resolving relative paths 27 | * @return array The updated configuration array 28 | */ 29 | public function updateConfig(array $config, string $rootPath): array; 30 | 31 | /** 32 | * Parse the configuration section and return a registry 33 | * 34 | * @param array $config The full configuration array 35 | * @param string $rootPath The root path for resolving relative paths 36 | */ 37 | public function parse(array $config, string $rootPath): ?RegistryInterface; 38 | 39 | /** 40 | * Check if this plugin supports the given configuration 41 | * 42 | * @param array $config The full configuration array 43 | */ 44 | public function supports(array $config): bool; 45 | } 46 | -------------------------------------------------------------------------------- /src/Config/Parser/ParserPluginRegistry.php: -------------------------------------------------------------------------------- 1 | */ 14 | private array $plugins = [], 15 | ) {} 16 | 17 | /** 18 | * Register a parser plugin 19 | */ 20 | public function register(ConfigParserPluginInterface $plugin): void 21 | { 22 | $this->plugins[] = $plugin; 23 | } 24 | 25 | /** 26 | * Get all registered parser plugins 27 | * 28 | * @return array 29 | */ 30 | public function getPlugins(): array 31 | { 32 | return $this->plugins; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Config/Reader/ConfigReaderRegistry.php: -------------------------------------------------------------------------------- 1 | */ 11 | private array $readers, 12 | ) {} 13 | 14 | public function has(string $ext): bool 15 | { 16 | foreach ($this->readers as $reader) { 17 | if (\in_array($ext, $reader->getSupportedExtensions(), true)) { 18 | return true; 19 | } 20 | } 21 | 22 | return false; 23 | } 24 | 25 | public function get(string $ext): ReaderInterface 26 | { 27 | foreach ($this->readers as $reader) { 28 | if (\in_array($ext, $reader->getSupportedExtensions(), true)) { 29 | return $reader; 30 | } 31 | } 32 | 33 | throw new \RuntimeException(\sprintf('No reader found for extension: %s', $ext)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Config/Reader/JsonReader.php: -------------------------------------------------------------------------------- 1 | The parsed configuration data 19 | * @throws ReaderException If reading or parsing fails 20 | */ 21 | public function read(string $path): array; 22 | 23 | /** 24 | * Check if this reader supports the given path 25 | * 26 | * @param string $path Path to configuration source 27 | * @return bool True if the reader can handle this path 28 | */ 29 | public function supports(string $path): bool; 30 | 31 | public function getSupportedExtensions(): array; 32 | } 33 | -------------------------------------------------------------------------------- /src/Config/Reader/StringJsonReader.php: -------------------------------------------------------------------------------- 1 | logger?->debug('Reading JSON configuration from string', [ 20 | 'contentLength' => \strlen($this->jsonContent), 21 | ]); 22 | 23 | try { 24 | $config = \json_decode($this->jsonContent, true, flags: JSON_THROW_ON_ERROR); 25 | 26 | if (!\is_array($config)) { 27 | throw new ReaderException('JSON configuration must decode to an array'); 28 | } 29 | 30 | $this->logger?->debug('JSON string successfully parsed'); 31 | return $config; 32 | } catch (\JsonException $e) { 33 | $errorMessage = 'Invalid JSON in configuration string'; 34 | $this->logger?->error($errorMessage, [ 35 | 'error' => $e->getMessage(), 36 | ]); 37 | throw new ReaderException($errorMessage, previous: $e); 38 | } 39 | } 40 | 41 | public function supports(string $path): bool 42 | { 43 | // This reader doesn't care about the path - it always supports reading from its string 44 | return true; 45 | } 46 | 47 | public function getSupportedExtensions(): array 48 | { 49 | return []; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Config/Reader/YamlReader.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface RegistryInterface extends \JsonSerializable, \IteratorAggregate 13 | { 14 | /** 15 | * Get the registry type identifier 16 | */ 17 | public function getType(): string; 18 | 19 | /** 20 | * Get all items in the registry 21 | * 22 | * @return list 23 | */ 24 | public function getItems(): array; 25 | } 26 | -------------------------------------------------------------------------------- /src/Config/context.yaml: -------------------------------------------------------------------------------- 1 | documents: 2 | - description: Configuration System 3 | outputPath: core/config-loader.md 4 | sources: 5 | - type: file 6 | sourcePaths: 7 | - . 8 | - ../Document 9 | - ../Fetcher 10 | - ../Application/Bootloader/ConfigLoaderBootloader.php 11 | - ../Application/Bootloader/ImportBootloader.php 12 | 13 | - description: Configuration Import 14 | outputPath: core/config-import.md 15 | sources: 16 | - type: file 17 | sourcePaths: 18 | - ./Import 19 | 20 | - description: Configuration Exclude 21 | outputPath: core/config-exclude.md 22 | sources: 23 | - type: file 24 | sourcePaths: 25 | - ./Exclude 26 | 27 | - description: Configuration Parser 28 | outputPath: core/config-parser.md 29 | sources: 30 | - type: file 31 | sourcePaths: 32 | - ./Parser 33 | 34 | -------------------------------------------------------------------------------- /src/Console/context.yaml: -------------------------------------------------------------------------------- 1 | documents: 2 | - description: Console Commands 3 | outputPath: console/commands.md 4 | sources: 5 | - type: file 6 | sourcePaths: . 7 | filePattern: '*Command.php' 8 | showTreeView: true 9 | 10 | - description: Console Renderers 11 | outputPath: console/renderers.md 12 | sources: 13 | - type: file 14 | sourcePaths: ./Renderer 15 | filePattern: '*.php' 16 | showTreeView: true 17 | 18 | -------------------------------------------------------------------------------- /src/Document/Compiler/CompiledDocument.php: -------------------------------------------------------------------------------- 1 | content, 22 | errors: $this->errors, 23 | outputPath: $outputPath, 24 | contextPath: $contextPath, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Document/Compiler/Error/ErrorCollection.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ErrorCollection implements \Countable, \IteratorAggregate, \JsonSerializable 13 | { 14 | public function __construct( 15 | /** 16 | * @var array 17 | */ 18 | private array $errors = [], 19 | ) {} 20 | 21 | /** 22 | * Add a source error to the collection 23 | * @param TError $error The error message 24 | */ 25 | public function add(\Stringable|string $error): void 26 | { 27 | $this->errors[] = $error; 28 | } 29 | 30 | /** 31 | * Check if the collection has any errors 32 | */ 33 | public function hasErrors(): bool 34 | { 35 | return $this->count() > 0; 36 | } 37 | 38 | /** 39 | * Get the number of errors in the collection 40 | */ 41 | public function count(): int 42 | { 43 | return \count($this->errors); 44 | } 45 | 46 | /** 47 | * Make the collection iterable 48 | */ 49 | public function getIterator(): \Traversable 50 | { 51 | return new \ArrayIterator($this->errors); 52 | } 53 | 54 | public function jsonSerialize(): array 55 | { 56 | return \array_map( 57 | static fn(\Stringable|string $error): string => (string) $error, 58 | $this->errors, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Document/Compiler/Error/SourceError.php: -------------------------------------------------------------------------------- 1 | source); 25 | return $this->source->hasDescription() 26 | ? $this->source->getDescription() 27 | : $source->getShortName(); 28 | } 29 | 30 | /** 31 | * Get a formatted error message 32 | */ 33 | public function __toString(): string 34 | { 35 | return \sprintf( 36 | "Error in %s: %s", 37 | $this->getSourceDescription(), 38 | $this->exception->getMessage(), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Lib/BinaryUpdater/Strategy/UpdateStrategyInterface.php: -------------------------------------------------------------------------------- 1 | new WindowsUpdateStrategy($this->files, $this->logger), 34 | default => new UnixUpdateStrategy($this->files, $this->logger), 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Lib/ComposerClient/ComposerClientInterface.php: -------------------------------------------------------------------------------- 1 | Parsed composer.json data 19 | * @throws ComposerNotFoundException If composer.json can't be found or parsed 20 | */ 21 | public function loadComposerData(string $path): array; 22 | 23 | /** 24 | * Try to load composer.lock file 25 | * 26 | * @param string $path Path to directory containing composer.lock 27 | * @return array|null Parsed composer.lock data or null if not found 28 | */ 29 | public function tryLoadComposerLock(string $path): ?array; 30 | 31 | /** 32 | * Get the vendor directory from composer.json or use default 33 | * 34 | * @param array $composerData Parsed composer.json data 35 | * @param string $basePath Base path 36 | * @return string Vendor directory path (relative to composer.json) 37 | */ 38 | public function getVendorDir(array $composerData, string $basePath): string; 39 | } 40 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/AbstractBlock.php: -------------------------------------------------------------------------------- 1 | content; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/BlockInterface.php: -------------------------------------------------------------------------------- 1 | language; 32 | } 33 | 34 | /** 35 | * Get the file path for the code block 36 | */ 37 | public function getFilePath(): ?string 38 | { 39 | return $this->filepath; 40 | } 41 | 42 | public function render(RendererInterface $renderer): string 43 | { 44 | return $renderer->renderCodeBlock($this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/CommentBlock.php: -------------------------------------------------------------------------------- 1 | renderCommentBlock($this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/DescriptionBlock.php: -------------------------------------------------------------------------------- 1 | renderDescriptionBlock($this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/SeparatorBlock.php: -------------------------------------------------------------------------------- 1 | length; 33 | } 34 | 35 | public function render(RendererInterface $renderer): string 36 | { 37 | return $renderer->renderSeparatorBlock($this); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/TextBlock.php: -------------------------------------------------------------------------------- 1 | renderTextBlock($this); 22 | } 23 | 24 | public function getTag(): string 25 | { 26 | return \trim($this->tag); 27 | } 28 | 29 | public function hasTag(): bool 30 | { 31 | return $this->getTag() !== ''; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/TitleBlock.php: -------------------------------------------------------------------------------- 1 | level; 31 | } 32 | 33 | public function render(RendererInterface $renderer): string 34 | { 35 | return $renderer->renderTitleBlock($this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Lib/Content/Block/TreeViewBlock.php: -------------------------------------------------------------------------------- 1 | renderTreeViewBlock($this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lib/Content/ContentBlock.php: -------------------------------------------------------------------------------- 1 | blocks[] = $block; 28 | return $this; 29 | } 30 | 31 | /** 32 | * Get all blocks in the collection 33 | * 34 | * @return BlockInterface[] 35 | */ 36 | public function getBlocks(): array 37 | { 38 | return $this->blocks; 39 | } 40 | 41 | /** 42 | * Render all blocks using the provided renderer 43 | * 44 | * @param RendererInterface $renderer The renderer to use 45 | * @return string The rendered content 46 | */ 47 | public function render(RendererInterface $renderer): string 48 | { 49 | return $renderer->renderContent($this->blocks); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Lib/Content/ContentBuilderFactory.php: -------------------------------------------------------------------------------- 1 | defaultRenderer); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Lib/Content/Renderer/AbstractRenderer.php: -------------------------------------------------------------------------------- 1 | render($this)); 24 | } 25 | 26 | return $content; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Lib/Content/Renderer/RendererInterface.php: -------------------------------------------------------------------------------- 1 | $files The found files 14 | * @param string $treeView Text representation of the file tree structure 15 | */ 16 | public function __construct( 17 | public array $files, 18 | public string $treeView, 19 | ) {} 20 | 21 | public function count(): int 22 | { 23 | return \count($this->files); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Lib/Git/Command.php: -------------------------------------------------------------------------------- 1 | |string $command Git command to execute (without 'git' prefix) 15 | */ 16 | public function __construct( 17 | public string $repository, 18 | private array|string $command, 19 | ) {} 20 | 21 | /** 22 | * Get the command as an array. 23 | * 24 | * @return array 25 | */ 26 | public function getCommandParts(): array 27 | { 28 | if (\is_array($this->command)) { 29 | return $this->command; 30 | } 31 | 32 | $command = \trim($this->command); 33 | 34 | // If the command already starts with 'git', remove it 35 | if (\str_starts_with($command, 'git ')) { 36 | $command = \substr($command, 4); 37 | } 38 | 39 | return \array_filter(\explode(' ', $command)); 40 | } 41 | 42 | public function __toString(): string 43 | { 44 | if (\is_string($this->command)) { 45 | $command = \trim($this->command); 46 | 47 | // If the command already starts with 'git', use it as is 48 | if (\str_starts_with($command, 'git ')) { 49 | $command = \substr($command, 4); 50 | } 51 | 52 | return $command; 53 | } 54 | 55 | return \implode(' ', $this->command); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Lib/Git/CommandsExecutorInterface.php: -------------------------------------------------------------------------------- 1 | $errorOutput The error output from the command 16 | */ 17 | public function __construct( 18 | private readonly string $command, 19 | private readonly int $exitCode, 20 | private readonly array $errorOutput = [], 21 | ) { 22 | $message = \sprintf( 23 | 'Git command "%s" failed with exit code %d: %s', 24 | $command, 25 | $exitCode, 26 | \implode("\n", $errorOutput), 27 | ); 28 | 29 | parent::__construct($message, $exitCode); 30 | } 31 | 32 | /** 33 | * Get the Git command that failed 34 | */ 35 | public function getCommand(): string 36 | { 37 | return $this->command; 38 | } 39 | 40 | /** 41 | * Get the exit code returned by the command 42 | */ 43 | public function getExitCode(): int 44 | { 45 | return $this->exitCode; 46 | } 47 | 48 | /** 49 | * Get the error output from the command 50 | * 51 | * @return array 52 | */ 53 | public function getErrorOutput(): array 54 | { 55 | return $this->errorOutput; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Lib/Git/Exception/GitCommandException.php: -------------------------------------------------------------------------------- 1 | self::Amd64, 18 | 'aarch64', 'arm64' => self::Arm64, 19 | // Add more mappings as needed 20 | default => throw new \RuntimeException('Unsupported architecture: ' . $arch), 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Lib/GithubClient/GithubClientInterface.php: -------------------------------------------------------------------------------- 1 | repository); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Lib/GithubClient/Platform.php: -------------------------------------------------------------------------------- 1 | self::Linux, 17 | 'Darwin' => self::Macos, 18 | 'Windows' => self::Windows, 19 | default => throw new \RuntimeException('Unsupported platform: ' . PHP_OS_FAMILY), 20 | }; 21 | } 22 | 23 | public function isWindows(): bool 24 | { 25 | return $this === self::Windows; 26 | } 27 | 28 | public function extension(): string 29 | { 30 | return $this->isWindows() ? '.exe' : ''; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lib/GitlabClient/GitlabClientInterface.php: -------------------------------------------------------------------------------- 1 | > Repository contents 20 | */ 21 | public function getContents(GitlabRepository $repository, string $path = ''): array; 22 | 23 | /** 24 | * Get file content from GitLab API 25 | * 26 | * @param GitlabRepository $repository GitLab repository 27 | * @param string $path Path to the file 28 | * @return string File content 29 | */ 30 | public function getFileContent(GitlabRepository $repository, string $path): string; 31 | 32 | /** 33 | * Set the GitLab API token 34 | * 35 | * @param string|null $token GitLab API token 36 | */ 37 | public function setToken(?string $token): void; 38 | 39 | /** 40 | * Set the GitLab server URL 41 | * 42 | * @param string $serverUrl GitLab server URL 43 | */ 44 | public function setServerUrl(string $serverUrl): void; 45 | 46 | /** 47 | * Set custom HTTP headers for API requests 48 | * 49 | * @param array $headers Custom HTTP headers 50 | */ 51 | public function setHeaders(array $headers): void; 52 | } 53 | -------------------------------------------------------------------------------- /src/Lib/GitlabClient/Model/GitlabRepository.php: -------------------------------------------------------------------------------- 1 | projectId = \urlencode($repository); 26 | } 27 | 28 | /** 29 | * Get the full repository URL (without server URL) 30 | */ 31 | public function getPath(): string 32 | { 33 | return $this->repository; 34 | } 35 | 36 | /** 37 | * Get the full repository URL (for display purposes) 38 | * 39 | * @param string $serverUrl Base GitLab server URL 40 | */ 41 | public function getUrl(string $serverUrl = 'https://gitlab.com'): string 42 | { 43 | return \rtrim($serverUrl, '/') . '/' . $this->repository; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Lib/Html/HtmlCleaner.php: -------------------------------------------------------------------------------- 1 | htmlConverter->getConfig()->setOption('strip_tags', true); 18 | } 19 | 20 | public function clean(string $html): string 21 | { 22 | if ($html === '') { 23 | return ''; 24 | } 25 | 26 | return $this->htmlConverter->convert($html); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Lib/Html/HtmlCleanerInterface.php: -------------------------------------------------------------------------------- 1 | $headers Optional request headers 16 | * @return HttpResponse The response object 17 | * 18 | * @throws HttpException If the request fails 19 | */ 20 | public function get(string $url, array $headers = []): HttpResponse; 21 | 22 | /** 23 | * Send a POST request to the specified URL 24 | * 25 | * @param string $url The URL to request 26 | * @param array $headers Optional request headers 27 | * @param string|null $body Optional request body 28 | * @return HttpResponse The response object 29 | * 30 | * @throws HttpException If the request fails 31 | */ 32 | public function post(string $url, array $headers = [], ?string $body = null): HttpResponse; 33 | 34 | /** 35 | * Send a request to the specified URL and follow redirects if needed 36 | * 37 | * @param string $url The URL to request 38 | * @param array $headers Optional request headers 39 | * @return HttpResponse The final response after following redirects 40 | * 41 | * @throws HttpException If the request fails 42 | */ 43 | public function getWithRedirects(string $url, array $headers = []): HttpResponse; 44 | } 45 | -------------------------------------------------------------------------------- /src/Lib/PathFilter/AbstractFilter.php: -------------------------------------------------------------------------------- 1 | > 10 | */ 11 | abstract class AbstractFilter implements FilterInterface 12 | { 13 | /** 14 | * Helper method to check if a value matches a pattern 15 | * 16 | * @param string $value The value to check 17 | * @param string|array $pattern The pattern or patterns to match against 18 | */ 19 | protected function matchPattern(string $value, string|array $pattern): bool 20 | { 21 | if (empty($pattern)) { 22 | return true; // No pattern means match all 23 | } 24 | 25 | $patterns = \is_array($pattern) ? $pattern : [$pattern]; 26 | 27 | foreach ($patterns as $p) { 28 | if (\str_contains($value, $p)) { 29 | return true; 30 | } 31 | 32 | if (!FileHelper::isRegex($pattern)) { 33 | $p = FileHelper::toRegex($p); 34 | } 35 | 36 | if ($this->matchGlob($value, $p)) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | 44 | /** 45 | * Match a value against a glob pattern 46 | */ 47 | protected function matchGlob(string $value, string $regex): bool 48 | { 49 | return (bool) \preg_match($regex, $value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Lib/PathFilter/FilePatternFilter.php: -------------------------------------------------------------------------------- 1 | $pattern File pattern(s) to match 16 | */ 17 | public function __construct( 18 | private readonly string|array $pattern, 19 | ) {} 20 | 21 | public function apply(array $items): array 22 | { 23 | if (empty($this->pattern)) { 24 | return $items; 25 | } 26 | 27 | return \array_filter($items, function (array $item): bool { 28 | // Skip directories 29 | if ($item['type'] === 'dir') { 30 | return true; 31 | } 32 | 33 | $filename = $item['name']; 34 | return $this->matchPattern($filename, $this->pattern); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Lib/PathFilter/FilterInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface FilterInterface 12 | { 13 | /** 14 | * Apply the filter to the list of file items 15 | * 16 | * @param array $items GitHub API response items 17 | * @return array Filtered items 18 | */ 19 | public function apply(array $items): array; 20 | } 21 | -------------------------------------------------------------------------------- /src/Lib/PathFilter/PathFilter.php: -------------------------------------------------------------------------------- 1 | $pathPattern Path pattern(s) to include 16 | */ 17 | public function __construct( 18 | private readonly string|array $pathPattern, 19 | ) {} 20 | 21 | public function apply(array $items): array 22 | { 23 | if (empty($this->pathPattern)) { 24 | return $items; 25 | } 26 | 27 | return \array_filter($items, function (array $item): bool { 28 | $path = $item['path'] ?? ''; 29 | return $this->matchPattern($path, $this->pathPattern); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lib/TokenCounter/CharTokenCounter.php: -------------------------------------------------------------------------------- 1 | calculateDirectoryCount($children); 31 | } else { 32 | $totalChars += $this->countFile($children); 33 | } 34 | } 35 | 36 | return $totalChars; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lib/TokenCounter/TokenCounterInterface.php: -------------------------------------------------------------------------------- 1 | $directory Directory structure 24 | * @return int Total number of characters 25 | */ 26 | public function calculateDirectoryCount(array $directory): int; 27 | } 28 | -------------------------------------------------------------------------------- /src/Lib/TreeBuilder/TreeRendererInterface.php: -------------------------------------------------------------------------------- 1 | $tree The tree structure to render 16 | * @param array $options Additional rendering options 17 | * @return string The rendered tree 18 | */ 19 | public function render( 20 | array $tree, 21 | array $options = [], 22 | ): string; 23 | } 24 | -------------------------------------------------------------------------------- /src/Lib/Variable/CompositeProcessor.php: -------------------------------------------------------------------------------- 1 | processors as $processor) { 19 | $text = $processor->process($text); 20 | } 21 | 22 | return $text; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Lib/Variable/Provider/ConfigVariableProvider.php: -------------------------------------------------------------------------------- 1 | Custom variables from config 17 | */ 18 | private array $variables = []; 19 | 20 | public function __construct(array $variables = []) 21 | { 22 | $this->setVariables($variables); 23 | } 24 | 25 | /** 26 | * Set variables from config 27 | * 28 | * @param array $variables Variables from config 29 | */ 30 | public function setVariables(array $variables): self 31 | { 32 | $this->variables = []; 33 | 34 | // Convert all values to strings 35 | foreach ($variables as $key => $value) { 36 | // Skip non-scalar values 37 | if (!\is_scalar($value)) { 38 | continue; 39 | } 40 | 41 | $this->variables[(string) $key] = (string) $value; 42 | } 43 | 44 | return $this; 45 | } 46 | 47 | public function has(string $name): bool 48 | { 49 | return \array_key_exists($name, $this->variables); 50 | } 51 | 52 | public function get(string $name): ?string 53 | { 54 | return $this->variables[$name] ?? null; 55 | } 56 | 57 | /** 58 | * Get all variables 59 | * 60 | * @return array 61 | */ 62 | public function getAll(): array 63 | { 64 | return $this->variables; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Lib/Variable/Provider/DotEnvVariableProvider.php: -------------------------------------------------------------------------------- 1 | rootPath) { 20 | $dotenv = Dotenv::create($repository, $this->rootPath, $this->envFileName); 21 | $dotenv->load(); 22 | } 23 | 24 | $this->repository = $repository; 25 | } 26 | 27 | public function has(string $name): bool 28 | { 29 | return $this->repository->has($name); 30 | } 31 | 32 | public function get(string $name): ?string 33 | { 34 | return $this->repository->get($name); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Lib/Variable/Provider/PredefinedVariableProvider.php: -------------------------------------------------------------------------------- 1 | getPredefinedVariables()); 15 | } 16 | 17 | public function get(string $name): ?string 18 | { 19 | return $this->getPredefinedVariables()[$name] ?? null; 20 | } 21 | 22 | /** 23 | * Get all predefined variables 24 | * 25 | * @return array 26 | */ 27 | private function getPredefinedVariables(): array 28 | { 29 | return [ 30 | 'DATETIME' => \date('Y-m-d H:i:s'), 31 | 'DATE' => \date('Y-m-d'), 32 | 'TIME' => \date('H:i:s'), 33 | 'TIMESTAMP' => (string) \time(), 34 | 'USER' => \get_current_user(), 35 | 'HOME_DIR' => \getenv('HOME') ?: (\getenv('USERPROFILE') ?: '/'), 36 | 'TEMP_DIR' => \sys_get_temp_dir(), 37 | 'OS' => PHP_OS, 38 | 'HOSTNAME' => \gethostname() ?: 'unknown', 39 | 'PWD' => \getcwd() ?: '.', 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Lib/Variable/Provider/VariableProviderInterface.php: -------------------------------------------------------------------------------- 1 | processor, 20 | $processor, 21 | ])); 22 | } 23 | 24 | /** 25 | * Resolve variables in the given text 26 | */ 27 | public function resolve(string|array|null $strings): string|array|null 28 | { 29 | if ($strings === null) { 30 | return null; 31 | } 32 | 33 | if (\is_array($strings)) { 34 | return \array_map($this->resolve(...), $strings); 35 | } 36 | 37 | return $this->processor->process($strings); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lib/context.yaml: -------------------------------------------------------------------------------- 1 | documents: 2 | - description: Path Filtering Utilities 3 | outputPath: utilities/path-filters.md 4 | sources: 5 | - type: file 6 | sourcePaths: ./PathFilter 7 | 8 | - description: Tree Building Utilities 9 | outputPath: utilities/tree-builder.md 10 | sources: 11 | - type: file 12 | sourcePaths: ./TreeBuilder 13 | 14 | - description: HTTP Client 15 | outputPath: utilities/http-client.md 16 | sources: 17 | - type: file 18 | sourcePaths: ./HttpClient 19 | 20 | - description: Binary Updater 21 | outputPath: utilities/binary-updater.md 22 | sources: 23 | - type: file 24 | sourcePaths: ./BinaryUpdater 25 | 26 | - description: GitHub Client 27 | outputPath: utilities/github-client.md 28 | sources: 29 | - type: file 30 | sourcePaths: 31 | - ./GithubClient 32 | 33 | - description: Gitlab Client 34 | outputPath: utilities/gitlab-client.md 35 | sources: 36 | - type: file 37 | sourcePaths: 38 | - ./GitlabClient 39 | 40 | - description: Git Client 41 | outputPath: utilities/git-client.md 42 | sources: 43 | - type: file 44 | sourcePaths: 45 | - ./Git 46 | 47 | - description: Variable System 48 | outputPath: utilities/variable-system.md 49 | sources: 50 | - type: file 51 | sourcePaths: ./Variable 52 | 53 | - description: HTML Utilities 54 | outputPath: utilities/html.md 55 | sources: 56 | - type: file 57 | sourcePaths: ./Html -------------------------------------------------------------------------------- /src/McpServer/Action/Prompts/GetPromptAction.php: -------------------------------------------------------------------------------- 1 | getAttribute('id'); 26 | $this->logger->info('Getting prompt', ['id' => $id]); 27 | 28 | if (!$this->prompts->has($id)) { 29 | return new GetPromptResult([]); 30 | } 31 | 32 | $prompt = $this->prompts->get($id, $request->getAttributes()); 33 | 34 | return new GetPromptResult( 35 | messages: $prompt->messages, 36 | description: $prompt->prompt->description, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/McpServer/Action/Prompts/ListPromptsAction.php: -------------------------------------------------------------------------------- 1 | configLoader->load(); 30 | $this->logger->info('Listing available prompts'); 31 | 32 | $prompts = []; 33 | foreach ($this->registry->getPrompts() as $prompt) { 34 | $prompts[] = $prompt; 35 | } 36 | 37 | foreach ($this->prompts->allPrompts() as $prompt) { 38 | $prompts[] = $prompt->prompt; 39 | } 40 | 41 | return new ListPromptsResult($prompts); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/McpServer/Action/Prompts/ProjectStructurePromptAction.php: -------------------------------------------------------------------------------- 1 | logger->info('Getting project-structure prompt'); 32 | 33 | return new GetPromptResult( 34 | messages: [ 35 | new PromptMessage( 36 | role: Role::USER, 37 | content: new TextContent( 38 | text: "Look at available contexts and try to find the project structure. If there is no context for structure. Request structure from context using JSON schema. Provide the result in JSON format", 39 | ), 40 | ), 41 | ], 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/McpServer/Attribute/InputSchema.php: -------------------------------------------------------------------------------- 1 | env->get('MCP_PROJECT_NAME'); 18 | $projectSlug = $this->env->get('MCP_PROJECT_SLUG'); 19 | if ($projectName !== null && $projectSlug === null) { 20 | $projectSlug = \preg_replace('#[^a-z0-9]+#i', '', (string) $projectName); 21 | } 22 | 23 | return new ProjectService( 24 | projectName: $projectName, 25 | projectSlug: $projectSlug, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/McpServer/ProjectService/ProjectServiceInterface.php: -------------------------------------------------------------------------------- 1 | toString(), 34 | addedAt: $data['added_at'] ?? \date('Y-m-d H:i:s'), 35 | configFile: $data['config_file'] ?? null, 36 | envFile: $data['env_file'] ?? null, 37 | ); 38 | } 39 | 40 | /** 41 | * Convert to array representation 42 | */ 43 | public function jsonSerialize(): array 44 | { 45 | return [ 46 | 'added_at' => $this->addedAt, 47 | 'config_file' => $this->configFile, 48 | 'env_file' => $this->envFile, 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/McpServer/Projects/McpProjectsBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 24 | FactoryInterface $factory, 25 | DirectoriesInterface $dirs, 26 | ) => $factory->make(ProjectStateRepository::class, [ 27 | 'stateDirectory' => $dirs->get('global-state'), 28 | ]), 29 | ProjectServiceInterface::class => ProjectService::class, 30 | ]; 31 | } 32 | 33 | public function init(ConsoleBootloader $console): void 34 | { 35 | $console->addCommand( 36 | ProjectCommand::class, 37 | ProjectAddCommand::class, 38 | ProjectListCommand::class, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/McpServer/Projects/Repository/ProjectStateRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | $config The extension configuration 25 | * @return self The created PromptExtension 26 | * @throws \InvalidArgumentException If the configuration is invalid 27 | */ 28 | public static function fromArray(array $config): self 29 | { 30 | if (empty($config['id']) || !\is_string($config['id'])) { 31 | throw new \InvalidArgumentException('Extension must have a template ID'); 32 | } 33 | 34 | $arguments = []; 35 | if (isset($config['arguments']) && \is_array($config['arguments'])) { 36 | foreach ($config['arguments'] as $name => $value) { 37 | if (!\is_string($name) || !\is_string($value)) { 38 | throw new \InvalidArgumentException( 39 | \sprintf('Extension argument "%s" must have a string value', $name), 40 | ); 41 | } 42 | 43 | $arguments[] = new PromptExtensionArgument($name, $value); 44 | } 45 | } 46 | 47 | return new self($config['id'], $arguments); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/Extension/PromptExtensionArgument.php: -------------------------------------------------------------------------------- 1 | The variables from extension arguments 16 | */ 17 | private array $variables; 18 | 19 | /** 20 | * @param PromptExtensionArgument[] $arguments The extension arguments 21 | */ 22 | public function __construct(array $arguments) 23 | { 24 | $this->variables = $this->createVariablesFromArguments($arguments); 25 | } 26 | 27 | public function has(string $name): bool 28 | { 29 | return \array_key_exists($name, $this->variables); 30 | } 31 | 32 | public function get(string $name): ?string 33 | { 34 | return $this->variables[$name] ?? null; 35 | } 36 | 37 | /** 38 | * Creates a variables map from extension arguments. 39 | * 40 | * @param PromptExtensionArgument[] $arguments The extension arguments 41 | * @return array The variables map 42 | */ 43 | private function createVariablesFromArguments(array $arguments): array 44 | { 45 | $variables = []; 46 | 47 | foreach ($arguments as $argument) { 48 | $variables[$argument->name] = $argument->value; 49 | } 50 | 51 | return $variables; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/Filter/FilterStrategy.php: -------------------------------------------------------------------------------- 1 | self::ALL, 23 | default => self::ANY, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/Filter/PromptFilterInterface.php: -------------------------------------------------------------------------------- 1 | ids)) { 25 | return true; 26 | } 27 | 28 | // If prompt has no ID, exclude it 29 | if (!isset($promptConfig['id']) || !\is_string($promptConfig['id'])) { 30 | return false; 31 | } 32 | 33 | // Include if the prompt ID is in the list 34 | return \in_array($promptConfig['id'], $this->ids, true); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/McpPromptBootloader.php: -------------------------------------------------------------------------------- 1 | PromptRegistry::class, 19 | PromptProviderInterface::class => PromptRegistry::class, 20 | PromptRegistry::class => PromptRegistry::class, 21 | ]; 22 | } 23 | 24 | public function init( 25 | ConfigLoaderBootloader $configLoader, 26 | PromptParserPlugin $parserPlugin, 27 | PromptConfigMerger $promptConfigMerger, 28 | ConsoleBootloader $console, 29 | ): void { 30 | $configLoader->registerParserPlugin($parserPlugin); 31 | $configLoader->registerMerger($promptConfigMerger); 32 | 33 | $console->addCommand(ListPromptsCommand::class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/PromptMessageProcessor.php: -------------------------------------------------------------------------------- 1 | variables; 23 | 24 | return $prompt->withMessages(\array_map(static function ($message) use ($variables, $arguments) { 25 | $content = $message->content; 26 | 27 | if ($content instanceof TextContent) { 28 | $text = $variables->with( 29 | new VariableReplacementProcessor(new ConfigVariableProvider($arguments)), 30 | )->resolve($content->text); 31 | 32 | $content = new TextContent($text); 33 | } 34 | 35 | return new PromptMessage( 36 | role: $message->role, 37 | content: $content, 38 | ); 39 | }, $prompt->messages)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/PromptProviderInterface.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | public function all(): array; 32 | 33 | /** 34 | * Gets all non-template prompts. 35 | * 36 | * @return array 37 | */ 38 | public function allTemplates(): array; 39 | 40 | /** 41 | * Gets all prompts. 42 | * 43 | * @return list 44 | */ 45 | public function allPrompts(): array; 46 | } 47 | -------------------------------------------------------------------------------- /src/McpServer/Prompt/PromptRegistryInterface.php: -------------------------------------------------------------------------------- 1 | self::Template, 26 | default => self::Prompt, 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/McpServer/Routing/ActionCaller.php: -------------------------------------------------------------------------------- 1 | container->runScope( 24 | bindings: new Scope( 25 | name: AppScope::McpServerRequest, 26 | bindings: [ 27 | ServerRequestInterface::class => $request, 28 | ], 29 | ), 30 | scope: fn(InvokerInterface $invoker) => $invoker->invoke([$this->class, '__invoke']), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/McpServer/Routing/Attribute/Get.php: -------------------------------------------------------------------------------- 1 | $value) { 32 | $request = $request->withAttribute($key, $value); 33 | } 34 | 35 | // For POST requests, also add parameters to parsed body 36 | if ($httpMethod === 'POST' && !empty($mcpParams)) { 37 | $request = $request->withParsedBody($mcpParams); 38 | } 39 | 40 | return $request; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/McpServer/Routing/McpResponseStrategy.php: -------------------------------------------------------------------------------- 1 | logger->info('Invoking route callable', [ 25 | 'route' => $route->getName(), 26 | 'method' => $request->getMethod(), 27 | 'uri' => (string) $request->getUri(), 28 | ]); 29 | 30 | $controller = $route->getCallable($this->getContainer()); 31 | $response = $controller($request, $route->getVars()); 32 | 33 | if ($response instanceof ResponseInterface) { 34 | return $response; 35 | } 36 | 37 | return new JsonResponse($response); 38 | } catch (\Throwable $e) { 39 | $this->logger->error('Error while handling request', [ 40 | 'exception' => $e, 41 | 'request' => $request, 42 | ]); 43 | 44 | return new JsonResponse([ 45 | 'error' => 'Internal Server Error', 46 | 'message' => $e->getMessage(), 47 | ], 500); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/McpServer/ServerRunnerInterface.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function all(): array; 29 | } 30 | -------------------------------------------------------------------------------- /src/McpServer/Tool/ToolRegistryInterface.php: -------------------------------------------------------------------------------- 1 | logger?->info('Executing tool', [ 22 | 'id' => $tool->id, 23 | ]); 24 | 25 | try { 26 | $result = $this->doExecute($tool, $arguments); 27 | 28 | $this->logger?->info('Tool execution completed', [ 29 | 'id' => $tool->id, 30 | 'success' => true, 31 | ]); 32 | 33 | return $result; 34 | } catch (\Throwable $e) { 35 | $this->logger?->error('Tool execution failed', [ 36 | 'id' => $tool->id, 37 | 'error' => $e->getMessage(), 38 | 'exception' => $e::class, 39 | ]); 40 | 41 | throw $e; 42 | } 43 | } 44 | 45 | /** 46 | * Performs the actual tool execution. 47 | * 48 | * @param ToolDefinition $tool The tool to execute 49 | * @return array Execution result 50 | * @throws \Throwable If execution fails 51 | */ 52 | abstract protected function doExecute(ToolDefinition $tool, array $arguments = []): array; 53 | } 54 | -------------------------------------------------------------------------------- /src/McpServer/Tool/Types/ToolHandlerInterface.php: -------------------------------------------------------------------------------- 1 | Execution result 23 | * @throws \Throwable If execution fails 24 | */ 25 | public function execute(ToolDefinition $tool, array $arguments = []): array; 26 | } 27 | -------------------------------------------------------------------------------- /src/McpServer/context.yaml: -------------------------------------------------------------------------------- 1 | documents: 2 | - description: "MCP bootstrap" 3 | outputPath: "mcp/bootstrap.md" 4 | sources: 5 | - type: file 6 | sourcePaths: 7 | - ../Console/MCPServerCommand.php 8 | - ../Application/Bootloader/MCPServerBootloader.php 9 | - ./McpConfig.php 10 | 11 | - description: "MCP Server Actions" 12 | outputPath: "mcp/actions.md" 13 | sources: 14 | - type: file 15 | sourcePaths: 16 | - ./Action 17 | - ./Attribute 18 | - ./Registry 19 | 20 | - description: "MCP Server Prompts" 21 | outputPath: "mcp/prompts.md" 22 | sources: 23 | - type: file 24 | sourcePaths: 25 | - ./Prompt 26 | 27 | - description: "MCP Server Tools" 28 | outputPath: "mcp/tools.md" 29 | sources: 30 | - type: file 31 | sourcePaths: 32 | - ./Tool 33 | - ./Action/Tools 34 | 35 | - description: "MCP Server routing" 36 | outputPath: "mcp/routing.md" 37 | sources: 38 | - type: file 39 | sourcePaths: 40 | - ./Routing 41 | - ./Server.php 42 | - ./ServerFactory.php 43 | 44 | - description: "MCP Server Projects" 45 | outputPath: "mcp/projects.md" 46 | sources: 47 | - type: file 48 | sourcePaths: 49 | - ./Projects -------------------------------------------------------------------------------- /src/Modifier/Alias/AliasesRegistry.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private array $aliases = []; 18 | 19 | /** 20 | * Register a named modifier configuration 21 | */ 22 | public function register(string $alias, Modifier $modifier): self 23 | { 24 | $this->aliases[$alias] = $modifier; 25 | 26 | return $this; 27 | } 28 | 29 | /** 30 | * Check if an alias exists 31 | */ 32 | public function has(string $alias): bool 33 | { 34 | return isset($this->aliases[$alias]); 35 | } 36 | 37 | /** 38 | * Get a modifier by its alias 39 | */ 40 | public function get(string $alias): ?Modifier 41 | { 42 | return $this->aliases[$alias] ?? null; 43 | } 44 | 45 | /** 46 | * Get all registered aliases 47 | * 48 | * @return array 49 | */ 50 | public function all(): array 51 | { 52 | return $this->aliases; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Modifier/Alias/ModifierAliasesParserPlugin.php: -------------------------------------------------------------------------------- 1 | supports($config)) { 40 | return null; 41 | } 42 | 43 | $modifiersConfig = $config['settings']['modifiers']; 44 | 45 | foreach ($modifiersConfig as $alias => $modifierConfig) { 46 | $modifier = Modifier::from($modifierConfig); 47 | $this->aliasesRegistry->register($alias, $modifier); 48 | } 49 | 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Modifier/ModifiersApplierInterface.php: -------------------------------------------------------------------------------- 1 | $modifiers Additional modifiers to include 16 | * @return self New instance with combined modifiers 17 | */ 18 | public function withModifiers(array $modifiers): self; 19 | 20 | /** 21 | * Apply all collected modifiers to the content 22 | * 23 | * @param string $content Content to modify 24 | * @param string $filename Content type identifier (e.g., file extension) 25 | * @return string Modified content 26 | */ 27 | public function apply(string $content, string $filename): string; 28 | } 29 | -------------------------------------------------------------------------------- /src/Modifier/PhpContentFilter/PhpContentFilterBootloader.php: -------------------------------------------------------------------------------- 1 | register(new PhpContentFilter()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modifier/PhpDocs/PhpDocsModifierBootloader.php: -------------------------------------------------------------------------------- 1 | register(new AstDocTransformer()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modifier/PhpSignature/PhpSignatureModifierBootloader.php: -------------------------------------------------------------------------------- 1 | register(new PhpSignature()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modifier/Sanitizer/Rule/ContextSanitizer.php: -------------------------------------------------------------------------------- 1 | $rules Collection of sanitization rules 11 | */ 12 | public function __construct( 13 | private array $rules = [], 14 | ) {} 15 | 16 | /** 17 | * Register a sanitization rule 18 | */ 19 | public function addRule(RuleInterface $rule): self 20 | { 21 | $this->rules[$rule->getName()] = $rule; 22 | return $this; 23 | } 24 | 25 | /** 26 | * Get all registered rules 27 | * 28 | * @return array 29 | */ 30 | public function getRules(): array 31 | { 32 | return $this->rules; 33 | } 34 | 35 | /** 36 | * Sanitize a context file and save the result 37 | */ 38 | public function sanitize(string $content): string 39 | { 40 | foreach ($this->rules as $rule) { 41 | $content = $rule->apply($content); 42 | } 43 | 44 | return $content; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Modifier/Sanitizer/Rule/RegexReplacementRule.php: -------------------------------------------------------------------------------- 1 | $patterns Array of regex patterns and their replacements 15 | */ 16 | public function __construct( 17 | private string $name, 18 | private array $patterns, 19 | ) {} 20 | 21 | public function getName(): string 22 | { 23 | return $this->name; 24 | } 25 | 26 | public function apply(string $content): string 27 | { 28 | foreach ($this->patterns as $pattern => $replacement) { 29 | /** @var string $content */ 30 | $content = \preg_replace($pattern, $replacement, $content); 31 | } 32 | 33 | return $content; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Modifier/Sanitizer/Rule/RuleInterface.php: -------------------------------------------------------------------------------- 1 | register(new SanitizerModifier()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modifier/SourceModifierInterface.php: -------------------------------------------------------------------------------- 1 | $context Additional context information 24 | * @return string Formatted markdown documentation 25 | */ 26 | public function modify(string $content, array $context = []): string; 27 | } 28 | -------------------------------------------------------------------------------- /src/Modifier/context.yaml: -------------------------------------------------------------------------------- 1 | documents: 2 | - description: Modifiers System 3 | outputPath: modifiers/modifiers-core.md 4 | sources: 5 | - type: file 6 | sourcePaths: . 7 | filePattern: 8 | - '*.php' 9 | - 'Alias/*.php' 10 | notPath: 11 | - 'PhpContentFilter.php' 12 | - 'PhpSignature.php' 13 | - 'ContextSanitizerModifier.php' 14 | 15 | - description: PHP Content Modifiers 16 | outputPath: modifiers/php-modifiers.md 17 | sources: 18 | - type: file 19 | sourcePaths: . 20 | filePattern: 21 | - 'PhpContentFilter.php' 22 | - 'PhpSignature.php' 23 | 24 | - description: Sanitizer Modifier 25 | outputPath: modifiers/sanitizer.md 26 | sources: 27 | - type: file 28 | sourcePaths: 29 | - ./Sanitizer 30 | - ../Lib/Sanitizer -------------------------------------------------------------------------------- /src/Source/BaseSource.php: -------------------------------------------------------------------------------- 1 | description); 23 | } 24 | 25 | public function hasDescription(): bool 26 | { 27 | return $this->getDescription() !== ''; 28 | } 29 | 30 | public function getTags(): array 31 | { 32 | return \array_values(\array_unique($this->tags)); 33 | } 34 | 35 | public function hasTags(): bool 36 | { 37 | return !empty($this->getTags()); 38 | } 39 | 40 | public function parseContent( 41 | SourceParserInterface $parser, 42 | ModifiersApplierInterface $modifiersApplier, 43 | ): string { 44 | return $parser->parse($this, $modifiersApplier); 45 | } 46 | 47 | public function jsonSerialize(): array 48 | { 49 | return \array_filter([ 50 | 'description' => $this->getDescription(), 51 | 'tags' => $this->getTags(), 52 | ], static fn($value) => $value !== null && $value !== '' && $value !== []); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Source/Composer/ComposerSourceBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 27 | FactoryInterface $factory, 28 | DirectoriesInterface $dirs, 29 | ): ComposerSourceFetcher => $factory->make(ComposerSourceFetcher::class, [ 30 | 'basePath' => (string) $dirs->getRootPath(), 31 | ]), 32 | ]; 33 | } 34 | 35 | public function init( 36 | SourceFetcherBootloader $registry, 37 | SourceRegistryInterface $sourceRegistry, 38 | ComposerSourceFactory $factory, 39 | ): void { 40 | $registry->register(ComposerSourceFetcher::class); 41 | $sourceRegistry->register($factory); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Source/Composer/Exception/ComposerNotFoundException.php: -------------------------------------------------------------------------------- 1 | $tags 20 | */ 21 | public function __construct( 22 | public readonly string $library, 23 | public readonly string $topic, 24 | string $description = '', 25 | public readonly int $tokens = 2000, 26 | array $tags = [], 27 | ) { 28 | parent::__construct(description: $description, tags: $tags); 29 | } 30 | 31 | #[\Override] 32 | public function jsonSerialize(): array 33 | { 34 | return \array_filter([ 35 | 'type' => 'docs', 36 | ...parent::jsonSerialize(), 37 | 'library' => $this->library, 38 | 'topic' => $this->topic, 39 | 'tokens' => $this->tokens, 40 | ], static fn($value) => $value !== null && $value !== '' && $value !== []); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Source/Docs/DocsSourceFactory.php: -------------------------------------------------------------------------------- 1 | logger?->debug('Creating Docs source', [ 24 | 'path' => $this->dirs->getRootPath(), 25 | 'config' => $config, 26 | ]); 27 | 28 | if (!isset($config['library']) || !\is_string($config['library'])) { 29 | throw new \RuntimeException('Docs source must have a "library" string property'); 30 | } 31 | 32 | if (!isset($config['topic']) || !\is_string($config['topic'])) { 33 | throw new \RuntimeException('Docs source must have a "topic" string property'); 34 | } 35 | 36 | $tokens = 2000; 37 | if (isset($config['tokens']) && (\is_int($config['tokens']) || \is_string($config['tokens']))) { 38 | $tokens = (int) $config['tokens']; 39 | } 40 | 41 | return new DocsSource( 42 | library: $config['library'], 43 | topic: $config['topic'], 44 | description: $config['description'] ?? '', 45 | tokens: $tokens, 46 | tags: $config['tags'] ?? [], 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Source/Fetcher/SourceFetcherInterface.php: -------------------------------------------------------------------------------- 1 | fetchers as $fetcher) { 29 | if ($fetcher->supports($source)) { 30 | return $fetcher; 31 | } 32 | } 33 | 34 | throw new \RuntimeException( 35 | \sprintf( 36 | 'No fetcher found for source of type %s', 37 | $source::class, 38 | ), 39 | ); 40 | } 41 | 42 | public function parse(SourceInterface $source, ModifiersApplierInterface $modifiersApplier): string 43 | { 44 | return $this->findFetcher($source)->fetch($source, $modifiersApplier); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Source/File/FileSourceBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 22 | FactoryInterface $factory, 23 | DirectoriesInterface $dirs, 24 | ContentBuilderFactory $builderFactory, 25 | HasPrefixLoggerInterface $logger, 26 | ): FileSourceFetcher => $factory->make(FileSourceFetcher::class, [ 27 | 'basePath' => (string) $dirs->getRootPath(), 28 | 'finder' => $factory->make(SymfonyFinder::class), 29 | ]), 30 | ]; 31 | } 32 | 33 | public function init( 34 | SourceFetcherBootloader $registry, 35 | SourceRegistryInterface $sourceRegistry, 36 | FileSourceFactory $factory, 37 | ): void { 38 | $registry->register(FileSourceFetcher::class); 39 | $sourceRegistry->register($factory); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Source/GitDiff/Fetcher/Source/StagedGitSource.php: -------------------------------------------------------------------------------- 1 | executeGitCommand( 23 | repository: $repository, 24 | command: 'diff --name-only --cached', 25 | ); 26 | } 27 | 28 | public function getFileDiff(string $repository, string $commitReference, string $file): string 29 | { 30 | return $this->executeGitCommandString( 31 | repository: $repository, 32 | command: \sprintf('diff --cached -- %s', $file), 33 | ); 34 | } 35 | 36 | public function formatReferenceForDisplay(string $commitReference): string 37 | { 38 | return "Changes staged for commit"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Source/GitDiff/Fetcher/Source/UnstagedGitSource.php: -------------------------------------------------------------------------------- 1 | executeGitCommand( 23 | repository: $repository, 24 | command: 'diff --name-only', 25 | ); 26 | } 27 | 28 | public function getFileDiff(string $repository, string $commitReference, string $file): string 29 | { 30 | return $this->executeGitCommandString( 31 | repository: $repository, 32 | command: \sprintf('diff -- %s', $file), 33 | ); 34 | } 35 | 36 | public function formatReferenceForDisplay(string $commitReference): string 37 | { 38 | return "Unstaged changes"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Source/GitDiff/RenderStrategy/Enum/RenderStrategyEnum.php: -------------------------------------------------------------------------------- 1 | self::Raw, 21 | 'llm' => self::LLM, 22 | default => throw new \InvalidArgumentException( 23 | \sprintf( 24 | 'Invalid render strategy "%s". Valid strategies are: %s', 25 | $value, 26 | \implode(', ', \array_column(self::cases(), 'value')), 27 | ), 28 | ), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Source/GitDiff/RenderStrategy/RawRenderStrategy.php: -------------------------------------------------------------------------------- 1 | $diffData) { 24 | // Only show stats if configured to do so 25 | if ($config->showStats && !empty($diffData['stats'])) { 26 | $builder 27 | ->addTitle("Stats for {$file}", 2) 28 | ->addCodeBlock($diffData['stats']); 29 | } 30 | 31 | $builder 32 | ->addTitle("Diff for {$file}", 2) 33 | ->addCodeBlock($diffData['diff'], 'diff'); 34 | } 35 | 36 | return $builder; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Source/GitDiff/RenderStrategy/RenderStrategyFactory.php: -------------------------------------------------------------------------------- 1 | > Map of strategy enum to class name 18 | */ 19 | private const array STRATEGIES = [ 20 | RenderStrategyEnum::Raw->value => RawRenderStrategy::class, 21 | RenderStrategyEnum::LLM->value => LLMFriendlyRenderStrategy::class, 22 | ]; 23 | 24 | /** 25 | * Create a render strategy instance based on the render config 26 | * 27 | * @param RenderStrategyEnum $strategy The render strategy enum 28 | * @return RenderStrategyInterface The render strategy instance 29 | * @throws \InvalidArgumentException If the strategy is not valid 30 | */ 31 | public function create(RenderStrategyEnum $strategy): RenderStrategyInterface 32 | { 33 | if (!isset(self::STRATEGIES[$strategy->value])) { 34 | throw new \InvalidArgumentException( 35 | \sprintf( 36 | 'Invalid render strategy "%s". Valid strategies are: %s', 37 | $strategy->value, 38 | \implode(', ', \array_keys(self::STRATEGIES)), 39 | ), 40 | ); 41 | } 42 | 43 | $className = self::STRATEGIES[$strategy->value]; 44 | return new $className(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Source/GitDiff/RenderStrategy/RenderStrategyInterface.php: -------------------------------------------------------------------------------- 1 | $diffs Array of diffs with metadata 19 | */ 20 | public function render(array $diffs, RenderConfig $config): ContentBuilder; 21 | } 22 | -------------------------------------------------------------------------------- /src/Source/Github/GithubSourceBootloader.php: -------------------------------------------------------------------------------- 1 | GithubSourceFetcher::class, 25 | ]; 26 | } 27 | 28 | public function init( 29 | SourceFetcherBootloader $registry, 30 | SourceRegistryInterface $sourceRegistry, 31 | GithubSourceFactory $factory, 32 | ): void { 33 | $registry->register(GithubSourceFetcher::class); 34 | $sourceRegistry->register($factory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Source/Gitlab/Config/ServerRegistry.php: -------------------------------------------------------------------------------- 1 | */ 13 | private array $servers = []; 14 | 15 | public function __construct( 16 | #[LoggerPrefix(prefix: 'gitlab-server-registry')] 17 | private readonly ?LoggerInterface $logger = null, 18 | ) {} 19 | 20 | public function register(string $name, ServerConfig $config): self 21 | { 22 | $this->logger?->debug('Registering GitLab server', [ 23 | 'name' => $name, 24 | 'config' => $config, 25 | ]); 26 | 27 | $this->servers[$name] = $config; 28 | 29 | return $this; 30 | } 31 | 32 | public function has(string $name): bool 33 | { 34 | return isset($this->servers[$name]); 35 | } 36 | 37 | public function get(string $name): ServerConfig 38 | { 39 | if (!$this->has($name)) { 40 | throw new \InvalidArgumentException(\sprintf('GitLab server "%s" not found in registry', $name)); 41 | } 42 | 43 | return $this->servers[$name]; 44 | } 45 | 46 | public function all(): array 47 | { 48 | return $this->servers; 49 | } 50 | 51 | public function getDefault(): ?ServerConfig 52 | { 53 | return $this->servers['default'] ?? null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Source/Gitlab/GitlabSourceBootloader.php: -------------------------------------------------------------------------------- 1 | GitlabSourceFetcher::class, 21 | ServerRegistry::class => ServerRegistry::class, 22 | ]; 23 | } 24 | 25 | public function init( 26 | SourceFetcherBootloader $registry, 27 | SourceRegistryInterface $sourceRegistry, 28 | GitlabSourceFactory $factory, 29 | ): void { 30 | // Register the GitLab source fetcher with the fetcher registry 31 | $registry->register(GitlabSourceFetcher::class); 32 | 33 | // Register the GitLab source factory with the source registry 34 | $sourceRegistry->register($factory); 35 | } 36 | 37 | public function boot( 38 | ConfigLoaderBootloader $parserRegistry, 39 | GitlabServerParserPlugin $plugin, 40 | ): void { 41 | // Register the GitLab server parser plugin with the config loader 42 | $parserRegistry->registerParserPlugin($plugin); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Source/MCP/McpSource.php: -------------------------------------------------------------------------------- 1 | $tags 21 | * @param array<\Butschster\ContextGenerator\Modifier\Modifier> $modifiers 22 | */ 23 | public function __construct( 24 | public readonly ServerConfig $serverConfig, 25 | public readonly OperationInterface $operation, 26 | string $description = '', 27 | array $tags = [], 28 | array $modifiers = [], 29 | ) { 30 | parent::__construct(description: $description, tags: $tags, modifiers: $modifiers); 31 | } 32 | 33 | #[\Override] 34 | public function jsonSerialize(): array 35 | { 36 | return \array_filter([ 37 | 'type' => 'mcp', 38 | ...parent::jsonSerialize(), 39 | 'server' => $this->serverConfig, 40 | 'operation' => $this->operation, 41 | ], static fn($value) => $value !== null && $value !== '' && $value !== []); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Source/MCP/McpSourceBootloader.php: -------------------------------------------------------------------------------- 1 | McpSourceFetcher::class, 23 | ServerRegistry::class => ServerRegistry::class, 24 | OperationFactory::class => OperationFactory::class, 25 | ConnectionManager::class => ConnectionManager::class, 26 | ]; 27 | } 28 | 29 | public function init( 30 | SourceFetcherBootloader $registry, 31 | SourceRegistryInterface $sourceRegistry, 32 | McpSourceFactory $factory, 33 | ): void { 34 | $registry->register(McpSourceFetcher::class); 35 | $sourceRegistry->register($factory); 36 | } 37 | 38 | public function boot( 39 | ConfigLoaderBootloader $parserRegistry, 40 | McpServerParserPlugin $plugin, 41 | ): void { 42 | $parserRegistry->registerParserPlugin($plugin); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Source/MCP/Operations/AbstractOperation.php: -------------------------------------------------------------------------------- 1 | type; 27 | } 28 | 29 | /** 30 | * Default implementation of buildContent 31 | * 32 | * @param ContentBuilder $builder Content builder to add content to 33 | * @param string $content The content to be built 34 | */ 35 | public function buildContent(ContentBuilder $builder, string $content): void 36 | { 37 | $builder->addText($content); 38 | } 39 | 40 | /** 41 | * Default implementation of jsonSerialize 42 | */ 43 | public function jsonSerialize(): array 44 | { 45 | return [ 46 | 'type' => $this->type, 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Source/MCP/Operations/OperationInterface.php: -------------------------------------------------------------------------------- 1 | $modifiersConfig 26 | * @return array<\Butschster\ContextGenerator\Modifier\Modifier> 27 | */ 28 | protected function parseModifiers(array $modifiersConfig): array 29 | { 30 | return $this->modifierResolver->resolveAll($modifiersConfig); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Source/Registry/SourceFactoryInterface.php: -------------------------------------------------------------------------------- 1 | $config Source configuration 23 | */ 24 | public function create(array $config): SourceInterface; 25 | } 26 | -------------------------------------------------------------------------------- /src/Source/Registry/SourceProviderInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private array $factories = []; 20 | 21 | public function __construct( 22 | #[LoggerPrefix(prefix: 'source-registry')] 23 | private readonly ?LoggerInterface $logger = null, 24 | ) {} 25 | 26 | public function register(SourceFactoryInterface $factory): self 27 | { 28 | $this->factories[$factory->getType()] = $factory; 29 | 30 | $this->logger?->debug(\sprintf('Registered source factory for type "%s"', $factory->getType()), [ 31 | 'factoryClass' => $factory::class, 32 | ]); 33 | 34 | return $this; 35 | } 36 | 37 | public function create(string $type, array $config): SourceInterface 38 | { 39 | if (!isset($this->factories[$type])) { 40 | throw new \RuntimeException(\sprintf('Source factory for type "%s" not found', $type)); 41 | } 42 | 43 | $factory = $this->factories[$type]; 44 | 45 | return $factory->create($config); 46 | } 47 | 48 | public function has(string $type): bool 49 | { 50 | return isset($this->factories[$type]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Source/Registry/SourceRegistryBootloader.php: -------------------------------------------------------------------------------- 1 | SourceRegistry::class, 18 | SourceProviderInterface::class => SourceRegistry::class, 19 | SourceRegistry::class => SourceRegistry::class, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Source/Registry/SourceRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function getTags(): array; 25 | 26 | /** 27 | * Check if source has any tags 28 | */ 29 | public function hasTags(): bool; 30 | 31 | /** 32 | * Parse the content for this source 33 | * 34 | * @param SourceParserInterface $parser Parser for the source content 35 | * @param ModifiersApplierInterface $modifiersApplier Applier for content modifiers 36 | * @return string Parsed content 37 | */ 38 | public function parseContent( 39 | SourceParserInterface $parser, 40 | ModifiersApplierInterface $modifiersApplier, 41 | ): string; 42 | } 43 | -------------------------------------------------------------------------------- /src/Source/SourceWithModifiers.php: -------------------------------------------------------------------------------- 1 | $tags 15 | * @param array $modifiers Identifiers for content modifiers to apply 16 | */ 17 | public function __construct( 18 | string $description, 19 | array $tags = [], 20 | public readonly array $modifiers = [], 21 | ) { 22 | parent::__construct(description: $description, tags: $tags); 23 | } 24 | 25 | #[\Override] 26 | public function parseContent( 27 | SourceParserInterface $parser, 28 | ModifiersApplierInterface $modifiersApplier, 29 | ): string { 30 | // If we have source-specific modifiers and a document-level modifiers applier, 31 | // create a new applier that includes both sets of modifiers 32 | $modifiersApplier = $modifiersApplier->withModifiers($this->modifiers); 33 | 34 | return $parser->parse($this, $modifiersApplier); 35 | } 36 | 37 | #[\Override] 38 | public function jsonSerialize(): array 39 | { 40 | return [ 41 | ...parent::jsonSerialize(), 42 | 'modifiers' => $this->modifiers, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Source/Text/TextSource.php: -------------------------------------------------------------------------------- 1 | $tags 18 | */ 19 | public function __construct( 20 | public readonly string $content, 21 | string $description = '', 22 | public readonly string $tag = 'INSTRUCTION', 23 | array $tags = [], 24 | ) { 25 | parent::__construct(description: $description, tags: $tags); 26 | } 27 | 28 | #[\Override] 29 | public function jsonSerialize(): array 30 | { 31 | return \array_filter([ 32 | 'type' => 'text', 33 | ...parent::jsonSerialize(), 34 | 'content' => $this->content, 35 | 'tag' => $this->tag, 36 | ], static fn($value) => $value !== null && $value !== '' && $value !== []); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Source/Text/TextSourceBootloader.php: -------------------------------------------------------------------------------- 1 | TextSourceFetcher::class, 18 | ]; 19 | } 20 | 21 | public function init( 22 | SourceFetcherBootloader $registry, 23 | SourceRegistryInterface $sourceRegistry, 24 | TextSourceFactory $factory, 25 | ): void { 26 | $registry->register(TextSourceFetcher::class); 27 | $sourceRegistry->register($factory); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Source/Text/TextSourceFactory.php: -------------------------------------------------------------------------------- 1 | logger?->debug('Creating text source', [ 25 | 'config' => $config, 26 | ]); 27 | 28 | if (!isset($config['content']) || !\is_string($config['content'])) { 29 | throw new \RuntimeException('Text source must have a "content" string property'); 30 | } 31 | 32 | return new TextSource( 33 | content: $config['content'], 34 | description: $config['description'] ?? '', 35 | tag: $config['tag'] ?? 'INSTRUCTION', 36 | tags: $config['tags'] ?? [], 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Source/Tree/TreeSourceBootloader.php: -------------------------------------------------------------------------------- 1 | static fn( 20 | FactoryInterface $factory, 21 | DirectoriesInterface $dirs, 22 | ): TreeSourceFetcher => $factory->make(TreeSourceFetcher::class, [ 23 | 'basePath' => (string) $dirs->getRootPath(), 24 | ]), 25 | ]; 26 | } 27 | 28 | public function init( 29 | SourceFetcherBootloader $registry, 30 | SourceRegistryInterface $sourceRegistry, 31 | TreeSourceFactory $factory, 32 | ): void { 33 | $registry->register(TreeSourceFetcher::class); 34 | $sourceRegistry->register($factory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Source/Url/UrlSourceFactory.php: -------------------------------------------------------------------------------- 1 | logger?->debug('Creating URL source', [ 24 | 'path' => $this->dirs->getRootPath(), 25 | 'config' => $config, 26 | ]); 27 | 28 | 29 | if (!isset($config['urls']) || !\is_array($config['urls'])) { 30 | throw new \RuntimeException('URL source must have a "urls" array property'); 31 | } 32 | 33 | // Add headers validation and parsing 34 | $headers = []; 35 | if (isset($config['headers']) && \is_array($config['headers'])) { 36 | $headers = $config['headers']; 37 | } 38 | 39 | return new UrlSource( 40 | urls: $config['urls'], 41 | description: $config['description'] ?? '', 42 | headers: $headers, 43 | selector: $config['selector'] ?? null, 44 | tags: $config['tags'] ?? [], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SourceParserInterface.php: -------------------------------------------------------------------------------- 1 |