From 6b21065a31c9219f5a5dcd8a07d984eee85d4fbd Mon Sep 17 00:00:00 2001 From: butschster Date: Fri, 26 Sep 2025 01:49:26 +0400 Subject: [PATCH 1/4] feat: split into modules. Replace MCP server. Add support for HTTP + SSE connection --- composer.json | 18 +- .../Bootloader/ExcludeBootloader.php | 1 + .../Bootloader/ModifierBootloader.php | 1 + .../Bootloader/SchemaMapperBootloader.php | 32 -- .../Bootloader/VariableBootloader.php | 1 + src/Application/JsonSchema.php | 13 - src/Application/Kernel.php | 8 +- .../ConfigLoaderBootloader.php | 2 +- .../ImportBootloader.php | 3 +- src/Config/context.yaml | 4 +- src/Console/SchemaCommand.php | 7 +- .../SchemaMapper/SchemaMapperInterface.php | 20 - .../SchemaMapper/Valinor/MapperBuilder.php | 29 - src/Lib/SchemaMapper/Valinor/SchemaMapper.php | 36 -- .../Prompts/FilesystemOperationsAction.php | 10 +- .../Action/Prompts/GetPromptAction.php | 4 +- .../Action/Prompts/ListPromptsAction.php | 7 +- .../Prompts/ProjectStructurePromptAction.php | 10 +- .../Action/Resources/GenerateConfigAction.php | 8 +- .../GetDocumentContentResourceAction.php | 6 +- .../Resources/JsonSchemaResourceAction.php | 6 +- .../Action/Resources/ListResourcesAction.php | 12 +- src/McpServer/Action/ToolResult.php | 69 --- .../Action/Tools/Context/ContextAction.php | 4 +- .../Action/Tools/Context/ContextGetAction.php | 2 +- .../Tools/Context/ContextRequestAction.php | 4 +- .../Tools/Docs/FetchLibraryDocsAction.php | 2 +- .../Action/Tools/Docs/LibrarySearchAction.php | 2 +- .../Action/Tools/ExecuteCustomToolAction.php | 4 +- .../Tools/Filesystem/DirectoryListAction.php | 2 +- .../Tools/Filesystem/FileApplyPatchAction.php | 2 +- .../Tools/Filesystem/FileMoveAction.php | 2 +- .../Tools/Filesystem/FileReadAction.php | 2 +- .../Tools/Filesystem/FileWriteAction.php | 2 +- .../Action/Tools/Git/GitAddAction.php | 2 +- .../Action/Tools/Git/GitCommitAction.php | 2 +- .../Action/Tools/Git/GitStatusAction.php | 2 +- .../Action/Tools/ListToolsAction.php | 26 +- .../Tools/Prompts/GetPromptToolAction.php | 9 +- .../Tools/Prompts/ListPromptsToolAction.php | 8 +- ...erBootloader.php => ActionsBootloader.php} | 58 +- src/McpServer/Attribute/InputSchema.php | 16 - src/McpServer/Attribute/McpItem.php | 14 - src/McpServer/Attribute/Prompt.php | 11 - src/McpServer/Attribute/Resource.php | 18 - src/McpServer/Attribute/Tool.php | 11 - src/McpServer/Console/MCPServerCommand.php | 4 - .../McpConfig/ConfigGeneratorInterface.php | 20 - .../Generator/McpConfigGenerator.php | 52 -- .../Console/McpConfig/McpConfigBootloader.php | 48 -- .../Console/McpConfig/Model/McpConfig.php | 46 -- .../Console/McpConfig/Model/OsInfo.php | 67 --- .../McpConfig/Renderer/McpConfigRenderer.php | 248 --------- .../McpConfig/Service/OsDetectionService.php | 128 ----- .../McpConfig/Template/BaseConfigTemplate.php | 105 ---- .../Template/ConfigTemplateInterface.php | 20 - .../Template/LinuxConfigTemplate.php | 42 -- .../Template/MacOsConfigTemplate.php | 42 -- .../Template/WindowsConfigTemplate.php | 59 -- .../McpConfig/Template/WslConfigTemplate.php | 96 ---- src/McpServer/Console/McpConfigCommand.php | 6 +- src/McpServer/McpConfig.php | 108 ---- .../ProjectService/ProjectService.php | 156 ------ .../ProjectService/ProjectServiceFactory.php | 28 - .../ProjectServiceInterface.php | 23 - .../Actions/ProjectSwitchToolAction.php | 2 +- .../Actions/ProjectsListToolAction.php | 2 +- .../Projects/DTO/CurrentProjectDTO.php | 89 --- src/McpServer/Projects/DTO/ProjectDTO.php | 51 -- .../Projects/DTO/ProjectStateDTO.php | 91 --- .../Projects/McpProjectsBootloader.php | 41 -- src/McpServer/Projects/ProjectService.php | 190 ------- .../Projects/ProjectServiceInterface.php | 62 --- .../Repository/ProjectStateRepository.php | 85 --- .../ProjectStateRepositoryInterface.php | 20 - .../Prompt/Console/ListPromptsCommand.php | 2 +- .../Prompt/Console/ShowPromptCommand.php | 2 +- .../Prompt/Content/FileContentProvider.php | 25 - .../Content/FileMessageContentLoader.php | 100 ---- .../Content/LocalFileContentProvider.php | 67 --- .../Prompt/Content/MessageContentLoader.php | 37 -- .../Content/TextMessageContentLoader.php | 29 - .../Prompt/Content/UrlFileContentProvider.php | 62 --- .../Exception/FileMessageContentException.php | 36 -- .../Exception/PromptParsingException.php | 13 - .../Exception/TemplateResolutionException.php | 13 - .../Prompt/Extension/PromptDefinition.php | 85 --- .../Prompt/Extension/PromptExtension.php | 49 -- .../Extension/PromptExtensionArgument.php | 16 - .../PromptExtensionVariableProvider.php | 53 -- .../Prompt/Extension/TemplateResolver.php | 149 ----- .../Prompt/Filter/FilterStrategy.php | 26 - .../Prompt/Filter/PromptFilterFactory.php | 89 --- .../Prompt/Filter/PromptFilterInterface.php | 13 - .../Filter/Strategy/CompositePromptFilter.php | 60 -- .../Prompt/Filter/Strategy/IdPromptFilter.php | 36 -- .../Filter/Strategy/TagPromptFilter.php | 108 ---- src/McpServer/Prompt/McpPromptBootloader.php | 69 --- src/McpServer/Prompt/PromptConfigFactory.php | 301 ---------- src/McpServer/Prompt/PromptConfigMerger.php | 100 ---- .../Prompt/PromptMessageProcessor.php | 41 -- src/McpServer/Prompt/PromptParserPlugin.php | 107 ---- .../Prompt/PromptProviderInterface.php | 46 -- src/McpServer/Prompt/PromptRegistry.php | 96 ---- .../Prompt/PromptRegistryInterface.php | 15 - src/McpServer/Prompt/PromptType.php | 29 - src/McpServer/Registry/McpItemsRegistry.php | 117 ---- src/McpServer/Routing/ActionCaller.php | 51 -- src/McpServer/Routing/Attribute/Get.php | 17 - src/McpServer/Routing/Attribute/Post.php | 17 - src/McpServer/Routing/Attribute/Route.php | 16 - .../Routing/Mcp2PsrRequestAdapter.php | 52 -- src/McpServer/Routing/McpResponseStrategy.php | 54 -- src/McpServer/Routing/RouteRegistrar.php | 94 ---- src/McpServer/Server.php | 215 ------- src/McpServer/ServerRunner.php | 69 --- src/McpServer/ServerRunnerInterface.php | 13 - .../Tool/Command/CommandExecutor.php | 91 --- .../Tool/Command/CommandExecutorInterface.php | 23 - src/McpServer/Tool/Config/HttpToolRequest.php | 129 ----- src/McpServer/Tool/Config/ToolArg.php | 77 --- src/McpServer/Tool/Config/ToolCommand.php | 109 ---- src/McpServer/Tool/Config/ToolDefinition.php | 146 ----- src/McpServer/Tool/Config/ToolSchema.php | 84 --- .../Tool/Exception/ToolExecutionException.php | 31 -- src/McpServer/Tool/McpToolBootloader.php | 60 -- .../Tool/Provider/ToolArgumentsProvider.php | 205 ------- src/McpServer/Tool/ToolAttributesParser.php | 57 -- src/McpServer/Tool/ToolConfigMerger.php | 63 --- src/McpServer/Tool/ToolHandlerFactory.php | 65 --- src/McpServer/Tool/ToolParserPlugin.php | 73 --- src/McpServer/Tool/ToolProviderInterface.php | 29 - src/McpServer/Tool/ToolRegistry.php | 70 --- src/McpServer/Tool/ToolRegistryInterface.php | 17 - .../Tool/Types/AbstractToolHandler.php | 53 -- src/McpServer/Tool/Types/HttpToolHandler.php | 194 ------- src/McpServer/Tool/Types/RunToolHandler.php | 207 ------- .../Tool/Types/ToolHandlerInterface.php | 26 - src/Research/Config/ResearchConfig.php | 46 -- .../Config/ResearchConfigInterface.php | 33 -- src/Research/Console/ResearchInfoCommand.php | 195 ------- src/Research/Console/ResearchListCommand.php | 73 --- src/Research/Console/TemplateListCommand.php | 88 --- src/Research/Domain/Model/Category.php | 30 - src/Research/Domain/Model/Entry.php | 80 --- src/Research/Domain/Model/EntryType.php | 47 -- src/Research/Domain/Model/Research.php | 91 --- src/Research/Domain/Model/Status.php | 16 - src/Research/Domain/Model/Template.php | 114 ---- src/Research/Domain/ValueObject/EntryId.php | 48 -- .../Domain/ValueObject/ResearchId.php | 36 -- .../Domain/ValueObject/TemplateKey.php | 40 -- .../Exception/EntryNotFoundException.php | 10 - src/Research/Exception/ResearchException.php | 7 - .../Exception/ResearchNotFoundException.php | 7 - .../Exception/TemplateNotFoundException.php | 10 - .../Exception/ValidationException.php | 38 -- src/Research/MCP/DTO/EntryCreateRequest.php | 170 ------ src/Research/MCP/DTO/EntryFilters.php | 141 ----- src/Research/MCP/DTO/EntryUpdateRequest.php | 191 ------- src/Research/MCP/DTO/GetResearchRequest.php | 32 -- src/Research/MCP/DTO/ListEntriesRequest.php | 82 --- .../MCP/DTO/ListResearchesRequest.php | 90 --- src/Research/MCP/DTO/ListTemplatesRequest.php | 57 -- src/Research/MCP/DTO/ReadEntryRequest.php | 46 -- .../MCP/DTO/ResearchCreateRequest.php | 58 -- src/Research/MCP/DTO/ResearchFilters.php | 102 ---- src/Research/MCP/DTO/ResearchMemory.php | 12 - .../MCP/DTO/ResearchUpdateRequest.php | 79 --- .../MCP/Tools/CreateEntryToolAction.php | 105 ---- .../MCP/Tools/CreateResearchToolAction.php | 100 ---- .../MCP/Tools/GetResearchToolAction.php | 105 ---- .../MCP/Tools/ListEntriesToolAction.php | 125 ----- .../MCP/Tools/ListResearchesToolAction.php | 92 --- .../MCP/Tools/ListTemplatesToolAction.php | 111 ---- .../MCP/Tools/ReadEntryToolAction.php | 109 ---- .../MCP/Tools/UpdateEntryToolAction.php | 150 ----- .../MCP/Tools/UpdateResearchToolAction.php | 128 ----- .../Repository/EntryRepositoryInterface.php | 43 -- .../ResearchRepositoryInterface.php | 42 -- .../TemplateRepositoryInterface.php | 36 -- src/Research/ResearchBootloader.php | 70 --- src/Research/Service/EntryService.php | 395 ------------- .../Service/EntryServiceInterface.php | 70 --- src/Research/Service/ResearchService.php | 286 ---------- .../Service/ResearchServiceInterface.php | 64 --- src/Research/Service/TemplateService.php | 159 ------ .../Service/TemplateServiceInterface.php | 70 --- .../Storage/FileStorage/DirectoryScanner.php | 123 ---- .../FileStorage/FileEntryRepository.php | 309 ---------- .../FileStorage/FileResearchRepository.php | 211 ------- .../Storage/FileStorage/FileStorageConfig.php | 84 --- .../Storage/FileStorage/FileStorageDriver.php | 383 ------------- .../FileStorage/FileStorageRepositoryBase.php | 151 ----- .../FileStorage/FileTemplateRepository.php | 213 ------- .../Storage/FileStorage/FrontmatterParser.php | 104 ---- .../Storage/FileStorageBootloader.php | 50 -- .../Storage/StorageDriverInterface.php | 57 -- src/Source/Gitlab/GitlabSourceBootloader.php | 2 +- src/Source/MCP/McpSourceBootloader.php | 2 +- src/Template/Analysis/AnalysisResult.php | 34 -- .../Analyzer/AbstractFrameworkAnalyzer.php | 233 -------- .../Analysis/Analyzer/ComposerAnalyzer.php | 78 --- .../Analysis/Analyzer/FallbackAnalyzer.php | 78 --- src/Template/Analysis/Analyzer/GoAnalyzer.php | 394 ------------- .../Analysis/Analyzer/PackageJsonAnalyzer.php | 222 -------- .../Analysis/Analyzer/PythonAnalyzer.php | 386 ------------- src/Template/Analysis/AnalyzerChain.php | 94 ---- .../Analysis/ProjectAnalysisService.php | 46 -- .../Analysis/ProjectAnalyzerInterface.php | 33 -- .../Analysis/Util/ComposerFileReader.php | 88 --- .../Util/ProjectStructureDetector.php | 98 ---- .../Builder/TemplateConfigurationBuilder.php | 196 ------- src/Template/Console/InitCommand.php | 341 ------------ src/Template/Console/ListCommand.php | 160 ------ .../Definition/AbstractTemplateDefinition.php | 171 ------ .../Definition/DjangoTemplateDefinition.php | 99 ---- .../Definition/ExpressTemplateDefinition.php | 110 ---- .../Definition/FastApiTemplateDefinition.php | 111 ---- .../Definition/FlaskTemplateDefinition.php | 85 --- .../GenericPhpTemplateDefinition.php | 120 ---- .../Definition/GinTemplateDefinition.php | 99 ---- .../Definition/GoTemplateDefinition.php | 106 ---- .../Definition/LaravelTemplateDefinition.php | 109 ---- .../Definition/NextJsTemplateDefinition.php | 110 ---- .../Definition/NuxtTemplateDefinition.php | 114 ---- .../Definition/PythonTemplateDefinition.php | 107 ---- .../Definition/ReactTemplateDefinition.php | 110 ---- .../Definition/SpiralTemplateDefinition.php | 106 ---- .../Definition/SymfonyTemplateDefinition.php | 118 ---- .../TemplateDefinitionInterface.php | 49 -- .../Definition/TemplateDefinitionRegistry.php | 80 --- .../Definition/VueTemplateDefinition.php | 111 ---- .../Definition/Yii2TemplateDefinition.php | 118 ---- .../Definition/Yii3TemplateDefinition.php | 119 ---- .../Detection/ProjectMetadataExtractor.php | 195 ------- .../AnalyzerBasedDetectionStrategy.php | 68 --- .../Strategy/CompositeDetectionStrategy.php | 133 ----- .../TemplateBasedDetectionStrategy.php | 67 --- .../Strategy/TemplateDetectionStrategy.php | 34 -- .../Detection/TemplateDetectionResult.php | 74 --- .../Detection/TemplateDetectionService.php | 75 --- .../Detection/TemplateMatchResult.php | 24 - .../Detection/TemplateMatchingService.php | 43 -- .../Provider/BuiltinTemplateProvider.php | 53 -- .../Registry/TemplateProviderInterface.php | 30 - src/Template/Registry/TemplateRegistry.php | 68 --- src/Template/Template.php | 175 ------ src/Template/TemplateSystemBootloader.php | 125 ----- .../Projects/DTO/CurrentProjectDTOTest.php | 87 --- .../McpServer/Projects/DTO/ProjectDTOTest.php | 96 ---- .../Projects/DTO/ProjectStateDTOTest.php | 192 ------- .../McpServer/Projects/ProjectServiceTest.php | 264 --------- .../Repository/ProjectStateRepositoryTest.php | 196 ------- .../McpServer/Prompt/BasicPromptTest.php | 54 -- .../Prompt/Filter/CompositeFilterTest.php | 120 ---- .../Prompt/Filter/FilterImportTest.php | 213 ------- .../Prompt/Filter/FilterIntegrationTest.php | 62 --- .../Prompt/Filter/FilterStrategyTest.php | 45 -- .../Prompt/Filter/PromptFilterFactoryTest.php | 161 ------ .../Prompt/Filter/PromptFilterTest.php | 132 ----- .../Prompt/JsonSerializationTest.php | 86 --- .../Prompt/RegistryOperationsTest.php | 85 --- .../McpServer/Prompt/SchemaPromptTest.php | 88 --- .../McpServer/Prompt/TemplatePromptTest.php | 60 -- .../McpServer/Tool/BasicToolConfigTest.php | 48 -- .../Feature/McpServer/Tool/HttpToolTest.php | 87 --- .../Feature/McpServer/Tool/RunToolTest.php | 72 --- .../McpServer/Tool/ToolHandlerTest.php | 174 ------ .../Unit/McpServer/Action/ToolResultTest.php | 267 --------- .../Tools/Filesystem/FileWriteActionTest.php | 526 ------------------ .../Research/Domain/Model/ProjectTest.php | 217 -------- .../Domain/ValueObject/EntryIdTest.php | 67 --- .../Domain/ValueObject/ResearchIdTest.php | 67 --- .../Domain/ValueObject/TemplateKeyTest.php | 75 --- .../MCP/DTO/ResearchCreateRequestTest.php | 210 ------- .../MCP/DTO/ResearchUpdateRequestTest.php | 195 ------- .../Tools/UpdateResearchToolActionTest.php | 313 ----------- .../Research/Service/ProjectServiceTest.php | 509 ----------------- 279 files changed, 122 insertions(+), 23302 deletions(-) delete mode 100644 src/Application/Bootloader/SchemaMapperBootloader.php delete mode 100644 src/Application/JsonSchema.php rename src/{Application/Bootloader => Config}/ConfigLoaderBootloader.php (98%) rename src/{Application/Bootloader => Config}/ImportBootloader.php (94%) delete mode 100644 src/Lib/SchemaMapper/SchemaMapperInterface.php delete mode 100644 src/Lib/SchemaMapper/Valinor/MapperBuilder.php delete mode 100644 src/Lib/SchemaMapper/Valinor/SchemaMapper.php delete mode 100644 src/McpServer/Action/ToolResult.php rename src/McpServer/{McpServerBootloader.php => ActionsBootloader.php} (83%) delete mode 100644 src/McpServer/Attribute/InputSchema.php delete mode 100644 src/McpServer/Attribute/McpItem.php delete mode 100644 src/McpServer/Attribute/Prompt.php delete mode 100644 src/McpServer/Attribute/Resource.php delete mode 100644 src/McpServer/Attribute/Tool.php delete mode 100644 src/McpServer/Console/McpConfig/ConfigGeneratorInterface.php delete mode 100644 src/McpServer/Console/McpConfig/Generator/McpConfigGenerator.php delete mode 100644 src/McpServer/Console/McpConfig/McpConfigBootloader.php delete mode 100644 src/McpServer/Console/McpConfig/Model/McpConfig.php delete mode 100644 src/McpServer/Console/McpConfig/Model/OsInfo.php delete mode 100644 src/McpServer/Console/McpConfig/Renderer/McpConfigRenderer.php delete mode 100644 src/McpServer/Console/McpConfig/Service/OsDetectionService.php delete mode 100644 src/McpServer/Console/McpConfig/Template/BaseConfigTemplate.php delete mode 100644 src/McpServer/Console/McpConfig/Template/ConfigTemplateInterface.php delete mode 100644 src/McpServer/Console/McpConfig/Template/LinuxConfigTemplate.php delete mode 100644 src/McpServer/Console/McpConfig/Template/MacOsConfigTemplate.php delete mode 100644 src/McpServer/Console/McpConfig/Template/WindowsConfigTemplate.php delete mode 100644 src/McpServer/Console/McpConfig/Template/WslConfigTemplate.php delete mode 100644 src/McpServer/McpConfig.php delete mode 100644 src/McpServer/ProjectService/ProjectService.php delete mode 100644 src/McpServer/ProjectService/ProjectServiceFactory.php delete mode 100644 src/McpServer/ProjectService/ProjectServiceInterface.php delete mode 100644 src/McpServer/Projects/DTO/CurrentProjectDTO.php delete mode 100644 src/McpServer/Projects/DTO/ProjectDTO.php delete mode 100644 src/McpServer/Projects/DTO/ProjectStateDTO.php delete mode 100644 src/McpServer/Projects/McpProjectsBootloader.php delete mode 100644 src/McpServer/Projects/ProjectService.php delete mode 100644 src/McpServer/Projects/ProjectServiceInterface.php delete mode 100644 src/McpServer/Projects/Repository/ProjectStateRepository.php delete mode 100644 src/McpServer/Projects/Repository/ProjectStateRepositoryInterface.php delete mode 100644 src/McpServer/Prompt/Content/FileContentProvider.php delete mode 100644 src/McpServer/Prompt/Content/FileMessageContentLoader.php delete mode 100644 src/McpServer/Prompt/Content/LocalFileContentProvider.php delete mode 100644 src/McpServer/Prompt/Content/MessageContentLoader.php delete mode 100644 src/McpServer/Prompt/Content/TextMessageContentLoader.php delete mode 100644 src/McpServer/Prompt/Content/UrlFileContentProvider.php delete mode 100644 src/McpServer/Prompt/Exception/FileMessageContentException.php delete mode 100644 src/McpServer/Prompt/Exception/PromptParsingException.php delete mode 100644 src/McpServer/Prompt/Exception/TemplateResolutionException.php delete mode 100644 src/McpServer/Prompt/Extension/PromptDefinition.php delete mode 100644 src/McpServer/Prompt/Extension/PromptExtension.php delete mode 100644 src/McpServer/Prompt/Extension/PromptExtensionArgument.php delete mode 100644 src/McpServer/Prompt/Extension/PromptExtensionVariableProvider.php delete mode 100644 src/McpServer/Prompt/Extension/TemplateResolver.php delete mode 100644 src/McpServer/Prompt/Filter/FilterStrategy.php delete mode 100644 src/McpServer/Prompt/Filter/PromptFilterFactory.php delete mode 100644 src/McpServer/Prompt/Filter/PromptFilterInterface.php delete mode 100644 src/McpServer/Prompt/Filter/Strategy/CompositePromptFilter.php delete mode 100644 src/McpServer/Prompt/Filter/Strategy/IdPromptFilter.php delete mode 100644 src/McpServer/Prompt/Filter/Strategy/TagPromptFilter.php delete mode 100644 src/McpServer/Prompt/McpPromptBootloader.php delete mode 100644 src/McpServer/Prompt/PromptConfigFactory.php delete mode 100644 src/McpServer/Prompt/PromptConfigMerger.php delete mode 100644 src/McpServer/Prompt/PromptMessageProcessor.php delete mode 100644 src/McpServer/Prompt/PromptParserPlugin.php delete mode 100644 src/McpServer/Prompt/PromptProviderInterface.php delete mode 100644 src/McpServer/Prompt/PromptRegistry.php delete mode 100644 src/McpServer/Prompt/PromptRegistryInterface.php delete mode 100644 src/McpServer/Prompt/PromptType.php delete mode 100644 src/McpServer/Registry/McpItemsRegistry.php delete mode 100644 src/McpServer/Routing/ActionCaller.php delete mode 100644 src/McpServer/Routing/Attribute/Get.php delete mode 100644 src/McpServer/Routing/Attribute/Post.php delete mode 100644 src/McpServer/Routing/Attribute/Route.php delete mode 100644 src/McpServer/Routing/Mcp2PsrRequestAdapter.php delete mode 100644 src/McpServer/Routing/McpResponseStrategy.php delete mode 100644 src/McpServer/Routing/RouteRegistrar.php delete mode 100644 src/McpServer/Server.php delete mode 100644 src/McpServer/ServerRunner.php delete mode 100644 src/McpServer/ServerRunnerInterface.php delete mode 100644 src/McpServer/Tool/Command/CommandExecutor.php delete mode 100644 src/McpServer/Tool/Command/CommandExecutorInterface.php delete mode 100644 src/McpServer/Tool/Config/HttpToolRequest.php delete mode 100644 src/McpServer/Tool/Config/ToolArg.php delete mode 100644 src/McpServer/Tool/Config/ToolCommand.php delete mode 100644 src/McpServer/Tool/Config/ToolDefinition.php delete mode 100644 src/McpServer/Tool/Config/ToolSchema.php delete mode 100644 src/McpServer/Tool/Exception/ToolExecutionException.php delete mode 100644 src/McpServer/Tool/McpToolBootloader.php delete mode 100644 src/McpServer/Tool/Provider/ToolArgumentsProvider.php delete mode 100644 src/McpServer/Tool/ToolAttributesParser.php delete mode 100644 src/McpServer/Tool/ToolConfigMerger.php delete mode 100644 src/McpServer/Tool/ToolHandlerFactory.php delete mode 100644 src/McpServer/Tool/ToolParserPlugin.php delete mode 100644 src/McpServer/Tool/ToolProviderInterface.php delete mode 100644 src/McpServer/Tool/ToolRegistry.php delete mode 100644 src/McpServer/Tool/ToolRegistryInterface.php delete mode 100644 src/McpServer/Tool/Types/AbstractToolHandler.php delete mode 100644 src/McpServer/Tool/Types/HttpToolHandler.php delete mode 100644 src/McpServer/Tool/Types/RunToolHandler.php delete mode 100644 src/McpServer/Tool/Types/ToolHandlerInterface.php delete mode 100644 src/Research/Config/ResearchConfig.php delete mode 100644 src/Research/Config/ResearchConfigInterface.php delete mode 100644 src/Research/Console/ResearchInfoCommand.php delete mode 100644 src/Research/Console/ResearchListCommand.php delete mode 100644 src/Research/Console/TemplateListCommand.php delete mode 100644 src/Research/Domain/Model/Category.php delete mode 100644 src/Research/Domain/Model/Entry.php delete mode 100644 src/Research/Domain/Model/EntryType.php delete mode 100644 src/Research/Domain/Model/Research.php delete mode 100644 src/Research/Domain/Model/Status.php delete mode 100644 src/Research/Domain/Model/Template.php delete mode 100644 src/Research/Domain/ValueObject/EntryId.php delete mode 100644 src/Research/Domain/ValueObject/ResearchId.php delete mode 100644 src/Research/Domain/ValueObject/TemplateKey.php delete mode 100644 src/Research/Exception/EntryNotFoundException.php delete mode 100644 src/Research/Exception/ResearchException.php delete mode 100644 src/Research/Exception/ResearchNotFoundException.php delete mode 100644 src/Research/Exception/TemplateNotFoundException.php delete mode 100644 src/Research/Exception/ValidationException.php delete mode 100644 src/Research/MCP/DTO/EntryCreateRequest.php delete mode 100644 src/Research/MCP/DTO/EntryFilters.php delete mode 100644 src/Research/MCP/DTO/EntryUpdateRequest.php delete mode 100644 src/Research/MCP/DTO/GetResearchRequest.php delete mode 100644 src/Research/MCP/DTO/ListEntriesRequest.php delete mode 100644 src/Research/MCP/DTO/ListResearchesRequest.php delete mode 100644 src/Research/MCP/DTO/ListTemplatesRequest.php delete mode 100644 src/Research/MCP/DTO/ReadEntryRequest.php delete mode 100644 src/Research/MCP/DTO/ResearchCreateRequest.php delete mode 100644 src/Research/MCP/DTO/ResearchFilters.php delete mode 100644 src/Research/MCP/DTO/ResearchMemory.php delete mode 100644 src/Research/MCP/DTO/ResearchUpdateRequest.php delete mode 100644 src/Research/MCP/Tools/CreateEntryToolAction.php delete mode 100644 src/Research/MCP/Tools/CreateResearchToolAction.php delete mode 100644 src/Research/MCP/Tools/GetResearchToolAction.php delete mode 100644 src/Research/MCP/Tools/ListEntriesToolAction.php delete mode 100644 src/Research/MCP/Tools/ListResearchesToolAction.php delete mode 100644 src/Research/MCP/Tools/ListTemplatesToolAction.php delete mode 100644 src/Research/MCP/Tools/ReadEntryToolAction.php delete mode 100644 src/Research/MCP/Tools/UpdateEntryToolAction.php delete mode 100644 src/Research/MCP/Tools/UpdateResearchToolAction.php delete mode 100644 src/Research/Repository/EntryRepositoryInterface.php delete mode 100644 src/Research/Repository/ResearchRepositoryInterface.php delete mode 100644 src/Research/Repository/TemplateRepositoryInterface.php delete mode 100644 src/Research/ResearchBootloader.php delete mode 100644 src/Research/Service/EntryService.php delete mode 100644 src/Research/Service/EntryServiceInterface.php delete mode 100644 src/Research/Service/ResearchService.php delete mode 100644 src/Research/Service/ResearchServiceInterface.php delete mode 100644 src/Research/Service/TemplateService.php delete mode 100644 src/Research/Service/TemplateServiceInterface.php delete mode 100644 src/Research/Storage/FileStorage/DirectoryScanner.php delete mode 100644 src/Research/Storage/FileStorage/FileEntryRepository.php delete mode 100644 src/Research/Storage/FileStorage/FileResearchRepository.php delete mode 100644 src/Research/Storage/FileStorage/FileStorageConfig.php delete mode 100644 src/Research/Storage/FileStorage/FileStorageDriver.php delete mode 100644 src/Research/Storage/FileStorage/FileStorageRepositoryBase.php delete mode 100644 src/Research/Storage/FileStorage/FileTemplateRepository.php delete mode 100644 src/Research/Storage/FileStorage/FrontmatterParser.php delete mode 100644 src/Research/Storage/FileStorageBootloader.php delete mode 100644 src/Research/Storage/StorageDriverInterface.php delete mode 100644 src/Template/Analysis/AnalysisResult.php delete mode 100644 src/Template/Analysis/Analyzer/AbstractFrameworkAnalyzer.php delete mode 100644 src/Template/Analysis/Analyzer/ComposerAnalyzer.php delete mode 100644 src/Template/Analysis/Analyzer/FallbackAnalyzer.php delete mode 100644 src/Template/Analysis/Analyzer/GoAnalyzer.php delete mode 100644 src/Template/Analysis/Analyzer/PackageJsonAnalyzer.php delete mode 100644 src/Template/Analysis/Analyzer/PythonAnalyzer.php delete mode 100644 src/Template/Analysis/AnalyzerChain.php delete mode 100644 src/Template/Analysis/ProjectAnalysisService.php delete mode 100644 src/Template/Analysis/ProjectAnalyzerInterface.php delete mode 100644 src/Template/Analysis/Util/ComposerFileReader.php delete mode 100644 src/Template/Analysis/Util/ProjectStructureDetector.php delete mode 100644 src/Template/Builder/TemplateConfigurationBuilder.php delete mode 100644 src/Template/Console/InitCommand.php delete mode 100644 src/Template/Console/ListCommand.php delete mode 100644 src/Template/Definition/AbstractTemplateDefinition.php delete mode 100644 src/Template/Definition/DjangoTemplateDefinition.php delete mode 100644 src/Template/Definition/ExpressTemplateDefinition.php delete mode 100644 src/Template/Definition/FastApiTemplateDefinition.php delete mode 100644 src/Template/Definition/FlaskTemplateDefinition.php delete mode 100644 src/Template/Definition/GenericPhpTemplateDefinition.php delete mode 100644 src/Template/Definition/GinTemplateDefinition.php delete mode 100644 src/Template/Definition/GoTemplateDefinition.php delete mode 100644 src/Template/Definition/LaravelTemplateDefinition.php delete mode 100644 src/Template/Definition/NextJsTemplateDefinition.php delete mode 100644 src/Template/Definition/NuxtTemplateDefinition.php delete mode 100644 src/Template/Definition/PythonTemplateDefinition.php delete mode 100644 src/Template/Definition/ReactTemplateDefinition.php delete mode 100644 src/Template/Definition/SpiralTemplateDefinition.php delete mode 100644 src/Template/Definition/SymfonyTemplateDefinition.php delete mode 100644 src/Template/Definition/TemplateDefinitionInterface.php delete mode 100644 src/Template/Definition/TemplateDefinitionRegistry.php delete mode 100644 src/Template/Definition/VueTemplateDefinition.php delete mode 100644 src/Template/Definition/Yii2TemplateDefinition.php delete mode 100644 src/Template/Definition/Yii3TemplateDefinition.php delete mode 100644 src/Template/Detection/ProjectMetadataExtractor.php delete mode 100644 src/Template/Detection/Strategy/AnalyzerBasedDetectionStrategy.php delete mode 100644 src/Template/Detection/Strategy/CompositeDetectionStrategy.php delete mode 100644 src/Template/Detection/Strategy/TemplateBasedDetectionStrategy.php delete mode 100644 src/Template/Detection/Strategy/TemplateDetectionStrategy.php delete mode 100644 src/Template/Detection/TemplateDetectionResult.php delete mode 100644 src/Template/Detection/TemplateDetectionService.php delete mode 100644 src/Template/Detection/TemplateMatchResult.php delete mode 100644 src/Template/Detection/TemplateMatchingService.php delete mode 100644 src/Template/Provider/BuiltinTemplateProvider.php delete mode 100644 src/Template/Registry/TemplateProviderInterface.php delete mode 100644 src/Template/Registry/TemplateRegistry.php delete mode 100644 src/Template/Template.php delete mode 100644 src/Template/TemplateSystemBootloader.php delete mode 100644 tests/src/Feature/McpServer/Projects/DTO/CurrentProjectDTOTest.php delete mode 100644 tests/src/Feature/McpServer/Projects/DTO/ProjectDTOTest.php delete mode 100644 tests/src/Feature/McpServer/Projects/DTO/ProjectStateDTOTest.php delete mode 100644 tests/src/Feature/McpServer/Projects/ProjectServiceTest.php delete mode 100644 tests/src/Feature/McpServer/Projects/Repository/ProjectStateRepositoryTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/BasicPromptTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/Filter/CompositeFilterTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/Filter/FilterImportTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/Filter/FilterIntegrationTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/Filter/FilterStrategyTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/Filter/PromptFilterFactoryTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/Filter/PromptFilterTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/JsonSerializationTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/RegistryOperationsTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/SchemaPromptTest.php delete mode 100644 tests/src/Feature/McpServer/Prompt/TemplatePromptTest.php delete mode 100644 tests/src/Feature/McpServer/Tool/BasicToolConfigTest.php delete mode 100644 tests/src/Feature/McpServer/Tool/HttpToolTest.php delete mode 100644 tests/src/Feature/McpServer/Tool/RunToolTest.php delete mode 100644 tests/src/Feature/McpServer/Tool/ToolHandlerTest.php delete mode 100644 tests/src/Unit/McpServer/Action/ToolResultTest.php delete mode 100644 tests/src/Unit/McpServer/Action/Tools/Filesystem/FileWriteActionTest.php delete mode 100644 tests/src/Unit/Research/Domain/Model/ProjectTest.php delete mode 100644 tests/src/Unit/Research/Domain/ValueObject/EntryIdTest.php delete mode 100644 tests/src/Unit/Research/Domain/ValueObject/ResearchIdTest.php delete mode 100644 tests/src/Unit/Research/Domain/ValueObject/TemplateKeyTest.php delete mode 100644 tests/src/Unit/Research/MCP/DTO/ResearchCreateRequestTest.php delete mode 100644 tests/src/Unit/Research/MCP/DTO/ResearchUpdateRequestTest.php delete mode 100644 tests/src/Unit/Research/MCP/Tools/UpdateResearchToolActionTest.php delete mode 100644 tests/src/Unit/Research/Service/ProjectServiceTest.php diff --git a/composer.json b/composer.json index c272f240..14366780 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "context-hub/generator", + "name": "ctx/ctx", "description": "A tool for generating contextual documentation from your codebase.", "keywords": [ "php8", @@ -18,13 +18,10 @@ "require": { "php": "^8.3", "ext-curl": "*", - "guzzlehttp/guzzle": "^7.0", - "cuyz/valinor": "^1.7", + "ctx/module-research": "^1.0", + "ctx/module-config-templates": "^1.0", + "ctx/mcp-server": "^1.1", "league/html-to-markdown": "^5.1", - "psr-discovery/http-client-implementations": "^1.0", - "psr-discovery/http-factory-implementations": "^1.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", "psr/log": "^3.0", "symfony/finder": "^6.0 | ^7.0 | ^8.0", "symfony/console": "^6.0 | ^7.0 | ^8.0", @@ -39,12 +36,7 @@ "spiral/boot": "^3.15", "spiral/files": "^3.15", "spiral/snapshots": "^3.15", - "spiral/json-schema-generator": "^2.1", - "logiscape/mcp-sdk-php": "^1.0", - "league/route": "^6.2", - "laminas/laminas-diactoros": "^3.5", - "monolog/monolog": "^3.9", - "cocur/slugify": "^4.6" + "monolog/monolog": "^3.9" }, "require-dev": { "buggregator/trap": "^1.13", diff --git a/src/Application/Bootloader/ExcludeBootloader.php b/src/Application/Bootloader/ExcludeBootloader.php index 801ac20c..1608db15 100644 --- a/src/Application/Bootloader/ExcludeBootloader.php +++ b/src/Application/Bootloader/ExcludeBootloader.php @@ -4,6 +4,7 @@ namespace Butschster\ContextGenerator\Application\Bootloader; +use Butschster\ContextGenerator\Config\ConfigLoaderBootloader; use Butschster\ContextGenerator\Config\Exclude\ExcludeParserPlugin; use Butschster\ContextGenerator\Config\Exclude\ExcludeRegistry; use Butschster\ContextGenerator\Config\Exclude\ExcludeRegistryInterface; diff --git a/src/Application/Bootloader/ModifierBootloader.php b/src/Application/Bootloader/ModifierBootloader.php index 57e9ce4e..218f6c31 100644 --- a/src/Application/Bootloader/ModifierBootloader.php +++ b/src/Application/Bootloader/ModifierBootloader.php @@ -4,6 +4,7 @@ namespace Butschster\ContextGenerator\Application\Bootloader; +use Butschster\ContextGenerator\Config\ConfigLoaderBootloader; use Butschster\ContextGenerator\Modifier\Alias\ModifierAliasesParserPlugin; use Butschster\ContextGenerator\Modifier\SourceModifierRegistry; use Spiral\Boot\Bootloader\Bootloader; diff --git a/src/Application/Bootloader/SchemaMapperBootloader.php b/src/Application/Bootloader/SchemaMapperBootloader.php deleted file mode 100644 index 18e37f0e..00000000 --- a/src/Application/Bootloader/SchemaMapperBootloader.php +++ /dev/null @@ -1,32 +0,0 @@ - static function ( - DirectoriesInterface $dirs, - JsonSchemaGenerator $generator, - ): SchemaMapper { - $mapper = new MapperBuilder(); - - $treeMapper = $mapper->build(); - - return new SchemaMapper($generator, $treeMapper); - }, - ]; - } -} diff --git a/src/Application/Bootloader/VariableBootloader.php b/src/Application/Bootloader/VariableBootloader.php index 3e63c255..45e2a4d9 100644 --- a/src/Application/Bootloader/VariableBootloader.php +++ b/src/Application/Bootloader/VariableBootloader.php @@ -4,6 +4,7 @@ namespace Butschster\ContextGenerator\Application\Bootloader; +use Butschster\ContextGenerator\Config\ConfigLoaderBootloader; use Butschster\ContextGenerator\Config\Parser\VariablesParserPlugin; use Butschster\ContextGenerator\DirectoriesInterface; use Butschster\ContextGenerator\Lib\Variable\CompositeProcessor; diff --git a/src/Application/JsonSchema.php b/src/Application/JsonSchema.php deleted file mode 100644 index 46971947..00000000 --- a/src/Application/JsonSchema.php +++ /dev/null @@ -1,13 +0,0 @@ -getRootPath()->join($this->outputPath); // Always show the URL where the schema is hosted - $this->output->info('JSON schema URL: ' . JsonSchema::SCHEMA_URL); + $this->output->info('JSON schema URL: ' . self::SCHEMA_URL); // If no download requested, exit early if (!$this->download) { @@ -55,7 +56,7 @@ public function __invoke( // Download and save the schema try { - $response = $httpClient->get(JsonSchema::SCHEMA_URL, [ + $response = $httpClient->get(self::SCHEMA_URL, [ 'User-Agent' => 'Context-Generator-Schema-Download', 'Accept' => 'application/json', ]); diff --git a/src/Lib/SchemaMapper/SchemaMapperInterface.php b/src/Lib/SchemaMapper/SchemaMapperInterface.php deleted file mode 100644 index bc8d1149..00000000 --- a/src/Lib/SchemaMapper/SchemaMapperInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - $class - * @return T - */ - public function toObject(array $json, string $class): object; -} diff --git a/src/Lib/SchemaMapper/Valinor/MapperBuilder.php b/src/Lib/SchemaMapper/Valinor/MapperBuilder.php deleted file mode 100644 index 3ad379c5..00000000 --- a/src/Lib/SchemaMapper/Valinor/MapperBuilder.php +++ /dev/null @@ -1,29 +0,0 @@ -enableFlexibleCasting() - ->allowPermissiveTypes(); - - if ($this->cache) { - $builder = $builder->withCache($this->cache); - } - - return $builder->mapper(); - } -} diff --git a/src/Lib/SchemaMapper/Valinor/SchemaMapper.php b/src/Lib/SchemaMapper/Valinor/SchemaMapper.php deleted file mode 100644 index 3da71aa6..00000000 --- a/src/Lib/SchemaMapper/Valinor/SchemaMapper.php +++ /dev/null @@ -1,36 +0,0 @@ -generator->generate($class)->jsonSerialize(); - } - - throw new \InvalidArgumentException(\sprintf('Invalid class or JSON schema provided: %s', $class)); - } - - public function toObject(array $json, string $class): object - { - return $this->mapper->map($class, $json); - } -} diff --git a/src/McpServer/Action/Prompts/FilesystemOperationsAction.php b/src/McpServer/Action/Prompts/FilesystemOperationsAction.php index f60b0890..4a2be043 100644 --- a/src/McpServer/Action/Prompts/FilesystemOperationsAction.php +++ b/src/McpServer/Action/Prompts/FilesystemOperationsAction.php @@ -7,10 +7,10 @@ use Butschster\ContextGenerator\Application\Logger\LoggerPrefix; use Butschster\ContextGenerator\McpServer\Attribute\Prompt; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\GetPromptResult; -use Mcp\Types\PromptMessage; -use Mcp\Types\Role; -use Mcp\Types\TextContent; +use PhpMcp\Schema\Content\PromptMessage; +use PhpMcp\Schema\Content\TextContent; +use PhpMcp\Schema\Enum\Role; +use PhpMcp\Schema\Result\GetPromptResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -40,7 +40,7 @@ public function __invoke(ServerRequestInterface $request): GetPromptResult return new GetPromptResult( messages: [ new PromptMessage( - role: Role::USER, + role: Role::User, content: new TextContent( text: \implode("\n", $rules), ), diff --git a/src/McpServer/Action/Prompts/GetPromptAction.php b/src/McpServer/Action/Prompts/GetPromptAction.php index 5cfbc530..4636fb0d 100644 --- a/src/McpServer/Action/Prompts/GetPromptAction.php +++ b/src/McpServer/Action/Prompts/GetPromptAction.php @@ -7,7 +7,7 @@ use Butschster\ContextGenerator\Application\Logger\LoggerPrefix; use Butschster\ContextGenerator\McpServer\Prompt\PromptProviderInterface; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\GetPromptResult; +use PhpMcp\Schema\Result\GetPromptResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -26,7 +26,7 @@ public function __invoke(ServerRequestInterface $request): GetPromptResult $this->logger->info('Getting prompt', ['id' => $id]); if (!$this->prompts->has($id)) { - return new GetPromptResult([]); + return new GetPromptResult(messages: []); } $prompt = $this->prompts->get($id, $request->getAttributes()); diff --git a/src/McpServer/Action/Prompts/ListPromptsAction.php b/src/McpServer/Action/Prompts/ListPromptsAction.php index b8c861df..5eec1c05 100644 --- a/src/McpServer/Action/Prompts/ListPromptsAction.php +++ b/src/McpServer/Action/Prompts/ListPromptsAction.php @@ -9,7 +9,8 @@ use Butschster\ContextGenerator\McpServer\Prompt\PromptProviderInterface; use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\ListPromptsResult; +use Mcp\Server\Contracts\ReferenceProviderInterface; +use PhpMcp\Schema\Result\ListPromptsResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -18,7 +19,7 @@ public function __construct( #[LoggerPrefix(prefix: 'prompts.list')] private LoggerInterface $logger, - private McpItemsRegistry $registry, + private ReferenceProviderInterface $provider, private PromptProviderInterface $prompts, private ConfigLoaderInterface $configLoader, ) {} @@ -30,7 +31,7 @@ public function __invoke(ServerRequestInterface $request): ListPromptsResult $this->logger->info('Listing available prompts'); $prompts = []; - foreach ($this->registry->getPrompts() as $prompt) { + foreach ($this->provider->getPrompts() as $prompt) { $prompts[] = $prompt; } diff --git a/src/McpServer/Action/Prompts/ProjectStructurePromptAction.php b/src/McpServer/Action/Prompts/ProjectStructurePromptAction.php index 4bc22755..f2465129 100644 --- a/src/McpServer/Action/Prompts/ProjectStructurePromptAction.php +++ b/src/McpServer/Action/Prompts/ProjectStructurePromptAction.php @@ -7,10 +7,10 @@ use Butschster\ContextGenerator\Application\Logger\LoggerPrefix; use Butschster\ContextGenerator\McpServer\Attribute\Prompt; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\GetPromptResult; -use Mcp\Types\PromptMessage; -use Mcp\Types\Role; -use Mcp\Types\TextContent; +use PhpMcp\Schema\Content\PromptMessage; +use PhpMcp\Schema\Content\TextContent; +use PhpMcp\Schema\Enum\Role; +use PhpMcp\Schema\Result\GetPromptResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -33,7 +33,7 @@ public function __invoke(ServerRequestInterface $request): GetPromptResult return new GetPromptResult( messages: [ new PromptMessage( - role: Role::USER, + role: Role::User, content: new TextContent( 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", ), diff --git a/src/McpServer/Action/Resources/GenerateConfigAction.php b/src/McpServer/Action/Resources/GenerateConfigAction.php index 4a15c727..b3eeefc7 100644 --- a/src/McpServer/Action/Resources/GenerateConfigAction.php +++ b/src/McpServer/Action/Resources/GenerateConfigAction.php @@ -9,8 +9,8 @@ use Butschster\ContextGenerator\McpServer\Attribute\Resource; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; use Butschster\ContextGenerator\Template\Detection\TemplateDetectionService; -use Mcp\Types\ReadResourceResult; -use Mcp\Types\TextResourceContents; +use PhpMcp\Schema\Content\TextResourceContents; +use PhpMcp\Schema\Result\ReadResourceResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -82,9 +82,9 @@ public function __invoke(ServerRequestInterface $request): ReadResourceResult return new ReadResourceResult([ new TextResourceContents( - text: $response, uri: 'ctx://schema-builder-instructions', mimeType: 'application/json', + text: $response, ), ]); @@ -96,9 +96,9 @@ public function __invoke(ServerRequestInterface $request): ReadResourceResult return new ReadResourceResult([ new TextResourceContents( - text: 'Error: ' . $e->getMessage(), uri: 'ctx://schema-builder-instructions', mimeType: 'application/json', + text: 'Error: ' . $e->getMessage(), ), ]); } diff --git a/src/McpServer/Action/Resources/GetDocumentContentResourceAction.php b/src/McpServer/Action/Resources/GetDocumentContentResourceAction.php index cdf4043e..5cceb8cd 100644 --- a/src/McpServer/Action/Resources/GetDocumentContentResourceAction.php +++ b/src/McpServer/Action/Resources/GetDocumentContentResourceAction.php @@ -10,8 +10,8 @@ use Butschster\ContextGenerator\Document\Compiler\DocumentCompiler; use Butschster\ContextGenerator\Document\Compiler\Error\ErrorCollection; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\ReadResourceResult; -use Mcp\Types\TextResourceContents; +use PhpMcp\Schema\Content\TextResourceContents; +use PhpMcp\Schema\Result\ReadResourceResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -36,9 +36,9 @@ public function __invoke(ServerRequestInterface $request): ReadResourceResult foreach ($config->getDocuments() as $document) { if ($document->outputPath === $path) { $contents[] = new TextResourceContents( - text: (string) $this->compiler->buildContent(new ErrorCollection(), $document)->content, uri: 'ctx://document/' . $document->outputPath, mimeType: 'text/markdown', + text: (string) $this->compiler->buildContent(new ErrorCollection(), $document)->content, ); break; diff --git a/src/McpServer/Action/Resources/JsonSchemaResourceAction.php b/src/McpServer/Action/Resources/JsonSchemaResourceAction.php index 69016a4c..fd220318 100644 --- a/src/McpServer/Action/Resources/JsonSchemaResourceAction.php +++ b/src/McpServer/Action/Resources/JsonSchemaResourceAction.php @@ -8,8 +8,8 @@ use Butschster\ContextGenerator\McpServer\Action\Resources\Service\JsonSchemaService; use Butschster\ContextGenerator\McpServer\Attribute\Resource; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\ReadResourceResult; -use Mcp\Types\TextResourceContents; +use PhpMcp\Schema\Content\TextResourceContents; +use PhpMcp\Schema\Result\ReadResourceResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -34,9 +34,9 @@ public function __invoke(ServerRequestInterface $request): ReadResourceResult return new ReadResourceResult([ new TextResourceContents( - text: \json_encode($this->jsonSchema->getSimplifiedSchema()), uri: 'ctx://json-schema', mimeType: 'application/json', + text: \json_encode($this->jsonSchema->getSimplifiedSchema()), ), ]); } diff --git a/src/McpServer/Action/Resources/ListResourcesAction.php b/src/McpServer/Action/Resources/ListResourcesAction.php index 78a3a6d5..1c01849f 100644 --- a/src/McpServer/Action/Resources/ListResourcesAction.php +++ b/src/McpServer/Action/Resources/ListResourcesAction.php @@ -8,10 +8,10 @@ use Butschster\ContextGenerator\Config\Loader\ConfigLoaderInterface; use Butschster\ContextGenerator\Config\Registry\ConfigRegistryAccessor; use Butschster\ContextGenerator\McpServer\McpConfig; -use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; -use Mcp\Types\ListResourcesResult; -use Mcp\Types\Resource; +use Mcp\Server\Contracts\ReferenceProviderInterface; +use PhpMcp\Schema\Resource; +use PhpMcp\Schema\Result\ListResourcesResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -21,7 +21,7 @@ public function __construct( #[LoggerPrefix(prefix: 'resources.list')] private LoggerInterface $logger, private ConfigLoaderInterface $configLoader, - private McpItemsRegistry $registry, + private ReferenceProviderInterface $provider, private McpConfig $config, ) {} @@ -33,7 +33,7 @@ public function __invoke(ServerRequestInterface $request): ListResourcesResult $resources = []; // Get resources from registry - foreach ($this->registry->getResources() as $resource) { + foreach ($this->provider->getResources() as $resource) { $resources[] = $resource; } @@ -44,12 +44,12 @@ public function __invoke(ServerRequestInterface $request): ListResourcesResult $tags = \implode(', ', $document->getTags()); $resources[] = new Resource( + uri: 'ctx://document/' . $document->outputPath, name: $this->config->getDocumentNameFormat( path: $document->outputPath, description: $document->description, tags: $tags, ), - uri: 'ctx://document/' . $document->outputPath, description: \sprintf( '%s. Tags: %s', $document->description, diff --git a/src/McpServer/Action/ToolResult.php b/src/McpServer/Action/ToolResult.php deleted file mode 100644 index 516d86d6..00000000 --- a/src/McpServer/Action/ToolResult.php +++ /dev/null @@ -1,69 +0,0 @@ - false, - 'error' => $error, - ]), - ), - ], isError: true); - } - - /** - * Create an error tool result with validation details. - */ - public static function validationError(array $validationErrors): CallToolResult - { - return new CallToolResult([ - new TextContent( - text: \json_encode([ - 'success' => false, - 'error' => 'Validation failed', - 'details' => $validationErrors, - ]), - ), - ], isError: true); - } - - /** - * Create a simple text result (for cases where just text is returned). - */ - public static function text(string $text): CallToolResult - { - return new CallToolResult([ - new TextContent( - text: $text, - ), - ]); - } -} diff --git a/src/McpServer/Action/Tools/Context/ContextAction.php b/src/McpServer/Action/Tools/Context/ContextAction.php index afa48cac..0871a410 100644 --- a/src/McpServer/Action/Tools/Context/ContextAction.php +++ b/src/McpServer/Action/Tools/Context/ContextAction.php @@ -9,8 +9,8 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; -use Mcp\Types\TextContent; +use PhpMcp\Schema\Content\TextContent; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; diff --git a/src/McpServer/Action/Tools/Context/ContextGetAction.php b/src/McpServer/Action/Tools/Context/ContextGetAction.php index 1762f617..c7ea31e7 100644 --- a/src/McpServer/Action/Tools/Context/ContextGetAction.php +++ b/src/McpServer/Action/Tools/Context/ContextGetAction.php @@ -13,7 +13,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/Context/ContextRequestAction.php b/src/McpServer/Action/Tools/Context/ContextRequestAction.php index 04b1a55b..34e6f732 100644 --- a/src/McpServer/Action/Tools/Context/ContextRequestAction.php +++ b/src/McpServer/Action/Tools/Context/ContextRequestAction.php @@ -13,8 +13,8 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; -use Mcp\Types\TextContent; +use PhpMcp\Schema\Content\TextContent; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/Docs/FetchLibraryDocsAction.php b/src/McpServer/Action/Tools/Docs/FetchLibraryDocsAction.php index 5c2063f1..23d0c4de 100644 --- a/src/McpServer/Action/Tools/Docs/FetchLibraryDocsAction.php +++ b/src/McpServer/Action/Tools/Docs/FetchLibraryDocsAction.php @@ -11,7 +11,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/Docs/LibrarySearchAction.php b/src/McpServer/Action/Tools/Docs/LibrarySearchAction.php index 6d875466..581ccbc2 100644 --- a/src/McpServer/Action/Tools/Docs/LibrarySearchAction.php +++ b/src/McpServer/Action/Tools/Docs/LibrarySearchAction.php @@ -11,7 +11,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/ExecuteCustomToolAction.php b/src/McpServer/Action/Tools/ExecuteCustomToolAction.php index 3beb2b80..3b96e895 100644 --- a/src/McpServer/Action/Tools/ExecuteCustomToolAction.php +++ b/src/McpServer/Action/Tools/ExecuteCustomToolAction.php @@ -8,8 +8,8 @@ use Butschster\ContextGenerator\McpServer\Tool\Exception\ToolExecutionException; use Butschster\ContextGenerator\McpServer\Tool\ToolHandlerFactory; use Butschster\ContextGenerator\McpServer\Tool\ToolProviderInterface; -use Mcp\Types\CallToolResult; -use Mcp\Types\TextContent; +use PhpMcp\Schema\Content\TextContent; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; diff --git a/src/McpServer/Action/Tools/Filesystem/DirectoryListAction.php b/src/McpServer/Action/Tools/Filesystem/DirectoryListAction.php index e4fe45e5..8bb02b24 100644 --- a/src/McpServer/Action/Tools/Filesystem/DirectoryListAction.php +++ b/src/McpServer/Action/Tools/Filesystem/DirectoryListAction.php @@ -11,7 +11,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; use Symfony\Component\Finder\Finder; diff --git a/src/McpServer/Action/Tools/Filesystem/FileApplyPatchAction.php b/src/McpServer/Action/Tools/Filesystem/FileApplyPatchAction.php index 19f8201f..b871bcf8 100644 --- a/src/McpServer/Action/Tools/Filesystem/FileApplyPatchAction.php +++ b/src/McpServer/Action/Tools/Filesystem/FileApplyPatchAction.php @@ -11,7 +11,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/Filesystem/FileMoveAction.php b/src/McpServer/Action/Tools/Filesystem/FileMoveAction.php index 1ea27b3b..d2a5417d 100644 --- a/src/McpServer/Action/Tools/Filesystem/FileMoveAction.php +++ b/src/McpServer/Action/Tools/Filesystem/FileMoveAction.php @@ -10,7 +10,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; use Spiral\Files\Exception\FilesException; use Spiral\Files\FilesInterface; diff --git a/src/McpServer/Action/Tools/Filesystem/FileReadAction.php b/src/McpServer/Action/Tools/Filesystem/FileReadAction.php index 501e2f8b..f00d77b7 100644 --- a/src/McpServer/Action/Tools/Filesystem/FileReadAction.php +++ b/src/McpServer/Action/Tools/Filesystem/FileReadAction.php @@ -10,7 +10,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\InputSchema; use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; use Spiral\Files\Exception\FilesException; use Spiral\Files\FilesInterface; diff --git a/src/McpServer/Action/Tools/Filesystem/FileWriteAction.php b/src/McpServer/Action/Tools/Filesystem/FileWriteAction.php index 3220926c..2a02a275 100644 --- a/src/McpServer/Action/Tools/Filesystem/FileWriteAction.php +++ b/src/McpServer/Action/Tools/Filesystem/FileWriteAction.php @@ -10,7 +10,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; use Spiral\Files\FilesInterface; diff --git a/src/McpServer/Action/Tools/Git/GitAddAction.php b/src/McpServer/Action/Tools/Git/GitAddAction.php index 03678f4f..fc882b09 100644 --- a/src/McpServer/Action/Tools/Git/GitAddAction.php +++ b/src/McpServer/Action/Tools/Git/GitAddAction.php @@ -13,7 +13,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/Git/GitCommitAction.php b/src/McpServer/Action/Tools/Git/GitCommitAction.php index f3e88b6b..6995e91c 100644 --- a/src/McpServer/Action/Tools/Git/GitCommitAction.php +++ b/src/McpServer/Action/Tools/Git/GitCommitAction.php @@ -13,7 +13,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/Git/GitStatusAction.php b/src/McpServer/Action/Tools/Git/GitStatusAction.php index 099710d7..72013ec2 100644 --- a/src/McpServer/Action/Tools/Git/GitStatusAction.php +++ b/src/McpServer/Action/Tools/Git/GitStatusAction.php @@ -14,7 +14,7 @@ use Butschster\ContextGenerator\McpServer\Attribute\InputSchema; use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Log\LoggerInterface; #[Tool( diff --git a/src/McpServer/Action/Tools/ListToolsAction.php b/src/McpServer/Action/Tools/ListToolsAction.php index 23506139..413551f6 100644 --- a/src/McpServer/Action/Tools/ListToolsAction.php +++ b/src/McpServer/Action/Tools/ListToolsAction.php @@ -4,13 +4,12 @@ namespace Butschster\ContextGenerator\McpServer\Action\Tools; -use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get; use Butschster\ContextGenerator\McpServer\Tool\Config\ToolDefinition; use Butschster\ContextGenerator\McpServer\Tool\ToolProviderInterface; -use Mcp\Types\ListToolsResult; -use Mcp\Types\Tool; -use Mcp\Types\ToolInputSchema; +use Mcp\Server\Contracts\ReferenceProviderInterface; +use PhpMcp\Schema\Result\ListToolsResult; +use PhpMcp\Schema\Tool; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -18,7 +17,7 @@ { public function __construct( private LoggerInterface $logger, - private McpItemsRegistry $registry, + private ReferenceProviderInterface $provider, private ToolProviderInterface $toolProvider, ) {} @@ -27,15 +26,13 @@ public function __invoke(ServerRequestInterface $request): ListToolsResult { $this->logger->info('Listing available tools'); - $tools = $this->registry->getTools(); + $tools = \array_values($this->provider->getTools()); foreach ($this->toolProvider->all() as $toolDefinition) { - // Create the input schema object based on the tool's schema - $inputSchema = $this->buildInputSchema($toolDefinition); - $tools[] = new Tool( name: $toolDefinition->id, - inputSchema: $inputSchema, + inputSchema: $this->buildInputSchema($toolDefinition), description: $toolDefinition->description, + annotations: null, ); } @@ -45,11 +42,14 @@ public function __invoke(ServerRequestInterface $request): ListToolsResult /** * Build a ToolInputSchema object from the tool definition's schema. */ - private function buildInputSchema(ToolDefinition $toolDefinition): ToolInputSchema + private function buildInputSchema(ToolDefinition $toolDefinition): array { // If no schema is defined, return an empty schema if ($toolDefinition->schema === null) { - return new ToolInputSchema(); + return [ + 'type' => 'object', + 'properties' => new \stdClass(), + ]; } // Convert the tool's schema to array format expected by ToolInputSchema @@ -64,6 +64,6 @@ private function buildInputSchema(ToolDefinition $toolDefinition): ToolInputSche } // Use the fromArray method to create the ToolInputSchema - return ToolInputSchema::fromArray($schemaData); + return $schemaData; } } diff --git a/src/McpServer/Action/Tools/Prompts/GetPromptToolAction.php b/src/McpServer/Action/Tools/Prompts/GetPromptToolAction.php index fcbbc500..680ef6f8 100644 --- a/src/McpServer/Action/Tools/Prompts/GetPromptToolAction.php +++ b/src/McpServer/Action/Tools/Prompts/GetPromptToolAction.php @@ -11,8 +11,9 @@ use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; -use Mcp\Types\TextContent; +use PhpMcp\Schema\Content\PromptMessage; +use PhpMcp\Schema\Content\TextContent; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -81,7 +82,7 @@ public function __invoke(GetPromptRequest $request, ServerRequestInterface $serv /** * Processes message templates with the given arguments. * - * @param array<\Mcp\Types\PromptMessage> $messages The messages to process + * @param array $messages The messages to process * @param array $arguments The arguments to use * @return array The processed messages */ @@ -103,7 +104,7 @@ private function processMessageTemplates(array $messages, array $arguments): arr $content = new TextContent($text); } - return new \Mcp\Types\PromptMessage( + return new PromptMessage( role: $message->role, content: $content, ); diff --git a/src/McpServer/Action/Tools/Prompts/ListPromptsToolAction.php b/src/McpServer/Action/Tools/Prompts/ListPromptsToolAction.php index bcf57dab..da87c46e 100644 --- a/src/McpServer/Action/Tools/Prompts/ListPromptsToolAction.php +++ b/src/McpServer/Action/Tools/Prompts/ListPromptsToolAction.php @@ -6,11 +6,11 @@ use Butschster\ContextGenerator\Config\Loader\ConfigLoaderInterface; use Butschster\ContextGenerator\McpServer\Prompt\PromptProviderInterface; -use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry; use Butschster\ContextGenerator\McpServer\Attribute\Tool; use Butschster\ContextGenerator\McpServer\Action\ToolResult; use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post; -use Mcp\Types\CallToolResult; +use Mcp\Server\Contracts\ReferenceProviderInterface; +use PhpMcp\Schema\Result\CallToolResult; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LoggerInterface; @@ -23,7 +23,7 @@ { public function __construct( private LoggerInterface $logger, - private McpItemsRegistry $registry, + private ReferenceProviderInterface $provider, private PromptProviderInterface $prompts, private ConfigLoaderInterface $configLoader, ) {} @@ -39,7 +39,7 @@ public function __invoke(ServerRequestInterface $request): CallToolResult $promptsList = []; // Get prompts from registry - foreach ($this->registry->getPrompts() as $prompt) { + foreach ($this->provider->getPrompts() as $prompt) { $promptsList[] = [ 'id' => $prompt->name, 'description' => $prompt->description, diff --git a/src/McpServer/McpServerBootloader.php b/src/McpServer/ActionsBootloader.php similarity index 83% rename from src/McpServer/McpServerBootloader.php rename to src/McpServer/ActionsBootloader.php index 89546dc3..08db1a34 100644 --- a/src/McpServer/McpServerBootloader.php +++ b/src/McpServer/ActionsBootloader.php @@ -5,8 +5,15 @@ namespace Butschster\ContextGenerator\McpServer; use Butschster\ContextGenerator\Application\Bootloader\ConsoleBootloader; -use Butschster\ContextGenerator\Application\Bootloader\HttpClientBootloader; use Butschster\ContextGenerator\Config\Loader\ConfigLoaderInterface; +use Butschster\ContextGenerator\McpServer\Console\McpConfigCommand; +use Butschster\ContextGenerator\McpServer\Projects\Console\ProjectAddCommand; +use Butschster\ContextGenerator\McpServer\Projects\Console\ProjectCommand; +use Butschster\ContextGenerator\McpServer\Projects\Console\ProjectListCommand; +use Butschster\ContextGenerator\McpServer\Prompt\Console\ListPromptsCommand; +use Butschster\ContextGenerator\McpServer\Prompt\Console\ShowPromptCommand; +use Butschster\ContextGenerator\McpServer\Tool\Console\ToolListCommand; +use Butschster\ContextGenerator\McpServer\Tool\Console\ToolRunCommand; use Butschster\ContextGenerator\Research\MCP\Tools\CreateEntryToolAction; use Butschster\ContextGenerator\Research\MCP\Tools\CreateResearchToolAction; use Butschster\ContextGenerator\Research\MCP\Tools\GetResearchToolAction; @@ -42,41 +49,22 @@ use Butschster\ContextGenerator\McpServer\Action\Tools\Prompts\GetPromptToolAction; use Butschster\ContextGenerator\McpServer\Action\Tools\Prompts\ListPromptsToolAction; use Butschster\ContextGenerator\McpServer\Console\MCPServerCommand; -use Butschster\ContextGenerator\McpServer\Console\McpConfigCommand; -use Butschster\ContextGenerator\McpServer\Console\McpConfig\McpConfigBootloader; use Butschster\ContextGenerator\McpServer\Projects\Actions\ProjectsListToolAction; use Butschster\ContextGenerator\McpServer\Projects\Actions\ProjectSwitchToolAction; -use Butschster\ContextGenerator\McpServer\Projects\McpProjectsBootloader; -use Butschster\ContextGenerator\McpServer\ProjectService\ProjectServiceInterface; -use Butschster\ContextGenerator\McpServer\Prompt\McpPromptBootloader; -use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry; -use Butschster\ContextGenerator\McpServer\Routing\McpResponseStrategy; -use Butschster\ContextGenerator\McpServer\Routing\RouteRegistrar; -use Butschster\ContextGenerator\McpServer\Tool\McpToolBootloader; -use League\Route\Router; -use League\Route\Strategy\StrategyInterface; -use Psr\Container\ContainerInterface; use Spiral\Boot\Bootloader\Bootloader; use Spiral\Boot\EnvironmentInterface; use Spiral\Config\ConfiguratorInterface; -use Spiral\Core\Attribute\Proxy; -use Spiral\Core\Config\Proxy as ConfigProxy; -final class McpServerBootloader extends Bootloader +final class ActionsBootloader extends Bootloader { public function __construct( private readonly ConfiguratorInterface $config, ) {} - #[\Override] public function defineDependencies(): array { return [ - HttpClientBootloader::class, - McpToolBootloader::class, - McpPromptBootloader::class, - McpProjectsBootloader::class, - McpConfigBootloader::class, + McpServerBootloader::class, ]; } @@ -122,8 +110,20 @@ public function init(EnvironmentInterface $env): void public function boot(ConsoleBootloader $console): void { + $console->addCommand(ToolListCommand::class); + $console->addCommand(ToolRunCommand::class); + $console->addCommand(MCPServerCommand::class); $console->addCommand(McpConfigCommand::class); + + $console->addCommand(ListPromptsCommand::class); + $console->addCommand(ShowPromptCommand::class); + + $console->addCommand( + ProjectCommand::class, + ProjectAddCommand::class, + ProjectListCommand::class, + ); } #[\Override] @@ -143,20 +143,6 @@ public function defineSingletons(): array return $factory; }, - RouteRegistrar::class => RouteRegistrar::class, - McpItemsRegistry::class => McpItemsRegistry::class, - StrategyInterface::class => McpResponseStrategy::class, - ProjectServiceInterface::class => new ConfigProxy( - interface: ProjectServiceInterface::class, - ), - Router::class => static function (StrategyInterface $strategy, #[Proxy] ContainerInterface $container) { - $router = new Router(); - \assert($strategy instanceof McpResponseStrategy); - $strategy->setContainer($container); - $router->setStrategy($strategy); - - return $router; - }, ]; } diff --git a/src/McpServer/Attribute/InputSchema.php b/src/McpServer/Attribute/InputSchema.php deleted file mode 100644 index 61567646..00000000 --- a/src/McpServer/Attribute/InputSchema.php +++ /dev/null @@ -1,16 +0,0 @@ -getRootPath(); $logger->info(\sprintf('Using root path: %s', $rootPathStr)); @@ -134,7 +131,6 @@ public function __invoke( name: AppScope::Mcp, bindings: [ DirectoriesInterface::class => $dirs, - ProjectServiceInterface::class => $projectServiceFactory->create(), HasPrefixLoggerInterface::class => $logger, ConfigLoaderInterface::class => $loader, CommandExecutorInterface::class => $container->make(CommandExecutor::class, [ diff --git a/src/McpServer/Console/McpConfig/ConfigGeneratorInterface.php b/src/McpServer/Console/McpConfig/ConfigGeneratorInterface.php deleted file mode 100644 index 210fd6ab..00000000 --- a/src/McpServer/Console/McpConfig/ConfigGeneratorInterface.php +++ /dev/null @@ -1,20 +0,0 @@ -selectTemplate($osInfo); - - return $template->generate( - client: $client, - osInfo: $osInfo, - projectPath: $projectPath, - options: $options, - ); - } - - public function getSupportedClients(): array - { - return ['claude', 'generic']; - } - - private function selectTemplate(OsInfo $osInfo): ConfigTemplateInterface - { - return match (true) { - $osInfo->isWsl => $this->wslTemplate, - $osInfo->isWindows => $this->windowsTemplate, - $osInfo->isLinux => $this->linuxTemplate, - $osInfo->isMacOs => $this->macosTemplate, - default => $this->linuxTemplate, // fallback to Linux template - }; - } -} diff --git a/src/McpServer/Console/McpConfig/McpConfigBootloader.php b/src/McpServer/Console/McpConfig/McpConfigBootloader.php deleted file mode 100644 index faeb3105..00000000 --- a/src/McpServer/Console/McpConfig/McpConfigBootloader.php +++ /dev/null @@ -1,48 +0,0 @@ - OsDetectionService::class, - ConfigGeneratorInterface::class => static fn( - LinuxConfigTemplate $linuxTemplate, - WindowsConfigTemplate $windowsTemplate, - WslConfigTemplate $wslTemplate, - MacOsConfigTemplate $macosTemplate, - ): McpConfigGenerator => new McpConfigGenerator( - windowsTemplate: $windowsTemplate, - linuxTemplate: $linuxTemplate, - wslTemplate: $wslTemplate, - macosTemplate: $macosTemplate, - ), - LinuxConfigTemplate::class => LinuxConfigTemplate::class, - WindowsConfigTemplate::class => WindowsConfigTemplate::class, - WslConfigTemplate::class => WslConfigTemplate::class, - MacOsConfigTemplate::class => MacOsConfigTemplate::class, - ]; - } - - #[\Override] - public function defineBindings(): array - { - return [ - ConfigTemplateInterface::class => LinuxConfigTemplate::class, - ]; - } -} diff --git a/src/McpServer/Console/McpConfig/Model/McpConfig.php b/src/McpServer/Console/McpConfig/Model/McpConfig.php deleted file mode 100644 index 7f6400c8..00000000 --- a/src/McpServer/Console/McpConfig/Model/McpConfig.php +++ /dev/null @@ -1,46 +0,0 @@ -configData, $flags); - } - - public function getConfigForClient(): array - { - return $this->configData; - } - - public function hasEnvironmentVariables(): bool - { - return !empty($this->env); - } - - public function getDisplayCommand(): string - { - $command = $this->command; - $args = \implode(' ', $this->args); - - return \trim("{$command} {$args}"); - } -} diff --git a/src/McpServer/Console/McpConfig/Model/OsInfo.php b/src/McpServer/Console/McpConfig/Model/OsInfo.php deleted file mode 100644 index 688e02f9..00000000 --- a/src/McpServer/Console/McpConfig/Model/OsInfo.php +++ /dev/null @@ -1,67 +0,0 @@ -isWsl) { - $distro = $this->additionalInfo['wsl_distro'] ?? 'Unknown'; - return "WSL ({$distro})"; - } - - return $this->osName; - } - - public function requiresSpecialHandling(): bool - { - return $this->isWsl || $this->isWindows; - } - - public function getShellCommand(): string - { - if ($this->isWsl) { - return 'bash.exe'; - } - - if ($this->isWindows) { - return 'ctx.exe'; - } - - return 'ctx'; - } - - public function getConfigType(): string - { - return match (true) { - $this->isWsl => 'wsl', - $this->isWindows => 'windows', - $this->isLinux => 'linux', - $this->isMacOs => 'macos', - default => 'generic', - }; - } - - public function isWindows(): bool - { - return $this->isWindows; - } - - public function isWsl(): bool - { - return $this->isWsl; - } -} diff --git a/src/McpServer/Console/McpConfig/Renderer/McpConfigRenderer.php b/src/McpServer/Console/McpConfig/Renderer/McpConfigRenderer.php deleted file mode 100644 index 619b086b..00000000 --- a/src/McpServer/Console/McpConfig/Renderer/McpConfigRenderer.php +++ /dev/null @@ -1,248 +0,0 @@ -output->title('MCP Configuration Generator'); - $this->output->text([ - 'This tool generates configuration snippets for connecting CTX to MCP clients like Claude Desktop.', - 'It automatically detects your operating system and generates the appropriate configuration format.', - ]); - $this->output->newLine(); - } - - public function renderInteractiveWelcome(): void - { - $this->output->section('Interactive Configuration Mode'); - $this->output->text('Let\'s configure your MCP client step by step...'); - $this->output->newLine(); - } - - public function renderDetectedEnvironment(OsInfo $osInfo): void - { - $this->output->section('Environment Detection'); - - $this->output->definitionList( - ['Operating System' => $osInfo->getDisplayName()], - ['PHP OS' => $osInfo->phpOs], - ['Architecture' => $osInfo->additionalInfo['architecture'] ?? 'Unknown'], - ); - - if ($osInfo->isWsl) { - $this->output->note('WSL environment detected. Configuration will use bash.exe wrapper.'); - } - - $this->output->newLine(); - } - - public function renderConfiguration(McpConfig $config, OsInfo $osInfo, array $options = []): void - { - $this->output->section('Generated Configuration'); - - $configType = ($options['use_project_path'] ?? false) ? 'Project-specific' : 'Global project registry'; - - $this->output->text([ - "Configuration type: {$config->clientType}", - "Operating system: {$osInfo->getDisplayName()}", - "Project mode: {$configType}", - "Command: {$config->getDisplayCommand()}", - ]); - - $this->output->newLine(); - - if ($config->clientType === 'claude') { - $this->renderClaudeConfig($config, $osInfo); - } else { - $this->renderGenericConfig($config, $osInfo); - } - } - - public function renderExplanation(McpConfig $config, OsInfo $osInfo, array $options = []): void - { - $this->output->section('Setup Instructions'); - - if ($config->clientType === 'claude') { - $this->renderClaudeSetupInstructions($config, $osInfo, $options); - } else { - $this->renderGenericSetupInstructions($config, $osInfo, $options); - } - - $this->renderTroubleshootingTips($osInfo); - } - - private function renderClaudeConfig(McpConfig $config, OsInfo $osInfo): void - { - $this->output->text('Add this configuration to your Claude Desktop config file:'); - $this->output->newLine(); - - $this->output->writeln('' . $config->toJson() . ''); - $this->output->newLine(); - - // Show config file location hints - $this->renderClaudeConfigLocation($osInfo); - } - - private function renderGenericConfig(McpConfig $config, OsInfo $osInfo): void - { - $this->output->text('Generic MCP client configuration:'); - $this->output->newLine(); - - $this->output->writeln('' . $config->toJson() . ''); - $this->output->newLine(); - } - - private function renderClaudeConfigLocation(OsInfo $osInfo): void - { - $this->output->text('Claude Desktop configuration file location:'); - - $configPaths = match (true) { - $osInfo->isWindows || $osInfo->isWsl => [ - '%APPDATA%\\Claude\\claude_desktop_config.json', - 'C:\\Users\\\\AppData\\Roaming\\Claude\\claude_desktop_config.json', - ], - $osInfo->isMacOs => [ - '~/Library/Application Support/Claude/claude_desktop_config.json', - ], - default => [ - '~/.config/Claude/claude_desktop_config.json', - '$XDG_CONFIG_HOME/Claude/claude_desktop_config.json', - ], - }; - - foreach ($configPaths as $path) { - $this->output->text(" • {$path}"); - } - - $this->output->newLine(); - } - - private function renderClaudeSetupInstructions(McpConfig $config, OsInfo $osInfo, array $options = []): void - { - $this->output->text('To set up Claude Desktop with CTX:'); - - $steps = [ - '1. Close Claude Desktop if it\'s running', - '2. Open the Claude Desktop configuration file (see paths above)', - '3. If the file doesn\'t exist, create it with the generated configuration', - '4. If the file exists, merge the "mcpServers" section with your existing configuration', - '5. Save the file and restart Claude Desktop', - '6. You should see CTX listed in the MCP servers when you start a new conversation', - ]; - - foreach ($steps as $step) { - $this->output->text(" {$step}"); - } - - $this->output->newLine(); - - // Add explanation about configuration mode - if (isset($options['use_project_path']) && $options['use_project_path']) { - $this->output->note([ - 'Project-specific configuration:', - '• This configuration is tied to a specific project path', - '• CTX will only have access to the specified project', - '• Good for single-project workflows', - ]); - } else { - $this->output->note([ - 'Global project registry configuration:', - '• This configuration uses CTX\'s project registry system', - '• You can switch between different registered projects dynamically', - '• Use "ctx project:add" to register projects first', - '• Good for multi-project workflows', - ]); - } - - if ($config->hasEnvironmentVariables()) { - $this->output->note([ - 'This configuration includes environment variables.', - 'Make sure the specified environment variables are available in your system.', - ]); - } - - if ($osInfo->isWsl) { - $this->output->warning([ - 'WSL Configuration Notes:', - '• Make sure CTX is installed and available in your WSL environment', - '• The path should be a WSL path (e.g., /home/user/project), not a Windows path', - '• Environment variables are exported within the bash command', - ]); - } - } - - private function renderGenericSetupInstructions(McpConfig $config, OsInfo $osInfo, array $options = []): void - { - $this->output->text('To use this configuration with your MCP client:'); - - $steps = [ - '1. Refer to your MCP client\'s documentation for configuration format', - '2. Use the provided command and arguments to configure the server', - '3. Ensure CTX is installed and available in your system PATH', - '4. Test the connection by starting your MCP client', - ]; - - foreach ($steps as $step) { - $this->output->text(" {$step}"); - } - - $this->output->newLine(); - } - - private function renderTroubleshootingTips(OsInfo $osInfo): void - { - $this->output->section('Troubleshooting Tips'); - - $tips = [ - 'If Claude doesn\'t show CTX as available:', - ' • Check that the configuration file syntax is valid JSON', - ' • Verify that the CTX binary is installed and accessible', - ' • Check the Claude Desktop logs for any error messages', - ' • Try restarting Claude Desktop completely', - ]; - - if ($osInfo->isWindows) { - $tips = \array_merge($tips, [ - '', - 'Windows-specific tips:', - ' • Make sure ctx.exe is in your PATH or use the full path in the configuration', - ' • Use double backslashes (\\\\) in paths if you encounter issues', - ]); - } - - if ($osInfo->isWsl) { - $tips = \array_merge($tips, [ - '', - 'WSL-specific tips:', - ' • Ensure CTX is installed in your WSL distribution, not just Windows', - ' • Use WSL paths (/mnt/c/... for Windows drives) in the configuration', - ' • Test the command manually in WSL: bash.exe -c "ctx server"', - ]); - } - - foreach ($tips as $tip) { - $this->output->text($tip); - } - - $this->output->newLine(); - - $this->output->note([ - 'For more help:', - '• Visit the CTX documentation at https://context-hub.github.io/generator/', - '• Check the MCP server documentation for troubleshooting guides', - '• Join the community discussions on GitHub', - ]); - } -} diff --git a/src/McpServer/Console/McpConfig/Service/OsDetectionService.php b/src/McpServer/Console/McpConfig/Service/OsDetectionService.php deleted file mode 100644 index 5c9a815f..00000000 --- a/src/McpServer/Console/McpConfig/Service/OsDetectionService.php +++ /dev/null @@ -1,128 +0,0 @@ -gatherEnvironmentInfo(), - ); - } - - $phpOs = PHP_OS; - $isWindows = $this->isWindows($phpOs); - $isLinux = $this->isLinux($phpOs); - $isMacOs = $this->isMacOs($phpOs); - $isWsl = $this->detectWsl(); - - // If we detected WSL, override the OS detection - if ($isWsl) { - $osName = 'WSL'; - $isLinux = false; // WSL is technically Windows with Linux compatibility - } else { - $osName = match (true) { - $isWindows => 'Windows', - $isLinux => 'Linux', - $isMacOs => 'macOS', - default => 'Unknown', - }; - } - - return new OsInfo( - osName: $osName, - isWindows: $isWindows, - isLinux: $isLinux, - isMacOs: $isMacOs, - isWsl: $isWsl, - phpOs: $phpOs, - additionalInfo: $this->gatherEnvironmentInfo(), - ); - } - - private function isWindows(string $phpOs): bool - { - return \str_starts_with(\strtoupper($phpOs), 'WIN'); - } - - private function isLinux(string $phpOs): bool - { - return \strtoupper($phpOs) === 'LINUX'; - } - - private function isMacOs(string $phpOs): bool - { - return \strtoupper($phpOs) === 'DARWIN'; - } - - private function detectWsl(): bool - { - // Multiple methods to detect WSL - - // Method 1: Check for WSL environment variables - if (\getenv('WSL_DISTRO_NAME') !== false || \getenv('WSLENV') !== false) { - return true; - } - - // Method 2: Check for /proc/version (Linux-based detection) - if (\file_exists('/proc/version')) { - $version = \file_get_contents('/proc/version'); - if ($version !== false && (\str_contains($version, 'Microsoft') || \str_contains($version, 'WSL'))) { - return true; - } - } - - // Method 3: Check for WSL-specific paths - if (\file_exists('/mnt/c') || \file_exists('/proc/sys/fs/binfmt_misc/WSLInterop')) { - return true; - } - - // Method 4: Check uname output - // if (\function_exists('shell_exec')) { - // /** @psalm-suppress ForbiddenCode */ - // $uname = \shell_exec('uname -r 2>/dev/null'); - // if ($uname !== null && (\str_contains($uname, 'Microsoft') || \str_contains($uname, 'WSL'))) { - // return true; - // } - // } - - return false; - } - - private function gatherEnvironmentInfo(): array - { - $info = [ - 'php_version' => PHP_VERSION, - 'php_os' => PHP_OS, - ]; - - // Add WSL-specific info if available - if ($wslDistro = \getenv('WSL_DISTRO_NAME')) { - $info['wsl_distro'] = $wslDistro; - } - - // Add architecture info - if (\function_exists('php_uname')) { - $info['architecture'] = \php_uname('m'); - } - - // Add shell info if available - if ($shell = \getenv('SHELL')) { - $info['shell'] = $shell; - } - - return $info; - } -} diff --git a/src/McpServer/Console/McpConfig/Template/BaseConfigTemplate.php b/src/McpServer/Console/McpConfig/Template/BaseConfigTemplate.php deleted file mode 100644 index 544d5f98..00000000 --- a/src/McpServer/Console/McpConfig/Template/BaseConfigTemplate.php +++ /dev/null @@ -1,105 +0,0 @@ -getCommand($osInfo); - $args = $this->getArgs($osInfo, $projectPath, $options); - $env = $this->getEnvironmentVariables($options); - - $configData = [ - 'mcpServers' => [ - 'ctx' => [ - 'command' => $command, - 'args' => $args, - ], - ], - ]; - - // Add environment variables if present - if (!empty($env)) { - $configData['mcpServers']['ctx']['env'] = $env; - } - - return new McpConfig( - clientType: 'claude', - osType: $osInfo->getConfigType(), - configData: $configData, - command: $command, - args: $args, - env: $env, - metadata: [ - 'os_name' => $osInfo->osName, - 'project_path' => $projectPath, - ], - ); - } - - protected function generateGenericConfig( - OsInfo $osInfo, - string $projectPath, - array $options = [], - ): McpConfig { - $command = $this->getCommand($osInfo); - $args = $this->getArgs($osInfo, $projectPath, $options); - $env = $this->getEnvironmentVariables($options); - - $configData = [ - 'command' => $command, - 'args' => $args, - ]; - - if (!empty($env)) { - $configData['env'] = $env; - } - - return new McpConfig( - clientType: 'generic', - osType: $osInfo->getConfigType(), - configData: $configData, - command: $command, - args: $args, - env: $env, - metadata: [ - 'os_name' => $osInfo->osName, - 'project_path' => $projectPath, - ], - ); - } - - abstract protected function getCommand(OsInfo $osInfo): string; - - abstract protected function getArgs(OsInfo $osInfo, string $projectPath, array $options = []): array; - - protected function getEnvironmentVariables(array $options = []): array - { - $env = []; - - // Add commonly needed environment variables - if (isset($options['github_token'])) { - $env['GITHUB_PAT'] = $options['github_token']; - } - - if (isset($options['enable_file_operations'])) { - $env['MCP_FILE_OPERATIONS'] = $options['enable_file_operations'] ? 'true' : 'false'; - } - - return $env; - } -} diff --git a/src/McpServer/Console/McpConfig/Template/ConfigTemplateInterface.php b/src/McpServer/Console/McpConfig/Template/ConfigTemplateInterface.php deleted file mode 100644 index b04a45a7..00000000 --- a/src/McpServer/Console/McpConfig/Template/ConfigTemplateInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - $this->generateClaudeConfig($osInfo, $projectPath, $options), - 'generic' => $this->generateGenericConfig($osInfo, $projectPath, $options), - default => throw new \InvalidArgumentException("Unsupported client: {$client}"), - }; - } - - protected function getCommand(OsInfo $osInfo): string - { - return 'ctx'; - } - - protected function getArgs(OsInfo $osInfo, string $projectPath, array $options = []): array - { - $args = ['server']; - - // Only add -c option if project path is explicitly requested - if (isset($options['use_project_path']) && $options['use_project_path']) { - $args[] = '-c'; - $args[] = $projectPath; - } - - return $args; - } -} diff --git a/src/McpServer/Console/McpConfig/Template/MacOsConfigTemplate.php b/src/McpServer/Console/McpConfig/Template/MacOsConfigTemplate.php deleted file mode 100644 index ff150a87..00000000 --- a/src/McpServer/Console/McpConfig/Template/MacOsConfigTemplate.php +++ /dev/null @@ -1,42 +0,0 @@ - $this->generateClaudeConfig($osInfo, $projectPath, $options), - 'generic' => $this->generateGenericConfig($osInfo, $projectPath, $options), - default => throw new \InvalidArgumentException("Unsupported client: {$client}"), - }; - } - - protected function getCommand(OsInfo $osInfo): string - { - return 'ctx'; - } - - protected function getArgs(OsInfo $osInfo, string $projectPath, array $options = []): array - { - $args = ['server']; - - // Only add -c option if project path is explicitly requested - if (isset($options['use_project_path']) && $options['use_project_path']) { - $args[] = '-c'; - $args[] = $projectPath; - } - - return $args; - } -} diff --git a/src/McpServer/Console/McpConfig/Template/WindowsConfigTemplate.php b/src/McpServer/Console/McpConfig/Template/WindowsConfigTemplate.php deleted file mode 100644 index eda1110e..00000000 --- a/src/McpServer/Console/McpConfig/Template/WindowsConfigTemplate.php +++ /dev/null @@ -1,59 +0,0 @@ - $this->generateClaudeConfig($osInfo, $projectPath, $options), - 'generic' => $this->generateGenericConfig($osInfo, $projectPath, $options), - default => throw new \InvalidArgumentException("Unsupported client: {$client}"), - }; - } - - protected function getCommand(OsInfo $osInfo): string - { - // On Windows, we prefer the full path to ctx.exe or just ctx.exe if it's in PATH - return 'ctx.exe'; - } - - protected function getArgs(OsInfo $osInfo, string $projectPath, array $options = []): array - { - $args = ['server']; - - // Only add -c option if project path is explicitly requested - if (isset($options['use_project_path']) && $options['use_project_path']) { - // Convert Unix-style paths to Windows paths if needed - $windowsPath = $this->convertToWindowsPath($projectPath); - $args[] = "-c{$windowsPath}"; - } - - return $args; - } - - private function convertToWindowsPath(string $path): string - { - // Convert forward slashes to backslashes for Windows - $windowsPath = \str_replace('/', '\\', $path); - - // Ensure we have a proper Windows path format - if (!\preg_match('/^[A-Za-z]:/', $windowsPath)) { - // If it doesn't start with a drive letter, assume it's a relative path - // and keep it as-is since it will be resolved relative to current directory - return $windowsPath; - } - - return $windowsPath; - } -} diff --git a/src/McpServer/Console/McpConfig/Template/WslConfigTemplate.php b/src/McpServer/Console/McpConfig/Template/WslConfigTemplate.php deleted file mode 100644 index b3f75730..00000000 --- a/src/McpServer/Console/McpConfig/Template/WslConfigTemplate.php +++ /dev/null @@ -1,96 +0,0 @@ - $this->generateClaudeConfig($osInfo, $projectPath, $options), - 'generic' => $this->generateGenericConfig($osInfo, $projectPath, $options), - default => throw new \InvalidArgumentException("Unsupported client: {$client}"), - }; - } - - protected function getCommand(OsInfo $osInfo): string - { - return 'bash.exe'; - } - - protected function getArgs(OsInfo $osInfo, string $projectPath, array $options = []): array - { - // Build the base command - $bashCommand = 'ctx server'; - - // Only add -c option if project path is explicitly requested - if (isset($options['use_project_path']) && $options['use_project_path']) { - $bashCommand .= " -c {$projectPath}"; - } - - // Handle environment variables by exporting them in the bash command - $env = $this->getEnvironmentVariables($options); - if (!empty($env)) { - $exports = []; - foreach ($env as $key => $value) { - $exports[] = "export {$key}={$value}"; - } - $exportString = \implode(' && ', $exports); - $bashCommand = "{$exportString} && {$bashCommand}"; - } - - return [ - '-c', - $bashCommand, - ]; - } - - #[\Override] - protected function generateClaudeConfig( - OsInfo $osInfo, - string $projectPath, - array $options = [], - ): McpConfig { - $command = $this->getCommand($osInfo); - $args = $this->getArgs($osInfo, $projectPath, $options); - - // For WSL, we don't use the env property in Claude config since - // environment variables are handled within the bash command - $configData = [ - 'mcpServers' => [ - 'ctx' => [ - 'command' => $command, - 'args' => $args, - ], - ], - ]; - - $metadata = [ - 'os_name' => $osInfo->osName, - 'wsl_note' => 'Environment variables are exported within the bash command', - ]; - - if (isset($options['use_project_path']) && $options['use_project_path']) { - $metadata['project_path'] = $projectPath; - } - - return new McpConfig( - clientType: 'claude', - osType: $osInfo->getConfigType(), - configData: $configData, - command: $command, - args: $args, - env: $this->getEnvironmentVariables($options), - metadata: $metadata, - ); - } -} diff --git a/src/McpServer/Console/McpConfigCommand.php b/src/McpServer/Console/McpConfigCommand.php index a538a02e..7fa8345e 100644 --- a/src/McpServer/Console/McpConfigCommand.php +++ b/src/McpServer/Console/McpConfigCommand.php @@ -6,9 +6,9 @@ use Butschster\ContextGenerator\Console\BaseCommand; use Butschster\ContextGenerator\DirectoriesInterface; -use Butschster\ContextGenerator\McpServer\Console\McpConfig\ConfigGeneratorInterface; -use Butschster\ContextGenerator\McpServer\Console\McpConfig\Renderer\McpConfigRenderer; -use Butschster\ContextGenerator\McpServer\Console\McpConfig\Service\OsDetectionService; +use Butschster\ContextGenerator\McpServer\McpConfig\ConfigGeneratorInterface; +use Butschster\ContextGenerator\McpServer\McpConfig\Renderer\McpConfigRenderer; +use Butschster\ContextGenerator\McpServer\McpConfig\Service\OsDetectionService; use Spiral\Console\Attribute\Option; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; diff --git a/src/McpServer/McpConfig.php b/src/McpServer/McpConfig.php deleted file mode 100644 index 949f986c..00000000 --- a/src/McpServer/McpConfig.php +++ /dev/null @@ -1,108 +0,0 @@ - '[{path}] {description}', - 'common_prompts' => [ - 'enable' => true, - ], - 'file_operations' => [ - 'enable' => true, - 'write' => true, - 'apply-patch' => false, - 'directories-list' => true, - ], - 'context_operations' => [ - 'enable' => true, - ], - 'prompt_operations' => [ - 'enable' => true, - ], - 'docs_tools' => [ - 'enable' => false, - ], - 'custom_tools' => [ - 'enable' => true, - 'max_runtime' => 30, - ], - 'git_operations' => [ - 'enable' => true, - ], - 'project_operations' => [ - 'enable' => true, - ], - ]; - - public function getDocumentNameFormat(string $path, string $description, string $tags): string - { - return \str_replace( - ['{path}', '{description}', '{tags}'], - [$path, $description, $tags], - (string) $this->config['document_name_format'] ?: '', - ); - } - - public function isFileOperationsEnabled(): bool - { - return $this->config['file_operations']['enable'] ?? false; - } - - public function isFileWriteEnabled(): bool - { - return $this->config['file_operations']['write'] ?? false; - } - - public function isFileApplyPatchEnabled(): bool - { - return $this->config['file_operations']['apply-patch'] ?? false; - } - - public function isFileDirectoriesListEnabled(): bool - { - return $this->config['file_operations']['directories-list'] ?? false; - } - - public function isContextOperationsEnabled(): bool - { - return $this->config['context_operations']['enable'] ?? false; - } - - public function isPromptOperationsEnabled(): bool - { - return $this->config['prompt_operations']['enable'] ?? false; - } - - public function isDocsToolsEnabled(): bool - { - return $this->config['docs_tools']['enable'] ?? true; - } - - public function commonPromptsEnabled(): bool - { - return $this->config['common_prompts']['enable'] ?? true; - } - - public function isCustomToolsEnabled(): bool - { - return $this->config['custom_tools']['enable'] ?? true; - } - - public function isGitOperationsEnabled(): bool - { - return $this->config['git_operations']['enable'] ?? true; - } - - public function isProjectOperationsEnabled(): bool - { - return $this->config['project_operations']['enable'] ?? true; - } -} diff --git a/src/McpServer/ProjectService/ProjectService.php b/src/McpServer/ProjectService/ProjectService.php deleted file mode 100644 index d0f9965b..00000000 --- a/src/McpServer/ProjectService/ProjectService.php +++ /dev/null @@ -1,156 +0,0 @@ -isEnable()) { - return $payload; - } - - if ($payload instanceof ListToolsResult) { - return new ListToolsResult( - tools: \array_map(fn($item) => $this->processTool($item), $payload->tools), - ); - } - - if ($payload instanceof ListPromptsResult) { - return new ListPromptsResult( - prompts: \array_map(fn($item) => $this->processPrompt($item), $payload->prompts), - ); - } - - if ($payload instanceof ListResourcesResult) { - return new ListResourcesResult( - resources: \array_map(fn(Resource $item) => $this->processResource($item), $payload->resources), - ); - } - - return $payload; - } - - public function processToolRequestParams(CallToolRequestParams $params): CallToolRequestParams - { - if (!$this->isEnable()) { - return $params; - } - - return new CallToolRequestParams( - name: $this->removeToolPostfix($params->name), - arguments: $params->arguments, - _meta: $params->_meta, - ); - } - - public function processPromptRequestParams(GetPromptRequestParams $params): GetPromptRequestParams - { - return $params; - } - - public function processResourceRequestParams(ReadResourceRequestParams $params): ReadResourceRequestParams - { - if (!$this->isEnable()) { - return $params; - } - - return new ReadResourceRequestParams( - uri: $this->removeResourceUriPrefix($params->uri), - _meta: $params->_meta, - ); - } - - private function addProjectSignature(string $description): string - { - return 'Important ONLY for project "' . $this->projectName . '". ' . $description; - } - - private function processTool(Tool $tool): Tool - { - return new Tool( - name: $this->addToolPostfix($tool->name), - inputSchema: $tool->inputSchema, - description: $this->addProjectSignature($tool->description), - ); - } - - private function processPrompt(Prompt $item): Prompt - { - return new Prompt( - name: $item->name, - description: $this->addProjectSignature($item->description), - arguments: $item->arguments, - ); - } - - private function processResource(Resource $item): Resource - { - return new Resource( - name: $this->addResourceNamePrefix($item->name), - uri: $this->addResourceUriPrefix($item->uri), - description: $this->addProjectSignature($item->description), - mimeType: $item->mimeType, - annotations: $item->annotations, - ); - } - - private function isEnable(): bool - { - return $this->projectName !== null; - } - - private function postfix(): string - { - return '_from_' . $this->projectSlug; - } - - private function addToolPostfix(string $name = ''): string - { - return $name . $this->postfix(); - } - - private function removeToolPostfix(string $name): string - { - return \str_replace($this->postfix(), '', $name); - } - - private function addResourceNamePrefix(string $name): string - { - return "[{$this->projectName}] " . $name; - } - - private function uriPrefix(): string - { - return $this->projectSlug . '://'; - } - - private function addResourceUriPrefix(string $uri): string - { - return \str_replace(self::URI_CTX_PREFIX, $this->uriPrefix(), $uri); - } - - private function removeResourceUriPrefix(string $uri): string - { - return \str_replace($this->uriPrefix(), self::URI_CTX_PREFIX, $uri); - } -} diff --git a/src/McpServer/ProjectService/ProjectServiceFactory.php b/src/McpServer/ProjectService/ProjectServiceFactory.php deleted file mode 100644 index 2887dfd4..00000000 --- a/src/McpServer/ProjectService/ProjectServiceFactory.php +++ /dev/null @@ -1,28 +0,0 @@ -env->get('MCP_PROJECT_NAME'); - $projectSlug = $this->env->get('MCP_PROJECT_SLUG'); - if ($projectName !== null && $projectSlug === null) { - $projectSlug = \preg_replace('#[^a-z0-9]+#i', '', (string) $projectName); - } - - return new ProjectService( - projectName: $projectName, - projectSlug: $projectSlug, - ); - } -} diff --git a/src/McpServer/ProjectService/ProjectServiceInterface.php b/src/McpServer/ProjectService/ProjectServiceInterface.php deleted file mode 100644 index ec1dd981..00000000 --- a/src/McpServer/ProjectService/ProjectServiceInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -configFile !== null; - } - - public function hasEnvFile(): bool - { - return $this->envFile !== null; - } - - /** - * Get the configuration file path - */ - public function getConfigFile(): ?string - { - if ($this->configFile === null) { - return null; - } - - return (string) FSPath::create($this->path)->join($this->configFile); - } - - /** - * Get the .env file path - */ - public function getEnvFile(): ?string - { - if ($this->envFile === null) { - return null; - } - - return (string) FSPath::create($this->path)->join($this->envFile); - } - - /** - * Convert to array representation - */ - public function jsonSerialize(): array - { - return [ - 'path' => (string) FSPath::create($this->path), - 'config_file' => $this->configFile ? (string) FSPath::create($this->configFile) : null, - 'env_file' => $this->envFile ? (string) FSPath::create($this->envFile) : null, - ]; - } -} diff --git a/src/McpServer/Projects/DTO/ProjectDTO.php b/src/McpServer/Projects/DTO/ProjectDTO.php deleted file mode 100644 index 55bb5ba2..00000000 --- a/src/McpServer/Projects/DTO/ProjectDTO.php +++ /dev/null @@ -1,51 +0,0 @@ -toString(), - addedAt: $data['added_at'] ?? \date('Y-m-d H:i:s'), - configFile: $data['config_file'] ?? null, - envFile: $data['env_file'] ?? null, - ); - } - - /** - * Convert to array representation - */ - public function jsonSerialize(): array - { - return [ - 'added_at' => $this->addedAt, - 'config_file' => $this->configFile, - 'env_file' => $this->envFile, - ]; - } -} diff --git a/src/McpServer/Projects/DTO/ProjectStateDTO.php b/src/McpServer/Projects/DTO/ProjectStateDTO.php deleted file mode 100644 index 9391939f..00000000 --- a/src/McpServer/Projects/DTO/ProjectStateDTO.php +++ /dev/null @@ -1,91 +0,0 @@ - $projects Collection of all projects indexed by path - * @param array $aliases Project aliases mapping (alias => path) - */ - public function __construct( - public ?CurrentProjectDTO $currentProject = null, - public array $projects = [], - public array $aliases = [], - ) {} - - /** - * Create from array data (typically loaded from storage) - */ - public static function fromArray(array $data): self - { - // Process current project - $currentProject = CurrentProjectDTO::fromArray($data['current_project'] ?? null); - - // Process projects - $projects = []; - foreach ($data['projects'] ?? [] as $path => $projectData) { - $path = FSPath::create($path)->toString(); - $projects[$path] = ProjectDTO::fromArray($path, $projectData); - } - - // Load aliases - $aliases = $data['aliases'] ?? []; - - return new self( - currentProject: $currentProject, - projects: $projects, - aliases: $aliases, - ); - } - - /** - * Get aliases for a specific project path - * - * @return string[] - */ - public function getAliasesForPath(string $projectPath): array - { - $projectPath = FSPath::create($projectPath)->toString(); - - $result = []; - - foreach ($this->aliases as $alias => $path) { - if ($path === $projectPath) { - $result[] = $alias; - } - } - - return $result; - } - - /** - * Resolve a path or alias to the actual project path - */ - public function resolvePathOrAlias(string $pathOrAlias): string - { - return $this->aliases[$pathOrAlias] ?? FSPath::create($pathOrAlias)->toString(); - } - - public function jsonSerialize(): array - { - $projects = []; - foreach ($this->projects as $path => $project) { - $projects[$path] = $project; - } - - return [ - 'current_project' => $this->currentProject, - 'projects' => $projects, - 'aliases' => $this->aliases, - ]; - } -} diff --git a/src/McpServer/Projects/McpProjectsBootloader.php b/src/McpServer/Projects/McpProjectsBootloader.php deleted file mode 100644 index a288c157..00000000 --- a/src/McpServer/Projects/McpProjectsBootloader.php +++ /dev/null @@ -1,41 +0,0 @@ - static fn( - FactoryInterface $factory, - DirectoriesInterface $dirs, - ) => $factory->make(ProjectStateRepository::class, [ - 'stateDirectory' => $dirs->get('global-state'), - ]), - ProjectServiceInterface::class => ProjectService::class, - ]; - } - - public function init(ConsoleBootloader $console): void - { - $console->addCommand( - ProjectCommand::class, - ProjectAddCommand::class, - ProjectListCommand::class, - ); - } -} diff --git a/src/McpServer/Projects/ProjectService.php b/src/McpServer/Projects/ProjectService.php deleted file mode 100644 index 5d2428be..00000000 --- a/src/McpServer/Projects/ProjectService.php +++ /dev/null @@ -1,190 +0,0 @@ -getState(); - $currentProject = $state->currentProject; - - if ($currentProject === null) { - return null; - } - - // Verify the project path still exists - if (!$this->files->exists($currentProject->path)) { - return null; - } - - return $currentProject; - } - - public function setCurrentProject( - string $projectPath, - ?string $alias = null, - ?string $configFile = null, - ?string $envFile = null, - ): void { - $state = $this->getState(); - - // Set current project - $state->currentProject = new CurrentProjectDTO( - path: $projectPath, - configFile: $configFile, - envFile: $envFile, - ); - - // Register the alias if provided - if ($alias !== null) { - $state->aliases[$alias] = $projectPath; - } - - // Add to projects list if not already there - if (!isset($state->projects[$projectPath])) { - $state->projects[$projectPath] = new ProjectDTO( - path: $projectPath, - addedAt: \date('Y-m-d H:i:s'), - configFile: $configFile, - envFile: $envFile, - ); - } else { - // Update existing project if config file or env file provided - $existingProject = $state->projects[$projectPath]; - $needsUpdate = ($configFile !== null && $existingProject->configFile !== $configFile) || - ($envFile !== null && $existingProject->envFile !== $envFile); - - if ($needsUpdate) { - $state->projects[$projectPath] = new ProjectDTO( - path: $projectPath, - addedAt: $existingProject->addedAt, - configFile: $configFile ?? $existingProject->configFile, - envFile: $envFile ?? $existingProject->envFile, - ); - } - } - - $this->saveState($state); - } - - public function switchToProject(string $projectPath): bool - { - $state = $this->getState(); - - // Check if project exists - if (!isset($state->projects[$projectPath])) { - return false; - } - - // Get existing project details - $projectInfo = $state->projects[$projectPath]; - - // Switch to the project without changing its configuration - $state->currentProject = new CurrentProjectDTO( - path: $projectPath, - configFile: $projectInfo->configFile, - envFile: $projectInfo->envFile, - ); - - $this->saveState($state); - return true; - } - - public function addProject( - string $projectPath, - ?string $alias = null, - ?string $configFile = null, - ?string $envFile = null, - ): void { - $state = $this->getState(); - - // Register the alias if provided - if ($alias !== null) { - $state->aliases[$alias] = $projectPath; - } - - // Add to projects list if not already there - if (!isset($state->projects[$projectPath])) { - $state->projects[$projectPath] = new ProjectDTO( - path: $projectPath, - addedAt: \date('Y-m-d H:i:s'), - configFile: $configFile, - envFile: $envFile, - ); - } else { - // Update existing project - $existingProject = $state->projects[$projectPath]; - $state->projects[$projectPath] = new ProjectDTO( - path: $projectPath, - addedAt: $existingProject->addedAt, - configFile: $configFile ?? $existingProject->configFile, - envFile: $envFile ?? $existingProject->envFile, - ); - } - - // if current project is set and matches the new project path, update it - if ($this->getCurrentProject()?->path === $projectPath) { - $this->switchToProject($projectPath); - } - - $this->saveState($state); - } - - public function getProjects(): array - { - return $this->getState()->projects; - } - - public function getAliases(): array - { - return $this->getState()->aliases; - } - - public function getAliasesForPath(string $projectPath): array - { - return $this->getState()->getAliasesForPath($projectPath); - } - - public function resolvePathOrAlias(string $pathOrAlias): string - { - return $this->getState()->resolvePathOrAlias($pathOrAlias); - } - - /** - * Get the current project state, loading from storage if necessary - */ - private function getState(): ProjectStateDTO - { - if ($this->state === null) { - $this->state = $this->repository->load(); - } - - return $this->state; - } - - /** - * Save the project state to disk - */ - private function saveState(ProjectStateDTO $state): void - { - $this->state = $state; - $this->repository->save($state); - } -} diff --git a/src/McpServer/Projects/ProjectServiceInterface.php b/src/McpServer/Projects/ProjectServiceInterface.php deleted file mode 100644 index ea396cdb..00000000 --- a/src/McpServer/Projects/ProjectServiceInterface.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ - public function getProjects(): array; - - /** - * Get all project aliases - * - * @return array Alias to path mapping - */ - public function getAliases(): array; - - /** - * Get aliases for a specific project path - * - * @return string[] - */ - public function getAliasesForPath(string $projectPath): array; - - /** - * Resolve a path or alias to the actual project path - */ - public function resolvePathOrAlias(string $pathOrAlias): string; -} diff --git a/src/McpServer/Projects/Repository/ProjectStateRepository.php b/src/McpServer/Projects/Repository/ProjectStateRepository.php deleted file mode 100644 index c779893c..00000000 --- a/src/McpServer/Projects/Repository/ProjectStateRepository.php +++ /dev/null @@ -1,85 +0,0 @@ -getStateFilePath(); - - if (!$this->files->exists($stateFile)) { - return new ProjectStateDTO(); - } - - try { - $content = $this->files->read($stateFile); - $stateArray = \json_decode($content, true, 512, JSON_THROW_ON_ERROR); - - if (!\is_array($stateArray)) { - throw new \RuntimeException('Invalid state file format'); - } - - return ProjectStateDTO::fromArray($stateArray); - } catch (\Throwable $e) { - $this->logger->error('Failed to load project state', [ - 'error' => $e->getMessage(), - 'file' => $stateFile, - ]); - - // Return empty state on error - return new ProjectStateDTO(); - } - } - - public function save(ProjectStateDTO $state): bool - { - $stateFile = $this->getStateFilePath(); - - try { - $content = \json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - $this->files->ensureDirectory(\dirname($stateFile)); - $this->files->write($stateFile, $content); - - return true; - } catch (\Throwable $e) { - $this->logger->error('Failed to save project state', [ - 'error' => $e->getMessage(), - 'file' => $stateFile, - ]); - - return false; - } - } - - /** - * Get the path to the state file - */ - private function getStateFilePath(): string - { - return (string) FSPath::create($this->stateDirectory)->join(self::STATE_FILE); - } -} diff --git a/src/McpServer/Projects/Repository/ProjectStateRepositoryInterface.php b/src/McpServer/Projects/Repository/ProjectStateRepositoryInterface.php deleted file mode 100644 index 4383e0ea..00000000 --- a/src/McpServer/Projects/Repository/ProjectStateRepositoryInterface.php +++ /dev/null @@ -1,20 +0,0 @@ -validateMessageConfig($messageConfig); - - if (!isset($messageConfig['file']) || !\is_string($messageConfig['file'])) { - throw new PromptParsingException('Message must have a valid file property'); - } - - // Validate that both content and file are not specified - if (isset($messageConfig['content'])) { - throw new PromptParsingException('Message cannot have both content and file properties'); - } - - $filePath = $messageConfig['file']; - - if (empty(\trim($filePath))) { - throw new PromptParsingException('File path cannot be empty'); - } - - // Find appropriate provider - $provider = $this->findProvider($filePath); - - if ($provider === null) { - throw new PromptParsingException( - \sprintf('No suitable provider found for file source: %s', $filePath), - ); - } - - try { - $this->logger->debug('Loading message content from file', [ - 'source' => $filePath, - 'provider' => $provider::class, - ]); - - $content = $provider->load($filePath); - - $this->logger->debug('Successfully loaded message content', [ - 'source' => $filePath, - 'contentLength' => \strlen($content), - ]); - - return $content; - } catch (FileMessageContentException $e) { - $this->logger->error('Failed to load message content from file', [ - 'source' => $filePath, - 'error' => $e->getMessage(), - ]); - - throw new PromptParsingException( - \sprintf('Failed to load content from "%s": %s', $filePath, $e->getMessage()), - previous: $e, - ); - } - } - - /** - * Finds the appropriate provider for the given file path. - */ - private function findProvider(string $filePath): ?FileContentProvider - { - // Try providers in order - foreach ($this->providers as $provider) { - if ($provider->canHandle($filePath)) { - return $provider; - } - } - - return null; - } -} diff --git a/src/McpServer/Prompt/Content/LocalFileContentProvider.php b/src/McpServer/Prompt/Content/LocalFileContentProvider.php deleted file mode 100644 index 680e11a7..00000000 --- a/src/McpServer/Prompt/Content/LocalFileContentProvider.php +++ /dev/null @@ -1,67 +0,0 @@ -resolveFilePath($source); - - if (!$filePath->exists()) { - throw FileMessageContentException::fileNotFound($filePath->toString()); - } - - if (!$filePath->isFile()) { - throw FileMessageContentException::fileNotReadable($filePath->toString()); - } - - if (!\is_readable($filePath->toString())) { - throw FileMessageContentException::fileNotReadable($filePath->toString()); - } - - $content = $this->files->read($filePath->toString()); - - if (empty(\trim($content))) { - throw FileMessageContentException::emptyContent($source); - } - - return $content; - } - - /** - * Resolves the file path using FSPath for robust path handling. - */ - private function resolveFilePath(string $source): FSPath - { - $sourcePath = FSPath::create($source); - - // If absolute path, use as-is - if ($sourcePath->isAbsolute()) { - return $sourcePath; - } - - // Resolve relative to root path - return $this->rootPath->join($source); - } -} diff --git a/src/McpServer/Prompt/Content/MessageContentLoader.php b/src/McpServer/Prompt/Content/MessageContentLoader.php deleted file mode 100644 index 19edabb3..00000000 --- a/src/McpServer/Prompt/Content/MessageContentLoader.php +++ /dev/null @@ -1,37 +0,0 @@ -validateMessageConfig($messageConfig); - - if (!isset($messageConfig['content']) || !\is_string($messageConfig['content'])) { - throw new PromptParsingException('Message must have a valid content property'); - } - - return $messageConfig['content']; - } -} diff --git a/src/McpServer/Prompt/Content/UrlFileContentProvider.php b/src/McpServer/Prompt/Content/UrlFileContentProvider.php deleted file mode 100644 index ff63f503..00000000 --- a/src/McpServer/Prompt/Content/UrlFileContentProvider.php +++ /dev/null @@ -1,62 +0,0 @@ -httpClient->getWithRedirects($source, [ - 'User-Agent' => 'ContextGenerator/1.0', - 'Accept' => 'text/plain, text/markdown, text/*, */*', - ]); - - if (!$response->isSuccess()) { - throw FileMessageContentException::urlLoadFailed( - $source, - \sprintf('HTTP %d', $response->getStatusCode()), - ); - } - - $content = $response->getBody(); - - if (empty(\trim($content))) { - throw FileMessageContentException::emptyContent($source); - } - - return $content; - } catch (HttpException $e) { - throw FileMessageContentException::urlLoadFailed($source, $e->getMessage()); - } - } -} diff --git a/src/McpServer/Prompt/Exception/FileMessageContentException.php b/src/McpServer/Prompt/Exception/FileMessageContentException.php deleted file mode 100644 index 6db1612b..00000000 --- a/src/McpServer/Prompt/Exception/FileMessageContentException.php +++ /dev/null @@ -1,36 +0,0 @@ -id, - prompt: $this->prompt, - messages: $messages, - type: $this->type, - extensions: $this->extensions, - tags: $this->tags, - ); - } - - public function jsonSerialize(): array - { - $schema = [ - 'properties' => [], - 'required' => [], - ]; - - foreach ($this->prompt->arguments as $argument) { - $schema['properties'][$argument->name] = [ - 'description' => $argument->description, - ]; - - if ($argument->required) { - $schema['required'][] = $argument->name; - } - } - - return \array_filter([ - 'id' => $this->id, - 'type' => $this->type->value, - 'description' => $this->prompt->description, - 'schema' => $schema, - 'messages' => $this->messages, - 'extend' => $this->serializeExtensions(), - 'tags' => $this->tags, - ], static fn($value) => $value !== null && $value !== []); - } - - /** - * Serializes the extensions for JSON output. - * - * @return array|null The serialized extensions or null if empty - */ - private function serializeExtensions(): ?array - { - if (empty($this->extensions)) { - return null; - } - - // Convert extensions to the format used in configuration - return \array_map(static function (PromptExtension $ext) { - $args = []; - foreach ($ext->arguments as $arg) { - $args[$arg->name] = $arg->value; - } - return ['id' => $ext->templateId, 'arguments' => $args]; - }, $this->extensions); - } -} diff --git a/src/McpServer/Prompt/Extension/PromptExtension.php b/src/McpServer/Prompt/Extension/PromptExtension.php deleted file mode 100644 index 6c305a92..00000000 --- a/src/McpServer/Prompt/Extension/PromptExtension.php +++ /dev/null @@ -1,49 +0,0 @@ - $config The extension configuration - * @return self The created PromptExtension - * @throws \InvalidArgumentException If the configuration is invalid - */ - public static function fromArray(array $config): self - { - if (empty($config['id']) || !\is_string($config['id'])) { - throw new \InvalidArgumentException('Extension must have a template ID'); - } - - $arguments = []; - if (isset($config['arguments']) && \is_array($config['arguments'])) { - foreach ($config['arguments'] as $name => $value) { - if (!\is_string($name) || !\is_string($value)) { - throw new \InvalidArgumentException( - \sprintf('Extension argument "%s" must have a string value', $name), - ); - } - - $arguments[] = new PromptExtensionArgument($name, $value); - } - } - - return new self($config['id'], $arguments); - } -} diff --git a/src/McpServer/Prompt/Extension/PromptExtensionArgument.php b/src/McpServer/Prompt/Extension/PromptExtensionArgument.php deleted file mode 100644 index febdf9af..00000000 --- a/src/McpServer/Prompt/Extension/PromptExtensionArgument.php +++ /dev/null @@ -1,16 +0,0 @@ - The variables from extension arguments - */ - private array $variables; - - /** - * @param PromptExtensionArgument[] $arguments The extension arguments - */ - public function __construct(array $arguments) - { - $this->variables = $this->createVariablesFromArguments($arguments); - } - - public function has(string $name): bool - { - return \array_key_exists($name, $this->variables); - } - - public function get(string $name): ?string - { - return $this->variables[$name] ?? null; - } - - /** - * Creates a variables map from extension arguments. - * - * @param PromptExtensionArgument[] $arguments The extension arguments - * @return array The variables map - */ - private function createVariablesFromArguments(array $arguments): array - { - $variables = []; - - foreach ($arguments as $argument) { - $variables[$argument->name] = $argument->value; - } - - return $variables; - } -} diff --git a/src/McpServer/Prompt/Extension/TemplateResolver.php b/src/McpServer/Prompt/Extension/TemplateResolver.php deleted file mode 100644 index f57bdf03..00000000 --- a/src/McpServer/Prompt/Extension/TemplateResolver.php +++ /dev/null @@ -1,149 +0,0 @@ -extensions)) { - return $prompt; - } - - // Process each extension - $messages = $prompt->messages; - $processedExtensions = []; - - foreach ($prompt->extensions as $extension) { - // Prevent circular dependencies - if (\in_array($extension->templateId, $processedExtensions, true)) { - continue; - } - - $processedExtensions[] = $extension->templateId; - - // Get the template - try { - $template = $this->promptProvider->get($extension->templateId); - } catch (\InvalidArgumentException $e) { - throw new TemplateResolutionException( - \sprintf('Template "%s" not found', $extension->templateId), - previous: $e, - ); - } - - // Resolve nested templates first - if (!empty($template->extensions)) { - $template = $this->resolve($template); - } - - // Apply variable substitution - $messages = $this->mergeMessages( - $messages, - $template->messages, - $extension->arguments, - ); - } - - // Create a new prompt with the resolved messages - return new PromptDefinition( - id: $prompt->id, - prompt: $prompt->prompt, - messages: $messages, - type: $prompt->type, - extensions: $prompt->extensions, - ); - } - - /** - * Merges messages from a template with the prompt's messages, applying variable substitution. - * - * @param PromptMessage[] $promptMessages The prompt's messages - * @param PromptMessage[] $templateMessages The template's messages - * @param PromptExtensionArgument[] $arguments The variables to substitute - * @return PromptMessage[] The merged messages - */ - private function mergeMessages(array $promptMessages, array $templateMessages, array $arguments): array - { - // If the prompt has no messages, use the template's messages with substitution - if (empty($promptMessages)) { - return $this->substituteMessages($templateMessages, $arguments); - } - - foreach ($this->substituteMessages($templateMessages, $arguments) as $templateMessage) { - $promptMessages[] = $templateMessage; - } - - // Otherwise, keep the prompt's messages (extensions just provide structure) - return $promptMessages; - } - - /** - * Applies variable substitution to template messages. - * - * @param PromptMessage[] $messages The messages to process - * @param PromptExtensionArgument[] $arguments The variables to substitute - * @return PromptMessage[] The processed messages - */ - private function substituteMessages(array $messages, array $arguments): array - { - $result = []; - - // Create a variable provider for the extension arguments - $variableProvider = new PromptExtensionVariableProvider($arguments); - - // Create a resolver with the variable provider - $resolver = $this->container - ->get(VariableResolver::class) - ->with(new VariableReplacementProcessor($variableProvider, $this->logger)); - - foreach ($messages as $message) { - \assert($message->content instanceof TextContent); - $content = $message->content->text; - $substitutedContent = $resolver->resolve($content); - - $this->logger?->debug('Template message processed', [ - 'original' => $content, - 'resolved' => $substitutedContent, - ]); - - // Create a new message with the substituted content - $result[] = new PromptMessage( - role: $message->role, - content: new TextContent(text: $substitutedContent), - ); - } - - return $result; - } -} diff --git a/src/McpServer/Prompt/Filter/FilterStrategy.php b/src/McpServer/Prompt/Filter/FilterStrategy.php deleted file mode 100644 index de8e5111..00000000 --- a/src/McpServer/Prompt/Filter/FilterStrategy.php +++ /dev/null @@ -1,26 +0,0 @@ - self::ALL, - default => self::ANY, - }; - } -} diff --git a/src/McpServer/Prompt/Filter/PromptFilterFactory.php b/src/McpServer/Prompt/Filter/PromptFilterFactory.php deleted file mode 100644 index b6e010ad..00000000 --- a/src/McpServer/Prompt/Filter/PromptFilterFactory.php +++ /dev/null @@ -1,89 +0,0 @@ -|null $filterConfig The filter configuration - * @return PromptFilterInterface|null The created filter, or null if no filter needed - */ - public function createFromConfig(?array $filterConfig): ?PromptFilterInterface - { - // If no filter config, return null (no filtering) - if (empty($filterConfig)) { - return null; - } - - $filters = []; - - // Create ID filter if IDs specified - if (isset($filterConfig['ids']) && \is_array($filterConfig['ids'])) { - $ids = $this->extractStringValues($filterConfig['ids']); - if (!empty($ids)) { - $filters[] = new IdPromptFilter($ids); - } - } - - // Create tag filter if tags specified - if (isset($filterConfig['tags']) && \is_array($filterConfig['tags'])) { - $includeTags = []; - $excludeTags = []; - - // Extract include tags - if (isset($filterConfig['tags']['include']) && \is_array($filterConfig['tags']['include'])) { - $includeTags = $this->extractStringValues($filterConfig['tags']['include']); - } - - // Extract exclude tags - if (isset($filterConfig['tags']['exclude']) && \is_array($filterConfig['tags']['exclude'])) { - $excludeTags = $this->extractStringValues($filterConfig['tags']['exclude']); - } - - // Create tag filter if any tags specified - if (!empty($includeTags) || !empty($excludeTags)) { - $strategy = FilterStrategy::fromString($filterConfig['tags']['match'] ?? null); - $filters[] = new TagPromptFilter($includeTags, $excludeTags, $strategy); - } - } - - // If no filters created, return null - if (empty($filters)) { - return null; - } - - // If only one filter, return it directly - if (\count($filters) === 1) { - return $filters[0]; - } - - // Otherwise, create a composite filter - $strategy = FilterStrategy::fromString($filterConfig['match'] ?? null); - return new CompositePromptFilter($filters, $strategy); - } - - /** - * Extracts string values from an array, skipping non-string values. - * - * @param array $values The values to extract strings from - * @return array The extracted string values - */ - private function extractStringValues(array $values): array - { - $result = []; - foreach ($values as $value) { - if (\is_string($value)) { - $result[] = $value; - } - } - return $result; - } -} diff --git a/src/McpServer/Prompt/Filter/PromptFilterInterface.php b/src/McpServer/Prompt/Filter/PromptFilterInterface.php deleted file mode 100644 index 32f0e5a5..00000000 --- a/src/McpServer/Prompt/Filter/PromptFilterInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -filters)) { - return true; - } - - return match ($this->strategy) { - FilterStrategy::ALL => $this->allFiltersMatch($promptConfig), - FilterStrategy::ANY => $this->anyFilterMatches($promptConfig), - }; - } - - /** - * Checks if all filters match (AND logic). - */ - private function allFiltersMatch(array $promptConfig): bool - { - foreach ($this->filters as $filter) { - if (!$filter->shouldInclude($promptConfig)) { - return false; - } - } - - return true; - } - - /** - * Checks if any filter matches (OR logic). - */ - private function anyFilterMatches(array $promptConfig): bool - { - foreach ($this->filters as $filter) { - if ($filter->shouldInclude($promptConfig)) { - return true; - } - } - - return false; - } -} diff --git a/src/McpServer/Prompt/Filter/Strategy/IdPromptFilter.php b/src/McpServer/Prompt/Filter/Strategy/IdPromptFilter.php deleted file mode 100644 index dd9537e7..00000000 --- a/src/McpServer/Prompt/Filter/Strategy/IdPromptFilter.php +++ /dev/null @@ -1,36 +0,0 @@ -ids)) { - return true; - } - - // If prompt has no ID, exclude it - if (!isset($promptConfig['id']) || !\is_string($promptConfig['id'])) { - return false; - } - - // Include if the prompt ID is in the list - return \in_array($promptConfig['id'], $this->ids, true); - } -} diff --git a/src/McpServer/Prompt/Filter/Strategy/TagPromptFilter.php b/src/McpServer/Prompt/Filter/Strategy/TagPromptFilter.php deleted file mode 100644 index fec02f37..00000000 --- a/src/McpServer/Prompt/Filter/Strategy/TagPromptFilter.php +++ /dev/null @@ -1,108 +0,0 @@ -includeTags) && empty($this->excludeTags)) { - return true; - } - - // Get prompt tags - $promptTags = $this->getPromptTags($promptConfig); - - // If excluded tags match, exclude the prompt - if (!empty($this->excludeTags) && !empty($promptTags)) { - foreach ($this->excludeTags as $excludeTag) { - if (\in_array($excludeTag, $promptTags, true)) { - return false; - } - } - } - - // If no include tags specified, include the prompt - if (empty($this->includeTags)) { - return true; - } - - // If prompt has no tags but include tags are specified, exclude it - if (empty($promptTags)) { - return false; - } - - // Check if prompt tags match include tags based on strategy - return match ($this->strategy) { - FilterStrategy::ALL => $this->allTagsMatch($promptTags), - FilterStrategy::ANY => $this->anyTagMatches($promptTags), - }; - } - - /** - * Checks if all include tags are present in the prompt tags. - */ - private function allTagsMatch(array $promptTags): bool - { - foreach ($this->includeTags as $includeTag) { - if (!\in_array($includeTag, $promptTags, true)) { - return false; - } - } - - return true; - } - - /** - * Checks if any include tag is present in the prompt tags. - */ - private function anyTagMatches(array $promptTags): bool - { - foreach ($this->includeTags as $includeTag) { - if (\in_array($includeTag, $promptTags, true)) { - return true; - } - } - - return false; - } - - /** - * Gets the tags from a prompt configuration. - */ - private function getPromptTags(array $promptConfig): array - { - if (!isset($promptConfig['tags']) || !\is_array($promptConfig['tags'])) { - return []; - } - - $tags = []; - foreach ($promptConfig['tags'] as $tag) { - if (\is_string($tag)) { - $tags[] = $tag; - } - } - - return $tags; - } -} diff --git a/src/McpServer/Prompt/McpPromptBootloader.php b/src/McpServer/Prompt/McpPromptBootloader.php deleted file mode 100644 index 66ac012b..00000000 --- a/src/McpServer/Prompt/McpPromptBootloader.php +++ /dev/null @@ -1,69 +0,0 @@ - PromptRegistry::class, - PromptProviderInterface::class => PromptRegistry::class, - PromptRegistry::class => PromptRegistry::class, - PromptConfigFactory::class => static fn( - TextMessageContentLoader $textMessageLoader, - FileMessageContentLoader $fileMessageLoader, - ) => new PromptConfigFactory( - contentLoaders: [ - $textMessageLoader, - $fileMessageLoader, - ], - ), - FileMessageContentLoader::class => static fn( - FilesInterface $files, - LoggerInterface $logger, - HttpClientInterface $httpClient, - DirectoriesInterface $dirs, - ) => new FileMessageContentLoader( - logger: $logger, - providers: [ - new Content\LocalFileContentProvider( - files: $files, - rootPath: $dirs->getRootPath(), - ), - new Content\UrlFileContentProvider( - httpClient: $httpClient, - ), - ], - ), - ]; - } - - public function init( - ConfigLoaderBootloader $configLoader, - PromptParserPlugin $parserPlugin, - PromptConfigMerger $promptConfigMerger, - ConsoleBootloader $console, - ): void { - $configLoader->registerParserPlugin($parserPlugin); - $configLoader->registerMerger($promptConfigMerger); - - $console->addCommand(ListPromptsCommand::class); - $console->addCommand(ShowPromptCommand::class); - } -} diff --git a/src/McpServer/Prompt/PromptConfigFactory.php b/src/McpServer/Prompt/PromptConfigFactory.php deleted file mode 100644 index d346fbb4..00000000 --- a/src/McpServer/Prompt/PromptConfigFactory.php +++ /dev/null @@ -1,301 +0,0 @@ - $config The prompt configuration - * @throws PromptParsingException If the configuration is invalid - */ - public function createFromConfig(array $config): PromptDefinition - { - // Validate required fields - if (empty($config['id']) || !\is_string($config['id'])) { - throw new PromptParsingException('Prompt must have a non-empty id'); - } - - // Create arguments from schema if provided - $arguments = $this->createArgumentsFromSchema($config['schema'] ?? null); - - // Parse messages - $messages = []; - if (isset($config['messages']) && \is_array($config['messages'])) { - // We store messages in a separate property in the registry - // but don't add them to the Prompt DTO directly - $messages = $this->parseMessages($config['messages']); - } - - // Determine prompt type - $type = PromptType::fromString($config['type'] ?? null); - - // Parse extensions if provided - $extensions = []; - if (isset($config['extend']) && \is_array($config['extend'])) { - $extensions = $this->parseExtensions($config['extend']); - } - - // Parse tags if provided - $tags = []; - if (isset($config['tags']) && \is_array($config['tags'])) { - $tags = $this->parseTags($config['tags']); - } - - // Validate that prompts have instructions (messages or extensions) - // Templates can have empty messages as they may be just structural - if ($type === PromptType::Prompt && empty($messages) && empty($extensions)) { - throw new PromptParsingException( - \sprintf('Prompt "%s" must have either messages or extend a template', $config['id']), - ); - } - - // Templates should have messages to be useful for extension - if ($type === PromptType::Template && empty($messages)) { - throw new PromptParsingException( - \sprintf('Template "%s" must have messages to be extended by prompts', $config['id']), - ); - } - - return new PromptDefinition( - id: $config['id'], - prompt: new Prompt( - name: $config['id'], - description: $config['description'] ?? null, - arguments: $arguments, - ), - messages: $messages, - type: $type, - extensions: $extensions, - tags: $tags, - ); - } - - /** - * Parses tags from configuration. - * - * @param array $tagsConfig The tags configuration - * @return array The parsed tags - * @throws PromptParsingException If the tags configuration is invalid - */ - private function parseTags(array $tagsConfig): array - { - $tags = []; - - foreach ($tagsConfig as $index => $tag) { - if (!\is_string($tag)) { - throw new PromptParsingException( - \sprintf( - 'Tag at index %d must be a string', - $index, - ), - ); - } - - // Add the tag if it's not empty - if (!empty($tag)) { - $tags[] = $tag; - } - } - - return $tags; - } - - /** - * Parses extension configurations. - * - * @param array $extensionConfigs The extension configurations - * @return array The parsed extensions - * @throws PromptParsingException If the extension configuration is invalid - */ - private function parseExtensions(array $extensionConfigs): array - { - if (empty($extensionConfigs)) { - return []; - } - - $extensions = []; - - foreach ($extensionConfigs as $index => $extensionConfig) { - if (!\is_array($extensionConfig)) { - throw new PromptParsingException( - \sprintf( - 'Extension at index %d must be an array', - $index, - ), - ); - } - - try { - $extensions[] = PromptExtension::fromArray($extensionConfig); - } catch (\InvalidArgumentException $e) { - throw new PromptParsingException( - \sprintf( - 'Invalid extension at index %d: %s', - $index, - $e->getMessage(), - ), - previous: $e, - ); - } - } - - return $extensions; - } - - /** - * Creates PromptArgument objects from a JSON schema. - * - * @param array|null $schema The JSON schema - * @return array The created arguments - */ - private function createArgumentsFromSchema(?array $schema): array - { - if (empty($schema)) { - return []; - } - - $arguments = []; - - // Process properties from schema - if (isset($schema['properties']) && \is_array($schema['properties'])) { - foreach ($schema['properties'] as $name => $property) { - $required = false; - - // Check if this property is required - if (isset($schema['required']) && \is_array($schema['required'])) { - $required = \in_array($name, $schema['required'], true); - } - - $description = $property['description'] ?? null; - - $arguments[] = new PromptArgument( - name: $name, - description: $description, - required: $required, - ); - } - } - - return $arguments; - } - - /** - * Parses message configurations into PromptMessage objects. - * - * @param array $messagesConfig The messages configuration - * @return array The parsed messages - * @throws PromptParsingException If the message configuration is invalid - */ - private function parseMessages(array $messagesConfig): array - { - if (empty($messagesConfig)) { - return []; - } - - $messages = []; - - foreach ($messagesConfig as $index => $messageConfig) { - if (!\is_array($messageConfig)) { - throw new PromptParsingException( - \sprintf( - 'Message at index %d must be an array', - $index, - ), - ); - } - - try { - $messages[] = $this->parseMessage($messageConfig, $index); - } catch (PromptParsingException $e) { - throw new PromptParsingException( - \sprintf( - 'Invalid message at index %d: %s', - $index, - $e->getMessage(), - ), - previous: $e, - ); - } - } - - return $messages; - } - - /** - * Parses a single message configuration into a PromptMessage object. - * - * @param array $messageConfig The message configuration - * @param int $index The message index (for error reporting) - * @return PromptMessage The parsed message - * @throws PromptParsingException If the message configuration is invalid - */ - private function parseMessage(array $messageConfig, int $index): PromptMessage - { - if (!isset($messageConfig['role']) || !\is_string($messageConfig['role'])) { - throw new PromptParsingException('Message must have a valid role'); - } - - try { - $role = Role::from($messageConfig['role']); - } catch (\ValueError) { - throw new PromptParsingException( - \sprintf('Invalid role "%s"', $messageConfig['role']), - ); - } - - // Find appropriate content loader - $contentLoader = $this->findContentLoader($messageConfig); - - if ($contentLoader === null) { - throw new PromptParsingException( - 'Message must have either a "content" or "file" property', - ); - } - - // Load content using the appropriate loader - $content = $contentLoader->loadContent($messageConfig); - - return new PromptMessage( - role: $role, - content: new TextContent(text: $content), - ); - } - - /** - * Finds the appropriate content loader for the given message configuration. - */ - private function findContentLoader(array $messageConfig): ?MessageContentLoader - { - foreach ($this->contentLoaders as $loader) { - if ($loader->canHandle($messageConfig)) { - return $loader; - } - } - - return null; - } -} diff --git a/src/McpServer/Prompt/PromptConfigMerger.php b/src/McpServer/Prompt/PromptConfigMerger.php deleted file mode 100644 index 58eb7e6b..00000000 --- a/src/McpServer/Prompt/PromptConfigMerger.php +++ /dev/null @@ -1,100 +0,0 @@ -sourceConfig; - $filterConfig = $sourceConfig->getFilter(); - - if ($filterConfig !== null && !$filterConfig->isEmpty()) { - $filter = $this->filterFactory->createFromConfig($filterConfig->getConfig()); - - if ($filter !== null) { - $this->logger->debug('Created prompt filter for import', [ - 'path' => $importedConfig->path, - 'filterConfig' => $filterConfig->getConfig(), - ]); - } - } - - // Index main prompts by ID for efficient lookups - $indexedPrompts = []; - foreach ($mainSection as $prompt) { - if (!isset($prompt['id'])) { - continue; - } - $indexedPrompts[$prompt['id']] = $prompt; - } - - // Process each imported prompt - $importedCount = 0; - $filteredCount = 0; - - foreach ($importedSection as $prompt) { - if (!isset($prompt['id'])) { - $this->logger->warning('Skipping prompt without ID', [ - 'prompt' => $prompt, - 'path' => $importedConfig->path, - ]); - continue; - } - - // Apply filter if it exists - if ($filter !== null && !$filter->shouldInclude($prompt)) { - $this->logger->debug('Filtered out prompt', [ - 'id' => $prompt['id'], - 'path' => $importedConfig->path, - ]); - $filteredCount++; - continue; - } - - $promptId = $prompt['id']; - $indexedPrompts[$promptId] = $prompt; - $importedCount++; - - $this->logger->debug('Merged prompt', [ - 'id' => $promptId, - 'path' => $importedConfig->path, - ]); - } - - if ($filter !== null) { - $this->logger->info('Import filtering results', [ - 'path' => $importedConfig->path, - 'imported' => $importedCount, - 'filtered' => $filteredCount, - 'total' => \count($importedSection), - ]); - } - - // Convert back to numerically indexed array - return \array_values($indexedPrompts); - } -} diff --git a/src/McpServer/Prompt/PromptMessageProcessor.php b/src/McpServer/Prompt/PromptMessageProcessor.php deleted file mode 100644 index 8ec659bc..00000000 --- a/src/McpServer/Prompt/PromptMessageProcessor.php +++ /dev/null @@ -1,41 +0,0 @@ -variables; - - return $prompt->withMessages(\array_map(static function ($message) use ($variables, $arguments) { - $content = $message->content; - - if ($content instanceof TextContent) { - $text = $variables->with( - new VariableReplacementProcessor(new ConfigVariableProvider($arguments)), - )->resolve($content->text); - - $content = new TextContent($text); - } - - return new PromptMessage( - role: $message->role, - content: $content, - ); - }, $prompt->messages)); - } -} diff --git a/src/McpServer/Prompt/PromptParserPlugin.php b/src/McpServer/Prompt/PromptParserPlugin.php deleted file mode 100644 index aae3f589..00000000 --- a/src/McpServer/Prompt/PromptParserPlugin.php +++ /dev/null @@ -1,107 +0,0 @@ -promptRegistry instanceof RegistryInterface); - - if (!$this->supports($config)) { - return null; - } - - $this->logger?->debug('Parsing prompts configuration', [ - 'count' => \count($config['prompts']), - ]); - - // First pass: Register all prompts and templates - foreach ($config['prompts'] as $index => $promptConfig) { - try { - $prompt = $this->promptFactory->createFromConfig($promptConfig); - $this->promptRegistry->register($prompt); - - $this->logger?->debug('Prompt parsed and registered', [ - 'id' => $prompt->id, - 'type' => $prompt->type->value, - ]); - } catch (\Throwable $e) { - $this->logger?->warning('Failed to parse prompt', [ - 'index' => $index, - 'error' => $e->getMessage(), - ]); - - throw new PromptParsingException( - \sprintf('Failed to parse prompt at index %d: %s', $index, $e->getMessage()), - previous: $e, - ); - } - } - - // Second pass: Resolve templates for non-template prompts - $resolver = $this->templateResolver; - - foreach ($this->promptRegistry->getItems() as $id => $prompt) { - if (!empty($prompt->extensions)) { - try { - $resolvedPrompt = $resolver->resolve($prompt); - $this->promptRegistry->register($resolvedPrompt); - - $this->logger?->debug('Prompt template resolved', [ - 'id' => $resolvedPrompt->id, - ]); - } catch (TemplateResolutionException $e) { - $this->logger?->warning('Failed to resolve prompt template', [ - 'id' => $id, - 'error' => $e->getMessage(), - ]); - - throw new PromptParsingException( - \sprintf('Failed to resolve template for prompt "%s": %s', $id, $e->getMessage()), - previous: $e, - ); - } - } - } - - return $this->promptRegistry; - } - - public function supports(array $config): bool - { - return isset($config['prompts']) && \is_array($config['prompts']); - } - - public function updateConfig(array $config, string $rootPath): array - { - // This plugin doesn't modify the configuration - return $config; - } -} diff --git a/src/McpServer/Prompt/PromptProviderInterface.php b/src/McpServer/Prompt/PromptProviderInterface.php deleted file mode 100644 index bf9f8674..00000000 --- a/src/McpServer/Prompt/PromptProviderInterface.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ - public function all(): array; - - /** - * Gets all non-template prompts. - * - * @return array - */ - public function allTemplates(): array; - - /** - * Gets all prompts. - * - * @return list - */ - public function allPrompts(): array; -} diff --git a/src/McpServer/Prompt/PromptRegistry.php b/src/McpServer/Prompt/PromptRegistry.php deleted file mode 100644 index 3f55d405..00000000 --- a/src/McpServer/Prompt/PromptRegistry.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ -#[Singleton] -final class PromptRegistry implements RegistryInterface, PromptProviderInterface, PromptRegistryInterface -{ - /** @var array */ - private array $prompts = []; - - public function __construct( - private readonly Container $container, - ) {} - - public function register(PromptDefinition $prompt): void - { - /** - * @psalm-suppress InvalidPropertyAssignmentValue - */ - $this->prompts[$prompt->id] = $prompt; - } - - public function get(string $name, array $arguments = []): PromptDefinition - { - if (!$this->has($name)) { - throw new \InvalidArgumentException( - \sprintf( - 'No prompt with the name "%s" exists', - $name, - ), - ); - } - - return $this->container->get(PromptMessageProcessor::class)->process($this->prompts[$name], $arguments); - } - - public function has(string $name): bool - { - return isset($this->prompts[$name]); - } - - public function all(): array - { - return $this->prompts; - } - - public function allTemplates(): array - { - return \array_filter( - $this->prompts, - static fn(PromptDefinition $prompt) => $prompt->type === PromptType::Template, - ); - } - - public function getType(): string - { - return 'prompts'; - } - - public function allPrompts(): array - { - return $this->getItems(); - } - - public function getItems(): array - { - return \array_values( - \array_filter( - $this->prompts, - static fn(PromptDefinition $prompt) => $prompt->type === PromptType::Prompt, - ), - ); - } - - public function jsonSerialize(): array - { - return $this->getItems(); - } - - public function getIterator(): \Traversable - { - return new \ArrayIterator($this->getItems()); - } -} diff --git a/src/McpServer/Prompt/PromptRegistryInterface.php b/src/McpServer/Prompt/PromptRegistryInterface.php deleted file mode 100644 index 0ae3aa19..00000000 --- a/src/McpServer/Prompt/PromptRegistryInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - self::Template, - default => self::Prompt, - }; - } -} diff --git a/src/McpServer/Registry/McpItemsRegistry.php b/src/McpServer/Registry/McpItemsRegistry.php deleted file mode 100644 index d5eb33a1..00000000 --- a/src/McpServer/Registry/McpItemsRegistry.php +++ /dev/null @@ -1,117 +0,0 @@ - */ - private array $prompts = []; - - /** @var array<\Mcp\Types\Resource> */ - private array $resources = []; - - /** @var array<\Mcp\Types\Tool> */ - private array $tools = []; - - public function __construct( - private readonly ToolAttributesParser $toolAttributesParser, - private readonly LoggerInterface $logger, - ) {} - - /** - * Register a class and scan for MCP item attributes - */ - public function register(string $className): void - { - $reflection = new \ReflectionClass($className); - - // Check for Prompt attribute - $promptAttributes = $reflection->getAttributes(Prompt::class); - if (!empty($promptAttributes)) { - $prompt = $promptAttributes[0]->newInstance(); - $this->prompts[$prompt->name] = new \Mcp\Types\Prompt( - name: $prompt->name, - description: $prompt->description, - ); - - $this->logger->info('Registered prompt', [ - 'name' => $prompt->name, - 'description' => $prompt->description, - ]); - } - - // Check for Resource attribute - $resourceAttributes = $reflection->getAttributes(Resource::class); - if (!empty($resourceAttributes)) { - $resource = $resourceAttributes[0]->newInstance(); - $this->resources[$resource->name] = new \Mcp\Types\Resource( - name: $resource->name, - uri: $resource->uri, - description: $resource->description, - mimeType: $resource->mimeType, - ); - - $this->logger->info('Registered resource', [ - 'name' => $resource->name, - 'uri' => $resource->uri, - 'description' => $resource->description, - 'mimeType' => $resource->mimeType, - ]); - } - - // Check for Tool attribute - $toolAttributes = $reflection->getAttributes(Tool::class); - if (!empty($toolAttributes)) { - $tool = $toolAttributes[0]->newInstance(); - $this->tools[$tool->name] = $this->toolAttributesParser->parse($className); - - $this->logger->info('Registered tool', [ - 'name' => $tool->name, - 'description' => $tool->description, - ]); - } - } - - /** - * Register multiple classes at once - */ - public function registerMany(array $classNames): void - { - foreach ($classNames as $className) { - $this->register($className); - } - } - - /** - * Get all registered prompts - */ - public function getPrompts(): array - { - return \array_values($this->prompts); - } - - /** - * Get all registered resources - */ - public function getResources(): array - { - return \array_values($this->resources); - } - - /** - * Get all registered tools - * @return array<\Mcp\Types\Tool> - */ - public function getTools(): array - { - return \array_values($this->tools); - } -} diff --git a/src/McpServer/Routing/ActionCaller.php b/src/McpServer/Routing/ActionCaller.php deleted file mode 100644 index 2da1cc5d..00000000 --- a/src/McpServer/Routing/ActionCaller.php +++ /dev/null @@ -1,51 +0,0 @@ - $request, - ]; - - $reflection = new \ReflectionClass($this->class); - $inputSchemaClass = $reflection->getAttributes(InputSchema::class)[0] ?? null; - if ($inputSchemaClass !== null) { - $inputSchema = $inputSchemaClass->newInstance(); - - $input = $this->schemaMapper->toObject( - json: (array) ($request->getParsedBody() ?? []), - class: $inputSchema->class, - ); - - $bindings[$inputSchema->class] = $input; - } - - return $this->container->runScope( - bindings: new Scope( - name: AppScope::McpServerRequest, - bindings: $bindings, - ), - scope: fn(InvokerInterface $invoker): object => $invoker->invoke([$this->class, '__invoke']), - ); - } -} diff --git a/src/McpServer/Routing/Attribute/Get.php b/src/McpServer/Routing/Attribute/Get.php deleted file mode 100644 index 6864b9e0..00000000 --- a/src/McpServer/Routing/Attribute/Get.php +++ /dev/null @@ -1,17 +0,0 @@ - $value) { - $request = $request->withAttribute($key, $value); - } - - // For POST requests, also add parameters to parsed body - if ($httpMethod === 'POST' && !empty($mcpParams)) { - $parsedBody = []; - - foreach ($mcpParams as $key => $value) { - if (\is_string($value) && \json_validate($value)) { - $value = \json_decode($value, true); - } - - $parsedBody[$key] = $value; - } - - $request = $request->withParsedBody($parsedBody); - } - - return $request; - } -} diff --git a/src/McpServer/Routing/McpResponseStrategy.php b/src/McpServer/Routing/McpResponseStrategy.php deleted file mode 100644 index af80ef8c..00000000 --- a/src/McpServer/Routing/McpResponseStrategy.php +++ /dev/null @@ -1,54 +0,0 @@ -logger->info('Invoking route callable', [ - 'route' => $route->getName(), - 'method' => $request->getMethod(), - 'uri' => (string) $request->getUri(), - ]); - - $controller = $route->getCallable($this->getContainer()); - $response = $controller($request, $route->getVars()); - - if ($response instanceof ResponseInterface) { - return $response; - } - - return new JsonResponse($response); - } catch (\Throwable $e) { - $this->logger->error('Error while handling request', [ - 'exception' => $e, - 'request' => $request, - ]); - - $this->reporter->report($e); - - return new JsonResponse([ - 'error' => 'Internal Server Error', - 'message' => $e->getMessage(), - ], 500); - } - } -} diff --git a/src/McpServer/Routing/RouteRegistrar.php b/src/McpServer/Routing/RouteRegistrar.php deleted file mode 100644 index 0c5e120a..00000000 --- a/src/McpServer/Routing/RouteRegistrar.php +++ /dev/null @@ -1,94 +0,0 @@ -binder = $binder->getBinder('mcp.server'); - } - - /** - * Register routes from a controller class - */ - public function registerController(string $controllerClass): void - { - $this->binder->bindSingleton($controllerClass, $controllerClass); - - $reflectionClass = new \ReflectionClass($controllerClass); - - // Get the controller prefix if defined - // Find all methods with Route attribute - foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - // Find all Route attributes - $routeAttributes = $method->getAttributes(Route::class, \ReflectionAttribute::IS_INSTANCEOF); - if (empty($routeAttributes)) { - continue; - } - - $this->registerMethodRoutes($routeAttributes[0]->newInstance(), $controllerClass); - } - } - - /** - * Register multiple controllers at once - */ - public function registerControllers(array $controllerClasses): void - { - foreach ($controllerClasses as $controllerClass) { - $this->registerController($controllerClass); - } - } - - /** - * Register routes for a single controller method - */ - private function registerMethodRoutes(Route $route, string $controllerClass): void - { - // Combine prefix with route path - $path = $this->normalizePath($route->path); - - $registeredRoute = $this->router->map( - method: $route->method, - path: $path, - handler: $this->factory->make(ActionCaller::class, [ - 'class' => $controllerClass, - ]), - ); - - // Set route name if provided - if ($route->name !== null) { - $registeredRoute->setName($route->name); - } - } - - /** - * Normalize a path to ensure proper formatting - */ - private function normalizePath(string $path): string - { - // Replace multiple slashes with a single slash - $path = (string) \preg_replace('#/+#', '/', $path); - - // Ensure path starts with a slash - if (!\str_starts_with($path, '/')) { - $path = '/' . $path; - } - - return $path; - } -} diff --git a/src/McpServer/Server.php b/src/McpServer/Server.php deleted file mode 100644 index 8ec92b22..00000000 --- a/src/McpServer/Server.php +++ /dev/null @@ -1,215 +0,0 @@ -logger); - $this->configureServer($server); - - $initOptions = $server->createInitializationOptions(); - $runner = new ServerRunner(server: $server, initOptions: $initOptions, logger: $this->logger); - $runner->run(); - } - - /** - * Configure all handlers for the server - */ - private function configureServer(McpServer $server): void - { - // Register prompts handlers - $server->registerHandler( - 'prompts/list', - fn() => $this->handleRoute('prompts/list', ListPromptsResult::class), - ); - - $server->registerHandler( - 'prompts/get', - fn($params) => $this->handlePromptGetRoute($params), - ); - - // Register resources handlers - $server->registerHandler( - 'resources/list', - fn() => $this->handleRoute('resources/list', ListResourcesResult::class), - ); - - $server->registerHandler( - 'resources/read', - fn($params) => $this->handleResourceRead($params), - ); - - // Register tools handlers - $server->registerHandler( - 'tools/list', - fn() => $this->handleRoute('tools/list', ListToolsResult::class), - ); - - $server->registerHandler( - 'tools/call', - fn($params) => $this->handleToolCall($params), - ); - } - - /** - * Handle a route using the router - */ - private function handleRoute(string $method, string $class, array $params = []): mixed - { - $this->logger->debug("Handling route: $method", $params); - - // Create PSR request from MCP method and params - $request = $this->requestFactory->createPsrRequest($method, $params); - - // Dispatch the request through the router - try { - $response = $this->router->dispatch($request); - \assert($response instanceof JsonResponse); - - // Convert the response back to appropriate MCP type - return $this->projectService->processResponse($response->getPayload()); - } catch (\Throwable $e) { - $this->reporter->report($e); - $this->logger->error('Route handling error', [ - 'method' => $method, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - } - - return new $class([]); - } - - /** - * Special handler for tool calls to map to specific routes - */ - private function handleToolCall(CallToolRequestParams $params): CallToolResult - { - $params = $this->projectService->processToolRequestParams($params); - - $method = 'tools/call/' . $params->name; - $arguments = $params->arguments ?? []; - - $this->logger->debug('Handling tool call', [ - 'tool' => $params->name, - 'method' => $method, - 'arguments' => $arguments, - ]); - - // Create PSR request with the tool name in the path and arguments as POST body - $request = $this->requestFactory->createPsrRequest($method, $arguments); - - try { - $response = $this->router->dispatch($request); - \assert($response instanceof JsonResponse); - - return $response->getPayload(); - } catch (\Throwable $e) { - $this->logger->error('Tool call error', [ - 'tool' => $params->name, - 'error' => $e->getMessage(), - ]); - return new CallToolResult([new TextContent(text: $e->getMessage())], isError: true); - } - } - - private function handleResourceRead(ReadResourceRequestParams $params): ReadResourceResult - { - $params = $this->projectService->processResourceRequestParams($params); - - [$type, $path] = \explode('://', $params->uri, 2); - - $method = 'resource/' . $type . '/' . $path; - - $this->logger->debug('Handling resource read', [ - 'resource' => $params->uri, - 'type' => $type, - 'path' => $path, - 'method' => $method, - ]); - - // Create PSR request with the tool name in the path and arguments as POST body - $request = $this->requestFactory->createPsrRequest($method); - - try { - $response = $this->router->dispatch($request); - \assert($response instanceof JsonResponse); - // Convert the response back to appropriate MCP type - return $response->getPayload(); - } catch (\Throwable $e) { - $this->logger->error('Resource read error', [ - 'resource' => $params->uri, - 'error' => $e->getMessage(), - ]); - } - return new ReadResourceResult([]); - } - - private function handlePromptGetRoute(GetPromptRequestParams $params): GetPromptResult - { - $params = $this->projectService->processPromptRequestParams($params); - - $name = $params->name; - $arguments = $params->arguments; - - $method = 'prompt/' . $name; - - $this->logger->debug('Handling prompt get', [ - 'prompt' => $name, - 'method' => $method, - 'arguments' => (array) $arguments->jsonSerialize(), - ]); - - // Create PSR request with the tool name in the path and arguments as POST body - $request = $this->requestFactory->createPsrRequest($method, (array) $arguments->jsonSerialize()); - - try { - $response = $this->router->dispatch($request); - \assert($response instanceof JsonResponse); - - // Convert the response back to appropriate MCP type - return $response->getPayload(); - } catch (\Throwable $e) { - $this->logger->error('Prompt get error', [ - 'prompt' => $params->name, - 'error' => $e->getMessage(), - ]); - } - - return new GetPromptResult([]); - } -} diff --git a/src/McpServer/ServerRunner.php b/src/McpServer/ServerRunner.php deleted file mode 100644 index 98e7fb63..00000000 --- a/src/McpServer/ServerRunner.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ - private array $actions = []; - - public function __construct( - #[Proxy] private readonly ScopeInterface $scope, - private readonly ProjectServiceInterface $projectService, - ) {} - - /** - * Register a new action class - * - * @param class-string $class - */ - public function registerAction(string $class): void - { - $this->actions[] = $class; - } - - public function run(string $name): void - { - $this->scope->runScope( - bindings: new Scope( - name: AppScope::McpServer, - ), - scope: function ( - RouteRegistrar $registrar, - McpItemsRegistry $registry, - HasPrefixLoggerInterface $logger, - ExceptionReporterInterface $reporter, - ) use ($name): void { - // Register all classes with MCP item attributes. Should be before registering controllers! - $registry->registerMany($this->actions); - - // Register all controllers for routing - $registrar->registerControllers($this->actions); - - // Create the server - (new Server( - router: $registrar->router, - logger: $logger, - projectService: $this->projectService, - reporter: $reporter, - ))->run($name); - }, - ); - } -} diff --git a/src/McpServer/ServerRunnerInterface.php b/src/McpServer/ServerRunnerInterface.php deleted file mode 100644 index f9111fc3..00000000 --- a/src/McpServer/ServerRunnerInterface.php +++ /dev/null @@ -1,13 +0,0 @@ -logger?->info('Executing command', [ - 'command' => $command->cmd, - 'args' => $command->args, - ]); - - // Determine working directory - $workingDir = FSPath::create($this->projectRoot); - - if ($command->workingDir !== null) { - $workingDir = $workingDir->join($command->workingDir); - } - - // Create the process - $process = new Process( - command: \array_merge([$command->cmd], $command->args), - cwd: (string) $workingDir, - env: \array_merge($this->envs->getAll(), $envs, $command->env), - timeout: $this->timeout, - ); - - try { - $process->run(); - - $output = $process->getOutput() . $process->getErrorOutput(); - $exitCode = $process->getExitCode() ?? -1; - - if ($exitCode !== 0) { - $this->logger?->warning('Command exited with non-zero code', [ - 'command' => $command->cmd, - 'args' => $command->args, - 'exitCode' => $exitCode, - 'output' => $output, - ]); - } else { - $this->logger?->info('Command executed successfully', [ - 'command' => $command->cmd, - 'args' => $command->args, - 'exitCode' => $exitCode, - ]); - } - - return [ - 'output' => $output, - 'exitCode' => $exitCode, - ]; - } catch (\Throwable $e) { - $this->logger?->error('Command execution failed', [ - 'command' => $command->cmd, - 'args' => $command->args, - 'error' => $e->getMessage(), - 'exception' => $e::class, - ]); - - throw ToolExecutionException::fromCommand( - command: $command->cmd, - args: $command->args, - reason: $e->getMessage(), - previous: $e, - ); - } - } -} diff --git a/src/McpServer/Tool/Command/CommandExecutorInterface.php b/src/McpServer/Tool/Command/CommandExecutorInterface.php deleted file mode 100644 index 3583f7a8..00000000 --- a/src/McpServer/Tool/Command/CommandExecutorInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - $headers HTTP request headers - * @param array $query Query parameters - * @param string|array|null $body Request body (for POST, PUT, etc.) - */ - public function __construct( - public string $url, - public string $method = 'GET', - public array $headers = [], - public array $query = [], - public string|array|null $body = null, - ) {} - - /** - * Creates an HttpToolRequest from a configuration array. - * - * @param array $config The request configuration - * @throws \InvalidArgumentException If the configuration is invalid - */ - public static function fromArray(array $config, array $data): self - { - if (!isset($config['url']) || !\is_string($config['url'])) { - throw new \InvalidArgumentException('HTTP request must have a non-empty "url" property'); - } - - $method = 'GET'; - if (isset($config['method'])) { - if (!\is_string($config['method'])) { - throw new \InvalidArgumentException('HTTP request "method" must be a string'); - } - $method = \strtoupper($config['method']); - - // Validate method - if (!\in_array($method, ['GET', 'POST'], true)) { - throw new \InvalidArgumentException("Invalid HTTP method: {$method}"); - } - } - - $headers = []; - if (isset($config['headers']) && \is_array($config['headers'])) { - foreach ($config['headers'] as $key => $value) { - if (!\is_string($key) || !\is_string($value)) { - throw new \InvalidArgumentException('HTTP headers must be string key-value pairs'); - } - $headers[$key] = $value; - } - } - - $query = []; - $body = null; - if ($method === 'GET') { - $query = $data; - } else { - $body = $data; - } - - return new self( - url: $config['url'], - method: $method, - headers: $headers, - query: $query, - body: $body, - ); - } - - /** - * Returns the final URL with query parameters included. - */ - public function getFullUrl(): string - { - if (empty($this->query)) { - return $this->url; - } - - $url = $this->url; - $queryString = \http_build_query($this->query); - - // Check if URL already has query parameters - if (\str_contains($url, '?')) { - return $url . '&' . $queryString; - } - - return $url . '?' . $queryString; - } - - /** - * Converts body to string format suitable for sending in a request. - */ - public function getBodyAsString(): ?string - { - if ($this->body === null) { - return null; - } - - if (\is_string($this->body)) { - return $this->body; - } - - // Convert array to JSON - return \json_encode($this->body) ?: null; - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return \array_filter([ - 'url' => $this->url, - 'method' => $this->method, - 'headers' => $this->headers, - 'query' => $this->query, - 'body' => $this->body, - ], static fn($value) => $value !== null && $value !== []); - } -} diff --git a/src/McpServer/Tool/Config/ToolArg.php b/src/McpServer/Tool/Config/ToolArg.php deleted file mode 100644 index 396e7480..00000000 --- a/src/McpServer/Tool/Config/ToolArg.php +++ /dev/null @@ -1,77 +0,0 @@ - $config The argument configuration - * @throws \InvalidArgumentException If the configuration is invalid - */ - public static function fromMixed(mixed $config): self - { - // Simple string argument - if (\is_string($config)) { - return new self(name: $config); - } - - // Associative array with name and when - if (\is_array($config)) { - if (!isset($config['name']) || !\is_string($config['name'])) { - throw new \InvalidArgumentException('Argument must have a non-empty "name" property'); - } - - $when = null; - if (isset($config['when'])) { - if (!\is_string($config['when'])) { - throw new \InvalidArgumentException('Argument "when" condition must be a string'); - } - $when = $config['when']; - } - - return new self( - name: $config['name'], - when: $when, - ); - } - - throw new \InvalidArgumentException('Argument must be a string or an array with "name" property'); - } - - /** - * Converts to array representation for serialization. - * - * @return array - */ - public function toArray(): array - { - return \array_filter([ - 'name' => $this->name, - 'when' => $this->when, - ], static fn($value) => $value !== null); - } - - /** - * Returns the argument name when cast to string. - */ - public function __toString(): string - { - return $this->name; - } -} diff --git a/src/McpServer/Tool/Config/ToolCommand.php b/src/McpServer/Tool/Config/ToolCommand.php deleted file mode 100644 index ef881c6b..00000000 --- a/src/McpServer/Tool/Config/ToolCommand.php +++ /dev/null @@ -1,109 +0,0 @@ - $args Command arguments - * @param string|null $workingDir Optional working directory (relative to project root) - * @param array $env Optional environment variables - */ - public function __construct( - public string $cmd, - public array $args = [], - public ?string $workingDir = null, - public array $env = [], - ) {} - - /** - * Creates a ToolCommand from a configuration array. - * - * @param array $config The command configuration - * @throws \InvalidArgumentException If the configuration is invalid - */ - public static function fromArray(array $config, ?string $workingDir = null): self - { - if (!isset($config['cmd']) || !\is_string($config['cmd'])) { - throw new \InvalidArgumentException('Command must have a non-empty "cmd" property'); - } - - $args = []; - if (isset($config['args'])) { - if (!\is_array($config['args'])) { - throw new \InvalidArgumentException('Command "args" must be an array'); - } - - foreach ($config['args'] as $argConfig) { - try { - $args[] = ToolArg::fromMixed($argConfig); - } catch (\InvalidArgumentException $e) { - throw new \InvalidArgumentException( - 'Invalid argument configuration: ' . $e->getMessage(), - previous: $e, - ); - } - } - } - - if ( - isset($config['workingDir']) - && $config['workingDir'] !== '.' - && $config['workingDir'] !== '' - && $config['workingDir'] !== null - ) { - if (!\is_string($config['workingDir'])) { - throw new \InvalidArgumentException('Command "workingDir" must be a string'); - } - $workingDir = $config['workingDir']; - } - - $env = []; - if (isset($config['env']) && \is_array($config['env'])) { - foreach ($config['env'] as $key => $value) { - if (!\is_string($key) || !\is_string($value)) { - throw new \InvalidArgumentException('Environment variables must be string key-value pairs'); - } - $env[$key] = $value; - } - } - - return new self( - cmd: $config['cmd'], - args: $args, - workingDir: $workingDir, - env: $env, - ); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - $argsArray = []; - - foreach ($this->args as $arg) { - if ($arg->when === null) { - // Simple argument - $argsArray[] = $arg->name; - } else { - // Conditional argument - $argsArray[] = $arg->toArray(); - } - } - - return \array_filter([ - 'cmd' => $this->cmd, - 'args' => $argsArray, - 'workingDir' => $this->workingDir, - 'env' => $this->env, - ], static fn($value) => $value !== null && $value !== []); - } -} diff --git a/src/McpServer/Tool/Config/ToolDefinition.php b/src/McpServer/Tool/Config/ToolDefinition.php deleted file mode 100644 index 2bc936ba..00000000 --- a/src/McpServer/Tool/Config/ToolDefinition.php +++ /dev/null @@ -1,146 +0,0 @@ - $commands List of commands to execute (for 'run' type) - * @param ToolSchema|null $schema JSON schema for tool arguments - * @param array $env Environment variables for all commands - * @param array $extra Additional type-specific configuration data - */ - public function __construct( - public string $id, - public string $description, - public string $type = 'run', - public array $commands = [], - public ?ToolSchema $schema = null, - public array $env = [], - public array $extra = [], - ) {} - - /** - * Creates a ToolDefinition from a configuration array. - * - * @param array $config The tool configuration - * @throws \InvalidArgumentException If the configuration is invalid - */ - public static function fromArray(array $config): self - { - // Validate required fields - if (empty($config['id']) || !\is_string($config['id'])) { - throw new \InvalidArgumentException('Tool must have a non-empty id'); - } - - if (empty($config['description']) || !\is_string($config['description'])) { - throw new \InvalidArgumentException('Tool must have a non-empty description'); - } - - // Get tool type - $type = $config['type'] ?? 'run'; - - // Extract any extra configuration data (type-specific) - $extra = []; - $reservedKeys = ['id', 'description', 'type', 'commands', 'schema', 'env', 'workingDir']; - foreach ($config as $key => $value) { - if (!\in_array($key, $reservedKeys, true)) { - $extra[$key] = $value; - } - } - - // For 'run' type, validate and parse commands - $commands = []; - if ($type === 'run') { - if (!isset($config['commands']) || !\is_array($config['commands'])) { - throw new \InvalidArgumentException('Run-type tool must have a non-empty commands array'); - } - - foreach ($config['commands'] as $index => $commandConfig) { - if (!\is_array($commandConfig)) { - throw new \InvalidArgumentException( - \sprintf('Command at index %d must be an array', $index), - ); - } - - try { - $commands[] = ToolCommand::fromArray($commandConfig, $config['workingDir'] ?? null); - } catch (\InvalidArgumentException $e) { - throw new \InvalidArgumentException( - \sprintf('Invalid command at index %d: %s', $index, $e->getMessage()), - previous: $e, - ); - } - } - } elseif (isset($config['commands']) && \is_array($config['commands'])) { - // For non-run types, store commands in extra if provided - $extra['commands'] = $config['commands']; - } - - // Handle 'http' type specific validations - if ($type === 'http') { - if (!isset($config['requests']) || !\is_array($config['requests']) || empty($config['requests'])) { - throw new \InvalidArgumentException('HTTP tool must have a non-empty requests array'); - } - } - - $env = []; - if (isset($config['env']) && \is_array($config['env'])) { - foreach ($config['env'] as $key => $value) { - if (!\is_string($key) || !\is_string($value)) { - throw new \InvalidArgumentException('Environment variables must be string key-value pairs'); - } - $env[$key] = $value; - } - } - - return new self( - id: $config['id'], - description: $config['description'], - type: $type, - commands: $commands, - schema: ToolSchema::fromArray($config['schema'] ?? []), - env: $env, - extra: $extra, - ); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - $result = [ - 'id' => $this->id, - 'description' => $this->description, - 'type' => $this->type, - ]; - - if ($this->type === 'run' && !empty($this->commands)) { - $result['commands'] = $this->commands; - } - - if ($this->schema !== null) { - $result['schema'] = $this->schema; - } - - if (!empty($this->env)) { - $result['env'] = $this->env; - } - - // Include any extra type-specific data - foreach ($this->extra as $key => $value) { - $result[$key] = $value; - } - - return $result; - } -} diff --git a/src/McpServer/Tool/Config/ToolSchema.php b/src/McpServer/Tool/Config/ToolSchema.php deleted file mode 100644 index d91ae676..00000000 --- a/src/McpServer/Tool/Config/ToolSchema.php +++ /dev/null @@ -1,84 +0,0 @@ - $schema The JSON schema definition - */ - public function __construct( - private array $schema, - ) {} - - /** - * Creates a ToolSchema from a configuration array. - * - * @param array $config The schema configuration - * @throws \InvalidArgumentException If the configuration is invalid - */ - public static function fromArray(array $config): ?self - { - if (empty($config)) { - $config = ['properties' => []]; - } - - // Validate basic schema structure - if (!isset($config['type'])) { - $config['type'] = 'object'; - } - - return new self($config); - } - - /** - * Gets the required properties from the schema. - * - * @return array List of required property names - */ - public function getRequiredProperties(): array - { - return $this->schema['required'] ?? []; - } - - /** - * Gets all property definitions from the schema. - * - * @return array> Property definitions - */ - public function getProperties(): array - { - return $this->schema['properties'] ?? []; - } - - /** - * Gets the default value for a property if defined in the schema. - * - * @param string $propertyName The name of the property - * @return mixed The default value or null if not defined - */ - public function getDefaultValue(string $propertyName): mixed - { - $properties = $this->getProperties(); - - if (!isset($properties[$propertyName])) { - return null; - } - - // Return the default value if it exists, otherwise null - return $properties[$propertyName]['default'] ?? null; - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->schema; - } -} diff --git a/src/McpServer/Tool/Exception/ToolExecutionException.php b/src/McpServer/Tool/Exception/ToolExecutionException.php deleted file mode 100644 index b921474c..00000000 --- a/src/McpServer/Tool/Exception/ToolExecutionException.php +++ /dev/null @@ -1,31 +0,0 @@ - ToolRegistry::class, - ToolProviderInterface::class => ToolRegistry::class, - ToolRegistry::class => ToolRegistry::class, - CommandExecutorInterface::class => new Proxy( - interface: CommandExecutorInterface::class, - ), - ToolHandlerInterface::class => static fn( - FactoryInterface $factory, - EnvironmentInterface $env, - ) => $factory->make(RunToolHandler::class, [ - 'executionEnabled' => (bool) ($env->get('MCP_TOOL_COMMAND_EXECUTION') ?? true), - ]), - ]; - } - - public function init( - ConfigLoaderBootloader $configLoader, - ToolParserPlugin $parserPlugin, - ToolConfigMerger $toolConfigMerger, - ConsoleBootloader $console, - ): void { - $configLoader->registerParserPlugin($parserPlugin); - $configLoader->registerMerger($toolConfigMerger); - - $console->addCommand(ToolListCommand::class); - $console->addCommand(ToolRunCommand::class); - } -} diff --git a/src/McpServer/Tool/Provider/ToolArgumentsProvider.php b/src/McpServer/Tool/Provider/ToolArgumentsProvider.php deleted file mode 100644 index fe33631e..00000000 --- a/src/McpServer/Tool/Provider/ToolArgumentsProvider.php +++ /dev/null @@ -1,205 +0,0 @@ -arguments); - } - - /** - * Get a value with proper type casting based on schema (if available). - * Falls back to default value from schema if property is not provided. - */ - public function get(string $name): ?string - { - // If the argument is not provided, check if there's a default value in the schema - return $this->castValueFromSchema( - $name, - $this->arguments[$name] ?? $this->schema->getDefaultValue($name), - ); - } - - /** - * Get all arguments - * - * @return array - */ - public function getAll(): array - { - // Start with all explicitly provided arguments - $result = []; - $properties = $this->schema->getProperties(); - - // First apply any default values for properties not in arguments - foreach ($properties as $name => $definition) { - if (!$this->has($name) && isset($definition['default'])) { - $result[$name] = $this->castValueFromSchema($name, $definition['default']); - } - } - - // Then add explicitly provided arguments (these will override defaults) - foreach ($this->arguments as $name => $value) { - // Apply type casting to the arguments - $result[$name] = $this->castValueFromSchema($name, $value); - } - - return $result; - } - - /** - * Cast a value based on schema type definition - * - * @param string $name The argument name - * @param mixed $value The value to cast - * @return mixed The cast value - */ - private function castValueFromSchema(string $name, mixed $value): string - { - // Return null directly if value is null - if ($value === null) { - return 'null'; - } - - $properties = $this->schema->getProperties(); - if (!isset($properties[$name])) { - return $value; - } - - $propertyDef = $properties[$name]; - $type = $propertyDef['type'] ?? null; - - if ($type === null) { - return 'null'; - } - - return match ($type) { - 'string' => $this->castToString($value), - 'number' => $this->castToFloat($value), - 'integer' => $this->castToInt($value), - 'boolean' => $this->castToBool($value), - 'array' => \json_encode($this->castToArray($value)), - 'object' => \json_encode($this->castToObject($value)), - default => (string) $value, - }; - } - - /** - * Cast a value to string - */ - private function castToString(mixed $value): string - { - if (\is_array($value) || \is_object($value)) { - return \json_encode($value) ?: ''; - } - - return (string) $value; - } - - /** - * Cast a value to integer - */ - private function castToInt(mixed $value): string - { - if (\is_string($value) && \trim($value) === '') { - return '0'; - } - - return (string) $value; - } - - /** - * Cast a value to float - */ - private function castToFloat(mixed $value): string - { - if (\is_string($value) && \trim($value) === '') { - return '0.0'; - } - - return \number_format((float) $value, 2, '.', ''); - } - - /** - * Cast a value to boolean - */ - private function castToBool(mixed $value): string - { - if (\is_string($value)) { - $value = \strtolower(\trim($value)); - return ($value === 'true' || $value === '1' || $value === 'yes' || $value === 'y') ? 'true' : 'false'; - } - - return $value ? 'true' : 'false'; - } - - /** - * Cast a value to array - */ - private function castToArray(mixed $value): array - { - if (\is_string($value)) { - // Try to parse JSON string - $decoded = \json_decode($value, true); - if (\json_last_error() === JSON_ERROR_NONE && \is_array($decoded)) { - return $decoded; - } - - // Split by comma if not a valid JSON - return \array_map('trim', \explode(',', $value)); - } - - if (\is_object($value)) { - return (array) $value; - } - - if (!\is_array($value)) { - return [$value]; - } - - return $value; - } - - /** - * Cast a value to object (associative array) - */ - private function castToObject(mixed $value): array - { - if (\is_string($value)) { - // Try to parse JSON string - $decoded = \json_decode($value, true); - if (\json_last_error() === JSON_ERROR_NONE && \is_array($decoded)) { - return $decoded; - } - - // Can't convert simple string to object - return []; - } - - if (\is_object($value)) { - return (array) $value; - } - - if (!\is_array($value)) { - return []; - } - - return $value; - } -} diff --git a/src/McpServer/Tool/ToolAttributesParser.php b/src/McpServer/Tool/ToolAttributesParser.php deleted file mode 100644 index d8014f72..00000000 --- a/src/McpServer/Tool/ToolAttributesParser.php +++ /dev/null @@ -1,57 +0,0 @@ -getAttributes(Tool::class); - - $tool = $toolAttributes[0]->newInstance(); - - $inputSchemaClass = $reflection->getAttributes(InputSchema::class)[0] ?? null; - if ($inputSchemaClass === null) { - $schema = new ToolInputSchema(); - } else { - $inputSchema = $inputSchemaClass->newInstance(); - $schema = ToolInputSchema::fromArray($this->schemaMapper->toJsonSchema($inputSchema->class)); - } - - // Tool name can only contain alphanumeric characters and underscores - if (!\preg_match(self::TOOL_NAME_REGEX, $tool->name)) { - throw new \InvalidArgumentException( - \sprintf( - 'Tool name "%s" is invalid. It can only contain alphanumeric characters and underscores.', - $tool->name, - ), - ); - } - - return new \Mcp\Types\Tool( - name: $tool->name, - inputSchema: $schema, - description: $tool->description, - annotations: $tool->title ? new ToolAnnotations( - title: $tool->title, - ) : null, - ); - } -} diff --git a/src/McpServer/Tool/ToolConfigMerger.php b/src/McpServer/Tool/ToolConfigMerger.php deleted file mode 100644 index 3c069c1a..00000000 --- a/src/McpServer/Tool/ToolConfigMerger.php +++ /dev/null @@ -1,63 +0,0 @@ -logger->warning('Skipping tool without ID', [ - 'tool' => $tool, - 'path' => $importedConfig->path, - ]); - continue; - } - - $toolId = $tool['id']; - - // Special handling for working directory - $workingDir = $tool['workingDir'] ?? '.'; - if ($importedConfig->isLocal && $workingDir === '.') { - $tool['workingDir'] = \dirname($importedConfig->path); - $this->logger->debug('Updated tool working directory', [ - 'id' => $toolId, - 'workingDir' => $tool['workingDir'], - ]); - } - - $indexedTools[$toolId] = $tool; - - $this->logger->debug('Merged tool', [ - 'id' => $toolId, - 'path' => $importedConfig->path, - ]); - } - - // Convert back to numerically indexed array - return \array_values($indexedTools); - } -} diff --git a/src/McpServer/Tool/ToolHandlerFactory.php b/src/McpServer/Tool/ToolHandlerFactory.php deleted file mode 100644 index 8fe49f95..00000000 --- a/src/McpServer/Tool/ToolHandlerFactory.php +++ /dev/null @@ -1,65 +0,0 @@ -> $handlers Mapping of tool types to handler classes - */ - public function __construct( - private ContainerInterface $container, - private array $handlers = [ - 'run' => RunToolHandler::class, - 'http' => HttpToolHandler::class, - ], - ) {} - - /** - * Creates a handler for the given tool. - */ - public function createHandlerForTool(ToolDefinition $tool): ToolHandlerInterface - { - // First try to find a handler that explicitly supports this tool type - foreach ($this->getHandlerInstances() as $handler) { - if ($handler->supports($tool->type)) { - return $handler; - } - } - - // If no handler declares explicit support, use the class mapping - if (isset($this->handlers[$tool->type])) { - return $this->container->get($this->handlers[$tool->type]); - } - - // Fallback to the default run handler - return $this->container->get(RunToolHandler::class); - } - - /** - * Get all registered handler instances. - * - * @return array - */ - private function getHandlerInstances(): array - { - $instances = []; - foreach ($this->handlers as $handlerClass) { - $instances[] = $this->container->get($handlerClass); - } - return $instances; - } -} diff --git a/src/McpServer/Tool/ToolParserPlugin.php b/src/McpServer/Tool/ToolParserPlugin.php deleted file mode 100644 index dadc41c1..00000000 --- a/src/McpServer/Tool/ToolParserPlugin.php +++ /dev/null @@ -1,73 +0,0 @@ -toolRegistry instanceof RegistryInterface); - - if (!$this->supports($config)) { - return null; - } - - $this->logger?->debug('Parsing tools configuration', [ - 'count' => \count($config['tools']), - ]); - - foreach ($config['tools'] as $index => $toolConfig) { - try { - $tool = ToolDefinition::fromArray($toolConfig); - $this->toolRegistry->register($tool); - - $this->logger?->debug('Tool parsed and registered', [ - 'id' => $tool->id, - ]); - } catch (\Throwable $e) { - $this->logger?->warning('Failed to parse tool', [ - 'index' => $index, - 'error' => $e->getMessage(), - ]); - - throw new \InvalidArgumentException( - \sprintf('Failed to parse tool at index %d: %s', $index, $e->getMessage()), - previous: $e, - ); - } - } - - return $this->toolRegistry; - } - - public function supports(array $config): bool - { - return isset($config['tools']) && \is_array($config['tools']); - } - - public function updateConfig(array $config, string $rootPath): array - { - // This plugin doesn't modify the configuration - return $config; - } -} diff --git a/src/McpServer/Tool/ToolProviderInterface.php b/src/McpServer/Tool/ToolProviderInterface.php deleted file mode 100644 index 8dab5268..00000000 --- a/src/McpServer/Tool/ToolProviderInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ - public function all(): array; -} diff --git a/src/McpServer/Tool/ToolRegistry.php b/src/McpServer/Tool/ToolRegistry.php deleted file mode 100644 index 1cf5ada8..00000000 --- a/src/McpServer/Tool/ToolRegistry.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -#[Singleton] -final class ToolRegistry implements RegistryInterface, ToolProviderInterface, ToolRegistryInterface -{ - /** @var array */ - private array $tools = []; - - public function register(ToolDefinition $tool): void - { - /** - * @psalm-suppress InvalidPropertyAssignmentValue - */ - $this->tools[$tool->id] = $tool; - } - - public function get(string $id): ToolDefinition - { - if (!$this->has($id)) { - throw new \InvalidArgumentException( - \sprintf('No tool with the ID "%s" exists', $id), - ); - } - - return $this->tools[$id]; - } - - public function has(string $id): bool - { - return isset($this->tools[$id]); - } - - public function all(): array - { - return $this->getItems(); - } - - public function getType(): string - { - return 'tools'; - } - - public function getItems(): array - { - return \array_values($this->tools); - } - - public function jsonSerialize(): array - { - return $this->getItems(); - } - - public function getIterator(): \Traversable - { - return new \ArrayIterator($this->getItems()); - } -} diff --git a/src/McpServer/Tool/ToolRegistryInterface.php b/src/McpServer/Tool/ToolRegistryInterface.php deleted file mode 100644 index 83eea58a..00000000 --- a/src/McpServer/Tool/ToolRegistryInterface.php +++ /dev/null @@ -1,17 +0,0 @@ -logger?->info('Executing tool', [ - 'id' => $tool->id, - ]); - - try { - $result = $this->doExecute($tool, $arguments); - - $this->logger?->info('Tool execution completed', [ - 'id' => $tool->id, - 'success' => true, - ]); - - return $result; - } catch (\Throwable $e) { - $this->logger?->error('Tool execution failed', [ - 'id' => $tool->id, - 'error' => $e->getMessage(), - 'exception' => $e::class, - ]); - - throw $e; - } - } - - /** - * Performs the actual tool execution. - * - * @param ToolDefinition $tool The tool to execute - * @return array Execution result - * @throws \Throwable If execution fails - */ - abstract protected function doExecute(ToolDefinition $tool, array $arguments = []): array; -} diff --git a/src/McpServer/Tool/Types/HttpToolHandler.php b/src/McpServer/Tool/Types/HttpToolHandler.php deleted file mode 100644 index e9a984ec..00000000 --- a/src/McpServer/Tool/Types/HttpToolHandler.php +++ /dev/null @@ -1,194 +0,0 @@ -extra['requests']) || !\is_array($tool->extra['requests'])) { - throw new ToolExecutionException('HTTP tool must have a "requests" property with at least one request'); - } - - return $this->executeRequests($tool, $tool->extra['requests'], $arguments); - } - - /** - * Execute HTTP requests with optional arguments. - * - * @param ToolDefinition $tool The tool being executed - * @param array> $requestConfigs Request configurations to execute - * @param array $arguments Arguments for variable replacement - * @return array Execution result - */ - private function executeRequests(ToolDefinition $tool, array $requestConfigs, array $arguments = []): array - { - $results = []; - - foreach ($requestConfigs as $index => $requestConfig) { - $this->logger?->info('Processing HTTP request', [ - 'index' => $index, - 'config' => $requestConfig, - ]); - - try { - // Parse request configuration - $httpRequest = $this->processRequestWithArguments($tool, $requestConfig, $arguments); - - $this->logger?->info('Executing HTTP request', [ - 'method' => $httpRequest->method, - 'url' => $httpRequest->getFullUrl(), - 'headers' => \array_keys($httpRequest->headers), - ]); - - // Execute the request based on the HTTP method - $response = $this->executeHttpRequest($httpRequest); - - // Format the response for output - $responseData = $this->formatResponse($response); - - $results[] = [ - 'response' => $responseData, - 'success' => $response->isSuccess(), - ]; - } catch (\Throwable $e) { - $this->logger?->error('HTTP request execution failed', [ - 'index' => $index, - 'error' => $e->getMessage(), - ]); - - $results[] = [ - 'error' => $e->getMessage(), - 'success' => false, - ]; - break; - } - } - - return [ - 'output' => \json_encode($results), - ]; - } - - /** - * Process a request configuration by replacing variable placeholders. - * - * @param ToolDefinition $tool The tool definition with schema information - * @param array $requestConfig The request configuration - * @param array $arguments The arguments to use for replacement - * @return HttpToolRequest The processed HTTP request - */ - private function processRequestWithArguments( - ToolDefinition $tool, - array $requestConfig, - array $arguments, - ): HttpToolRequest { - // Create arguments provider - $argsProvider = new ToolArgumentsProvider($arguments, $tool->schema); - - // Create a processor for variable replacement - $variables = $this->variables->with(new VariableReplacementProcessor($argsProvider)); - - // Process URL - if (isset($requestConfig['url'])) { - $requestConfig['url'] = $variables->resolve($requestConfig['url']); - } - - // Process headers - if (isset($requestConfig['headers']) && \is_array($requestConfig['headers'])) { - foreach ($requestConfig['headers'] as $key => $value) { - $requestConfig['headers'][$key] = $variables->resolve($value); - } - } - - $data = []; - - foreach ($arguments as $key => $value) { - $data[$key] = $value; - } - - // Process query parameters - if (isset($requestConfig['query']) && \is_array($requestConfig['query'])) { - foreach ($requestConfig['query'] as $key => $value) { - $data[$key] = $variables->resolve($value); - } - } - - // Process query parameters - if (isset($requestConfig['body']) && \is_array($requestConfig['body'])) { - foreach ($requestConfig['body'] as $key => $value) { - $data[$key] = $variables->resolve($value); - } - } - - return HttpToolRequest::fromArray($requestConfig, $data); - } - - /** - * Execute an HTTP request based on its method - */ - private function executeHttpRequest(HttpToolRequest $request): HttpResponse - { - $headers = $request->headers; - - // Set default content type for requests with body - if ($request->body !== null && $request->method !== 'GET' && !isset($headers['Content-Type'])) { - if (\is_array($request->body)) { - $headers['Content-Type'] = 'application/json'; - } else { - $headers['Content-Type'] = 'text/plain'; - } - } - - return match ($request->method) { - 'GET' => $this->httpClient->get($request->getFullUrl(), $headers), - 'POST' => $this->httpClient->post( - $request->getFullUrl(), - $headers, - $request->getBodyAsString(), - ), - default => throw new ToolExecutionException("HTTP method {$request->method} not supported yet"), - }; - } - - /** - * Format an HTTP response for the output - */ - private function formatResponse(HttpResponse $response): mixed - { - try { - return $response->getJson(); - } catch (HttpException) { - } - // If not JSON, return raw body - return $response->getBody(); - } -} diff --git a/src/McpServer/Tool/Types/RunToolHandler.php b/src/McpServer/Tool/Types/RunToolHandler.php deleted file mode 100644 index a4b541d5..00000000 --- a/src/McpServer/Tool/Types/RunToolHandler.php +++ /dev/null @@ -1,207 +0,0 @@ -executionEnabled) { - $this->logger?->warning('Command execution is disabled', [ - 'id' => $tool->id, - ]); - - throw new ToolExecutionException( - 'Command execution is disabled by configuration. Enable it by setting MCP_TOOL_COMMAND_EXECUTION=true', - ); - } - - if (empty($tool->commands)) { - throw new ToolExecutionException('Tool has no commands to execute'); - } - - return $this->executeCommands($tool, $tool->commands, $arguments); - } - - /** - * Execute commands with optional arguments. - * - * @param ToolDefinition $tool The tool being executed - * @param array $commands Commands to execute - * @param array $arguments Arguments for variable replacement - * @return array Execution result - */ - private function executeCommands(ToolDefinition $tool, array $commands, array $arguments = []): array - { - $results = []; - $success = true; - $allOutput = ''; - - foreach ($commands as $index => $command) { - $this->logger?->info('Executing command', [ - 'index' => $index, - 'command' => $command->cmd, - 'args' => \array_map(static fn(ToolArg $arg) => (string) $arg, $command->args), - ]); - - try { - $processedCommand = $this->processCommandWithArguments($tool, $command, $arguments); - - $result = $this->commandExecutor->execute($processedCommand, $tool->env); - $allOutput .= $result['output'] . PHP_EOL; - - // Create a readable command string for reporting - $commandStr = $command->cmd . ' ' . $this->formatArgsForDisplay($command->args); - - $results[] = [ - 'command' => $commandStr, - 'output' => $result['output'], - 'exitCode' => $result['exitCode'], - 'success' => $result['exitCode'] === 0, - ]; - - if ($result['exitCode'] !== 0) { - $success = false; - } - } catch (ToolExecutionException $e) { - $this->logger?->error('Command execution failed', [ - 'index' => $index, - 'command' => $command->cmd, - 'error' => $e->getMessage(), - ]); - - // Create a readable command string for reporting - $commandStr = $command->cmd . ' ' . $this->formatArgsForDisplay($command->args); - - $results[] = [ - 'command' => $commandStr, - 'output' => $e->getMessage(), - 'exitCode' => -1, - 'success' => false, - ]; - - $success = false; - break; - } - } - - return [ - 'success' => $success, - 'output' => $allOutput, - 'commands' => $results, - ]; - } - - /** - * Process a command by replacing argument placeholders. - * - * @param ToolDefinition $tool The tool definition with schema information - * @param ToolCommand $command The command to process - * @param array $arguments The arguments to use for replacement - * @return ToolCommand The processed command - */ - private function processCommandWithArguments( - ToolDefinition $tool, - ToolCommand $command, - array $arguments, - ): ToolCommand { - // Create arguments provider - $argsProvider = new ToolArgumentsProvider($arguments, $tool->schema); - - // Create a processor for variable replacement - $processor = new VariableReplacementProcessor($argsProvider); - - // Process each argument, evaluating conditions - $processedArgs = []; - foreach ($command->args as $arg) { - // If there's a condition, evaluate it - if ($arg->when !== null) { - $condition = $processor->process($arg->when); - // Skip the argument if the condition evaluates to false - if (!$this->evaluateCondition($condition)) { - $this->logger?->debug('Skipping conditional argument', [ - 'argument' => $arg->name, - 'condition' => $arg->when, - ]); - continue; - } - } - - // Process the argument name (replacing variables) - $processedArgs[] = new ToolArg( - name: $processor->process($arg->name), - ); - } - - // Return a new command with processed values - return new ToolCommand( - $command->cmd, - $processedArgs, - $command->workingDir, - $command->env, - ); - } - - /** - * Formats an array of arguments for display in logs and results. - * - * @param array $args The arguments to format - * @return string The formatted arguments string - */ - private function formatArgsForDisplay(array $args): string - { - return \implode(' ', \array_map(static fn(ToolArg $arg) => $arg->name, $args)); - } - - /** - * Evaluates a condition string to determine if an argument should be included. - * - * @param string $condition The condition string to evaluate - * @return bool Whether the condition evaluates to true - */ - private function evaluateCondition(string $condition): bool - { - // Normalize the condition string to evaluate as boolean - $normalizedCondition = \strtolower(\trim($condition)); - - // Common boolean string representations - if ($normalizedCondition === '' || - $normalizedCondition === 'false' || - $normalizedCondition === '0' || - $normalizedCondition === 'no' || - $normalizedCondition === 'n' || - $normalizedCondition === 'null' || - $normalizedCondition === 'undefined') { - return false; - } - - // Everything else is considered true - return true; - } -} diff --git a/src/McpServer/Tool/Types/ToolHandlerInterface.php b/src/McpServer/Tool/Types/ToolHandlerInterface.php deleted file mode 100644 index 8cdc33fb..00000000 --- a/src/McpServer/Tool/Types/ToolHandlerInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - Execution result - * @throws \Throwable If execution fails - */ - public function execute(ToolDefinition $tool, array $arguments = []): array; -} diff --git a/src/Research/Config/ResearchConfig.php b/src/Research/Config/ResearchConfig.php deleted file mode 100644 index 2bb482fa..00000000 --- a/src/Research/Config/ResearchConfig.php +++ /dev/null @@ -1,46 +0,0 @@ - true, - 'templates_path' => '.templates', - 'researches_path' => '.researches', - 'storage_driver' => 'markdown', - 'default_entry_status' => 'draft', - 'env_config' => [], - ]; - - public function getTemplatesPath(): string - { - return (string) $this->config['templates_path']; - } - - public function getResearchesPath(): string - { - return (string) $this->config['researches_path']; - } - - public function getStorageDriver(): string - { - return (string) $this->config['storage_driver']; - } - - public function getDefaultEntryStatus(): string - { - return (string) $this->config['default_entry_status']; - } - - public function getEnvConfig(): array - { - return (array) $this->config['env_config']; - } -} diff --git a/src/Research/Config/ResearchConfigInterface.php b/src/Research/Config/ResearchConfigInterface.php deleted file mode 100644 index a25bd787..00000000 --- a/src/Research/Config/ResearchConfigInterface.php +++ /dev/null @@ -1,33 +0,0 @@ -researchId); - - // Get research information - $research = $service->get($researchId); - if ($research === null) { - $this->output->error("Research not found: {$this->researchId}"); - return Command::FAILURE; - } - - // Get template information - $template = $templateService->getTemplate(new TemplateKey($research->template)); - - // Display research information - $this->displayInfo($research, $template); - - // Show entries if requested - $this->displayEntries($entryService, $researchId); - - // Show statistics if requested - $this->displayStatistics($entryService, $researchId); - - return Command::SUCCESS; - - } catch (\Throwable $e) { - $this->output->error('Failed to get research information: ' . $e->getMessage()); - return Command::FAILURE; - } - } - - private function displayInfo(Research $research, ?Template $template): void - { - $this->output->title("Research Information"); - - $this->output->definitionList( - ['ID', Style::property($research->id)], - ['Name', $research->name], - ['Description', $research->description ?: 'None'], - ['Status', $research->status], - ['Template', $research->template . ($template ? " ({$template->name})" : ' (template not found)')], - ['Tags', empty($research->tags) ? 'None' : \implode(', ', $research->tags)], - ['Entry Directories', empty($research->entryDirs) ? 'None' : \implode(', ', $research->entryDirs)], - ['Research Path', $research->path ?? 'Not set'], - ); - - if ($template) { - $this->output->section('Template Information'); - $this->output->definitionList( - ['Template Name', $template->name], - ['Template Description', $template->description ?: 'None'], - ['Template Tags', empty($template->tags) ? 'None' : \implode(', ', $template->tags)], - ['Categories', \count($template->categories)], - ['Entry Types', \count($template->entryTypes)], - ); - } - } - - private function displayEntries(EntryServiceInterface $entryService, ResearchId $researchId): void - { - $this->output->section('Entries'); - - try { - $entries = $entryService->findAll($researchId); - - if (empty($entries)) { - $this->output->info('No entries found in this research.'); - return; - } - - $table = new Table($this->output); - $table->setHeaders(['ID', 'Title', 'Type', 'Category', 'Status', 'Created', 'Updated', 'Tags']); - - foreach ($entries as $entry) { - $table->addRow([ - Style::property(\substr($entry->entryId, 0, 8) . '...'), - $entry->title, - $entry->entryType, - $entry->category, - $entry->status, - $entry->createdAt->format('Y-m-d H:i'), - $entry->updatedAt->format('Y-m-d H:i'), - empty($entry->tags) ? '-' : \implode(', ', $entry->tags), - ]); - } - - $table->render(); - - } catch (\Throwable $e) { - $this->output->error('Failed to load research entries: ' . $e->getMessage()); - } - } - - private function displayStatistics(EntryServiceInterface $entryService, ResearchId $researchId): void - { - $this->output->section('Statistics'); - - try { - $entries = $entryService->findAll($researchId); - - // Calculate statistics - $totalEntries = \count($entries); - $entriesByType = []; - $entriesByCategory = []; - $entriesByStatus = []; - $totalContentLength = 0; - - foreach ($entries as $entry) { - // Count by type - if (!isset($entriesByType[$entry->entryType])) { - $entriesByType[$entry->entryType] = 0; - } - $entriesByType[$entry->entryType]++; - - // Count by category - if (!isset($entriesByCategory[$entry->category])) { - $entriesByCategory[$entry->category] = 0; - } - $entriesByCategory[$entry->category]++; - - // Count by status - if (!isset($entriesByStatus[$entry->status])) { - $entriesByStatus[$entry->status] = 0; - } - $entriesByStatus[$entry->status]++; - - // Content length - $totalContentLength += \strlen($entry->content); - } - - $this->output->definitionList( - ['Total Entries', (string) $totalEntries], - ['Total Content Length', \number_format($totalContentLength) . ' characters'], - ['Average Content Length', $totalEntries > 0 ? \number_format($totalContentLength / $totalEntries) . ' characters' : '0'], - ); - - if (!empty($entriesByType)) { - $this->output->writeln("\nEntries by Type:"); - foreach ($entriesByType as $type => $count) { - $this->output->writeln(" • {$type}: {$count}"); - } - } - - if (!empty($entriesByCategory)) { - $this->output->writeln("\nEntries by Category:"); - foreach ($entriesByCategory as $category => $count) { - $this->output->writeln(" • {$category}: {$count}"); - } - } - - if (!empty($entriesByStatus)) { - $this->output->writeln("\nEntries by Status:"); - foreach ($entriesByStatus as $status => $count) { - $this->output->writeln(" • {$status}: {$count}"); - } - } - - } catch (\Throwable $e) { - $this->output->error('Failed to calculate research statistics: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/Console/ResearchListCommand.php b/src/Research/Console/ResearchListCommand.php deleted file mode 100644 index e3ba162b..00000000 --- a/src/Research/Console/ResearchListCommand.php +++ /dev/null @@ -1,73 +0,0 @@ -status !== null) { - $filters['status'] = $this->status; - } - - if ($this->template !== null) { - $filters['template'] = $this->template; - } - - try { - $researches = $service->findAll($filters); - - if (empty($researches)) { - $this->output->info('No researches found.'); - return Command::SUCCESS; - } - - $this->output->title('Researches'); - - $table = new Table($this->output); - $table->setHeaders(['ID', 'Name', 'Status', 'Template', 'Description', 'Tags']); - - foreach ($researches as $research) { - $table->addRow([ - Style::property($research->id), - $research->name, - $research->status, - $research->template, - $research->description ?: '-', - \implode(', ', $research->tags), - ]); - } - - $table->render(); - - return Command::SUCCESS; - - } catch (\Throwable $e) { - $this->output->error('Failed to list researches: ' . $e->getMessage()); - return Command::FAILURE; - } - } -} diff --git a/src/Research/Console/TemplateListCommand.php b/src/Research/Console/TemplateListCommand.php deleted file mode 100644 index 0868941c..00000000 --- a/src/Research/Console/TemplateListCommand.php +++ /dev/null @@ -1,88 +0,0 @@ -findAll(); - - // Apply filters - if ($this->tag !== null) { - $templates = \array_filter( - $templates, - fn(Template $template) => - \in_array($this->tag, $template->tags, true), - ); - } - - if ($this->nameFilter !== null) { - $searchTerm = \strtolower(\trim($this->nameFilter)); - $templates = \array_filter( - $templates, - static fn($template) => - \str_contains(\strtolower($template->name), $searchTerm), - ); - } - - if (empty($templates)) { - $this->output->info('No templates found.'); - return Command::SUCCESS; - } - - $this->output->title('Templates'); - - foreach ($templates as $template) { - $this->displayDetails($template); - } - - return Command::SUCCESS; - - } catch (\Throwable $e) { - $this->output->error('Failed to list templates: ' . $e->getMessage()); - return Command::FAILURE; - } - } - - private function displayDetails(Template $template): void - { - $this->output->section($template->name); - $this->output->writeln("ID: " . Style::property($template->key)); - $this->output->writeln("Description: " . ($template->description ?: 'None')); - $this->output->writeln("Tags: " . \implode(', ', $template->tags)); - - if (!empty($template->categories)) { - $this->output->writeln("\nCategories:"); - foreach ($template->categories as $category) { - $this->output->writeln(" • {$category->displayName} ({$category->name})"); - if (!empty($category->entryTypes)) { - $this->output->writeln(" Entry types: " . \implode(', ', $category->entryTypes)); - } - } - } - - $this->output->newLine(); - } -} diff --git a/src/Research/Domain/Model/Category.php b/src/Research/Domain/Model/Category.php deleted file mode 100644 index 8382538a..00000000 --- a/src/Research/Domain/Model/Category.php +++ /dev/null @@ -1,30 +0,0 @@ -entryTypes, true); - } -} diff --git a/src/Research/Domain/Model/Entry.php b/src/Research/Domain/Model/Entry.php deleted file mode 100644 index 29c8089c..00000000 --- a/src/Research/Domain/Model/Entry.php +++ /dev/null @@ -1,80 +0,0 @@ -entryId, - title: $title ?? $this->title, - description: $description ?? $this->description, - entryType: $this->entryType, - category: $this->category, - status: $status ?? $this->status, - createdAt: $this->createdAt, - updatedAt: new \DateTime(), - tags: $tags ?? $this->tags, - content: $content ?? $this->content, - filePath: $this->filePath, - ); - } - - /** - * Specify data which should be serialized to JSON - */ - public function jsonSerialize(): array - { - return [ - 'entry_id' => $this->entryId, - 'title' => $this->title, - 'description' => $this->description, - 'entry_type' => $this->entryType, - 'category' => $this->category, - 'status' => $this->status, - 'tags' => $this->tags, - 'content' => $this->content, - ]; - } -} diff --git a/src/Research/Domain/Model/EntryType.php b/src/Research/Domain/Model/EntryType.php deleted file mode 100644 index 37ebbd0e..00000000 --- a/src/Research/Domain/Model/EntryType.php +++ /dev/null @@ -1,47 +0,0 @@ -statuses as $status) { - if ($status->value === $value) { - return $status; - } - } - return null; - } - - /** - * Check if status is valid for this entry type - */ - public function hasStatus(string $value): bool - { - return $this->getStatus($value) !== null; - } -} diff --git a/src/Research/Domain/Model/Research.php b/src/Research/Domain/Model/Research.php deleted file mode 100644 index de365b92..00000000 --- a/src/Research/Domain/Model/Research.php +++ /dev/null @@ -1,91 +0,0 @@ -id, - name: $name ?? $this->name, - description: $description ?? $this->description, - template: $this->template, - status: $status ?? $this->status, - tags: $tags ?? $this->tags, - entryDirs: $entryDirs ?? $this->entryDirs, - memory: $memory ?? $this->memory, - path: $this->path, - ); - } - - /** - * Create research with added memory entry - */ - public function withAddedMemory(string $memoryEntry): self - { - return new self( - id: $this->id, - name: $this->name, - description: $this->description, - template: $this->template, - status: $this->status, - tags: $this->tags, - entryDirs: $this->entryDirs, - memory: [...$this->memory, $memoryEntry], - path: $this->path, - ); - } - - /** - * Specify data which should be serialized to JSON - */ - public function jsonSerialize(): array - { - return [ - 'research_id' => $this->id, - 'title' => $this->name, - 'status' => $this->status, - 'research_type' => $this->template, - 'metadata' => [ - 'description' => $this->description, - 'tags' => $this->tags, - 'memory' => $this->memory, - ], - ]; - } -} diff --git a/src/Research/Domain/Model/Status.php b/src/Research/Domain/Model/Status.php deleted file mode 100644 index ec993247..00000000 --- a/src/Research/Domain/Model/Status.php +++ /dev/null @@ -1,16 +0,0 @@ -categories as $category) { - if ($category->name === $name) { - return $category; - } - } - return null; - } - - /** - * Get entry type by key - */ - public function getEntryType(string $key): ?EntryType - { - foreach ($this->entryTypes as $entryType) { - if ($entryType->key === $key) { - return $entryType; - } - } - return null; - } - - /** - * Check if category exists in template - */ - public function hasCategory(string $name): bool - { - return $this->getCategory($name) !== null; - } - - /** - * Check if entry type exists in template - */ - public function hasEntryType(string $key): bool - { - return $this->getEntryType($key) !== null; - } - - /** - * Validate entry type is allowed in category - */ - public function validateEntryInCategory(string $categoryName, string $entryTypeKey): bool - { - $category = $this->getCategory($categoryName); - if ($category === null) { - return false; - } - - return $category->allowsEntryType($entryTypeKey); - } - - public function jsonSerialize(): array - { - $formatted = [ - 'template_id' => $this->key, - 'name' => $this->name, - 'description' => $this->description, - 'tags' => $this->tags, - ]; - - $formatted['categories'] = \array_map(static fn($category) => [ - 'name' => $category->name, - 'display_name' => $category->displayName, - 'allowed_entry_types' => $category->entryTypes, - ], $this->categories); - - $formatted['entry_types'] = \array_map(static fn($entryType) => [ - 'key' => $entryType->key, - 'display_name' => $entryType->displayName, - 'default_status' => $entryType->defaultStatus, - 'statuses' => \array_map(static fn($status) => $status->value, $entryType->statuses), - ], $this->entryTypes); - - if ($this->prompt !== null) { - $formatted['prompt'] = $this->prompt; - } - - return $formatted; - } -} diff --git a/src/Research/Domain/ValueObject/EntryId.php b/src/Research/Domain/ValueObject/EntryId.php deleted file mode 100644 index efe6fceb..00000000 --- a/src/Research/Domain/ValueObject/EntryId.php +++ /dev/null @@ -1,48 +0,0 @@ -value))) { - throw new \InvalidArgumentException('Entry ID cannot be empty'); - } - } - - /** - * Generate new UUID-based entry ID - */ - public static function generate(): self - { - return new self(\uniqid('entry_', true)); - } - - /** - * Create from string - */ - public static function fromString(string $value): self - { - return new self($value); - } - - /** - * Check equality with another EntryId - */ - public function equals(self $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} diff --git a/src/Research/Domain/ValueObject/ResearchId.php b/src/Research/Domain/ValueObject/ResearchId.php deleted file mode 100644 index 0530952c..00000000 --- a/src/Research/Domain/ValueObject/ResearchId.php +++ /dev/null @@ -1,36 +0,0 @@ -value))) { - throw new \InvalidArgumentException('Research ID cannot be empty'); - } - } - - public static function generate(): self - { - return new self(\uniqid('research_', true)); - } - - public static function fromString(string $value): self - { - return new self($value); - } - - public function equals(self $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} diff --git a/src/Research/Domain/ValueObject/TemplateKey.php b/src/Research/Domain/ValueObject/TemplateKey.php deleted file mode 100644 index 77a74dc7..00000000 --- a/src/Research/Domain/ValueObject/TemplateKey.php +++ /dev/null @@ -1,40 +0,0 @@ -value))) { - throw new \InvalidArgumentException('Template key cannot be empty'); - } - } - - /** - * Create from string - */ - public static function fromString(string $value): self - { - return new self($value); - } - - /** - * Check equality with another TemplateKey - */ - public function equals(self $other): bool - { - return $this->value === $other->value; - } - - public function __toString(): string - { - return $this->value; - } -} diff --git a/src/Research/Exception/EntryNotFoundException.php b/src/Research/Exception/EntryNotFoundException.php deleted file mode 100644 index 1ef9dcd7..00000000 --- a/src/Research/Exception/EntryNotFoundException.php +++ /dev/null @@ -1,10 +0,0 @@ -errors)) { - $errorMessage .= ': ' . \implode(', ', $this->errors); - } - - parent::__construct($errorMessage, $code, $previous); - } - - /** - * Create from array of errors - * - * @param string[] $errors - */ - public static function fromErrors(array $errors, string $message = 'Validation failed'): self - { - return new self($errors, $message); - } -} diff --git a/src/Research/MCP/DTO/EntryCreateRequest.php b/src/Research/MCP/DTO/EntryCreateRequest.php deleted file mode 100644 index bfc82967..00000000 --- a/src/Research/MCP/DTO/EntryCreateRequest.php +++ /dev/null @@ -1,170 +0,0 @@ -title !== null && !empty(\trim($this->title))) { - return \trim($this->title); - } - - // Generate title from first line of content - $lines = \explode("\n", \trim($this->content)); - $firstLine = \trim($lines[0] ?? ''); - - if (empty($firstLine)) { - return 'Untitled Entry'; - } - - // Remove markdown heading markers - $title = \preg_replace('/^#+\s*/', '', $firstLine); - - // Limit title length - if (\strlen((string) $title) > 100) { - $title = \substr((string) $title, 0, 100) . '...'; - } - - return \trim((string) $title) ?: 'Untitled Entry'; - } - - /** - * Get the processed description for entry creation - * This should be called by the service layer to ensure consistent description handling - */ - public function getProcessedDescription(): string - { - if ($this->description !== null && !empty(\trim($this->description))) { - $desc = \trim($this->description); - // Limit to 200 characters - return \strlen($desc) > 200 ? \substr($desc, 0, 197) . '...' : $desc; - } - - // Generate description from content summary - $cleanContent = \strip_tags($this->content); - $lines = \explode("\n", \trim($cleanContent)); - - // Skip title line and get summary from content - $contentLines = \array_filter(\array_slice($lines, 1), static fn($line) => !empty(\trim($line))); - - if (empty($contentLines)) { - return 'Entry content'; - } - - $summary = \implode(' ', \array_slice($contentLines, 0, 3)); - $summary = \preg_replace('/\s+/', ' ', $summary) ?? $summary; - - return \strlen($summary) > 200 ? \substr($summary, 0, 197) . '...' : $summary; - } - - /** - * @deprecated Use getProcessedTitle() instead for consistency - */ - public function getTitle(): string - { - return $this->getProcessedTitle(); - } - - /** - * Validate the request data - */ - public function validate(): array - { - $errors = []; - - if (empty($this->researchId)) { - $errors[] = 'Research ID cannot be empty'; - } - - if (empty($this->category)) { - $errors[] = 'Category cannot be empty'; - } - - if (empty($this->entryType)) { - $errors[] = 'Entry type cannot be empty'; - } - - if (empty(\trim($this->content))) { - $errors[] = 'Content cannot be empty'; - } - - // Validate tags if provided - foreach ($this->tags as $tag) { - if (!\is_string($tag) || empty(\trim($tag))) { - $errors[] = 'All tags must be non-empty strings'; - break; - } - } - - // Validate description length if provided - if ($this->description !== null && \strlen(\trim($this->description)) > 200) { - $errors[] = 'Description must not exceed 200 characters'; - } - - return $errors; - } - - /** - * Create a copy with resolved internal keys (to be used by services after template lookup) - */ - public function withResolvedKeys( - string $resolvedCategory, - string $resolvedEntryType, - ?string $resolvedStatus = null, - ): self { - return new self( - researchId: $this->researchId, - category: $resolvedCategory, - entryType: $resolvedEntryType, - content: $this->content, - title: $this->title, - description: $this->description, - status: $resolvedStatus ?? $this->status, - tags: $this->tags, - ); - } -} diff --git a/src/Research/MCP/DTO/EntryFilters.php b/src/Research/MCP/DTO/EntryFilters.php deleted file mode 100644 index 543200a1..00000000 --- a/src/Research/MCP/DTO/EntryFilters.php +++ /dev/null @@ -1,141 +0,0 @@ -category !== null) { - $filters['category'] = $this->category; - } - - if ($this->entryType !== null) { - $filters['entry_type'] = $this->entryType; - } - - if ($this->status !== null) { - $filters['status'] = $this->status; - } - - if ($this->tags !== null && !empty($this->tags)) { - $filters['tags'] = $this->tags; - } - - if ($this->titleContains !== null) { - $filters['title_contains'] = $this->titleContains; - } - - if ($this->descriptionContains !== null) { - $filters['description_contains'] = $this->descriptionContains; - } - - if ($this->contentContains !== null) { - $filters['content_contains'] = $this->contentContains; - } - - return $filters; - } - - /** - * Check if any filters are applied - */ - public function hasFilters(): bool - { - return $this->category !== null - || $this->entryType !== null - || $this->status !== null - || ($this->tags !== null && !empty($this->tags)) - || $this->titleContains !== null - || $this->descriptionContains !== null - || $this->contentContains !== null; - } - - /** - * Validate the filters - */ - public function validate(): array - { - $errors = []; - - // Validate tags array if provided - if ($this->tags !== null) { - if (empty($this->tags)) { - $errors[] = 'Tags array cannot be empty when provided'; - } else { - foreach ($this->tags as $tag) { - if (!\is_string($tag) || empty(\trim($tag))) { - $errors[] = 'All tags must be non-empty strings'; - break; - } - } - } - } - - // Validate text filters if provided - $textFilters = [ - 'titleContains' => $this->titleContains, - 'descriptionContains' => $this->descriptionContains, - 'contentContains' => $this->contentContains, - ]; - - foreach ($textFilters as $field => $value) { - if ($value !== null && empty(\trim($value))) { - $errors[] = "{$field} filter cannot be empty when provided"; - } - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/EntryUpdateRequest.php b/src/Research/MCP/DTO/EntryUpdateRequest.php deleted file mode 100644 index e79e9807..00000000 --- a/src/Research/MCP/DTO/EntryUpdateRequest.php +++ /dev/null @@ -1,191 +0,0 @@ -title !== null - || $this->description !== null - || $this->content !== null - || $this->status !== null - || $this->contentType !== null - || $this->tags !== null - || $this->textReplace !== null; - } - - /** - * Get processed content applying text replacement if needed - * This method should be called by the service layer to ensure proper content handling - */ - public function getProcessedContent(?string $existingContent = null): ?string - { - $baseContent = $this->content ?? $existingContent; - - if ($baseContent === null || $this->textReplace === null) { - return $this->content; - } - - return \str_replace($this->textReplace->find, $this->textReplace->replace, $baseContent); - } - - /** - * Get the final content that should be saved - * Considers both direct content updates and text replacement operations - */ - public function getFinalContent(?string $existingContent = null): ?string - { - // If we have direct content update, use it as base - if ($this->content !== null) { - $baseContent = $this->content; - } else { - $baseContent = $existingContent; - } - - // Apply text replacement if specified - if ($this->textReplace !== null && $baseContent !== null) { - return \str_replace($this->textReplace->find, $this->textReplace->replace, $baseContent); - } - - return $this->content; // Return direct content update or null - } - - /** - * Validate the request data - */ - public function validate(): array - { - $errors = []; - - if (empty($this->researchId)) { - $errors[] = 'Research ID cannot be empty'; - } - - if (empty($this->entryId)) { - $errors[] = 'Entry ID cannot be empty'; - } - - if (!$this->hasUpdates()) { - $errors[] = 'At least one field must be provided for update'; - } - - // Validate tags if provided - if ($this->tags !== null) { - foreach ($this->tags as $tag) { - if (!\is_string($tag) || empty(\trim($tag))) { - $errors[] = 'All tags must be non-empty strings'; - break; - } - } - } - - // Validate description length if provided - if ($this->description !== null && \strlen(\trim($this->description)) > 200) { - $errors[] = 'Description must not exceed 200 characters'; - } - - // Validate text replace if provided - if ($this->textReplace !== null) { - $replaceErrors = $this->textReplace->validate(); - $errors = \array_merge($errors, $replaceErrors); - } - - return $errors; - } - - /** - * Create a copy with resolved internal keys (to be used by services after template lookup) - */ - public function withResolvedStatus(?string $resolvedStatus): self - { - return new self( - researchId: $this->researchId, - entryId: $this->entryId, - title: $this->title, - description: $this->description, - content: $this->content, - status: $resolvedStatus, - contentType: $this->contentType, - tags: $this->tags, - textReplace: $this->textReplace, - ); - } -} - -/** - * Nested DTO for text replace operations - */ -final readonly class TextReplaceRequest -{ - public function __construct( - #[Field(description: 'Text to find')] - public string $find, - #[Field(description: 'Replacement text')] - public string $replace, - ) {} - - /** - * Validate text replace request - */ - public function validate(): array - { - $errors = []; - - if (empty($this->find)) { - $errors[] = 'Find text cannot be empty for text replacement'; - } - - // Note: replace text can be empty (for deletion) - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/GetResearchRequest.php b/src/Research/MCP/DTO/GetResearchRequest.php deleted file mode 100644 index 801c0fa4..00000000 --- a/src/Research/MCP/DTO/GetResearchRequest.php +++ /dev/null @@ -1,32 +0,0 @@ -id)) { - $errors[] = 'Research ID cannot be empty'; - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/ListEntriesRequest.php b/src/Research/MCP/DTO/ListEntriesRequest.php deleted file mode 100644 index 79a55043..00000000 --- a/src/Research/MCP/DTO/ListEntriesRequest.php +++ /dev/null @@ -1,82 +0,0 @@ -researchId))) { - $errors[] = 'Research ID is required'; - } - - - // Validate pagination parameters - if ($this->limit < 1 || $this->limit > 200) { - $errors[] = 'Limit must be between 1 and 200'; - } - - if ($this->offset < 0) { - $errors[] = 'Offset must be non-negative'; - } - - // Validate filters if provided - if ($this->filters !== null) { - $filterErrors = $this->filters->validate(); - $errors = \array_merge($errors, $filterErrors); - } - - return $errors; - } - - /** - * Check if filters are applied - */ - public function hasFilters(): bool - { - return $this->filters !== null && $this->filters->hasFilters(); - } - - /** - * Get filters as array - */ - public function getFilters(): array - { - return $this->filters?->toArray() ?? []; - } -} diff --git a/src/Research/MCP/DTO/ListResearchesRequest.php b/src/Research/MCP/DTO/ListResearchesRequest.php deleted file mode 100644 index 93f2ad4d..00000000 --- a/src/Research/MCP/DTO/ListResearchesRequest.php +++ /dev/null @@ -1,90 +0,0 @@ -filters === null) { - return []; - } - - return $this->filters->toArray(); - } - - /** - * Get pagination options - */ - public function getPaginationOptions(): array - { - return [ - 'limit' => $this->limit, - 'offset' => $this->offset, - ]; - } - - /** - * Check if any filters are applied - */ - public function hasFilters(): bool - { - return $this->filters !== null && $this->filters->hasFilters(); - } - - /** - * Validate the request - */ - public function validate(): array - { - $errors = []; - - // Validate pagination - if ($this->limit < 1 || $this->limit > 100) { - $errors[] = 'Limit must be between 1 and 100'; - } - - if ($this->offset < 0) { - $errors[] = 'Offset must be non-negative'; - } - - // Validate filters if provided - if ($this->filters !== null) { - $filterErrors = $this->filters->validate(); - $errors = \array_merge($errors, $filterErrors); - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/ListTemplatesRequest.php b/src/Research/MCP/DTO/ListTemplatesRequest.php deleted file mode 100644 index b4ce3960..00000000 --- a/src/Research/MCP/DTO/ListTemplatesRequest.php +++ /dev/null @@ -1,57 +0,0 @@ -tag !== null || $this->nameContains !== null; - } - - /** - * Validate the request - */ - public function validate(): array - { - $errors = []; - - if ($this->tag !== null && empty(\trim($this->tag))) { - $errors[] = 'Tag filter cannot be empty when provided'; - } - - if ($this->nameContains !== null && empty(\trim($this->nameContains))) { - $errors[] = 'Name filter cannot be empty when provided'; - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/ReadEntryRequest.php b/src/Research/MCP/DTO/ReadEntryRequest.php deleted file mode 100644 index 20169717..00000000 --- a/src/Research/MCP/DTO/ReadEntryRequest.php +++ /dev/null @@ -1,46 +0,0 @@ -researchId))) { - $errors[] = 'Research ID is required'; - } - - // Validate entry ID - if (empty(\trim($this->entryId))) { - $errors[] = 'Entry ID is required'; - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/ResearchCreateRequest.php b/src/Research/MCP/DTO/ResearchCreateRequest.php deleted file mode 100644 index 7265e896..00000000 --- a/src/Research/MCP/DTO/ResearchCreateRequest.php +++ /dev/null @@ -1,58 +0,0 @@ -templateId))) { - $errors[] = 'Template ID cannot be empty'; - } - - if (empty(\trim($this->title))) { - $errors[] = 'Research title cannot be empty'; - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/ResearchFilters.php b/src/Research/MCP/DTO/ResearchFilters.php deleted file mode 100644 index 32660b45..00000000 --- a/src/Research/MCP/DTO/ResearchFilters.php +++ /dev/null @@ -1,102 +0,0 @@ -status !== null) { - $filters['status'] = $this->status; - } - - if ($this->template !== null) { - $filters['template'] = $this->template; - } - - if ($this->tags !== null && !empty($this->tags)) { - $filters['tags'] = $this->tags; - } - - if ($this->nameContains !== null) { - $filters['name_contains'] = $this->nameContains; - } - - return $filters; - } - - /** - * Check if any filters are applied - */ - public function hasFilters(): bool - { - return $this->status !== null - || $this->template !== null - || (!empty($this->tags)) - || $this->nameContains !== null; - } - - /** - * Validate the filters - */ - public function validate(): array - { - $errors = []; - - // Validate tags array if provided - if ($this->tags !== null) { - if (empty($this->tags)) { - $errors[] = 'Tags array cannot be empty when provided'; - } else { - foreach ($this->tags as $tag) { - if (!\is_string($tag) || empty(\trim($tag))) { - $errors[] = 'All tags must be non-empty strings'; - break; - } - } - } - } - - // Validate nameContains if provided - if ($this->nameContains !== null && empty(\trim($this->nameContains))) { - $errors[] = 'Name filter cannot be empty when provided'; - } - - return $errors; - } -} diff --git a/src/Research/MCP/DTO/ResearchMemory.php b/src/Research/MCP/DTO/ResearchMemory.php deleted file mode 100644 index d7d4a9ef..00000000 --- a/src/Research/MCP/DTO/ResearchMemory.php +++ /dev/null @@ -1,12 +0,0 @@ -title !== null - || $this->description !== null - || $this->status !== null - || $this->tags !== null - || $this->entryDirs !== null - || $this->memory !== null; - } - - /** - * Validate the request data - */ - public function validate(): array - { - $errors = []; - - if (empty($this->researchId)) { - $errors[] = 'Research ID cannot be empty'; - } - - if (!$this->hasUpdates()) { - $errors[] = 'At least one field must be provided for update'; - } - - return $errors; - } -} diff --git a/src/Research/MCP/Tools/CreateEntryToolAction.php b/src/Research/MCP/Tools/CreateEntryToolAction.php deleted file mode 100644 index 20c55f72..00000000 --- a/src/Research/MCP/Tools/CreateEntryToolAction.php +++ /dev/null @@ -1,105 +0,0 @@ -logger->info('Creating new entry', [ - 'research_id' => $request->researchId, - 'category' => $request->category, - 'entry_type' => $request->entryType, - 'has_description' => $request->description !== null, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Verify research exists - $researchId = ResearchId::fromString($request->researchId); - if (!$this->service->exists($researchId)) { - return ToolResult::error("Research '{$request->researchId}' not found"); - } - - // Create entry using domain service - $entry = $this->entryService->createEntry($researchId, $request); - - $this->logger->info('Entry created successfully', [ - 'research_id' => $request->researchId, - 'entry_id' => $entry->entryId, - 'title' => $entry->title, - ]); - - // Format successful response according to MCP specification - $response = [ - 'success' => true, - 'entry_id' => $entry->entryId, - 'title' => $entry->title, - 'entry_type' => $entry->entryType, - 'category' => $entry->category, - 'status' => $entry->status, - 'content_type' => 'markdown', - 'created_at' => $entry->createdAt->format('c'), - ]; - - return ToolResult::success($response); - - } catch (ResearchNotFoundException $e) { - $this->logger->error('Research not found', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Research error during entry creation', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error creating entry', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to create entry: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/MCP/Tools/CreateResearchToolAction.php b/src/Research/MCP/Tools/CreateResearchToolAction.php deleted file mode 100644 index 85b6d450..00000000 --- a/src/Research/MCP/Tools/CreateResearchToolAction.php +++ /dev/null @@ -1,100 +0,0 @@ -logger->info('Creating new research', [ - 'template_id' => $request->templateId, - 'title' => $request->title, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Verify template exists - $templateKey = TemplateKey::fromString($request->templateId); - if (!$this->templateService->templateExists($templateKey)) { - return ToolResult::error("Template '{$request->templateId}' not found"); - } - - // Create research using domain service - $research = $this->service->create($request); - - $this->logger->info('Research created successfully', [ - 'research_id' => $research->id, - 'template' => $research->template, - ]); - - // Format successful response according to MCP specification - $response = [ - 'success' => true, - 'research_id' => $research->id, - 'title' => $research->name, - 'template_id' => $research->template, - 'status' => $research->status, - 'created_at' => (new \DateTime())->format('c'), - ]; - - return ToolResult::success($response); - - } catch (TemplateNotFoundException $e) { - $this->logger->error('Template not found', [ - 'template_id' => $request->templateId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Error during research creation', [ - 'template_id' => $request->templateId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error creating research', [ - 'template_id' => $request->templateId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to create research: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/MCP/Tools/GetResearchToolAction.php b/src/Research/MCP/Tools/GetResearchToolAction.php deleted file mode 100644 index e80bbdfd..00000000 --- a/src/Research/MCP/Tools/GetResearchToolAction.php +++ /dev/null @@ -1,105 +0,0 @@ -logger->info('Getting research', [ - 'research_id' => $request->id, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Get research - $researchId = ResearchId::fromString($request->id); - $research = $this->service->get($researchId); - - if ($research === null) { - return ToolResult::error("Research '{$request->id}' not found"); - } - - $this->logger->info('Research retrieved successfully', [ - 'research_id' => $research->id, - 'template' => $research->template, - ]); - - $template = $this->templateService->getTemplate(TemplateKey::fromString($research->template)); - - // Format research for response - return ToolResult::success([ - 'success' => true, - 'research' => [ - 'id' => $research->id, - 'title' => $research->name, - 'status' => $research->status, - 'metadata' => [ - 'description' => $research->description, - 'tags' => $research->tags, - 'memory' => $research->memory, - ], - ], - 'template' => $template, - ]); - - } catch (ResearchNotFoundException $e) { - $this->logger->error('Research not found', [ - 'research_id' => $request->id, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Error getting research', [ - 'research_id' => $request->id, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error getting research', [ - 'research_id' => $request->id, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to get research: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/MCP/Tools/ListEntriesToolAction.php b/src/Research/MCP/Tools/ListEntriesToolAction.php deleted file mode 100644 index 3fbe6c3e..00000000 --- a/src/Research/MCP/Tools/ListEntriesToolAction.php +++ /dev/null @@ -1,125 +0,0 @@ -logger->info('Listing entries', [ - 'research_id' => $request->researchId, - 'has_filters' => $request->hasFilters(), - 'filters' => $request->getFilters(), - 'limit' => $request->limit, - 'offset' => $request->offset, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Verify research exists - $researchId = ResearchId::fromString($request->researchId); - if (!$this->service->exists($researchId)) { - return ToolResult::error("Research '{$request->researchId}' not found"); - } - - // Get entries with filters - $allEntries = $this->entryService->findAll($researchId, $request->getFilters()); - - // Apply pagination - $paginatedEntries = \array_slice( - $allEntries, - $request->offset, - $request->limit, - ); - - // Format entries for response (using JsonSerializable) - $entryData = \array_map(static function (Entry $entry) { - $data = $entry->jsonSerialize(); - unset($data['content']); - - return $data; - - }, $paginatedEntries); - - $response = [ - 'success' => true, - 'entries' => $entryData, - 'count' => \count($paginatedEntries), - 'total_count' => \count($allEntries), - 'pagination' => [ - 'limit' => $request->limit, - 'offset' => $request->offset, - 'has_more' => ($request->offset + \count($paginatedEntries)) < \count($allEntries), - ], - 'filters_applied' => $request->hasFilters() ? $request->getFilters() : null, - ]; - - $this->logger->info('Entries listed successfully', [ - 'research_id' => $request->researchId, - 'returned_count' => \count($paginatedEntries), - 'total_available' => \count($allEntries), - 'filters_applied' => $request->hasFilters(), - ]); - - return ToolResult::success($response); - - } catch (ResearchNotFoundException $e) { - $this->logger->error('Research not found', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Error listing research entries', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error listing entries', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to list entries: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/MCP/Tools/ListResearchesToolAction.php b/src/Research/MCP/Tools/ListResearchesToolAction.php deleted file mode 100644 index cfb72c87..00000000 --- a/src/Research/MCP/Tools/ListResearchesToolAction.php +++ /dev/null @@ -1,92 +0,0 @@ -logger->info('Listing researches', [ - 'has_filters' => $request->hasFilters(), - 'filters' => $request->getFilters(), - 'limit' => $request->limit, - 'offset' => $request->offset, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Get researches with filters - $researches = $this->service->findAll($request->getFilters()); - - // Apply pagination - $paginatedResearches = \array_slice( - $researches, - $request->offset, - $request->limit, - ); - - $response = [ - 'success' => true, - 'researches' => $paginatedResearches, - 'count' => \count($paginatedResearches), - 'total_count' => \count($researches), - 'pagination' => [ - 'limit' => $request->limit, - 'offset' => $request->offset, - 'has_more' => ($request->offset + \count($paginatedResearches)) < \count($researches), - ], - ]; - - $this->logger->info('Researches listed successfully', [ - 'returned_count' => \count($paginatedResearches), - 'total_available' => \count($researches), - 'filters_applied' => $request->hasFilters(), - ]); - - return ToolResult::success($response); - - } catch (ResearchException $e) { - $this->logger->error('Error listing researches', [ - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error listing researches', [ - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to list researches: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/MCP/Tools/ListTemplatesToolAction.php b/src/Research/MCP/Tools/ListTemplatesToolAction.php deleted file mode 100644 index 1a0c3484..00000000 --- a/src/Research/MCP/Tools/ListTemplatesToolAction.php +++ /dev/null @@ -1,111 +0,0 @@ -logger->info('Listing templates', [ - 'has_filters' => $request->hasFilters(), - 'tag_filter' => $request->tag, - 'name_filter' => $request->nameContains, - 'include_details' => $request->includeDetails, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Get all templates - $allTemplates = $this->templateService->findAll(); - - // Apply filters - $filteredTemplates = $this->applyFilters($allTemplates, $request); - - $response = [ - 'success' => true, - 'templates' => $filteredTemplates, - ]; - - $this->logger->info('Templates listed successfully', [ - 'total_available' => \count($allTemplates), - 'filters_applied' => $request->hasFilters(), - ]); - - return ToolResult::success($response); - - } catch (ResearchException $e) { - $this->logger->error('Error listing research templates', [ - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error listing templates', [ - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to list templates: ' . $e->getMessage()); - } - } - - /** - * Apply filters to templates array - */ - private function applyFilters(array $templates, ListTemplatesRequest $request): array - { - if (!$request->hasFilters()) { - return $templates; - } - - return \array_filter($templates, static function ($template) use ($request) { - // Filter by tag - if ($request->tag !== null) { - if (!\in_array($request->tag, $template->tags, true)) { - return false; - } - } - - // Filter by name (partial match, case insensitive) - if ($request->nameContains !== null) { - $searchTerm = \strtolower(\trim($request->nameContains)); - $templateName = \strtolower((string) $template->name); - - if (!\str_contains($templateName, $searchTerm)) { - return false; - } - } - - return true; - }); - } -} diff --git a/src/Research/MCP/Tools/ReadEntryToolAction.php b/src/Research/MCP/Tools/ReadEntryToolAction.php deleted file mode 100644 index c83f2603..00000000 --- a/src/Research/MCP/Tools/ReadEntryToolAction.php +++ /dev/null @@ -1,109 +0,0 @@ -logger->info('Reading entry', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - // Verify research exists - $researchId = ResearchId::fromString($request->researchId); - if (!$this->service->exists($researchId)) { - return ToolResult::error("Research '{$request->researchId}' not found"); - } - - // Get the entry - $entryId = EntryId::fromString($request->entryId); - $entry = $this->entryService->getEntry($researchId, $entryId); - - if ($entry === null) { - return ToolResult::error("Entry '{$request->entryId}' not found in research '{$request->researchId}'"); - } - - $this->logger->info('Entry read successfully', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'title' => $entry->title, - ]); - - return ToolResult::success($entry); - - } catch (ResearchNotFoundException $e) { - $this->logger->error('Research not found', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (EntryNotFoundException $e) { - $this->logger->error('Entry not found', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Error reading research entry', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error reading entry', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to read entry: ' . $e->getMessage()); - } - } -} diff --git a/src/Research/MCP/Tools/UpdateEntryToolAction.php b/src/Research/MCP/Tools/UpdateEntryToolAction.php deleted file mode 100644 index f6d14bbb..00000000 --- a/src/Research/MCP/Tools/UpdateEntryToolAction.php +++ /dev/null @@ -1,150 +0,0 @@ -logger->info('Updating entry', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'has_title' => $request->title !== null, - 'has_description' => $request->description !== null, - 'has_content' => $request->content !== null, - 'has_status' => $request->status !== null, - 'has_tags' => $request->tags !== null, - 'has_text_replace' => $request->textReplace !== null, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - $researchId = ResearchId::fromString($request->researchId); - if (!$this->service->exists($researchId)) { - return ToolResult::error("Research '{$request->researchId}' not found"); - } - - // Verify entry exists - $entryId = EntryId::fromString($request->entryId); - if (!$this->entryService->entryExists($researchId, $entryId)) { - return ToolResult::error("Entry '{$request->entryId}' not found in research '{$request->researchId}'"); - } - - // Update entry using domain service - $updatedEntry = $this->entryService->updateEntry($researchId, $entryId, $request); - - $this->logger->info('Entry updated successfully', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'title' => $updatedEntry->title, - ]); - - return ToolResult::success([ - 'success' => true, - ]); - } catch (ResearchNotFoundException $e) { - $this->logger->error('Research not found', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (EntryNotFoundException $e) { - $this->logger->error('Entry not found', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Error during research entry update', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error updating entry', [ - 'research_id' => $request->researchId, - 'entry_id' => $request->entryId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to update entry: ' . $e->getMessage()); - } - } - - /** - * Get list of changes applied based on the request - */ - private function getAppliedChanges(EntryUpdateRequest $request): array - { - $changes = []; - - if ($request->title !== null) { - $changes[] = 'title'; - } - - if ($request->description !== null) { - $changes[] = 'description'; - } - - if ($request->content !== null) { - $changes[] = 'content'; - } - - if ($request->status !== null) { - $changes[] = 'status'; - } - - if ($request->tags !== null) { - $changes[] = 'tags'; - } - - if ($request->textReplace !== null) { - $changes[] = 'text_replacement'; - } - - return $changes; - } -} diff --git a/src/Research/MCP/Tools/UpdateResearchToolAction.php b/src/Research/MCP/Tools/UpdateResearchToolAction.php deleted file mode 100644 index 940e054f..00000000 --- a/src/Research/MCP/Tools/UpdateResearchToolAction.php +++ /dev/null @@ -1,128 +0,0 @@ -logger->info('Updating research', [ - 'research_id' => $request->researchId, - 'has_title' => $request->title !== null, - 'has_description' => $request->description !== null, - 'has_status' => $request->status !== null, - 'has_tags' => $request->tags !== null, - 'has_entry_dirs' => $request->entryDirs !== null, - 'has_memory' => $request->memory !== null, - ]); - - try { - // Validate request - $validationErrors = $request->validate(); - if (!empty($validationErrors)) { - return ToolResult::validationError($validationErrors); - } - - $researchId = ResearchId::fromString($request->researchId); - if (!$this->service->exists($researchId)) { - return ToolResult::error("Research '{$request->researchId}' not found"); - } - - $research = $this->service->update($researchId, $request); - - $this->logger->info('Research updated successfully', [ - 'research_id' => $request->researchId, - 'title' => $research->name, - 'status' => $research->status, - ]); - - return ToolResult::success([ - 'success' => true, - ]); - - } catch (ResearchNotFoundException $e) { - $this->logger->error('Research not found', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (ResearchException $e) { - $this->logger->error('Error during research update', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error($e->getMessage()); - - } catch (\Throwable $e) { - $this->logger->error('Unexpected error updating research', [ - 'research_id' => $request->researchId, - 'error' => $e->getMessage(), - ]); - - return ToolResult::error('Failed to update research: ' . $e->getMessage()); - } - } - - /** - * Get list of changes applied based on the request - */ - private function getAppliedChanges(ResearchUpdateRequest $request): array - { - $changes = []; - - if ($request->title !== null) { - $changes[] = 'title'; - } - - if ($request->description !== null) { - $changes[] = 'description'; - } - - if ($request->status !== null) { - $changes[] = 'status'; - } - - if ($request->tags !== null) { - $changes[] = 'tags'; - } - - if ($request->entryDirs !== null) { - $changes[] = 'entry_directories'; - } - - if ($request->memory !== null) { - $changes[] = 'memory'; - } - - return $changes; - } -} diff --git a/src/Research/Repository/EntryRepositoryInterface.php b/src/Research/Repository/EntryRepositoryInterface.php deleted file mode 100644 index 327ab4be..00000000 --- a/src/Research/Repository/EntryRepositoryInterface.php +++ /dev/null @@ -1,43 +0,0 @@ - ResearchConfig::class, - TemplateServiceInterface::class => TemplateService::class, - ResearchServiceInterface::class => ResearchService::class, - EntryServiceInterface::class => EntryService::class, - ]; - } - - public function init(ConsoleBootloader $console, EnvironmentInterface $env): void - { - $console->addCommand( - ResearchListCommand::class, - TemplateListCommand::class, - ResearchInfoCommand::class, - ); - - // Initialize configuration from environment variables - $this->config->setDefaults( - ResearchConfig::CONFIG, - [ - 'templates_path' => $env->get('RESEARCH_TEMPLATES_PATH', '.templates'), - 'researches_path' => $env->get('RESEARCH_RESEARCHES_PATH', '.researches'), - 'storage_driver' => $env->get('RESEARCH_STORAGE_DRIVER', 'markdown'), - 'default_entry_status' => $env->get('RESEARCH_DEFAULT_STATUS', 'draft'), - 'env_config' => [], - ], - ); - } -} diff --git a/src/Research/Service/EntryService.php b/src/Research/Service/EntryService.php deleted file mode 100644 index 1b766987..00000000 --- a/src/Research/Service/EntryService.php +++ /dev/null @@ -1,395 +0,0 @@ -logger?->info('Creating new entry', [ - 'research_id' => $researchId->value, - 'category' => $request->category, - 'entry_type' => $request->entryType, - ]); - - // Verify research exists - $research = $this->researches->findById($researchId); - if ($research === null) { - $error = "Research '{$researchId->value}' not found"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - ]); - throw new ResearchNotFoundException($error); - } - - // Get and validate template - $templateKey = TemplateKey::fromString($research->template); - $template = $this->templateService->getTemplate($templateKey); - if ($template === null) { - $error = "Template '{$research->template}' not found"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'template' => $research->template, - ]); - throw new TemplateNotFoundException($error); - } - - // Resolve display names to internal keys - $resolvedCategory = $this->templateService->resolveCategoryKey($template, $request->category); - if ($resolvedCategory === null) { - $error = "Category '{$request->category}' not found in template '{$research->template}'"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'category' => $request->category, - 'template' => $research->template, - ]); - throw new ResearchException($error); - } - - $resolvedEntryType = $this->templateService->resolveEntryTypeKey($template, $request->entryType); - if ($resolvedEntryType === null) { - $error = "Entry type '{$request->entryType}' not found in template '{$research->template}'"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'entry_type' => $request->entryType, - 'template' => $research->template, - ]); - throw new ResearchException($error); - } - - // Validate entry type is allowed in category - if (!$template->validateEntryInCategory($resolvedCategory, $resolvedEntryType)) { - $error = "Entry type '{$request->entryType}' is not allowed in category '{$request->category}'"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'category' => $request->category, - 'entry_type' => $request->entryType, - ]); - throw new ResearchException($error); - } - - // Resolve status if provided, otherwise use entry type default - if ($request->status !== null) { - $resolvedStatus = $this->templateService->resolveStatusValue($template, $resolvedEntryType, $request->status); - if ($resolvedStatus === null) { - $error = "Status '{$request->status}' not found for entry type '{$request->entryType}'"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'status' => $request->status, - 'entry_type' => $request->entryType, - ]); - throw new ResearchException($error); - } - } else { - // Use default status from entry type - $entryType = $template->getEntryType($resolvedEntryType); - $resolvedStatus = $entryType?->defaultStatus; - } - - try { - // Create request with resolved keys - $resolvedRequest = $request->withResolvedKeys( - $resolvedCategory, - $resolvedEntryType, - $resolvedStatus, - ); - - // Use storage driver to create the entry - $entry = $this->storageDriver->createEntry($researchId, $resolvedRequest); - - // Save entry to repository - $this->entryRepository->save($researchId, $entry); - - $this->logger?->info('Entry created successfully', [ - 'research_id' => $researchId->value, - 'entry_id' => $entry->entryId, - 'title' => $entry->title, - 'category' => $entry->category, - 'entry_type' => $entry->entryType, - ]); - - return $entry; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to create entry', [ - 'research_id' => $researchId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to create entry: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function updateEntry(ResearchId $researchId, EntryId $entryId, EntryUpdateRequest $request): Entry - { - $this->logger?->info('Updating entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'has_title' => $request->title !== null, - 'has_content' => $request->content !== null, - 'has_status' => $request->status !== null, - ]); - - // Verify research exists - $research = $this->researches->findById($researchId); - if ($research === null) { - $error = "Research '{$researchId->value}' not found"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - ]); - throw new ResearchNotFoundException($error); - } - - // Verify entry exists - $existingEntry = $this->entryRepository->findById($researchId, $entryId); - if ($existingEntry === null) { - $error = "Entry '{$entryId->value}' not found in research '{$researchId->value}'"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - throw new EntryNotFoundException($error); - } - - // Resolve status if provided - $resolvedStatus = $request->status; - if ($request->status !== null) { - $templateKey = TemplateKey::fromString($research->template); - $template = $this->templateService->getTemplate($templateKey); - - if ($template !== null) { - $resolvedStatusValue = $this->templateService->resolveStatusValue( - $template, - $existingEntry->entryType, - $request->status, - ); - - if ($resolvedStatusValue === null) { - $error = "Status '{$request->status}' not found for entry type '{$existingEntry->entryType}'"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'status' => $request->status, - 'entry_type' => $existingEntry->entryType, - ]); - throw new ResearchException($error); - } - - $resolvedStatus = $resolvedStatusValue; - } - } - - try { - // Create request with resolved status - $resolvedRequest = $request->withResolvedStatus($resolvedStatus); - - // Use storage driver to update the entry - $updatedEntry = $this->storageDriver->updateEntry($researchId, $entryId, $resolvedRequest); - - // Save updated entry to repository - $this->entryRepository->save($researchId, $updatedEntry); - - $this->logger?->info('Entry updated successfully', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'title' => $updatedEntry->title, - ]); - - return $updatedEntry; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to update entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to update entry: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function entryExists(ResearchId $researchId, EntryId $entryId): bool - { - $exists = $this->entryRepository->exists($researchId, $entryId); - - $this->logger?->debug('Checking entry existence', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'exists' => $exists, - ]); - - return $exists; - } - - #[\Override] - public function getEntry(ResearchId $researchId, EntryId $entryId): ?Entry - { - $this->logger?->info('Retrieving single entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - - // Verify research exists - if (!$this->researches->exists($researchId)) { - $error = "Research '{$researchId->value}' not found"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - ]); - throw new ResearchNotFoundException($error); - } - - try { - $entry = $this->entryRepository->findById($researchId, $entryId); - - $this->logger?->info('Entry retrieval completed', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'found' => $entry !== null, - ]); - - return $entry; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to retrieve entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to retrieve entry: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function findAll(ResearchId $researchId, array $filters = []): array - { - $this->logger?->info('Retrieving entries', [ - 'research_id' => $researchId->value, - 'filters' => $filters, - ]); - - // Verify research exists - if (!$this->researches->exists($researchId)) { - $this->logger?->warning('Attempted to get entries for non-existent research', [ - 'research_id' => $researchId->value, - ]); - return []; - } - - try { - $entries = $this->entryRepository->findByResearch($researchId, $filters); - - $this->logger?->info('Entries retrieved successfully', [ - 'research_id' => $researchId->value, - 'count' => \count($entries), - 'filters_applied' => !empty($filters), - ]); - - return $entries; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to retrieve entries', [ - 'research_id' => $researchId->value, - 'filters' => $filters, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to retrieve entries: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function deleteEntry(ResearchId $researchId, EntryId $entryId): bool - { - $this->logger?->info('Deleting entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - - // Verify entry exists - if (!$this->entryRepository->exists($researchId, $entryId)) { - $this->logger?->warning('Attempted to delete non-existent entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - return false; - } - - try { - // Use storage driver to delete the entry - $deleted = $this->storageDriver->deleteEntry($researchId, $entryId); - - if ($deleted) { - // Remove from repository - $this->entryRepository->delete($researchId, $entryId); - - $this->logger?->info('Entry deleted successfully', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - } else { - $this->logger?->warning('Storage driver failed to delete entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - } - - return $deleted; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to delete entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to delete entry: {$e->getMessage()}", - previous: $e, - ); - } - } -} diff --git a/src/Research/Service/EntryServiceInterface.php b/src/Research/Service/EntryServiceInterface.php deleted file mode 100644 index ffd524db..00000000 --- a/src/Research/Service/EntryServiceInterface.php +++ /dev/null @@ -1,70 +0,0 @@ -logger?->info('Creating new research', [ - 'template_id' => $request->templateId, - 'title' => $request->title, - ]); - - // Validate template exists - $templateKey = TemplateKey::fromString($request->templateId); - if (!$this->templateService->templateExists($templateKey)) { - $error = "Template '{$request->templateId}' not found"; - $this->logger?->error($error, [ - 'template_id' => $request->templateId, - ]); - throw new TemplateNotFoundException($error); - } - - try { - // Use storage driver to create the research - $research = $this->storageDriver->createResearch($request); - - // Save research to repository - $this->researches->save($research); - - $this->logger?->info('Research created successfully', [ - 'research_id' => $research->id, - 'template' => $research->template, - 'name' => $research->name, - ]); - - return $research; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to create research', [ - 'template_id' => $request->templateId, - 'title' => $request->title, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to create research: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function update(ResearchId $researchId, ResearchUpdateRequest $request): Research - { - $this->logger?->info('Updating research', [ - 'research_id' => $researchId->value, - 'updates' => [ - 'title' => $request->title !== null, - 'description' => $request->description !== null, - 'status' => $request->status !== null, - 'tags' => $request->tags !== null, - 'entry_dirs' => $request->entryDirs !== null, - 'memory' => $request->memory !== null, - ], - ]); - - // Verify research exists - if (!$this->researches->exists($researchId)) { - $error = "Research '{$researchId->value}' not found"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - ]); - throw new ResearchNotFoundException($error); - } - - try { - // Use storage driver to update the research - $updatedResearch = $this->storageDriver->updateResearch($researchId, $request); - - // Save updated research to repository - $this->researches->save($updatedResearch); - - $this->logger?->info('Research updated successfully', [ - 'research_id' => $researchId->value, - 'name' => $updatedResearch->name, - 'status' => $updatedResearch->status, - ]); - - return $updatedResearch; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to update research', [ - 'research_id' => $researchId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to update research: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function exists(ResearchId $researchId): bool - { - $exists = $this->researches->exists($researchId); - - $this->logger?->debug('Checking research existence', [ - 'research_id' => $researchId->value, - 'exists' => $exists, - ]); - - return $exists; - } - - #[\Override] - public function get(ResearchId $researchId): ?Research - { - $this->logger?->debug('Retrieving research', [ - 'research_id' => $researchId->value, - ]); - - $research = $this->researches->findById($researchId); - - if ($research === null) { - $this->logger?->warning('Research not found', [ - 'research_id' => $researchId->value, - ]); - } else { - $this->logger?->debug('Research retrieved successfully', [ - 'research_id' => $research->id, - 'name' => $research->name, - 'template' => $research->template, - ]); - } - - return $research; - } - - #[\Override] - public function findAll(array $filters = []): array - { - $this->logger?->info('Listing researches', [ - 'filters' => $filters, - ]); - - try { - $researches = $this->researches->findAll($filters); - - $this->logger?->info('Researches retrieved successfully', [ - 'count' => \count($researches), - 'filters_applied' => !empty($filters), - ]); - - return $researches; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to list researches', [ - 'filters' => $filters, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to list researches: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function delete(ResearchId $researchId): bool - { - $this->logger?->info('Deleting research', [ - 'research_id' => $researchId->value, - ]); - - // Verify research exists - if (!$this->researches->exists($researchId)) { - $this->logger?->warning('Attempted to delete non-existent research', [ - 'research_id' => $researchId->value, - ]); - return false; - } - - try { - // Use storage driver to delete the research and its entries - $deleted = $this->storageDriver->deleteResearch($researchId); - - if ($deleted) { - // Remove from repository - $this->researches->delete($researchId); - - $this->logger?->info('Research deleted successfully', [ - 'research_id' => $researchId->value, - ]); - } else { - $this->logger?->warning('Storage driver failed to delete research', [ - 'research_id' => $researchId->value, - ]); - } - - return $deleted; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to delete research', [ - 'research_id' => $researchId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to delete research: {$e->getMessage()}", - previous: $e, - ); - } - } - - #[\Override] - public function addMemory(ResearchId $researchId, string $memory): Research - { - $this->logger?->info('Adding memory to research', [ - 'research_id' => $researchId->value, - 'memory_length' => \strlen($memory), - ]); - - // Verify research exists - $research = $this->researches->findById($researchId); - if ($research === null) { - $error = "Research '{$researchId->value}' not found"; - $this->logger?->error($error, [ - 'research_id' => $researchId->value, - ]); - throw new ResearchNotFoundException($error); - } - - try { - // Create updated research with added memory - $updatedResearch = $research->withAddedMemory($memory); - - // Save updated research to repository - $this->researches->save($updatedResearch); - - $this->logger?->info('Memory added to research successfully', [ - 'research_id' => $researchId->value, - 'memory_count' => \count($updatedResearch->memory), - 'name' => $updatedResearch->name, - ]); - - return $updatedResearch; - - } catch (\Throwable $e) { - $this->logger?->error('Failed to add memory to research', [ - 'research_id' => $researchId->value, - 'error' => $e->getMessage(), - ]); - - throw new ResearchException( - "Failed to add memory to research: {$e->getMessage()}", - previous: $e, - ); - } - } -} diff --git a/src/Research/Service/ResearchServiceInterface.php b/src/Research/Service/ResearchServiceInterface.php deleted file mode 100644 index 44804b61..00000000 --- a/src/Research/Service/ResearchServiceInterface.php +++ /dev/null @@ -1,64 +0,0 @@ -templateRepository->findAll(); - } - - #[\Override] - public function getTemplate(TemplateKey $key): ?Template - { - return $this->templateRepository->findByKey($key); - } - - #[\Override] - public function templateExists(TemplateKey $key): bool - { - return $this->templateRepository->exists($key); - } - - #[\Override] - public function resolveCategoryKey(Template $template, string $displayNameOrKey): ?string - { - foreach ($template->categories as $category) { - if ($category->name === $displayNameOrKey || $category->displayName === $displayNameOrKey) { - $this->logger?->debug('Resolved category key', [ - 'input' => $displayNameOrKey, - 'resolved' => $category->name, - 'template' => $template->key, - ]); - return $category->name; - } - } - - $this->logger?->warning('Could not resolve category key', [ - 'input' => $displayNameOrKey, - 'template' => $template->key, - 'available_categories' => \array_map(static fn($cat) => [ - 'name' => $cat->name, - 'display_name' => $cat->displayName, - ], $template->categories), - ]); - - return null; - } - - #[\Override] - public function resolveEntryTypeKey(Template $template, string $displayNameOrKey): ?string - { - foreach ($template->entryTypes as $entryType) { - if ($entryType->key === $displayNameOrKey || $entryType->displayName === $displayNameOrKey) { - $this->logger?->debug('Resolved entry type key', [ - 'input' => $displayNameOrKey, - 'resolved' => $entryType->key, - 'template' => $template->key, - ]); - return $entryType->key; - } - } - - $this->logger?->warning('Could not resolve entry type key', [ - 'input' => $displayNameOrKey, - 'template' => $template->key, - 'available_entry_types' => \array_map(static fn($type) => [ - 'key' => $type->key, - 'display_name' => $type->displayName, - ], $template->entryTypes), - ]); - - return null; - } - - #[\Override] - public function resolveStatusValue(Template $template, string $entryTypeKey, string $displayNameOrValue): ?string - { - $entryType = $this->getEntryTypeByKey($template, $entryTypeKey); - if ($entryType === null) { - $this->logger?->error('Entry type not found for status resolution', [ - 'entry_type_key' => $entryTypeKey, - 'template' => $template->key, - ]); - return null; - } - - foreach ($entryType->statuses as $status) { - if ($status->value === $displayNameOrValue || $status->displayName === $displayNameOrValue) { - $this->logger?->debug('Resolved status value', [ - 'input' => $displayNameOrValue, - 'resolved' => $status->value, - 'entry_type' => $entryTypeKey, - 'template' => $template->key, - ]); - return $status->value; - } - } - - $this->logger?->warning('Could not resolve status value', [ - 'input' => $displayNameOrValue, - 'entry_type' => $entryTypeKey, - 'template' => $template->key, - 'available_statuses' => \array_map(static fn($status) => [ - 'value' => $status->value, - 'display_name' => $status->displayName, - ], $entryType->statuses), - ]); - - return null; - } - - #[\Override] - public function getAvailableStatuses(Template $template, string $entryTypeKey): array - { - $entryType = $this->getEntryTypeByKey($template, $entryTypeKey); - if ($entryType === null) { - return []; - } - - return \array_map(static fn($status) => $status->value, $entryType->statuses); - } - - #[\Override] - public function refreshTemplates(): void - { - $this->templateRepository->refresh(); - $this->logger?->info('Templates refreshed from storage'); - } - - /** - * Get entry type from template by key - */ - private function getEntryTypeByKey(Template $template, string $key): ?\Butschster\ContextGenerator\Research\Domain\Model\EntryType - { - foreach ($template->entryTypes as $entryType) { - if ($entryType->key === $key) { - return $entryType; - } - } - return null; - } -} diff --git a/src/Research/Service/TemplateServiceInterface.php b/src/Research/Service/TemplateServiceInterface.php deleted file mode 100644 index 994aaf63..00000000 --- a/src/Research/Service/TemplateServiceInterface.php +++ /dev/null @@ -1,70 +0,0 @@ -files->isDirectory($path)) { - return []; - } - - $researches = []; - - try { - $finder = new Finder(); - $finder - ->directories() - ->in($path) - ->depth(0) // Only immediate subdirectories - ->filter(static function (\SplFileInfo $file): bool { - // Check if this directory contains a research.yaml file - $configPath = $file->getRealPath() . '/research.yaml'; - return \file_exists($configPath); - }); - - foreach ($finder as $directory) { - $researches[] = $directory->getRealPath(); - } - } catch (\Throwable $e) { - $this->reporter->report($e); - // Handle cases where directory is not accessible - // Return empty array - calling code can handle this gracefully - } - - return $researches; - } - - /** - * Scan research directory for entry files - * - * @param string $path Path to research directory - * @return array Array of entry file paths - */ - public function scanEntries(string $path): array - { - if (!$this->files->exists($path) || !$this->files->isDirectory($path)) { - return []; - } - - $entryFiles = []; - - try { - $finder = new Finder(); - $finder - ->files() - ->in($path) - ->name('*.md'); - - foreach ($finder as $file) { - $entryFiles[] = $file->getRealPath(); - } - } catch (\Throwable) { - // Handle cases where directories are not accessible - // Return empty array - calling code can handle this gracefully - } - - return $entryFiles; - } - - /** - * Get all subdirectories in research that could contain entries - */ - public function getEntryDirectories(string $path): array - { - if (!$this->files->exists($path) || !$this->files->isDirectory($path)) { - return []; - } - - $directories = []; - - try { - $finder = new Finder(); - $finder - ->directories() - ->in($path) - ->depth(0) // Only immediate subdirectories - ->filter(static function (\SplFileInfo $file): bool { - // Skip special directories - $name = $file->getFilename(); - return !\in_array($name, ['.research', 'resources', '.git', '.idea', 'node_modules'], true); - }); - - foreach ($finder as $directory) { - $directories[] = $directory->getFilename(); // Return relative directory name - } - } catch (\Throwable) { - // Handle cases where directory is not accessible - // Return empty array - } - - return $directories; - } -} diff --git a/src/Research/Storage/FileStorage/FileEntryRepository.php b/src/Research/Storage/FileStorage/FileEntryRepository.php deleted file mode 100644 index 4820b8f3..00000000 --- a/src/Research/Storage/FileStorage/FileEntryRepository.php +++ /dev/null @@ -1,309 +0,0 @@ -getResearchPath($researchId->value); - - if (!$this->files->exists($researchPath)) { - return []; - } - - $entries = []; - - try { - $entryFiles = $this->directoryScanner->scanEntries($researchPath); - - foreach ($entryFiles as $filePath) { - try { - $entry = $this->loadEntryFromFile($filePath); - if ($entry !== null && $this->matchesFilters($entry, $filters)) { - $entries[] = $entry; - } - } catch (\Throwable $e) { - $this->logError('Failed to load entry', ['file' => $filePath], $e); - } - } - - $this->logOperation('Loaded entries for research', [ - 'research_id' => $researchId->value, - 'count' => \count($entries), - 'total_scanned' => \count($entryFiles), - ]); - } catch (\Throwable $e) { - $this->logError('Failed to scan entries for research', ['research_id' => $researchId->value], $e); - } - - return $entries; - } - - #[\Override] - public function findById(ResearchId $researchId, EntryId $entryId): ?Entry - { - $researchPath = $this->getResearchPath($researchId->value); - $entryFile = $this->findEntryFile($researchPath, $entryId->value); - - if ($entryFile === null) { - return null; - } - - try { - return $this->loadEntryFromFile($entryFile); - } catch (\Throwable $e) { - $this->logError('Failed to load entry by ID', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ], $e); - return null; - } - } - - #[\Override] - public function save(ResearchId $researchId, Entry $entry): void - { - $researchPath = $this->getResearchPath($researchId->value); - - if (!$this->files->exists($researchPath)) { - throw new \RuntimeException("Research directory not found: {$researchPath}"); - } - - try { - $this->saveEntryToFile($researchPath, $entry); - - $this->logOperation('Saved entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entry->entryId, - 'title' => $entry->title, - ]); - } catch (\Throwable $e) { - $this->logError('Failed to save entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entry->entryId, - ], $e); - throw $e; - } - } - - #[\Override] - public function delete(ResearchId $researchId, EntryId $entryId): bool - { - $researchPath = $this->getResearchPath($researchId->value); - $entryFile = $this->findEntryFile($researchPath, $entryId->value); - - if ($entryFile === null) { - return false; - } - - try { - $deleted = $this->files->delete($entryFile); - - if ($deleted) { - $this->logOperation('Deleted entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - 'file' => $entryFile, - ]); - } - - return $deleted; - } catch (\Throwable $e) { - $this->logError('Failed to delete entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ], $e); - return false; - } - } - - #[\Override] - public function exists(ResearchId $researchId, EntryId $entryId): bool - { - $researchPath = $this->getResearchPath($researchId->value); - return $this->findEntryFile($researchPath, $entryId->value) !== null; - } - - /** - * Get research directory path from ID - */ - private function getResearchPath(string $researchId): string - { - $basePath = $this->getBasePath(); - return $this->files->normalizePath($basePath . '/' . $researchId); - } - - /** - * Find entry file by entry ID - */ - private function findEntryFile(string $researchPath, string $entryId): ?string - { - $entryFiles = $this->directoryScanner->scanEntries($researchPath); - - foreach ($entryFiles as $filePath) { - try { - $frontmatter = $this->frontmatterParser->extractFrontmatter( - $this->files->read($filePath), - ); - - if (isset($frontmatter['entry_id']) && $frontmatter['entry_id'] === $entryId) { - return $filePath; - } - } catch (\Throwable $e) { - $this->logError('Failed to check entry file', ['file' => $filePath], $e); - } - } - - return null; - } - - /** - * Load entry from markdown file - */ - private function loadEntryFromFile(string $filePath): ?Entry - { - try { - $parsed = $this->readMarkdownFile($filePath); - $frontmatter = $parsed['frontmatter']; - $content = $parsed['content']; - - // Validate required frontmatter fields - $requiredFields = ['entry_id', 'title', 'entry_type', 'category', 'status']; - foreach ($requiredFields as $field) { - if (!isset($frontmatter[$field])) { - throw new \RuntimeException("Missing required frontmatter field: {$field}"); - } - } - - // Parse dates - $createdAt = isset($frontmatter['created_at']) - ? new \DateTime($frontmatter['created_at']) - : new \DateTime(); - - $updatedAt = isset($frontmatter['updated_at']) - ? new \DateTime($frontmatter['updated_at']) - : new \DateTime(); - - return new Entry( - entryId: $frontmatter['entry_id'], - title: $frontmatter['title'], - description: $frontmatter['description'] ?? '', // Default to empty if not present in file - entryType: $frontmatter['entry_type'], - category: $frontmatter['category'], - status: $frontmatter['status'], - createdAt: $createdAt, - updatedAt: $updatedAt, - tags: $frontmatter['tags'] ?? [], - content: $content, - filePath: $filePath, - ); - } catch (\Throwable $e) { - $this->logError("Failed to load entry from file: {$filePath}", [], $e); - return null; - } - } - - /** - * Save entry to markdown file - */ - private function saveEntryToFile(string $researchPath, Entry $entry): void - { - // Determine file path - $filePath = $entry->filePath; - - if ($filePath === null) { - // New entry - generate file path - $categoryPath = $this->files->normalizePath($researchPath . '/' . $entry->category . '/' . $entry->entryType); - $this->ensureDirectory($categoryPath); - - $filename = $this->generateFilename($entry->title); - $filePath = $categoryPath . '/' . $filename; - } - - // Prepare frontmatter - $frontmatter = [ - 'entry_id' => $entry->entryId, - 'title' => $entry->title, - 'description' => $entry->description, - 'entry_type' => $entry->entryType, - 'category' => $entry->category, - 'status' => $entry->status, - 'created_at' => $entry->createdAt->format('c'), - 'updated_at' => $entry->updatedAt->format('c'), - 'tags' => $entry->tags, - ]; - - // Write markdown file - $this->writeMarkdownFile($filePath, $frontmatter, $entry->content); - } - - /** - * Check if entry matches the provided filters - */ - private function matchesFilters(Entry $entry, array $filters): bool - { - // Category filter - if (isset($filters['category']) && $entry->category !== $filters['category']) { - return false; - } - - // Status filter - if (isset($filters['status']) && $entry->status !== $filters['status']) { - return false; - } - - // Entry type filter - if (isset($filters['entry_type']) && $entry->entryType !== $filters['entry_type']) { - return false; - } - - // Tags filter (any of the provided tags should match) - if (isset($filters['tags']) && \is_array($filters['tags'])) { - $hasMatchingTag = false; - foreach ($filters['tags'] as $filterTag) { - if (\in_array($filterTag, $entry->tags, true)) { - $hasMatchingTag = true; - break; - } - } - if (!$hasMatchingTag) { - return false; - } - } - - // Title contains filter - if (isset($filters['title_contains']) && \is_string($filters['title_contains'])) { - if (\stripos($entry->title, $filters['title_contains']) === false) { - return false; - } - } - - // Description contains filter - if (isset($filters['description_contains']) && \is_string($filters['description_contains'])) { - if (\stripos($entry->description, $filters['description_contains']) === false) { - return false; - } - } - - // Content contains filter - if (isset($filters['content_contains']) && \is_string($filters['content_contains'])) { - if (\stripos($entry->content, $filters['content_contains']) === false) { - return false; - } - } - - return true; - } -} diff --git a/src/Research/Storage/FileStorage/FileResearchRepository.php b/src/Research/Storage/FileStorage/FileResearchRepository.php deleted file mode 100644 index 08b1ab91..00000000 --- a/src/Research/Storage/FileStorage/FileResearchRepository.php +++ /dev/null @@ -1,211 +0,0 @@ -directoryScanner->scanResearches($this->getBasePath()); - - foreach ($researchPaths as $researchPath) { - try { - $research = $this->loadResearchFromDirectory($researchPath); - if ($research !== null && $this->matchesFilters($research, $filters)) { - $researches[] = $research; - } - } catch (\Throwable $e) { - $this->logError('Failed to load research', ['path' => $researchPath], $e); - } - } - - $this->logOperation('Loaded researches', [ - 'count' => \count($researches), - 'total_scanned' => \count($researchPaths), - ]); - - return $researches; - } - - #[\Override] - public function findById(ResearchId $id): ?Research - { - $path = $this->getResearchPath($id->value); - - if (!$this->files->exists($path)) { - return null; - } - - try { - return $this->loadResearchFromDirectory($path); - } catch (\Throwable $e) { - $this->logError('Failed to load research by ID', ['id' => $id->value, 'path' => $path], $e); - return null; - } - } - - #[\Override] - public function save(Research $research): void - { - $path = $this->getResearchPath($research->id); - - try { - // Ensure research directory exists - $this->ensureDirectory($path); - - // Create entry directories if they don't exist - foreach ($research->entryDirs as $entryDir) { - $entryDirPath = $this->files->normalizePath($path . '/' . $entryDir); - $this->ensureDirectory($entryDirPath); - } - - // Save research configuration - $this->saveResearchConfig($path, $research); - - $this->logOperation('Saved research', [ - 'id' => $research->id, - 'name' => $research->name, - 'path' => $path, - ]); - } catch (\Throwable $e) { - $this->logError('Failed to save research', ['id' => $research->id], $e); - throw $e; - } - } - - #[\Override] - public function delete(ResearchId $id): bool - { - $path = $this->getResearchPath($id->value); - - if (!$this->files->exists($path)) { - return false; - } - - try { - $deleted = $this->files->deleteDirectory($path); - - if ($deleted) { - $this->logOperation('Deleted research', ['id' => $id->value, 'path' => $path]); - } - - return $deleted; - } catch (\Throwable $e) { - $this->logError('Failed to delete research', ['id' => $id->value], $e); - return false; - } - } - - #[\Override] - public function exists(ResearchId $id): bool - { - $path = $this->getResearchPath($id->value); - $configPath = $path . '/' . self::CONFIG_FILE; - - return $this->files->exists($configPath); - } - - /** - * Get research directory path from ID - */ - private function getResearchPath(string $researchId): string - { - $basePath = $this->getBasePath(); - - return $this->files->normalizePath($basePath . '/' . $researchId); - } - - /** - * Load research from directory path - */ - private function loadResearchFromDirectory(string $researchPath): ?Research - { - $configPath = $researchPath . '/' . self::CONFIG_FILE; - - if (!$this->files->exists($configPath)) { - throw new \RuntimeException("Research configuration not found: {$configPath}"); - } - - $config = $this->readYamlFile($configPath); - - // Extract research ID from directory name - $id = \basename($researchPath); - - return new Research( - id: $id, - name: $config['name'] ?? $id, - description: $config['description'] ?? '', - template: $config['template'] ?? '', - status: $config['status'] ?? 'draft', - tags: $config['tags'] ?? [], - entryDirs: $config['entries']['dirs'] ?? [], - memory: $config['memory'] ?? [], - path: $researchPath, - ); - } - - /** - * Save research configuration to YAML file - */ - private function saveResearchConfig(string $researchPath, Research $research): void - { - $configPath = $researchPath . '/' . self::CONFIG_FILE; - - $this->writeYamlFile($configPath, [ - 'name' => $research->name, - 'description' => $research->description, - 'template' => $research->template, - 'status' => $research->status, - 'tags' => $research->tags, - 'memory' => $research->memory, - 'entries' => [ - 'dirs' => $research->entryDirs, - ], - ]); - } - - /** - * Check if research matches the provided filters - */ - private function matchesFilters(Research $research, array $filters): bool - { - // Status filter - if (isset($filters['status']) && $research->status !== $filters['status']) { - return false; - } - - // Template filter - if (isset($filters['template']) && $research->template !== $filters['template']) { - return false; - } - - // Tags filter (any of the provided tags should match) - if (isset($filters['tags']) && \is_array($filters['tags'])) { - $hasMatchingTag = false; - foreach ($filters['tags'] as $filterTag) { - if (\in_array($filterTag, $research->tags, true)) { - $hasMatchingTag = true; - break; - } - } - if (!$hasMatchingTag) { - return false; - } - } - - return true; - } -} diff --git a/src/Research/Storage/FileStorage/FileStorageConfig.php b/src/Research/Storage/FileStorage/FileStorageConfig.php deleted file mode 100644 index 750e7bd4..00000000 --- a/src/Research/Storage/FileStorage/FileStorageConfig.php +++ /dev/null @@ -1,84 +0,0 @@ -basePath)) { - $errors[] = 'Base path cannot be empty'; - } - - if (empty($this->templatesPath)) { - $errors[] = 'Templates path cannot be empty'; - } - - if (empty($this->defaultEntryStatus)) { - $errors[] = 'Default entry status cannot be empty'; - } - - if ($this->maxFileSize <= 0) { - $errors[] = 'Max file size must be greater than 0'; - } - - if (empty($this->allowedExtensions)) { - $errors[] = 'At least one allowed extension must be specified'; - } - - return $errors; - } - - #[\Override] - public function jsonSerialize(): array - { - return [ - 'base_path' => $this->basePath, - 'templates_path' => $this->templatesPath, - 'default_entry_status' => $this->defaultEntryStatus, - 'create_directories_on_demand' => $this->createDirectoriesOnDemand, - 'validate_templates_on_boot' => $this->validateTemplatesOnBoot, - 'max_file_size' => $this->maxFileSize, - 'allowed_extensions' => $this->allowedExtensions, - 'file_encoding' => $this->fileEncoding, - ]; - } -} diff --git a/src/Research/Storage/FileStorage/FileStorageDriver.php b/src/Research/Storage/FileStorage/FileStorageDriver.php deleted file mode 100644 index cc64165f..00000000 --- a/src/Research/Storage/FileStorage/FileStorageDriver.php +++ /dev/null @@ -1,383 +0,0 @@ -templateId); - $template = $this->templateRepository->findByKey($templateKey); - - if ($template === null) { - throw new TemplateNotFoundException("Template '{$request->templateId}' not found"); - } - - $suffix = ''; - - do { - $researchId = $this->generateId($request->title . $suffix); - $suffix = '-' . \date('YmdHis'); - } while ($this->researchRepository->exists(ResearchId::fromString($researchId))); - - $research = new Research( - id: $researchId, - name: $request->title, - description: $request->description, - template: $request->templateId, - status: $this->driverConfig->defaultEntryStatus, - tags: $request->tags, - entryDirs: !empty($request->entryDirs) ? $request->entryDirs : $this->getDefaultEntryDirs($template), - memory: $request->memory, - ); - - $this->researchRepository->save($research); - $this->logger->debug('Created research', ['id' => $researchId, 'name' => $request->title]); - - return $research; - } - - public function updateResearch(ResearchId $researchId, ResearchUpdateRequest $request): Research - { - $research = $this->researchRepository->findById($researchId); - if ($research === null) { - throw new ResearchNotFoundException("Research '{$researchId->value}' not found"); - } - - if (!$request->hasUpdates()) { - return $research; - } - - $updated = $research->withUpdates( - name: $request->title, - description: $request->description, - status: $request->status, - tags: $request->tags, - entryDirs: $request->entryDirs, - memory: \array_map( - static fn(ResearchMemory $memory): string => $memory->record, - $request->memory, - ), - ); - - $this->researchRepository->save($updated); - $this->logger->debug('Updated research', ['id' => $researchId->value]); - - return $updated; - } - - public function deleteResearch(ResearchId $researchId): bool - { - if (!$this->researchRepository->exists($researchId)) { - return false; - } - - $deleted = $this->researchRepository->delete($researchId); - if ($deleted) { - $this->logger->debug('Deleted research', ['id' => $researchId->value]); - } - - return $deleted; - } - - public function createEntry(ResearchId $researchId, EntryCreateRequest $request): Entry - { - // Verify research exists - $research = $this->researchRepository->findById($researchId); - if ($research === null) { - throw new ResearchNotFoundException("Research '{$researchId->value}' not found"); - } - - // Get template for validation and key resolution - $templateKey = TemplateKey::fromString($research->template); - $template = $this->templateRepository->findByKey($templateKey); - if ($template === null) { - throw new TemplateNotFoundException("Template '{$research->template}' not found"); - } - - // Resolve display names to internal keys - $resolvedRequest = $this->resolveEntryCreateRequestKeys($request, $template); - - // Validate resolved request against template - $this->validateEntryAgainstTemplate($template, $resolvedRequest); - - // Generate entry ID and create entry - $entryId = $this->generateId('entry_'); - $now = new \DateTime(); - - $entry = new Entry( - entryId: $entryId, - title: $resolvedRequest->getProcessedTitle(), // Use processed title - description: $resolvedRequest->getProcessedDescription(), // Use processed description - entryType: $resolvedRequest->entryType, - category: $resolvedRequest->category, - status: $resolvedRequest->status ?? $this->driverConfig->defaultEntryStatus, - createdAt: $now, - updatedAt: $now, - tags: $resolvedRequest->tags, - content: $resolvedRequest->content, - ); - - $this->entryRepository->save($researchId, $entry); - $this->logger->debug('Created entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId, - 'title' => $entry->title, - ]); - - return $entry; - } - - public function updateEntry(ResearchId $researchId, EntryId $entryId, EntryUpdateRequest $request): Entry - { - $entry = $this->entryRepository->findById($researchId, $entryId); - if ($entry === null) { - throw new EntryNotFoundException("Entry '{$entryId->value}' not found in research '{$researchId->value}'"); - } - - if (!$request->hasUpdates()) { - return $entry; - } - - // Resolve status if provided - $resolvedRequest = $request; - if ($request->status !== null) { - $research = $this->researchRepository->findById($researchId); - if ($research !== null) { - $templateKey = TemplateKey::fromString($research->template); - $template = $this->templateRepository->findByKey($templateKey); - if ($template !== null) { - $resolvedStatus = $this->resolveStatusForEntryType($template, $entry->entryType, $request->status); - $resolvedRequest = $request->withResolvedStatus($resolvedStatus); - } - } - } - - // Get final content considering text replacement - $finalContent = $resolvedRequest->getFinalContent($entry->content); - - $updatedEntry = $entry->withUpdates( - title: $resolvedRequest->title, - description: $resolvedRequest->description, - status: $resolvedRequest->status, - tags: $resolvedRequest->tags, - content: $finalContent, // Use processed content with text replacement - ); - - $this->entryRepository->save($researchId, $updatedEntry); - $this->logger->debug('Updated entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - - return $updatedEntry; - } - - public function deleteEntry(ResearchId $researchId, EntryId $entryId): bool - { - if (!$this->entryRepository->exists($researchId, $entryId)) { - return false; - } - - $deleted = $this->entryRepository->delete($researchId, $entryId); - if ($deleted) { - $this->logger->debug('Deleted entry', [ - 'research_id' => $researchId->value, - 'entry_id' => $entryId->value, - ]); - } - - return $deleted; - } - - /** - * Generate unique ID for entities - */ - private function generateId(string $prefix = ''): string - { - return $this->slugify->slugify($prefix); - } - - /** - * Get default entry directories from template - */ - private function getDefaultEntryDirs(Template $template): array - { - $dirs = []; - foreach ($template->categories as $category) { - $dirs[] = $category->name; - } - return $dirs; - } - - /** - * Resolve display names in entry create request to internal keys - */ - private function resolveEntryCreateRequestKeys( - EntryCreateRequest $request, - Template $template, - ): EntryCreateRequest { - // Resolve category - $resolvedCategory = $this->resolveCategoryKey($template, $request->category); - if ($resolvedCategory === null) { - throw new \InvalidArgumentException( - "Category '{$request->category}' not found in template '{$template->key}'", - ); - } - - // Resolve entry type - $resolvedEntryType = $this->resolveEntryTypeKey($template, $request->entryType); - if ($resolvedEntryType === null) { - throw new \InvalidArgumentException( - "Entry type '{$request->entryType}' not found in template '{$template->key}'", - ); - } - - // Resolve status if provided - $resolvedStatus = null; - if ($request->status !== null) { - $resolvedStatus = $this->resolveStatusForEntryType($template, $resolvedEntryType, $request->status); - if ($resolvedStatus === null) { - throw new \InvalidArgumentException( - "Status '{$request->status}' not found for entry type '{$resolvedEntryType}' in template '{$template->key}'", - ); - } - } - - return $request->withResolvedKeys($resolvedCategory, $resolvedEntryType, $resolvedStatus); - } - - /** - * Validate entry request against research template - */ - private function validateEntryAgainstTemplate( - Template $template, - EntryCreateRequest $request, - ): void { - // Validate category exists - if (!$template->hasCategory($request->category)) { - throw new \InvalidArgumentException( - "Category '{$request->category}' not found in template '{$template->key}'", - ); - } - - // Validate entry type exists - if (!$template->hasEntryType($request->entryType)) { - throw new \InvalidArgumentException( - "Entry type '{$request->entryType}' not found in template '{$template->key}'", - ); - } - - // Validate entry type is allowed in category - if (!$template->validateEntryInCategory($request->category, $request->entryType)) { - throw new \InvalidArgumentException( - "Entry type '{$request->entryType}' is not allowed in category '{$request->category}'", - ); - } - - // Validate status if provided - if ($request->status !== null) { - $entryType = $template->getEntryType($request->entryType); - if ($entryType !== null && !$entryType->hasStatus($request->status)) { - throw new \InvalidArgumentException( - "Status '{$request->status}' is not valid for entry type '{$request->entryType}'", - ); - } - } - } - - /** - * Resolve category display name to internal key - */ - private function resolveCategoryKey( - Template $template, - string $displayNameOrKey, - ): ?string { - foreach ($template->categories as $category) { - if ($category->name === $displayNameOrKey || $category->displayName === $displayNameOrKey) { - return $category->name; - } - } - return null; - } - - /** - * Resolve entry type display name to internal key - */ - private function resolveEntryTypeKey( - Template $template, - string $displayNameOrKey, - ): ?string { - foreach ($template->entryTypes as $entryType) { - if ($entryType->key === $displayNameOrKey || $entryType->displayName === $displayNameOrKey) { - return $entryType->key; - } - } - return null; - } - - /** - * Resolve status display name to internal value for specific entry type - */ - private function resolveStatusForEntryType( - Template $template, - string $entryTypeKey, - string $displayNameOrValue, - ): ?string { - $entryType = $template->getEntryType($entryTypeKey); - if ($entryType === null) { - return null; - } - - foreach ($entryType->statuses as $status) { - if ($status->value === $displayNameOrValue || $status->displayName === $displayNameOrValue) { - return $status->value; - } - } - - return null; - } -} diff --git a/src/Research/Storage/FileStorage/FileStorageRepositoryBase.php b/src/Research/Storage/FileStorage/FileStorageRepositoryBase.php deleted file mode 100644 index 0b0bc18e..00000000 --- a/src/Research/Storage/FileStorage/FileStorageRepositoryBase.php +++ /dev/null @@ -1,151 +0,0 @@ -dirs->getRootPath()->join($this->config->getResearchesPath()); - } - - /** - * Get templates base path - */ - protected function getTemplatesPath(): string - { - return (string) $this->dirs->getRootPath()->join($this->config->getTemplatesPath()); - } - - /** - * Ensure directory exists - */ - protected function ensureDirectory(string $path): void - { - if (!$this->files->exists($path)) { - $this->files->ensureDirectory($path); - $this->logger?->debug('Created directory', ['path' => $path]); - } - } - - /** - * Read and parse YAML file - */ - protected function readYamlFile(string $filePath): array - { - if (!$this->files->exists($filePath)) { - throw new \RuntimeException("File not found: {$filePath}"); - } - - $content = $this->files->read($filePath); - - try { - return Yaml::parse($content) ?? []; - } catch (ParseException $e) { - $this->reporter->report($e); - throw new \RuntimeException("Failed to parse YAML file '{$filePath}': {$e->getMessage()}", 0, $e); - } - } - - /** - * Write array data as YAML file - */ - protected function writeYamlFile(string $filePath, array $data): void - { - $yamlContent = Yaml::dump( - $data, - 4, - 2, - Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK, - ); - - $this->ensureDirectory(\dirname($filePath)); - $this->files->write($filePath, $yamlContent); - - $this->logger?->debug('Wrote YAML file', ['path' => $filePath]); - } - - /** - * Read markdown file with frontmatter - */ - protected function readMarkdownFile(string $filePath): array - { - if (!$this->files->exists($filePath)) { - throw new \RuntimeException("Markdown file not found: {$filePath}"); - } - - $content = $this->files->read($filePath); - return $this->frontmatterParser->parse($content); - } - - /** - * Write markdown file with frontmatter - */ - protected function writeMarkdownFile(string $filePath, array $frontmatter, string $content): void - { - $fileContent = $this->frontmatterParser->combine($frontmatter, $content); - - $this->ensureDirectory(\dirname($filePath)); - $this->files->write($filePath, $fileContent); - - $this->logger?->debug('Wrote markdown file', ['path' => $filePath]); - } - - /** - * Generate safe filename from title - */ - protected function generateFilename(string $title, string $extension = 'md'): string - { - $slug = \strtolower($title); - $slug = \preg_replace('/[^a-z0-9\s\-]/', '', $slug); - $slug = \preg_replace('/[\s\-]+/', '-', (string) $slug); - $slug = \trim((string) $slug, '-'); - - return $slug . '.' . $extension; - } - - /** - * Log operation with context - */ - protected function logOperation(string $operation, array $context = []): void - { - $this->logger?->info("File storage operation: {$operation}", $context); - } - - /** - * Log error with context - */ - protected function logError(string $message, array $context = [], ?\Throwable $exception = null): void - { - $this->logger?->error($message, [ - 'exception' => $exception?->getMessage(), - ...$context, - ]); - } -} diff --git a/src/Research/Storage/FileStorage/FileTemplateRepository.php b/src/Research/Storage/FileStorage/FileTemplateRepository.php deleted file mode 100644 index f8ddbd1b..00000000 --- a/src/Research/Storage/FileStorage/FileTemplateRepository.php +++ /dev/null @@ -1,213 +0,0 @@ -loadTemplatesFromFilesystem(); - } - - #[\Override] - public function findByKey(TemplateKey $key): ?Template - { - $templates = $this->loadTemplatesFromFilesystem(); - - foreach ($templates as $template) { - if ($template->key === $key->value) { - return $template; - } - } - - return null; - } - - #[\Override] - public function exists(TemplateKey $key): bool - { - return $this->findByKey($key) !== null; - } - - #[\Override] - public function refresh(): void - { - // No-op since we don't cache anymore - $this->logOperation('Template refresh requested (no caching)'); - } - - /** - * Load templates from YAML files in templates directory - */ - private function loadTemplatesFromFilesystem(): array - { - $templatesPath = $this->getTemplatesPath(); - - $templates = []; - - if (!$this->files->exists($templatesPath) || !$this->files->isDirectory($templatesPath)) { - $this->logger?->warning('Templates directory not found', ['path' => $templatesPath]); - return $templates; - } - - $finder = new Finder(); - $finder->files() - ->in($templatesPath) - ->name('*.yaml') - ->name('*.yml'); - - foreach ($finder as $file) { - try { - $template = $this->loadTemplateFromFile($file->getRealPath()); - if ($template !== null) { - $templates[] = $template; - } - } catch (\Throwable $e) { - $this->reporter->report($e); - $this->logError('Failed to load template', ['file' => $file->getRealPath()], $e); - } - } - - $this->logOperation('Loaded templates from filesystem', [ - 'count' => \count($templates), - 'path' => $templatesPath, - ]); - - return $templates; - } - - /** - * Load template from individual YAML file - */ - private function loadTemplateFromFile(string $filePath): ?Template - { - try { - $templateData = $this->readYamlFile($filePath); - return $this->createTemplateFromData($templateData); - } catch (\Throwable $e) { - $this->reporter->report($e); - $this->logError("Failed to load template from file: {$filePath}", [], $e); - return null; - } - } - - /** - * Create Template object from parsed YAML data - */ - private function createTemplateFromData(array $data): Template - { - // Validate required fields - $requiredFields = ['key', 'name', 'description']; - foreach ($requiredFields as $field) { - if (!isset($data[$field])) { - throw new \RuntimeException("Missing required template field: {$field}"); - } - } - - // Parse categories - $categories = []; - if (isset($data['categories']) && \is_array($data['categories'])) { - foreach ($data['categories'] as $categoryData) { - $categories[] = $this->createCategoryFromData($categoryData); - } - } - - // Parse entry types - $entryTypes = []; - if (isset($data['entry_types']) && \is_array($data['entry_types'])) { - foreach ($data['entry_types'] as $key => $entryTypeData) { - $entryTypes[] = $this->createEntryTypeFromData($key, $entryTypeData); - } - } - - return new Template( - key: $data['key'], - name: $data['name'], - description: $data['description'], - tags: $data['tags'] ?? [], - categories: $categories, - entryTypes: $entryTypes, - prompt: $data['prompt'] ?? null, - ); - } - - /** - * Create Category object from parsed data - */ - private function createCategoryFromData(array $data): Category - { - $requiredFields = ['name', 'display_name', 'entry_types']; - foreach ($requiredFields as $field) { - if (!isset($data[$field])) { - throw new \RuntimeException("Missing required category field: {$field}"); - } - } - - return new Category( - name: $data['name'], - displayName: $data['display_name'], - entryTypes: $data['entry_types'], - ); - } - - /** - * Create EntryType object from parsed data - */ - private function createEntryTypeFromData(string $key, array $data): EntryType - { - $requiredFields = ['display_name']; - foreach ($requiredFields as $field) { - if (!isset($data[$field])) { - throw new \RuntimeException("Missing required entry type field: {$field}"); - } - } - - // Parse statuses - $statuses = []; - if (isset($data['statuses']) && \is_array($data['statuses'])) { - foreach ($data['statuses'] as $statusData) { - $statuses[] = $this->createStatusFromData($statusData); - } - } - - return new EntryType( - key: $key, - displayName: $data['display_name'], - contentType: $data['content_type'] ?? 'markdown', - defaultStatus: $data['default_status'] ?? 'draft', - statuses: $statuses, - ); - } - - /** - * Create Status object from parsed data - */ - private function createStatusFromData(array $data): Status - { - $requiredFields = ['value', 'display_name']; - foreach ($requiredFields as $field) { - if (!isset($data[$field])) { - throw new \RuntimeException("Missing required status field: {$field}"); - } - } - - return new Status( - value: $data['value'], - displayName: $data['display_name'], - ); - } -} diff --git a/src/Research/Storage/FileStorage/FrontmatterParser.php b/src/Research/Storage/FileStorage/FrontmatterParser.php deleted file mode 100644 index b9018598..00000000 --- a/src/Research/Storage/FileStorage/FrontmatterParser.php +++ /dev/null @@ -1,104 +0,0 @@ - [], - 'content' => $content, - ]; - } - - // Find the closing delimiter - $lines = \explode("\n", $content); - $frontmatterLines = []; - $contentLines = []; - $inFrontmatter = false; - $frontmatterClosed = false; - - foreach ($lines as $index => $line) { - if ($index === 0 && $line === self::FRONTMATTER_DELIMITER) { - $inFrontmatter = true; - continue; - } - - if ($inFrontmatter && $line === self::FRONTMATTER_DELIMITER) { - $inFrontmatter = false; - $frontmatterClosed = true; - continue; - } - - if ($inFrontmatter) { - $frontmatterLines[] = $line; - } elseif ($frontmatterClosed) { - $contentLines[] = $line; - } - } - - // Parse YAML frontmatter - $frontmatter = []; - if (!empty($frontmatterLines)) { - $yamlContent = \implode("\n", $frontmatterLines); - try { - $frontmatter = Yaml::parse($yamlContent) ?? []; - } catch (ParseException $e) { - throw new \RuntimeException("Failed to parse YAML frontmatter: {$e->getMessage()}", 0, $e); - } - } - - $content = \implode("\n", $contentLines); - - return [ - 'frontmatter' => $frontmatter, - 'content' => \trim($content), - ]; - } - - /** - * Combine frontmatter and content into markdown file format - */ - public function combine(array $frontmatter, string $content): string - { - $output = ''; - - if (!empty($frontmatter)) { - $output .= self::FRONTMATTER_DELIMITER . "\n"; - $output .= Yaml::dump($frontmatter, 2, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); - $output .= self::FRONTMATTER_DELIMITER . "\n"; - } - - $output .= $content; - - return $output; - } - - /** - * Extract only the frontmatter from file content - */ - public function extractFrontmatter(string $content): array - { - return $this->parse($content)['frontmatter']; - } -} diff --git a/src/Research/Storage/FileStorageBootloader.php b/src/Research/Storage/FileStorageBootloader.php deleted file mode 100644 index 4b5e38a1..00000000 --- a/src/Research/Storage/FileStorageBootloader.php +++ /dev/null @@ -1,50 +0,0 @@ - FrontmatterParser::class, - TemplateRepositoryInterface::class => FileTemplateRepository::class, - ResearchRepositoryInterface::class => FileResearchRepository::class, - EntryRepositoryInterface::class => FileEntryRepository::class, - - // Storage driver - StorageDriverInterface::class => static fn(ResearchConfigInterface $config, FilesInterface $files, LoggerInterface $logger, ExceptionReporterInterface $reporter, DirectoriesInterface $dirs, TemplateRepositoryInterface $templateRepository, ResearchRepositoryInterface $researchRepository, EntryRepositoryInterface $entryRepository): StorageDriverInterface => new FileStorageDriver( - driverConfig: FileStorageConfig::fromArray([ - 'base_path' => $config->getResearchesPath(), - 'templates_path' => $config->getTemplatesPath(), - 'default_entry_status' => $config->getDefaultEntryStatus(), - ]), - templateRepository: $templateRepository, - researchRepository: $researchRepository, - entryRepository: $entryRepository, - slugify: new Slugify(), - logger: $logger, - ), - ]; - } -} diff --git a/src/Research/Storage/StorageDriverInterface.php b/src/Research/Storage/StorageDriverInterface.php deleted file mode 100644 index 4ab796b7..00000000 --- a/src/Research/Storage/StorageDriverInterface.php +++ /dev/null @@ -1,57 +0,0 @@ - $suggestedTemplates List of template names that match this analysis - * @param array $metadata Additional metadata discovered during analysis - */ - public function __construct( - public string $analyzerName, - public string $detectedType, - public float $confidence, - public array $suggestedTemplates = [], - public array $metadata = [], - ) {} - - /** - * Get the primary suggested template (first in the list) - */ - public function getPrimaryTemplate(): ?string - { - return $this->suggestedTemplates[0] ?? null; - } -} diff --git a/src/Template/Analysis/Analyzer/AbstractFrameworkAnalyzer.php b/src/Template/Analysis/Analyzer/AbstractFrameworkAnalyzer.php deleted file mode 100644 index 307ac44e..00000000 --- a/src/Template/Analysis/Analyzer/AbstractFrameworkAnalyzer.php +++ /dev/null @@ -1,233 +0,0 @@ -canAnalyze($projectRoot)) { - return null; - } - - $composer = $this->composerReader->readComposerFile($projectRoot); - - if ($composer === null || !$this->hasFrameworkPackages($composer)) { - return null; - } - - $confidence = $this->calculateConfidence($projectRoot, $composer); - $metadata = $this->buildMetadata($projectRoot, $composer); - - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: $this->getFrameworkType(), - confidence: \min($confidence, 1.0), - suggestedTemplates: [$this->getFrameworkType()], - metadata: $metadata, - ); - } - - public function canAnalyze(FSPath $projectRoot): bool - { - // Must have composer.json to be a PHP framework - if (!$projectRoot->join('composer.json')->exists()) { - return false; - } - - $composer = $this->composerReader->readComposerFile($projectRoot); - return $composer !== null && $this->hasFrameworkPackages($composer); - } - - /** - * Get framework-specific packages to look for - * - * @return array - */ - abstract protected function getFrameworkPackages(): array; - - /** - * Get framework-specific directories that indicate this framework - * - * @return array - */ - abstract protected function getFrameworkDirectories(): array; - - /** - * Get framework-specific files that indicate this framework - * - * @return array - */ - abstract protected function getFrameworkFiles(): array; - - /** - * Get the base confidence score for having framework packages - */ - protected function getBaseConfidence(): float - { - return 0.6; - } - - /** - * Get the weight for directory structure matching - */ - protected function getDirectoryWeight(): float - { - return 0.2; - } - - /** - * Get the weight for file matching - */ - protected function getFileWeight(): float - { - return 0.2; - } - - /** - * Get the framework type identifier (usually same as getName()) - */ - protected function getFrameworkType(): string - { - return $this->getName(); - } - - /** - * Check if composer.json contains framework-specific packages - */ - protected function hasFrameworkPackages(array $composer): bool - { - foreach ($this->getFrameworkPackages() as $package) { - if ($this->composerReader->hasPackage($composer, $package)) { - return true; - } - } - - return false; - } - - /** - * Calculate confidence score based on framework indicators - */ - protected function calculateConfidence(FSPath $projectRoot, array $composer): float - { - $confidence = $this->getBaseConfidence(); - - // Check for framework-specific files - $fileScore = $this->checkFrameworkFiles($projectRoot); - $confidence += $fileScore * $this->getFileWeight(); - - // Check for framework-specific directories - $existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot); - $directoryScore = $this->structureDetector->getPatternMatchConfidence( - $existingDirs, - $this->getFrameworkDirectories(), - ); - $confidence += $directoryScore * $this->getDirectoryWeight(); - - // Allow subclasses to add custom confidence calculations - $confidence += $this->getAdditionalConfidence($projectRoot, $composer, $existingDirs); - - return $confidence; - } - - /** - * Check for framework-specific files and return confidence score - */ - protected function checkFrameworkFiles(FSPath $projectRoot): float - { - $frameworkFiles = $this->getFrameworkFiles(); - - if (empty($frameworkFiles)) { - return 0.0; - } - - $found = 0; - foreach ($frameworkFiles as $file) { - if ($projectRoot->join($file)->exists()) { - $found++; - } - } - - return $found / \count($frameworkFiles); - } - - /** - * Allow subclasses to add framework-specific confidence calculations - */ - protected function getAdditionalConfidence( - FSPath $projectRoot, - array $composer, - array $existingDirectories, - ): float { - return 0.0; - } - - /** - * Build metadata for the analysis result - */ - protected function buildMetadata(FSPath $projectRoot, array $composer): array - { - $existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot); - - return [ - 'composer' => $composer, - 'frameworkPackages' => $this->getDetectedPackages($composer), - 'existingDirectories' => $existingDirs, - 'frameworkDirectoriesFound' => \array_intersect($existingDirs, $this->getFrameworkDirectories()), - 'frameworkFilesFound' => $this->getDetectedFiles($projectRoot), - 'directoryScore' => $this->structureDetector->getPatternMatchConfidence( - $existingDirs, - $this->getFrameworkDirectories(), - ), - 'fileScore' => $this->checkFrameworkFiles($projectRoot), - ]; - } - - /** - * Get detected framework packages from composer.json - */ - protected function getDetectedPackages(array $composer): array - { - $detected = []; - foreach ($this->getFrameworkPackages() as $package) { - if ($this->composerReader->hasPackage($composer, $package)) { - $detected[$package] = $this->composerReader->getPackageVersion($composer, $package); - } - } - return $detected; - } - - /** - * Get detected framework files - */ - protected function getDetectedFiles(FSPath $projectRoot): array - { - $detected = []; - foreach ($this->getFrameworkFiles() as $file) { - if ($projectRoot->join($file)->exists()) { - $detected[] = $file; - } - } - return $detected; - } -} diff --git a/src/Template/Analysis/Analyzer/ComposerAnalyzer.php b/src/Template/Analysis/Analyzer/ComposerAnalyzer.php deleted file mode 100644 index b7eb3390..00000000 --- a/src/Template/Analysis/Analyzer/ComposerAnalyzer.php +++ /dev/null @@ -1,78 +0,0 @@ -canAnalyze($projectRoot)) { - return null; - } - - $composer = $this->composerReader->readComposerFile($projectRoot); - - if ($composer === null) { - return null; - } - - $existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot); - $structureConfidence = $this->structureDetector->calculateStructureConfidence($existingDirs); - - // Base confidence for having composer.json - $confidence = 0.4; - - // Boost confidence based on directory structure - $confidence += $structureConfidence * 0.4; - - // Boost confidence if it has a proper package name - if (isset($composer['name']) && \is_string($composer['name']) && \str_contains($composer['name'], '/')) { - $confidence += 0.2; - } - - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: 'generic-php', - confidence: \min($confidence, 1.0), - suggestedTemplates: ['generic-php'], - metadata: [ - 'composer' => $composer, - 'existingDirectories' => $existingDirs, - 'packageName' => $composer['name'] ?? null, - 'packages' => $this->composerReader->getAllPackages($composer), - ], - ); - } - - public function canAnalyze(FSPath $projectRoot): bool - { - return $projectRoot->join('composer.json')->exists(); - } - - public function getPriority(): int - { - return 50; // Medium priority - let specific framework analyzers go first - } - - public function getName(): string - { - return 'composer'; - } -} diff --git a/src/Template/Analysis/Analyzer/FallbackAnalyzer.php b/src/Template/Analysis/Analyzer/FallbackAnalyzer.php deleted file mode 100644 index 951f6ff6..00000000 --- a/src/Template/Analysis/Analyzer/FallbackAnalyzer.php +++ /dev/null @@ -1,78 +0,0 @@ -structureDetector->detectExistingDirectories($projectRoot); - $confidence = $this->structureDetector->calculateStructureConfidence($existingDirs); - - // Determine the best generic template based on what we found - $suggestedTemplate = $this->determineBestTemplate($existingDirs); - - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: 'generic', - confidence: $confidence, - suggestedTemplates: [$suggestedTemplate], - metadata: [ - 'existingDirectories' => $existingDirs, - 'isFallback' => true, - 'directoryCount' => \count($existingDirs), - ], - ); - } - - public function canAnalyze(FSPath $projectRoot): bool - { - // This analyzer can always analyze any project as a fallback - return true; - } - - public function getPriority(): int - { - return 1; // Lowest priority - only used when no other analyzers match - } - - public function getName(): string - { - return 'fallback'; - } - - /** - * Determine the best generic template based on existing directories - */ - private function determineBestTemplate(array $existingDirs): string - { - // If we have src or app directories, assume it's a PHP project - if (\in_array('src', $existingDirs, true) || \in_array('app', $existingDirs, true)) { - return 'generic-php'; - } - - // If we have lib directory, might be a library project - if (\in_array('lib', $existingDirs, true)) { - return 'generic-php'; - } - - // Default fallback - return 'generic-php'; - } -} diff --git a/src/Template/Analysis/Analyzer/GoAnalyzer.php b/src/Template/Analysis/Analyzer/GoAnalyzer.php deleted file mode 100644 index 02459737..00000000 --- a/src/Template/Analysis/Analyzer/GoAnalyzer.php +++ /dev/null @@ -1,394 +0,0 @@ - ['github.com/gin-gonic/gin'], - 'echo' => ['github.com/labstack/echo'], - 'fiber' => ['github.com/gofiber/fiber'], - 'chi' => ['github.com/go-chi/chi'], - 'mux' => ['github.com/gorilla/mux'], - 'beego' => ['github.com/beego/beego'], - 'iris' => ['github.com/kataras/iris'], - 'buffalo' => ['github.com/gobuffalo/buffalo'], - 'revel' => ['github.com/revel/revel'], - 'fasthttp' => ['github.com/valyala/fasthttp'], - 'grpc' => ['google.golang.org/grpc'], - 'cobra' => ['github.com/spf13/cobra'], // CLI framework - ]; - - /** - * Go project indicator files - */ - private const array GO_FILES = [ - 'go.mod', - 'go.sum', - 'go.work', - 'main.go', - 'cmd', - 'Makefile', - 'Dockerfile', - '.gitignore', - ]; - - /** - * Go project directories - */ - private const array GO_DIRECTORIES = [ - 'cmd', - 'pkg', - 'internal', - 'api', - 'web', - 'configs', - 'scripts', - 'build', - 'deployments', - 'test', - 'tests', - 'docs', - 'tools', - 'vendor', - 'bin', - 'assets', - 'static', - 'templates', - ]; - - public function __construct( - private FilesInterface $files, - private ProjectStructureDetector $structureDetector, - ) {} - - public function analyze(FSPath $projectRoot): ?AnalysisResult - { - if (!$this->canAnalyze($projectRoot)) { - return null; - } - - $goModData = $this->parseGoMod($projectRoot); - $dependencies = $goModData['dependencies'] ?? []; - $detectedFramework = $this->detectFramework($dependencies); - $existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot); - $projectType = $this->determineProjectType($projectRoot, $dependencies); - - $detectedType = $detectedFramework ?? $projectType; - $confidence = $this->calculateConfidence($projectRoot, $goModData, $detectedFramework, $existingDirs); - - $suggestedTemplates = $detectedFramework ? [$detectedFramework] : ['go']; - - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: $detectedType, - confidence: $confidence, - suggestedTemplates: $suggestedTemplates, - metadata: [ - 'goMod' => $goModData, - 'dependencies' => $dependencies, - 'detectedFramework' => $detectedFramework, - 'projectType' => $projectType, - 'existingDirectories' => $existingDirs, - 'goFiles' => $this->getDetectedFiles($projectRoot), - 'goVersion' => $goModData['goVersion'] ?? null, - 'moduleName' => $goModData['module'] ?? null, - ], - ); - } - - public function canAnalyze(FSPath $projectRoot): bool - { - // Primary indicator: go.mod file - if ($projectRoot->join('go.mod')->exists()) { - return true; - } - - // Secondary indicators: Go files in expected locations - if ($projectRoot->join('main.go')->exists()) { - return true; - } - - // Check cmd directory for Go files - $cmdDir = $projectRoot->join('cmd'); - if ($cmdDir->exists() && $this->hasGoFiles($cmdDir)) { - return true; - } - - return false; - } - - public function getPriority(): int - { - return 80; // High priority for Go framework detection - } - - public function getName(): string - { - return 'go'; - } - - /** - * Parse go.mod file for module info and dependencies - */ - private function parseGoMod(FSPath $projectRoot): array - { - $goModFile = $projectRoot->join('go.mod'); - - if (!$goModFile->exists()) { - return []; - } - - $content = $this->files->read($goModFile->toString()); - if ($content === '') { - return []; - } - - $data = [ - 'module' => null, - 'goVersion' => null, - 'dependencies' => [], - ]; - - $lines = \explode("\n", $content); - $inRequireBlock = false; - - foreach ($lines as $line) { - $line = \trim($line); - - // Skip empty lines and comments - if ($line === '' || \str_starts_with($line, '//')) { - continue; - } - - // Parse module name - if (\preg_match('/^module\s+(.+)$/', $line, $matches)) { - $data['module'] = \trim($matches[1]); - continue; - } - - // Parse go version - if (\preg_match('/^go\s+(.+)$/', $line, $matches)) { - $data['goVersion'] = \trim($matches[1]); - continue; - } - - // Handle require block - if (\str_starts_with($line, 'require (')) { - $inRequireBlock = true; - continue; - } - - if ($inRequireBlock && $line === ')') { - $inRequireBlock = false; - continue; - } - - // Parse single require line - if (\str_starts_with($line, 'require ') && !\str_contains($line, '(')) { - if (\preg_match('/^require\s+([^\s]+)/', $line, $matches)) { - $data['dependencies'][] = $matches[1]; - } - continue; - } - - // Parse require block content - if ($inRequireBlock) { - if (\preg_match('/^\s*([^\s]+)/', $line, $matches)) { - $data['dependencies'][] = $matches[1]; - } - } - } - - return $data; - } - - /** - * Detect Go framework from dependencies - */ - private function detectFramework(array $dependencies): ?string - { - foreach (self::FRAMEWORK_PATTERNS as $framework => $patterns) { - foreach ($patterns as $pattern) { - foreach ($dependencies as $dependency) { - if (\str_starts_with((string) $dependency, $pattern)) { - return $framework; - } - } - } - } - - return null; - } - - /** - * Determine project type based on structure and dependencies - */ - private function determineProjectType(FSPath $projectRoot, array $dependencies): string - { - // Check for CLI patterns - if ($projectRoot->join('cmd')->exists() || $this->hasCliDependencies($dependencies)) { - return 'go-cli'; - } - - // Check for web service patterns - if ($this->hasWebDependencies($dependencies)) { - return 'go-web'; - } - - // Check for gRPC patterns - if ($this->hasGrpcDependencies($dependencies)) { - return 'go-grpc'; - } - - // Default Go project - return 'go'; - } - - /** - * Calculate confidence score for Go project detection - */ - private function calculateConfidence( - FSPath $projectRoot, - array $goModData, - ?string $detectedFramework, - array $existingDirs, - ): float { - $confidence = 0.6; // Base confidence for having Go indicators - - // High confidence boost for go.mod file - if ($projectRoot->join('go.mod')->exists()) { - $confidence += 0.2; - } - - // Boost confidence if we detected a framework - if ($detectedFramework !== null) { - $confidence += 0.15; - } - - // Boost confidence for having dependencies - if (!empty($goModData['dependencies'] ?? [])) { - $confidence += 0.1; - } - - // Boost confidence based on directory structure - $goDirScore = $this->structureDetector->getPatternMatchConfidence( - $existingDirs, - self::GO_DIRECTORIES, - ); - $confidence += $goDirScore * 0.05; - - return \min($confidence, 1.0); - } - - /** - * Get detected Go files - */ - private function getDetectedFiles(FSPath $projectRoot): array - { - $detected = []; - foreach (self::GO_FILES as $file) { - if ($projectRoot->join($file)->exists()) { - $detected[] = $file; - } - } - return $detected; - } - - /** - * Check if directory contains Go files - */ - private function hasGoFiles(FSPath $directory): bool - { - if (!$this->files->isDirectory($directory->toString())) { - return false; - } - - $files = $this->files->getFiles($directory->toString(), '*.go'); - return !empty($files); - } - - /** - * Check for CLI framework dependencies - */ - private function hasCliDependencies(array $dependencies): bool - { - $cliPatterns = [ - 'github.com/spf13/cobra', - 'github.com/urfave/cli', - 'github.com/alecthomas/kingpin', - 'github.com/jessevdk/go-flags', - ]; - - foreach ($dependencies as $dependency) { - foreach ($cliPatterns as $pattern) { - if (\str_starts_with((string) $dependency, $pattern)) { - return true; - } - } - } - - return false; - } - - /** - * Check for web framework dependencies - */ - private function hasWebDependencies(array $dependencies): bool - { - $webPatterns = [ - 'github.com/gin-gonic/gin', - 'github.com/labstack/echo', - 'github.com/gofiber/fiber', - 'github.com/go-chi/chi', - 'github.com/gorilla/mux', - 'net/http', // Standard library - ]; - - foreach ($dependencies as $dependency) { - foreach ($webPatterns as $pattern) { - if (\str_starts_with((string) $dependency, $pattern)) { - return true; - } - } - } - - return false; - } - - /** - * Check for gRPC dependencies - */ - private function hasGrpcDependencies(array $dependencies): bool - { - $grpcPatterns = [ - 'google.golang.org/grpc', - 'google.golang.org/protobuf', - 'github.com/grpc-ecosystem', - ]; - - foreach ($dependencies as $dependency) { - foreach ($grpcPatterns as $pattern) { - if (\str_starts_with((string) $dependency, $pattern)) { - return true; - } - } - } - - return false; - } -} diff --git a/src/Template/Analysis/Analyzer/PackageJsonAnalyzer.php b/src/Template/Analysis/Analyzer/PackageJsonAnalyzer.php deleted file mode 100644 index 4e32fd5f..00000000 --- a/src/Template/Analysis/Analyzer/PackageJsonAnalyzer.php +++ /dev/null @@ -1,222 +0,0 @@ - ['react', 'react-dom'], - 'vue' => ['vue'], - 'next' => ['next'], - 'nuxt' => ['nuxt', '@nuxt/kit'], - 'express' => ['express'], - 'angular' => ['@angular/core'], - 'svelte' => ['svelte'], - 'gatsby' => ['gatsby'], - ]; - - public function __construct( - private FilesInterface $files, - private ProjectStructureDetector $structureDetector, - ) {} - - public function analyze(FSPath $projectRoot): ?AnalysisResult - { - if (!$this->canAnalyze($projectRoot)) { - return null; - } - - $packageJson = $this->readPackageJson($projectRoot); - - if ($packageJson === null) { - return null; - } - - $detectedFramework = $this->detectFramework($packageJson); - $existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot); - - if ($detectedFramework === null) { - // Generic Node.js project - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: 'node', - confidence: 0.6, - suggestedTemplates: ['node'], - metadata: [ - 'packageJson' => $packageJson, - 'existingDirectories' => $existingDirs, - 'packageName' => $packageJson['name'] ?? null, - 'scripts' => $packageJson['scripts'] ?? [], - ], - ); - } - - $confidence = $this->calculateFrameworkConfidence($packageJson, $detectedFramework); - - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: $detectedFramework, - confidence: $confidence, - suggestedTemplates: [$detectedFramework], - metadata: [ - 'packageJson' => $packageJson, - 'detectedFramework' => $detectedFramework, - 'existingDirectories' => $existingDirs, - 'packageName' => $packageJson['name'] ?? null, - 'scripts' => $packageJson['scripts'] ?? [], - 'dependencies' => $this->getAllDependencies($packageJson), - ], - ); - } - - public function canAnalyze(FSPath $projectRoot): bool - { - return $projectRoot->join('package.json')->exists(); - } - - public function getPriority(): int - { - return 80; // High priority for JavaScript framework detection - } - - public function getName(): string - { - return 'package-json'; - } - - /** - * Read and parse package.json file - */ - private function readPackageJson(FSPath $projectRoot): ?array - { - $packagePath = $projectRoot->join('package.json'); - - if (!$packagePath->exists()) { - return null; - } - - $content = $this->files->read($packagePath->toString()); - - if ($content === '') { - return null; - } - - $decoded = \json_decode($content, true); - - if (!\is_array($decoded)) { - return null; - } - - return $decoded; - } - - /** - * Detect the framework based on package.json dependencies - */ - private function detectFramework(array $packageJson): ?string - { - $allDependencies = $this->getAllDependencies($packageJson); - - // Check for framework-specific packages - foreach (self::FRAMEWORK_PATTERNS as $framework => $patterns) { - foreach ($patterns as $pattern) { - if (\array_key_exists($pattern, $allDependencies)) { - return $framework; - } - } - } - - return null; - } - - /** - * Calculate confidence score for detected framework - */ - private function calculateFrameworkConfidence(array $packageJson, string $framework): float - { - $confidence = 0.7; // Base confidence for framework detection - - // Boost confidence if multiple framework packages are present - $frameworkPatterns = self::FRAMEWORK_PATTERNS[$framework] ?? []; - $allDependencies = $this->getAllDependencies($packageJson); - - $matchCount = 0; - foreach ($frameworkPatterns as $pattern) { - if (\array_key_exists($pattern, $allDependencies)) { - $matchCount++; - } - } - - if ($matchCount > 1) { - $confidence += 0.2; - } - - // Boost confidence if there are relevant scripts - $scripts = $packageJson['scripts'] ?? []; - if ($this->hasRelevantScripts($scripts, $framework)) { - $confidence += 0.1; - } - - return \min($confidence, 1.0); - } - - /** - * Check if scripts are relevant to the detected framework - */ - private function hasRelevantScripts(array $scripts, string $framework): bool - { - $relevantScripts = match ($framework) { - 'react' => ['start', 'build', 'test'], - 'vue' => ['serve', 'build', 'test'], - 'next' => ['dev', 'build', 'start'], - 'nuxt' => ['dev', 'build', 'generate'], - 'express' => ['start', 'dev'], - 'angular' => ['ng', 'start', 'build'], - default => ['start', 'build'], - }; - - foreach ($relevantScripts as $script) { - if (isset($scripts[$script])) { - return true; - } - } - - return false; - } - - /** - * Get all dependencies from package.json - */ - private function getAllDependencies(array $packageJson): array - { - $dependencies = []; - - if (isset($packageJson['dependencies'])) { - $dependencies = \array_merge($dependencies, $packageJson['dependencies']); - } - - if (isset($packageJson['devDependencies'])) { - $dependencies = \array_merge($dependencies, $packageJson['devDependencies']); - } - - if (isset($packageJson['peerDependencies'])) { - $dependencies = \array_merge($dependencies, $packageJson['peerDependencies']); - } - - return $dependencies; - } -} diff --git a/src/Template/Analysis/Analyzer/PythonAnalyzer.php b/src/Template/Analysis/Analyzer/PythonAnalyzer.php deleted file mode 100644 index 7f9edbbc..00000000 --- a/src/Template/Analysis/Analyzer/PythonAnalyzer.php +++ /dev/null @@ -1,386 +0,0 @@ - ['Django', 'django'], - 'flask' => ['Flask', 'flask'], - 'fastapi' => ['fastapi', 'FastAPI'], - 'pyramid' => ['pyramid'], - 'tornado' => ['tornado'], - 'bottle' => ['bottle'], - 'cherrypy' => ['CherryPy', 'cherrypy'], - 'falcon' => ['falcon'], - 'sanic' => ['sanic'], - 'quart' => ['quart'], - 'starlette' => ['starlette'], - ]; - - /** - * Python project indicator files - */ - private const array PYTHON_FILES = [ - 'requirements.txt', - 'pyproject.toml', - 'setup.py', - 'setup.cfg', - 'Pipfile', - 'poetry.lock', - 'conda.yml', - 'environment.yml', - 'manage.py', // Django - 'app.py', // Common Flask pattern - 'main.py', // Common FastAPI pattern - 'wsgi.py', - 'asgi.py', - ]; - - /** - * Python project directories - */ - private const array PYTHON_DIRECTORIES = [ - 'src', - 'lib', - 'app', - 'apps', - 'project', - 'tests', - 'test', - 'static', - 'templates', - 'migrations', - 'venv', - 'env', - '.venv', - '__pycache__', - ]; - - public function __construct( - private FilesInterface $files, - private ProjectStructureDetector $structureDetector, - ) {} - - public function analyze(FSPath $projectRoot): ?AnalysisResult - { - if (!$this->canAnalyze($projectRoot)) { - return null; - } - - $dependencies = $this->extractDependencies($projectRoot); - $detectedFramework = $this->detectFramework($dependencies); - $existingDirs = $this->structureDetector->detectExistingDirectories($projectRoot); - - $detectedType = $detectedFramework ?? 'python'; - $confidence = $this->calculateConfidence($projectRoot, $dependencies, $detectedFramework, $existingDirs); - - $suggestedTemplates = $detectedFramework ? [$detectedFramework] : ['python']; - - return new AnalysisResult( - analyzerName: $this->getName(), - detectedType: $detectedType, - confidence: $confidence, - suggestedTemplates: $suggestedTemplates, - metadata: [ - 'dependencies' => $dependencies, - 'detectedFramework' => $detectedFramework, - 'existingDirectories' => $existingDirs, - 'pythonFiles' => $this->getDetectedFiles($projectRoot), - 'packageManagers' => $this->detectPackageManagers($projectRoot), - ], - ); - } - - public function canAnalyze(FSPath $projectRoot): bool - { - // Check for any Python indicator files - foreach (self::PYTHON_FILES as $file) { - if ($projectRoot->join($file)->exists()) { - return true; - } - } - - // Check for .py files in common directories - foreach (['src', 'app', '.'] as $dir) { - $dirPath = $projectRoot->join($dir); - if ($dirPath->exists() && $this->hasPythonFiles($dirPath)) { - return true; - } - } - - return false; - } - - public function getPriority(): int - { - return 75; // High priority for Python framework detection - } - - public function getName(): string - { - return 'python'; - } - - /** - * Extract dependencies from various Python dependency files - */ - private function extractDependencies(FSPath $projectRoot): array - { - $dependencies = []; - - // Parse requirements.txt - $requirementsFile = $projectRoot->join('requirements.txt'); - if ($requirementsFile->exists()) { - $dependencies = \array_merge($dependencies, $this->parseRequirementsTxt($requirementsFile)); - } - - // Parse pyproject.toml - $pyprojectFile = $projectRoot->join('pyproject.toml'); - if ($pyprojectFile->exists()) { - $dependencies = \array_merge($dependencies, $this->parsePyprojectToml($pyprojectFile)); - } - - // Parse setup.py (basic extraction) - $setupFile = $projectRoot->join('setup.py'); - if ($setupFile->exists()) { - $dependencies = \array_merge($dependencies, $this->parseSetupPy($setupFile)); - } - - // Parse Pipfile - $pipfile = $projectRoot->join('Pipfile'); - if ($pipfile->exists()) { - $dependencies = \array_merge($dependencies, $this->parsePipfile($pipfile)); - } - - return \array_unique($dependencies); - } - - /** - * Parse requirements.txt file - */ - private function parseRequirementsTxt(FSPath $requirementsFile): array - { - $content = $this->files->read($requirementsFile->toString()); - if ($content === '') { - return []; - } - - $dependencies = []; - $lines = \explode("\n", $content); - - foreach ($lines as $line) { - $line = \trim($line); - - // Skip comments and empty lines - if ($line === '' || \str_starts_with($line, '#')) { - continue; - } - - // Extract package name (everything before version specifiers) - if (\preg_match('/^([a-zA-Z0-9_-]+)/', $line, $matches)) { - $dependencies[] = $matches[1]; - } - } - - return $dependencies; - } - - /** - * Parse pyproject.toml file for dependencies - */ - private function parsePyprojectToml(FSPath $pyprojectFile): array - { - $content = $this->files->read($pyprojectFile->toString()); - if ($content === '') { - return []; - } - - $dependencies = []; - - // Basic TOML parsing for dependencies section - if (\preg_match('/\[tool\.poetry\.dependencies\](.*?)(?=\[|$)/s', $content, $matches)) { - $dependenciesSection = $matches[1]; - if (\preg_match_all('/^([a-zA-Z0-9_-]+)\s*=/m', $dependenciesSection, $matches)) { - $dependencies = \array_merge($dependencies, $matches[1]); - } - } - - // Also check for PEP 621 format - if (\preg_match('/\[project\](.*?)(?=\[|$)/s', $content, $matches)) { - $projectSection = $matches[1]; - if (\preg_match('/dependencies\s*=\s*\[(.*?)\]/s', $projectSection, $depMatches)) { - $depList = $depMatches[1]; - if (\preg_match_all('/"([a-zA-Z0-9_-]+)/', $depList, $matches)) { - $dependencies = \array_merge($dependencies, $matches[1]); - } - } - } - - return $dependencies; - } - - /** - * Basic parsing of setup.py for install_requires - */ - private function parseSetupPy(FSPath $setupFile): array - { - $content = $this->files->read($setupFile->toString()); - if ($content === '') { - return []; - } - - $dependencies = []; - - // Look for install_requires list - if (\preg_match('/install_requires\s*=\s*\[(.*?)\]/s', $content, $matches)) { - $requiresList = $matches[1]; - if (\preg_match_all('/"([a-zA-Z0-9_-]+)/', $requiresList, $matches)) { - $dependencies = $matches[1]; - } elseif (\preg_match_all("/'([a-zA-Z0-9_-]+)/", $requiresList, $matches)) { - $dependencies = $matches[1]; - } - } - - return $dependencies; - } - - /** - * Basic parsing of Pipfile - */ - private function parsePipfile(FSPath $pipfile): array - { - $content = $this->files->read($pipfile->toString()); - if ($content === '') { - return []; - } - - $dependencies = []; - - // Parse [packages] section - if (\preg_match('/\[packages\](.*?)(?=\[|$)/s', $content, $matches)) { - $packagesSection = $matches[1]; - if (\preg_match_all('/^([a-zA-Z0-9_-]+)\s*=/m', $packagesSection, $matches)) { - $dependencies = \array_merge($dependencies, $matches[1]); - } - } - - return $dependencies; - } - - /** - * Detect Python framework from dependencies - */ - private function detectFramework(array $dependencies): ?string - { - foreach (self::FRAMEWORK_PATTERNS as $framework => $patterns) { - foreach ($patterns as $pattern) { - if (\in_array($pattern, $dependencies, true)) { - return $framework; - } - } - } - - return null; - } - - /** - * Calculate confidence score for Python project detection - */ - private function calculateConfidence( - FSPath $projectRoot, - array $dependencies, - ?string $detectedFramework, - array $existingDirs, - ): float { - $confidence = 0.4; // Base confidence for having Python indicators - - // Boost confidence if we detected a framework - if ($detectedFramework !== null) { - $confidence += 0.3; - } - - // Boost confidence for having dependencies - if (!empty($dependencies)) { - $confidence += 0.2; - } - - // Boost confidence based on directory structure - $pythonDirScore = $this->structureDetector->getPatternMatchConfidence( - $existingDirs, - self::PYTHON_DIRECTORIES, - ); - $confidence += $pythonDirScore * 0.1; - - // Boost confidence if we have manage.py (Django indicator) - if ($projectRoot->join('manage.py')->exists()) { - $confidence += 0.1; - } - - return \min($confidence, 1.0); - } - - /** - * Get detected Python files - */ - private function getDetectedFiles(FSPath $projectRoot): array - { - $detected = []; - foreach (self::PYTHON_FILES as $file) { - if ($projectRoot->join($file)->exists()) { - $detected[] = $file; - } - } - return $detected; - } - - /** - * Detect which package managers are in use - */ - private function detectPackageManagers(FSPath $projectRoot): array - { - $managers = []; - - if ($projectRoot->join('requirements.txt')->exists()) { - $managers[] = 'pip'; - } - if ($projectRoot->join('pyproject.toml')->exists()) { - $managers[] = 'poetry'; - } - if ($projectRoot->join('Pipfile')->exists()) { - $managers[] = 'pipenv'; - } - if ($projectRoot->join('conda.yml')->exists() || $projectRoot->join('environment.yml')->exists()) { - $managers[] = 'conda'; - } - - return $managers; - } - - /** - * Check if directory contains Python files - */ - private function hasPythonFiles(FSPath $directory): bool - { - if (!$this->files->isDirectory($directory->toString())) { - return false; - } - - $files = $this->files->getFiles($directory->toString(), '*.py'); - return !empty($files); - } -} diff --git a/src/Template/Analysis/AnalyzerChain.php b/src/Template/Analysis/AnalyzerChain.php deleted file mode 100644 index a6749f82..00000000 --- a/src/Template/Analysis/AnalyzerChain.php +++ /dev/null @@ -1,94 +0,0 @@ - */ - private array $analyzers = []; - - /** - * @param array $analyzers - */ - public function __construct(array $analyzers = []) - { - foreach ($analyzers as $analyzer) { - $this->addAnalyzer($analyzer); - } - } - - /** - * Add an analyzer to the chain, maintaining priority order - */ - public function addAnalyzer(ProjectAnalyzerInterface $analyzer): void - { - $this->analyzers[] = $analyzer; - $this->sortByPriority(); - } - - /** - * Execute the chain and collect all results - * - * @return array - */ - public function analyze(FSPath $projectRoot): array - { - $results = []; - - foreach ($this->analyzers as $analyzer) { - if ($analyzer->canAnalyze($projectRoot)) { - $result = $analyzer->analyze($projectRoot); - if ($result !== null) { - $results[] = $result; - } - } - } - - // Sort results by confidence (highest first) - \usort($results, static fn($a, $b) => $b->confidence <=> $a->confidence); - - return $results; - } - - /** - * Get all registered analyzers - * - * @return array - */ - public function getAllAnalyzers(): array - { - return $this->analyzers; - } - - /** - * Check if chain has any analyzers - */ - public function isEmpty(): bool - { - return empty($this->analyzers); - } - - /** - * Get count of analyzers in chain - */ - public function count(): int - { - return \count($this->analyzers); - } - - /** - * Sort analyzers by priority (highest first) - */ - private function sortByPriority(): void - { - \usort($this->analyzers, static fn($a, $b) => $b->getPriority() <=> $a->getPriority()); - } -} diff --git a/src/Template/Analysis/ProjectAnalysisService.php b/src/Template/Analysis/ProjectAnalysisService.php deleted file mode 100644 index 56082e86..00000000 --- a/src/Template/Analysis/ProjectAnalysisService.php +++ /dev/null @@ -1,46 +0,0 @@ - $analyzers - */ - public function __construct(array $analyzers = []) - { - $this->analyzerChain = new AnalyzerChain($analyzers); - } - - /** - * Analyze a project and return analysis results - * - * This method guarantees to always return at least one result. - * If no specific analyzers match, the fallback analyzer will provide a default result. - * - * @return array - */ - public function analyzeProject(FSPath $projectRoot): array - { - $results = $this->analyzerChain->analyze($projectRoot); - - // This should never happen if FallbackAnalyzer is registered, - // but add safety check just in case - if (empty($results)) { - throw new \RuntimeException( - 'No analysis results returned. Ensure FallbackAnalyzer is registered.', - ); - } - - return $results; - } -} diff --git a/src/Template/Analysis/ProjectAnalyzerInterface.php b/src/Template/Analysis/ProjectAnalyzerInterface.php deleted file mode 100644 index 7426320c..00000000 --- a/src/Template/Analysis/ProjectAnalyzerInterface.php +++ /dev/null @@ -1,33 +0,0 @@ -join('composer.json'); - - if (!$composerPath->exists()) { - return null; - } - - $content = $this->files->read($composerPath->toString()); - - if ($content === '') { - return null; - } - - $decoded = \json_decode($content, true); - - if (!\is_array($decoded)) { - return null; - } - - return $decoded; - } - - /** - * Check if a package is present in composer dependencies - */ - public function hasPackage(array $composer, string $packageName): bool - { - // Check in require section - if (isset($composer['require']) && \array_key_exists($packageName, $composer['require'])) { - return true; - } - - // Check in require-dev section - if (isset($composer['require-dev']) && \array_key_exists($packageName, $composer['require-dev'])) { - return true; - } - - return false; - } - - /** - * Get package version from composer dependencies - */ - public function getPackageVersion(array $composer, string $packageName): ?string - { - return $composer['require'][$packageName] ?? $composer['require-dev'][$packageName] ?? null; - } - - /** - * Get all packages from composer file - */ - public function getAllPackages(array $composer): array - { - $packages = []; - - if (isset($composer['require'])) { - $packages = \array_merge($packages, $composer['require']); - } - - if (isset($composer['require-dev'])) { - $packages = \array_merge($packages, $composer['require-dev']); - } - - return $packages; - } -} diff --git a/src/Template/Analysis/Util/ProjectStructureDetector.php b/src/Template/Analysis/Util/ProjectStructureDetector.php deleted file mode 100644 index 703d18a5..00000000 --- a/src/Template/Analysis/Util/ProjectStructureDetector.php +++ /dev/null @@ -1,98 +0,0 @@ - List of existing directories - */ - public function detectExistingDirectories(FSPath $projectRoot): array - { - $existing = []; - - foreach (self::COMMON_DIRECTORIES as $directory) { - if ($projectRoot->join($directory)->exists()) { - $existing[] = $directory; - } - } - - return $existing; - } - - /** - * Calculate confidence score based on directory structure - * More directories = higher confidence that this is a real project - */ - public function calculateStructureConfidence(array $existingDirectories): float - { - $count = \count($existingDirectories); - - if ($count === 0) { - return 0.1; // Very low confidence - } - - if ($count === 1) { - return 0.3; // Low confidence - } - - if ($count === 2) { - return 0.5; // Medium confidence - } - - if ($count >= 3) { - return 0.7; // High confidence - } - - return 0.4; // Fallback - } - - /** - * Check if directory structure matches a specific pattern - */ - public function matchesPattern(array $existingDirectories, array $requiredDirectories): bool - { - foreach ($requiredDirectories as $required) { - if (!\in_array($required, $existingDirectories, true)) { - return false; - } - } - - return true; - } - - /** - * Get confidence score for matching a specific pattern - */ - public function getPatternMatchConfidence(array $existingDirectories, array $requiredDirectories): float - { - if (empty($requiredDirectories)) { - return 0.0; - } - - $matches = \count(\array_intersect($existingDirectories, $requiredDirectories)); - $total = \count($requiredDirectories); - - return $matches / $total; - } -} diff --git a/src/Template/Builder/TemplateConfigurationBuilder.php b/src/Template/Builder/TemplateConfigurationBuilder.php deleted file mode 100644 index a9312cbb..00000000 --- a/src/Template/Builder/TemplateConfigurationBuilder.php +++ /dev/null @@ -1,196 +0,0 @@ -templateName) . '-structure.md'; - $description ??= \ucfirst($this->templateName) . ' Project Structure'; - $treeViewConfig ??= new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ); - - $this->documents[] = new Document( - description: $description, - outputPath: $outputPath, - overwrite: true, - modifiers: [], - tags: [\strtolower($this->templateName), 'structure'], - treeSource: new TreeSource( - sourcePaths: $sourcePaths, - description: \ucfirst($this->templateName) . ' Directory Structure', - treeView: $treeViewConfig, - ), - ); - - return $this; - } - - /** - * Add a source code document showing files with optional modifiers - */ - public function addSourceDocument( - string $description, - string $outputPath, - array $sourcePaths, - array $filePatterns = ['*.php'], - array $modifiers = [], - array $tags = [], - ): self { - $this->documents[] = new Document( - description: $description, - outputPath: $outputPath, - overwrite: true, - modifiers: $modifiers, - tags: \array_merge([\strtolower($this->templateName)], $tags), - fileSource: new FileSource( - sourcePaths: $sourcePaths, - description: $description, - filePattern: $filePatterns, - modifiers: $modifiers, - ), - ); - - return $this; - } - - /** - * Add custom document with full control - */ - public function addDocument(Document $document): self - { - $this->documents[] = $document; - return $this; - } - - /** - * Set required files for template detection - */ - public function requireFiles(array $files): self - { - $this->detectionCriteria['files'] = \array_merge( - $this->detectionCriteria['files'] ?? [], - $files, - ); - return $this; - } - - /** - * Set required directories for template detection - */ - public function requireDirectories(array $directories): self - { - $this->detectionCriteria['directories'] = \array_merge( - $this->detectionCriteria['directories'] ?? [], - $directories, - ); - return $this; - } - - /** - * Set required packages for template detection - */ - public function requirePackages(array $packages): self - { - $this->detectionCriteria['patterns'] = \array_merge( - $this->detectionCriteria['patterns'] ?? [], - $packages, - ); - return $this; - } - - /** - * Set complete detection criteria - */ - public function setDetectionCriteria(array $criteria): self - { - $this->detectionCriteria = $criteria; - return $this; - } - - /** - * Build the final configuration registry - */ - public function build(): ConfigRegistry - { - $config = new ConfigRegistry(); - $documents = new DocumentRegistry($this->documents); - $config->register($documents); - return $config; - } - - /** - * Get the detection criteria that was built - */ - public function getDetectionCriteria(): array - { - return $this->detectionCriteria; - } - - /** - * Get all documents that were added - * - * @return array - */ - public function getDocuments(): array - { - return $this->documents; - } - - /** - * Clear all documents (useful for rebuilding) - */ - public function clearDocuments(): self - { - $this->documents = []; - return $this; - } - - /** - * Clear detection criteria - */ - public function clearDetectionCriteria(): self - { - $this->detectionCriteria = []; - return $this; - } - - /** - * Reset builder to initial state - */ - public function reset(): self - { - $this->documents = []; - $this->detectionCriteria = []; - return $this; - } -} diff --git a/src/Template/Console/InitCommand.php b/src/Template/Console/InitCommand.php deleted file mode 100644 index 3d31a230..00000000 --- a/src/Template/Console/InitCommand.php +++ /dev/null @@ -1,341 +0,0 @@ -configFilename; - $ext = \pathinfo($filename, \PATHINFO_EXTENSION); - - try { - $type = ConfigType::fromExtension($ext); - } catch (\ValueError) { - $this->output->error(\sprintf('Unsupported config type: %s', $ext)); - return Command::FAILURE; - } - - $filename = \pathinfo(\strtolower($filename), PATHINFO_FILENAME) . '.' . $type->value; - $filePath = (string) $dirs->getRootPath()->join($filename); - - if ($files->exists($filePath)) { - $this->output->error(\sprintf('Config %s already exists', $filePath)); - return Command::FAILURE; - } - - if ($this->template !== null) { - return $this->initWithSpecificTemplate($files, $templateRegistry, $this->template, $type, $filePath); - } - - return $this->initWithSmartDetection($dirs, $files, $detectionService, $type, $filePath); - } - - private function initWithSpecificTemplate( - FilesInterface $files, - TemplateRegistry $templateRegistry, - string $templateName, - ConfigType $type, - string $filePath, - ): int { - $template = $templateRegistry->getTemplate($templateName); - - if ($template === null) { - $this->output->error(\sprintf('Template "%s" not found', $templateName)); - $this->showAvailableTemplates($templateRegistry); - return Command::FAILURE; - } - - $this->output->success(\sprintf('Using template: %s', $template->description)); - return $this->writeConfig($files, $template->config, $type, $filePath); - } - - private function initWithSmartDetection( - DirectoriesInterface $dirs, - FilesInterface $files, - TemplateDetectionService $detectionService, - ConfigType $type, - string $filePath, - ): int { - if ($this->output->isVerbose()) { - $this->output->writeln('Analyzing project structure...'); - $this->showDetectionStrategies($detectionService); - } - - if ($this->showAll) { - return $this->showAllPossibleTemplates($dirs, $files, $detectionService, $type, $filePath); - } - - $detection = $detectionService->detectBestTemplate($dirs->getRootPath()); - - if (!$detection->hasTemplate()) { - $this->output->warning('No specific project type detected.'); - $this->showDetectionFallbackOptions($detectionService); - return Command::FAILURE; - } - - $this->displayDetectionResult($detection, $detectionService); - return $this->writeConfig($files, $detection->template->config, $type, $filePath); - } - - private function showDetectionStrategies(TemplateDetectionService $detectionService): void - { - $strategies = $detectionService->getStrategies(); - - $this->output->writeln('Available detection strategies:'); - foreach ($strategies as $strategy) { - $this->output->writeln(\sprintf( - ' - %s (priority: %d, threshold: %.0f%%)', - $strategy->getName(), - $strategy->getPriority(), - $strategy->getConfidenceThreshold() * 100.0, - )); - } - $this->output->newLine(); - } - - private function displayDetectionResult( - $detection, - TemplateDetectionService $detectionService, - ): void { - $confidencePercent = $detection->confidence * 100.0; - - if ($detection->isHighConfidenceTemplateDetection()) { - $this->output->success(\sprintf( - 'High-confidence template match: %s (%.0f%% confidence)', - $detection->template->description, - $confidencePercent, - )); - } else { - $this->output->writeln(\sprintf( - 'Detected via analysis: %s (%.0f%% confidence, method: %s)', - $detection->template->description, - $confidencePercent, - $detection->getDetectionMethodDescription(), - )); - } - - // Show additional context in verbose mode - if ($this->output->isVerbose()) { - if (isset($detection->metadata['reason'])) { - $this->output->writeln(\sprintf(' Reason: %s', $detection->metadata['reason'])); - } - - // Show strategy used - if (isset($detection->metadata['selectedStrategy'])) { - $this->output->writeln(\sprintf(' Strategy: %s', $detection->metadata['selectedStrategy'])); - } - - // Show template match details if available - if ($detection->detectionMethod === 'template_criteria' && isset($detection->metadata['matchingCriteria'])) { - $criteria = $detection->metadata['matchingCriteria']; - if (!empty($criteria)) { - $this->output->writeln(' Matched criteria:'); - foreach ($criteria as $type => $matches) { - if (!empty($matches)) { - $this->output->writeln(\sprintf(' - %s: %s', $type, \implode(', ', $matches))); - } - } - } - } - } - } - - private function showAllPossibleTemplates( - DirectoriesInterface $dirs, - FilesInterface $files, - TemplateDetectionService $detectionService, - ConfigType $type, - string $filePath, - ): int { - $this->output->writeln('Analyzing all possible templates...'); - - $bestDetection = $detectionService->detectBestTemplate($dirs->getRootPath()); - $allDetections = $detectionService->getAllPossibleTemplates($dirs->getRootPath()); - - if (empty($allDetections)) { - $this->output->warning('No templates detected for this project.'); - return Command::FAILURE; - } - - $this->output->title('All Possible Templates'); - - $tableData = []; - foreach ($allDetections as $detection) { - $confidencePercent = $detection->confidence * 100.0; - - $isSelected = $bestDetection->hasTemplate() && - $detection->template !== null && - $detection->template->name === $bestDetection->template->name; - - $status = $this->getTemplateStatus($detection, $isSelected, $detectionService); - - $strategyInfo = $detection->getDetectionMethodDescription(); - if (isset($detection->metadata['strategy'])) { - $strategyInfo = $detection->metadata['strategy']; - } - - $tableData[] = [ - $detection->template->name ?? 'Unknown', - $detection->template->description ?? 'Unknown', - \sprintf('%.0f%%', $confidencePercent), - $strategyInfo, - $status, - ]; - } - - $this->output->table(['Template', 'Description', 'Confidence', 'Strategy', 'Status'], $tableData); - - $this->output->note(\sprintf( - 'Template detection uses %.0f%% confidence threshold. Strategies are tried in priority order.', - $detectionService->getHighConfidenceThreshold() * 100.0, - )); - - if ($bestDetection->hasTemplate()) { - $this->displayDetectionResult($bestDetection, $detectionService); - return $this->writeConfig($files, $bestDetection->template->config, $type, $filePath); - } - - $this->output->error('No suitable template found'); - return Command::FAILURE; - } - - private function getTemplateStatus($detection, bool $isSelected, TemplateDetectionService $detectionService): string - { - if ($isSelected) { - return match ($detection->detectionMethod) { - 'template_criteria' => 'Selected (Template)', - 'analyzer' => 'Selected (Analyzer)', - default => 'Selected', - }; - } - - if ($detection->detectionMethod === 'template_criteria') { - $meetsThreshold = $detection->confidence > $detectionService->getHighConfidenceThreshold(); - return $meetsThreshold ? 'High confidence but not best' : 'Low confidence'; - } - - return 'Available'; - } - - private function showAvailableTemplates(TemplateRegistry $templateRegistry): void - { - $this->output->note('Available templates:'); - foreach ($templateRegistry->getAllTemplates() as $template) { - $this->output->writeln(\sprintf(' - %s: %s', $template->name, $template->description)); - } - $this->output->newLine(); - $this->output->writeln('Use ctx template:list to see detailed template information.'); - } - - private function showDetectionFallbackOptions(TemplateDetectionService $detectionService): void - { - $this->output->writeln('Options:'); - $this->output->writeln(' - Use ctx init to specify a template manually'); - $this->output->writeln(' - Use ctx template:list to see available templates'); - $this->output->writeln(' - Use ctx init --show-all to see all detection results'); - - if ($this->output->isVerbose()) { - $this->output->newLine(); - $this->output->writeln('Detection strategies in use:'); - foreach ($detectionService->getStrategies() as $strategy) { - $this->output->writeln(\sprintf( - ' - %s (threshold: %.0f%%)', - \ucfirst(\str_replace('-', ' ', $strategy->getName())), - $strategy->getConfidenceThreshold() * 100.0, - )); - } - } - } - - private function writeConfig( - FilesInterface $files, - ConfigRegistry $config, - ConfigType $type, - string $filePath, - ): int { - try { - // Create a new config registry with schema for output - $outputConfig = new ConfigRegistry(JsonSchema::SCHEMA_URL); - - // Copy all registries from the original config - foreach ($config->all() as $registry) { - $outputConfig->register($registry); - } - - $content = match ($type) { - ConfigType::Json => \json_encode($outputConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), - ConfigType::Yaml => Yaml::dump( - \json_decode(\json_encode($outputConfig), true), - 10, - 2, - Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK, - ), - default => throw new \InvalidArgumentException( - \sprintf('Unsupported config type: %s', $type->value), - ), - }; - } catch (\Throwable $e) { - $this->output->error(\sprintf('Failed to create config: %s', $e->getMessage())); - return Command::FAILURE; - } - - $files->ensureDirectory(\dirname($filePath)); - $files->write($filePath, $content); - - $this->output->success(\sprintf('Configuration created: %s', $filePath)); - - if ($this->output->isVerbose()) { - $this->output->writeln('Next steps:'); - $this->output->writeln(' - Review and customize the generated configuration'); - $this->output->writeln(' - Run ctx generate to create context documents'); - $this->output->writeln(' - Use ctx server to start MCP server for Claude integration'); - } - - return Command::SUCCESS; - } -} diff --git a/src/Template/Console/ListCommand.php b/src/Template/Console/ListCommand.php deleted file mode 100644 index 2439ec2d..00000000 --- a/src/Template/Console/ListCommand.php +++ /dev/null @@ -1,160 +0,0 @@ -getAllTemplates(); - - if (empty($templates)) { - $this->output->warning('No templates available'); - return Command::SUCCESS; - } - - // Filter by tags if specified - if (!empty($this->tags)) { - $templates = \array_filter($templates, fn($template) => !empty(\array_intersect($this->tags, $template->tags))); - } - - if (empty($templates)) { - $this->output->warning(\sprintf( - 'No templates found with tag(s): %s', - \implode(', ', $this->tags), - )); - return Command::SUCCESS; - } - - $this->output->title('Available Templates'); - - if ($this->detailed) { - return $this->showDetailedList($templates); - } - - return $this->showBasicList($templates); - } - - /** - * Show basic template list in table format - */ - private function showBasicList(array $templates): int - { - $tableData = []; - - foreach ($templates as $template) { - $tableData[] = [ - $template->name, - $template->description, - \implode(', ', $template->tags), - $template->priority, - ]; - } - - $this->output->table(['Name', 'Description', 'Tags', 'Priority'], $tableData); - - $this->output->note('Use "ctx init " to initialize with a specific template.'); - - return Command::SUCCESS; - } - - /** - * Show detailed template information - */ - private function showDetailedList(array $templates): int - { - foreach ($templates as $index => $template) { - if ($index > 0) { - $this->output->newLine(); - } - - $this->output->section(\sprintf('%s (%s)', $template->name, $template->description)); - - // Show basic info - $this->output->definitionList( - ['Priority' => (string) $template->priority], - ['Tags' => empty($template->tags) ? 'None' : \implode(', ', $template->tags)], - ); - - // Show detection criteria if available - if (!empty($template->detectionCriteria)) { - $this->output->writeln('Detection Criteria:'); - - // Show files - if (isset($template->detectionCriteria['files']) && !empty($template->detectionCriteria['files'])) { - $this->output->writeln(\sprintf( - ' • Required Files: %s', - \implode(', ', $template->detectionCriteria['files']), - )); - } - - // Show directories - if (isset($template->detectionCriteria['directories']) && !empty($template->detectionCriteria['directories'])) { - $this->output->writeln(\sprintf( - ' • Expected Directories: %s', - \implode(', ', $template->detectionCriteria['directories']), - )); - } - - // Show patterns (packages) - if (isset($template->detectionCriteria['patterns']) && !empty($template->detectionCriteria['patterns'])) { - $this->output->writeln(\sprintf( - ' • Required Packages: %s', - \implode(', ', $template->detectionCriteria['patterns']), - )); - } - } - - // Show generated documents - $documents = $template->config?->has('documents') - ? $template->config->get('documents', DocumentRegistry::class)->getItems() - : []; - - if (!empty($documents)) { - $this->output->writeln('Generated Documents:'); - foreach ($documents as $document) { - $this->output->writeln(\sprintf( - ' • %s → %s', - $document->description, - $document->outputPath, - )); - } - } - } - - $this->output->note([ - 'Use "ctx init " to initialize with a specific template.', - 'Use "ctx init" to let the system detect the best template automatically.', - 'Use "ctx init --show-all" to see all possible templates with confidence scores.', - ]); - - return Command::SUCCESS; - } -} diff --git a/src/Template/Definition/AbstractTemplateDefinition.php b/src/Template/Definition/AbstractTemplateDefinition.php deleted file mode 100644 index dcc7057f..00000000 --- a/src/Template/Definition/AbstractTemplateDefinition.php +++ /dev/null @@ -1,171 +0,0 @@ -createDocuments($projectMetadata)); - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->buildDetectionCriteria(), - config: $config, - ); - } - - /** - * Get detection criteria for automatic selection - */ - final public function getDetectionCriteria(): array - { - return $this->buildDetectionCriteria(); - } - - /** - * Get the main source directories for this template type - * - * @return array - */ - abstract protected function getSourceDirectories(): array; - - /** - * Get framework-specific detection criteria - * - * @return array - */ - abstract protected function getFrameworkSpecificCriteria(): array; - - /** - * Get the output filename for the structure document - */ - protected function getStructureDocumentPath(): string - { - return 'docs/' . $this->getName() . '-structure.md'; - } - - /** - * Get the description for the structure document - */ - protected function getStructureDocumentDescription(): string - { - return $this->getDescription() . ' - Project Structure'; - } - - /** - * Get source paths filtered by existing directories in project metadata - * - * @return array - */ - protected function getDetectedSourcePaths(array $projectMetadata): array - { - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - $sourceDirs = $this->getSourceDirectories(); - - // Filter to only include directories that exist - $detectedPaths = \array_intersect($existingDirs, $sourceDirs); - - // If no standard directories found, return all expected directories - if (empty($detectedPaths)) { - return $sourceDirs; - } - - return \array_values($detectedPaths); - } - - /** - * Create additional documents beyond the standard structure document - * Override in subclasses to add framework-specific documents - * - * @return array - */ - protected function createAdditionalDocuments(array $projectMetadata): array - { - return []; - } - - /** - * Create the tree view configuration for structure documents - */ - protected function createTreeViewConfig(): TreeViewConfig - { - return new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ); - } - - /** - * Customize tree view configuration in subclasses if needed - */ - protected function customizeTreeViewConfig(TreeViewConfig $config): TreeViewConfig - { - return $config; - } - - /** - * Create all documents for this template - * - * @return array - */ - protected function createDocuments(array $projectMetadata): array - { - return [ - $this->createStructureDocument($projectMetadata), - ...$this->createAdditionalDocuments($projectMetadata), - ]; - } - - /** - * Create the main structure document - */ - protected function createStructureDocument(array $projectMetadata): Document - { - $sourcePaths = $this->getDetectedSourcePaths($projectMetadata); - $treeViewConfig = $this->customizeTreeViewConfig($this->createTreeViewConfig()); - - return new Document( - description: $this->getStructureDocumentDescription(), - outputPath: $this->getStructureDocumentPath(), - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: $sourcePaths, - description: $this->getName() . ' Directory Structure', - treeView: $treeViewConfig, - ), - ); - } - - /** - * Build complete detection criteria by merging common and framework-specific criteria - */ - private function buildDetectionCriteria(): array - { - return \array_merge([ - 'directories' => $this->getSourceDirectories(), - ], $this->getFrameworkSpecificCriteria()); - } -} diff --git a/src/Template/Definition/DjangoTemplateDefinition.php b/src/Template/Definition/DjangoTemplateDefinition.php deleted file mode 100644 index 52143f61..00000000 --- a/src/Template/Definition/DjangoTemplateDefinition.php +++ /dev/null @@ -1,99 +0,0 @@ - ['manage.py', 'requirements.txt'], - 'patterns' => ['Django', 'django'], - ]; - } - - /** - * Add Django-specific documents - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - // Add Models and Views document if apps or project directory exists - $sourcePaths = []; - if (\in_array('app', $existingDirs, true)) { - $sourcePaths[] = 'app'; - } - if (\in_array('apps', $existingDirs, true)) { - $sourcePaths[] = 'apps'; - } - if (\in_array('project', $existingDirs, true)) { - $sourcePaths[] = 'project'; - } - - if (!empty($sourcePaths)) { - $documents[] = new Document( - description: 'Django Models and Views', - outputPath: 'docs/django-models-views.md', - tags: ['django', 'models', 'views'], - fileSource: new FileSource( - sourcePaths: $sourcePaths, - description: 'Django Models and Views', - filePattern: ['models.py', 'views.py', '*.py'], - path: ['models', 'views'], - ), - ); - } - - // Add Settings and URLs document - if (!empty($sourcePaths)) { - $documents[] = new Document( - description: 'Django Configuration', - outputPath: 'docs/django-config.md', - tags: ['django', 'configuration'], - fileSource: new FileSource( - sourcePaths: $sourcePaths, - description: 'Django Configuration', - filePattern: ['settings.py', 'urls.py', 'wsgi.py', 'asgi.py'], - ), - ); - } - - return $documents; - } -} diff --git a/src/Template/Definition/ExpressTemplateDefinition.php b/src/Template/Definition/ExpressTemplateDefinition.php deleted file mode 100644 index 0b562908..00000000 --- a/src/Template/Definition/ExpressTemplateDefinition.php +++ /dev/null @@ -1,110 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'express'; - } - - public function getDescription(): string - { - return 'Express.js Node.js framework project template'; - } - - public function getTags(): array - { - return ['javascript', 'nodejs', 'express', 'backend', 'api']; - } - - public function getPriority(): int - { - return 80; // Medium-high priority for backend framework - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => ['package.json'], - 'patterns' => self::EXPRESS_PACKAGES, - 'directories' => self::EXPRESS_DIRECTORIES, - ]; - } - - /** - * Create the Express project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - return new Document( - description: 'Express Project Structure', - outputPath: 'docs/express-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: ['routes', 'controllers', 'middleware', 'models'], - description: 'Express Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } -} diff --git a/src/Template/Definition/FastApiTemplateDefinition.php b/src/Template/Definition/FastApiTemplateDefinition.php deleted file mode 100644 index 64f8de63..00000000 --- a/src/Template/Definition/FastApiTemplateDefinition.php +++ /dev/null @@ -1,111 +0,0 @@ - ['main.py', 'requirements.txt'], - 'patterns' => ['fastapi', 'FastAPI'], - ]; - } - - /** - * Add FastAPI-specific documents - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - // Add API Routes and Models document - $sourcePaths = []; - if (\in_array('app', $existingDirs, true)) { - $sourcePaths[] = 'app'; - } - if (\in_array('src', $existingDirs, true)) { - $sourcePaths[] = 'src'; - } - if (\in_array('api', $existingDirs, true)) { - $sourcePaths[] = 'api'; - } - - // If no structured directories, check for main.py in root - if (empty($sourcePaths)) { - $sourcePaths = ['.']; - } - - $documents[] = new Document( - description: 'FastAPI Routes and Endpoints', - outputPath: 'docs/fastapi-routes.md', - tags: ['fastapi', 'routes', 'api'], - fileSource: new FileSource( - sourcePaths: $sourcePaths, - description: 'FastAPI Routes and Endpoints', - filePattern: ['*.py'], - contains: ['@app.', 'APIRouter', 'FastAPI'], - ), - ); - - // Add Models and Schemas document if those directories exist - if (\in_array('models', $existingDirs, true) || \in_array('schemas', $existingDirs, true)) { - $modelPaths = []; - if (\in_array('models', $existingDirs, true)) { - $modelPaths[] = 'models'; - } - if (\in_array('schemas', $existingDirs, true)) { - $modelPaths[] = 'schemas'; - } - - $documents[] = new Document( - description: 'FastAPI Models and Schemas', - outputPath: 'docs/fastapi-models-schemas.md', - tags: ['fastapi', 'models', 'schemas'], - fileSource: new FileSource( - sourcePaths: $modelPaths, - description: 'FastAPI Models and Schemas', - filePattern: ['*.py'], - contains: ['BaseModel', 'SQLModel', 'pydantic'], - ), - ); - } - - return $documents; - } -} diff --git a/src/Template/Definition/FlaskTemplateDefinition.php b/src/Template/Definition/FlaskTemplateDefinition.php deleted file mode 100644 index 22717c39..00000000 --- a/src/Template/Definition/FlaskTemplateDefinition.php +++ /dev/null @@ -1,85 +0,0 @@ - ['app.py', 'requirements.txt'], - 'patterns' => ['Flask', 'flask'], - ]; - } - - /** - * Add Flask-specific documents - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - // Add Flask App and Routes document - $sourcePaths = []; - if (\in_array('app', $existingDirs, true)) { - $sourcePaths[] = 'app'; - } - if (\in_array('src', $existingDirs, true)) { - $sourcePaths[] = 'src'; - } - - // If no app/src directory, check for app.py in root - if (empty($sourcePaths)) { - $sourcePaths = ['.']; - } - - $documents[] = new Document( - description: 'Flask Application and Routes', - outputPath: 'docs/flask-app-routes.md', - tags: ['flask', 'routes', 'application'], - fileSource: new FileSource( - sourcePaths: $sourcePaths, - description: 'Flask Application and Routes', - filePattern: ['*.py'], - contains: ['@app.route', 'Flask', 'blueprint'], - ), - ); - - return $documents; - } -} diff --git a/src/Template/Definition/GenericPhpTemplateDefinition.php b/src/Template/Definition/GenericPhpTemplateDefinition.php deleted file mode 100644 index 95badb91..00000000 --- a/src/Template/Definition/GenericPhpTemplateDefinition.php +++ /dev/null @@ -1,120 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'generic-php'; - } - - public function getDescription(): string - { - return 'Generic PHP project template'; - } - - public function getTags(): array - { - return ['php', 'generic']; - } - - public function getPriority(): int - { - return 10; - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => ['composer.json'], - 'directories' => self::PHP_SOURCE_DIRECTORIES, - ]; - } - - /** - * Create the PHP project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - $sourcePaths = $this->getDetectedSourcePaths($projectMetadata); - - return new Document( - description: 'PHP Project Structure', - outputPath: 'docs/php-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: $sourcePaths, - description: 'PHP Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } - - /** - * Get detected source paths from project metadata - */ - private function getDetectedSourcePaths(array $projectMetadata): array - { - $existingDirs = $projectMetadata['existingDirectories'] ?? []; - - // Filter to only include common PHP source directories that exist - $sourcePaths = \array_intersect($existingDirs, self::PHP_SOURCE_DIRECTORIES); - - // If no standard source directories found, fall back to 'src' - if (empty($sourcePaths)) { - return ['src']; - } - - return \array_values($sourcePaths); - } -} diff --git a/src/Template/Definition/GinTemplateDefinition.php b/src/Template/Definition/GinTemplateDefinition.php deleted file mode 100644 index d03bdada..00000000 --- a/src/Template/Definition/GinTemplateDefinition.php +++ /dev/null @@ -1,99 +0,0 @@ - ['go.mod'], - 'patterns' => ['github.com/gin-gonic/gin'], - ]; - } - - /** - * Add Gin-specific documents - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - // Add handlers and routes document - $handlerPaths = []; - if (\in_array('handlers', $existingDirs, true)) { - $handlerPaths[] = 'handlers'; - } - if (\in_array('api', $existingDirs, true)) { - $handlerPaths[] = 'api'; - } - if (\in_array('internal', $existingDirs, true)) { - $handlerPaths[] = 'internal'; - } - - if (!empty($handlerPaths)) { - $documents[] = new Document( - description: 'Gin Handlers and Routes', - outputPath: 'docs/gin-handlers-routes.md', - tags: ['gin', 'handlers', 'routes'], - fileSource: new FileSource( - sourcePaths: $handlerPaths, - description: 'Gin Handlers and Routes', - filePattern: ['*.go'], - contains: ['gin.', 'c.JSON', 'router.', 'engine.'], - ), - ); - } - - // Add middleware document if middleware directory exists - if (\in_array('middleware', $existingDirs, true)) { - $documents[] = new Document( - description: 'Gin Middleware', - outputPath: 'docs/gin-middleware.md', - tags: ['gin', 'middleware'], - fileSource: new FileSource( - sourcePaths: ['middleware'], - description: 'Gin Middleware', - filePattern: ['*.go'], - ), - ); - } - - return $documents; - } -} diff --git a/src/Template/Definition/GoTemplateDefinition.php b/src/Template/Definition/GoTemplateDefinition.php deleted file mode 100644 index 42c0f617..00000000 --- a/src/Template/Definition/GoTemplateDefinition.php +++ /dev/null @@ -1,106 +0,0 @@ - ['go.mod'], - ]; - } - - /** - * Add Go-specific documents - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - // Add Go packages document if source directories exist - $sourcePaths = \array_intersect($existingDirs, $this->getSourceDirectories()); - - if (!empty($sourcePaths)) { - $documents[] = new Document( - description: 'Go Packages and Modules', - outputPath: 'docs/go-packages.md', - tags: ['go', 'packages'], - fileSource: new FileSource( - sourcePaths: \array_values($sourcePaths), - description: 'Go Packages and Modules', - filePattern: ['*.go'], - notPath: ['vendor', 'bin'], - ), - ); - } - - // Add configuration files document - $configFiles = []; - $potentialConfigFiles = [ - 'go.mod', - 'go.sum', - 'go.work', - 'Makefile', - 'Dockerfile', - '.golangci.yml', - '.golangci.yaml', - ]; - - foreach ($potentialConfigFiles as $configFile) { - if (\in_array($configFile, $projectMetadata['files'] ?? [], true)) { - $configFiles[] = $configFile; - } - } - - if (!empty($configFiles)) { - $documents[] = new Document( - description: 'Go Configuration Files', - outputPath: 'docs/go-config.md', - tags: ['go', 'configuration'], - fileSource: new FileSource( - sourcePaths: ['.'], - description: 'Go Configuration Files', - filePattern: $configFiles, - ), - ); - } - - return $documents; - } -} diff --git a/src/Template/Definition/LaravelTemplateDefinition.php b/src/Template/Definition/LaravelTemplateDefinition.php deleted file mode 100644 index 1f0aec8d..00000000 --- a/src/Template/Definition/LaravelTemplateDefinition.php +++ /dev/null @@ -1,109 +0,0 @@ - ['composer.json', 'artisan'], - 'patterns' => ['laravel/framework'], - ]; - } - - /** - * Override to customize Laravel tree view with framework-specific directories - */ - #[\Override] - protected function customizeTreeViewConfig( - TreeViewConfig $config, - ): TreeViewConfig { - return new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - dirContext: [ - 'app' => 'Application core - models, controllers, services', - 'database' => 'Database migrations, seeders, and factories', - 'routes' => 'Application route definitions', - 'config' => 'Application configuration files', - ], - ); - } - - /** - * Add Laravel-specific documents beyond the basic structure - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - - // Add Controllers and Models document if they exist - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - if (\in_array('app', $existingDirs, true)) { - $documents[] = new Document( - description: 'Laravel Controllers and Models', - outputPath: 'docs/laravel-controllers-models.md', - tags: ['laravel', 'controllers', 'models'], - fileSource: new FileSource( - sourcePaths: ['app/Http/Controllers', 'app/Models'], - description: 'Laravel Controllers and Models', - filePattern: '*.php', - ), - ); - } - - // Add Routes document if routes directory exists - if (\in_array('routes', $existingDirs, true)) { - $documents[] = new Document( - description: 'Laravel Routes Configuration', - outputPath: 'docs/laravel-routes.md', - tags: ['laravel', 'routes'], - fileSource: new FileSource( - sourcePaths: ['routes'], - description: 'Laravel Routes', - filePattern: '*.php', - ), - ); - } - - return $documents; - } -} diff --git a/src/Template/Definition/NextJsTemplateDefinition.php b/src/Template/Definition/NextJsTemplateDefinition.php deleted file mode 100644 index 1485f50c..00000000 --- a/src/Template/Definition/NextJsTemplateDefinition.php +++ /dev/null @@ -1,110 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'nextjs'; - } - - public function getDescription(): string - { - return 'Next.js React framework project template'; - } - - public function getTags(): array - { - return ['javascript', 'react', 'nextjs', 'fullstack', 'ssr']; - } - - public function getPriority(): int - { - return 88; // Higher priority than React, as Next.js is more specific - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => ['package.json'], - 'patterns' => self::NEXTJS_PACKAGES, - 'directories' => self::NEXTJS_DIRECTORIES, - ]; - } - - /** - * Create the Next.js project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - return new Document( - description: 'Next.js Project Structure', - outputPath: 'docs/nextjs-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: ['pages', 'app', 'public', 'components'], - description: 'Next.js Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } -} diff --git a/src/Template/Definition/NuxtTemplateDefinition.php b/src/Template/Definition/NuxtTemplateDefinition.php deleted file mode 100644 index b71b63e1..00000000 --- a/src/Template/Definition/NuxtTemplateDefinition.php +++ /dev/null @@ -1,114 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'nuxt'; - } - - public function getDescription(): string - { - return 'Nuxt.js Vue framework project template'; - } - - public function getTags(): array - { - return ['javascript', 'vue', 'nuxt', 'fullstack', 'ssr']; - } - - public function getPriority(): int - { - return 88; // Higher priority than Vue, as Nuxt is more specific - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => ['package.json'], - 'patterns' => self::NUXT_PACKAGES, - 'directories' => self::NUXT_DIRECTORIES, - ]; - } - - /** - * Create the Nuxt project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - return new Document( - description: 'Nuxt Project Structure', - outputPath: 'docs/nuxt-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: ['pages', 'components', 'layouts', 'plugins'], - description: 'Nuxt Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } -} diff --git a/src/Template/Definition/PythonTemplateDefinition.php b/src/Template/Definition/PythonTemplateDefinition.php deleted file mode 100644 index 5174a2a2..00000000 --- a/src/Template/Definition/PythonTemplateDefinition.php +++ /dev/null @@ -1,107 +0,0 @@ - ['requirements.txt', 'pyproject.toml', 'setup.py'], - ]; - } - - /** - * Add Python-specific documents - */ - #[\Override] - protected function createAdditionalDocuments(array $projectMetadata): array - { - $documents = []; - $existingDirs = $projectMetadata['existingDirectories'] ?? $projectMetadata['directories'] ?? []; - - // Add Python modules document if source directories exist - $sourcePaths = \array_intersect($existingDirs, $this->getSourceDirectories()); - - if (!empty($sourcePaths)) { - $documents[] = new Document( - description: 'Python Modules and Packages', - outputPath: 'docs/python-modules.md', - tags: ['python', 'modules'], - fileSource: new FileSource( - sourcePaths: \array_values($sourcePaths), - description: 'Python Modules and Packages', - filePattern: ['*.py'], - notPath: ['__pycache__', '*.pyc', 'venv', 'env', '.venv'], - ), - ); - } - - // Add configuration files document - $configFiles = []; - $potentialConfigFiles = [ - 'setup.py', - 'setup.cfg', - 'pyproject.toml', - 'requirements.txt', - 'Pipfile', - 'tox.ini', - 'pytest.ini', - '.coveragerc', - ]; - - foreach ($potentialConfigFiles as $configFile) { - if (\in_array($configFile, $projectMetadata['files'] ?? [], true)) { - $configFiles[] = $configFile; - } - } - - if (!empty($configFiles)) { - $documents[] = new Document( - description: 'Python Configuration Files', - outputPath: 'docs/python-config.md', - tags: ['python', 'configuration'], - fileSource: new FileSource( - sourcePaths: ['.'], - description: 'Python Configuration Files', - filePattern: $configFiles, - ), - ); - } - - return $documents; - } -} diff --git a/src/Template/Definition/ReactTemplateDefinition.php b/src/Template/Definition/ReactTemplateDefinition.php deleted file mode 100644 index 261348ff..00000000 --- a/src/Template/Definition/ReactTemplateDefinition.php +++ /dev/null @@ -1,110 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'react'; - } - - public function getDescription(): string - { - return 'React.js application project template'; - } - - public function getTags(): array - { - return ['javascript', 'react', 'frontend', 'spa']; - } - - public function getPriority(): int - { - return 85; // High priority for specific framework detection - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => ['package.json'], - 'patterns' => self::REACT_PACKAGES, - 'directories' => self::REACT_DIRECTORIES, - ]; - } - - /** - * Create the React project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - return new Document( - description: 'React Project Structure', - outputPath: 'docs/react-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: ['src', 'public'], - description: 'React Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } -} diff --git a/src/Template/Definition/SpiralTemplateDefinition.php b/src/Template/Definition/SpiralTemplateDefinition.php deleted file mode 100644 index 431a8147..00000000 --- a/src/Template/Definition/SpiralTemplateDefinition.php +++ /dev/null @@ -1,106 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'spiral'; - } - - public function getDescription(): string - { - return 'Spiral PHP Framework project template'; - } - - public function getTags(): array - { - return ['php', 'spiral', 'framework', 'roadrunner']; - } - - public function getPriority(): int - { - return 95; // High priority for specific framework detection - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => ['composer.json', '.rr.yaml', 'rr', '.env.sample', 'app.php'], - 'patterns' => self::SPIRAL_PACKAGES, - 'directories' => [...self::SPIRAL_DIRECTORIES, 'runtime'], - ]; - } - - /** - * Create the Spiral project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - return new Document( - description: 'Spiral Project Structure', - outputPath: 'docs/spiral-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: ['app', 'resources', 'public', 'config'], - description: 'Spiral Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } -} diff --git a/src/Template/Definition/SymfonyTemplateDefinition.php b/src/Template/Definition/SymfonyTemplateDefinition.php deleted file mode 100644 index 6904e1f2..00000000 --- a/src/Template/Definition/SymfonyTemplateDefinition.php +++ /dev/null @@ -1,118 +0,0 @@ -createStructureDocument($projectMetadata), - ]); - - $config->register($documents); - - return new Template( - name: $this->getName(), - description: $this->getDescription(), - tags: $this->getTags(), - priority: $this->getPriority(), - detectionCriteria: $this->getDetectionCriteria(), - config: $config, - ); - } - - public function getName(): string - { - return 'symfony'; - } - - public function getDescription(): string - { - return 'Symfony PHP Framework project template'; - } - - public function getTags(): array - { - return ['php', 'symfony', 'framework', 'web']; - } - - public function getPriority(): int - { - return 95; // High priority for specific framework detection - } - - public function getDetectionCriteria(): array - { - return [ - 'files' => \array_merge(['composer.json'], self::SYMFONY_FILES), - 'patterns' => self::SYMFONY_PACKAGES, - 'directories' => self::SYMFONY_DIRECTORIES, - ]; - } - - /** - * Create the Symfony project structure document - */ - private function createStructureDocument(array $projectMetadata): Document - { - return new Document( - description: 'Symfony Project Structure', - outputPath: 'docs/symfony-structure.md', - overwrite: true, - modifiers: [], - tags: [], - treeSource: new TreeSource( - sourcePaths: ['src', 'config', 'templates', 'public'], - description: 'Symfony Directory Structure', - treeView: new TreeViewConfig( - showCharCount: true, - includeFiles: true, - maxDepth: 3, - ), - ), - ); - } -} diff --git a/src/Template/Definition/TemplateDefinitionInterface.php b/src/Template/Definition/TemplateDefinitionInterface.php deleted file mode 100644 index 2eb29880..00000000 --- a/src/Template/Definition/TemplateDefinitionInterface.php +++ /dev/null @@ -1,49 +0,0 @@ - $projectMetadata Optional project metadata for context-aware template creation - */ - public function createTemplate(array $projectMetadata = []): Template; - - /** - * Get the template name/identifier - */ - public function getName(): string; - - /** - * Get the template description - */ - public function getDescription(): string; - - /** - * Get template tags for categorization - * - * @return array - */ - public function getTags(): array; - - /** - * Get the template priority (higher = more preferred) - */ - public function getPriority(): int; - - /** - * Get detection criteria for automatic selection - * - * @return array - */ - public function getDetectionCriteria(): array; -} diff --git a/src/Template/Definition/TemplateDefinitionRegistry.php b/src/Template/Definition/TemplateDefinitionRegistry.php deleted file mode 100644 index 702d3f05..00000000 --- a/src/Template/Definition/TemplateDefinitionRegistry.php +++ /dev/null @@ -1,80 +0,0 @@ - */ - private array $definitions = []; - - /** - * @param array $definitions - */ - public function __construct(array $definitions = []) - { - foreach ($definitions as $definition) { - $this->registerDefinition($definition); - } - } - - /** - * Register a template definition - */ - public function registerDefinition(TemplateDefinitionInterface $definition): void - { - $this->definitions[] = $definition; - - // Sort by priority (highest first) - \usort($this->definitions, static fn($a, $b) => $b->getPriority() <=> $a->getPriority()); - } - - /** - * Get a specific template definition by name - */ - public function getDefinition(string $name): ?TemplateDefinitionInterface - { - foreach ($this->definitions as $definition) { - if ($definition->getName() === $name) { - return $definition; - } - } - - return null; - } - - /** - * Create all templates from registered definitions - * - * @param array $projectMetadata - * @return array