├── .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 |