Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
"license": "MIT",
"require": {
"php": ">=8.2",
"ext-sqlite3": "*",
"cakephp/cakephp": "^5.2",
"mcp/sdk": "^0.4",
"ext-sqlite3": "*"
"mcp/sdk": "^0.5",
"symfony/filesystem": "^7.0 || ^8.0",
"symfony/finder": "^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^11.1.3 || ^12.0",
Expand Down
23 changes: 7 additions & 16 deletions src/Builder/ServerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Mcp\Server;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Filesystem\Filesystem;
use Synapse\SynapsePlugin;
use Throwable;

Expand Down Expand Up @@ -111,23 +112,13 @@ public function getLogger(): ?LoggerInterface
*/
public function withPluginTools()
{
$scanDirs = [
'Tools',
'Prompts',
'Resources',
];

$pluginSrcPath = dirname(__DIR__);
$pluginSrcPath = str_replace(ROOT, '', $pluginSrcPath);
$pluginSrcPath = ltrim($pluginSrcPath, DIRECTORY_SEPARATOR);
// Normalize to forward slashes for consistency across platforms
$pluginSrcPath = str_replace(DIRECTORY_SEPARATOR, '/', $pluginSrcPath);

foreach ($scanDirs as $dir) {
$path = $pluginSrcPath . '/' . $dir;
if (!in_array($path, $this->scanDirs, true)) {
$this->scanDirs[] = $path;
}
$filesystem = new Filesystem();

foreach (['Tools', 'Prompts', 'Resources'] as $dir) {
$absoluteDir = $pluginSrcPath . DIRECTORY_SEPARATOR . $dir;
$relativePath = rtrim($filesystem->makePathRelative($absoluteDir, $this->basePath), '/');
Comment thread
josbeir marked this conversation as resolved.
$this->addScanDirectory($relativePath);
}

return $this;
Expand Down
7 changes: 2 additions & 5 deletions src/Documentation/DocumentSearchService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Synapse\Documentation;

use Cake\Core\Configure;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;
use Synapse\Documentation\Git\RepositoryManager;

/**
Expand Down Expand Up @@ -39,10 +39,7 @@ public function __construct(
);

// Ensure cache directory exists
$dir = dirname($databasePath);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
throw new RuntimeException(sprintf('Failed to create directory: %s', $dir));
}
(new Filesystem())->mkdir(dirname($databasePath));
Comment thread
josbeir marked this conversation as resolved.

$this->searchEngine = new SearchEngine($databasePath);
$this->repositoryManager = $repositoryManager ?? new RepositoryManager();
Expand Down
46 changes: 12 additions & 34 deletions src/Documentation/Git/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

namespace Synapse\Documentation\Git;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
use UnexpectedValueException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

/**
* Represents a git repository for documentation
Expand Down Expand Up @@ -59,13 +58,7 @@ public function clone(): void
}

// Ensure parent directory exists
$parentDir = dirname($this->path);
if (!is_dir($parentDir) && !mkdir($parentDir, 0755, true)) {
throw new RuntimeException(sprintf(
'Failed to create directory: %s',
$parentDir,
));
}
(new Filesystem())->mkdir(dirname($this->path));
Comment thread
josbeir marked this conversation as resolved.

// Delegate to git adapter
$this->gitAdapter->clone($this->url, $this->branch, $this->path);
Expand Down Expand Up @@ -110,32 +103,17 @@ public function getMarkdownFiles(): array
return [];
}

$finder = new Finder();
$finder->files()->name('*.md')->ignoreUnreadableDirs()->in($searchPath);

$files = [];
$pathPrefix = $this->path . DS;
$pathPrefixLen = strlen($pathPrefix);

try {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$searchPath,
RecursiveDirectoryIterator::SKIP_DOTS,
),
);

foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'md') {
$absolutePath = $file->getPathname();
if (str_starts_with($absolutePath, $pathPrefix)) {
$relativePath = substr($absolutePath, $pathPrefixLen);
$relativePath = str_replace(DS, '/', $relativePath);
$files[] = $relativePath;
}
}
foreach ($finder as $file) {
$relativePath = $file->getRelativePathname();
if ($this->root !== '') {
$relativePath = $this->root . '/' . $relativePath;
}
} catch (UnexpectedValueException $unexpectedValueException) {
// Permission denied or unreadable subdirectory encountered
// Return files collected so far to allow partial indexing
return $files;

$files[] = str_replace('\\', '/', $relativePath);
}
Comment thread
josbeir marked this conversation as resolved.

return $files;
Expand Down
6 changes: 3 additions & 3 deletions src/Prompts/QualityAssurancePrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,15 @@ protected function buildPromptContent(string $context, array $enabledTools): str
$content .= $this->getConfigurationWarning($enabledTools);
$content .= "---\n\n";

if (in_array($context, ['guidelines', 'all'])) {
if (in_array($context, ['guidelines', 'all'], true)) {
$content .= $this->getGuidelinesSection($enabledTools);
}

if (in_array($context, ['integration', 'all'])) {
if (in_array($context, ['integration', 'all'], true)) {
$content .= $this->getIntegrationSection($enabledTools);
}

if (in_array($context, ['troubleshooting', 'all'])) {
if (in_array($context, ['troubleshooting', 'all'], true)) {
$content .= $this->getTroubleshootingSection($enabledTools);
}

Expand Down
42 changes: 17 additions & 25 deletions tests/TestCase/Builder/ServerBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Mcp\Server;
use Psr\Log\LoggerInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Filesystem\Filesystem;
use Synapse\Builder\ServerBuilder;
use Synapse\SynapsePlugin;

Expand Down Expand Up @@ -118,15 +119,12 @@ public function testWithPluginToolsAddsPluginDirectory(): void
$this->assertGreaterThan(1, count($scanDirs));

// Plugin Tools, Prompts, and Resources paths should be in scan dirs
$pluginSrcPath = dirname(dirname(dirname(__DIR__)));
// Normalize dirname result to forward slashes first
$pluginSrcPath = str_replace(DIRECTORY_SEPARATOR, '/', $pluginSrcPath);
$pluginSrcPath .= '/src';
$pluginSrcPath = ltrim($pluginSrcPath, '/');
$filesystem = new Filesystem();
$pluginSrcPath = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'src';

$toolsPath = $pluginSrcPath . '/Tools';
$promptsPath = $pluginSrcPath . '/Prompts';
$resourcesPath = $pluginSrcPath . '/Resources';
$toolsPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Tools', ROOT), '/');
$promptsPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Prompts', ROOT), '/');
$resourcesPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Resources', ROOT), '/');
Comment thread
josbeir marked this conversation as resolved.

$this->assertContains($toolsPath, $scanDirs);
$this->assertContains($promptsPath, $scanDirs);
Expand All @@ -144,15 +142,12 @@ public function testWithPluginToolsDoesNotDuplicate(): void
$builder->withPluginTools();

$scanDirs = $builder->getScanDirs();
$pluginSrcPath = dirname(dirname(dirname(__DIR__)));
// Normalize dirname result to forward slashes first
$pluginSrcPath = str_replace(DIRECTORY_SEPARATOR, '/', $pluginSrcPath);
$pluginSrcPath .= '/src';
$pluginSrcPath = ltrim($pluginSrcPath, '/');
$filesystem = new Filesystem();
$pluginSrcPath = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'src';

$toolsPath = $pluginSrcPath . '/Tools';
$promptsPath = $pluginSrcPath . '/Prompts';
$resourcesPath = $pluginSrcPath . '/Resources';
$toolsPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Tools', ROOT), '/');
$promptsPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Prompts', ROOT), '/');
$resourcesPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Resources', ROOT), '/');
Comment thread
josbeir marked this conversation as resolved.

// Count occurrences of each path - should be 1 each
$toolsCount = count(array_filter($scanDirs, fn(string $dir): bool => $dir === $toolsPath));
Expand Down Expand Up @@ -283,15 +278,12 @@ public function testBuildWithPluginTools(): void
$this->assertInstanceOf(Server::class, $server);

// Verify Tools, Prompts, and Resources directories are in scan dirs
$pluginSrcPath = dirname(dirname(dirname(__DIR__)));
// Normalize dirname result to forward slashes first
$pluginSrcPath = str_replace(DIRECTORY_SEPARATOR, '/', $pluginSrcPath);
$pluginSrcPath .= '/src';
$pluginSrcPath = ltrim($pluginSrcPath, '/');

$toolsPath = $pluginSrcPath . '/Tools';
$promptsPath = $pluginSrcPath . '/Prompts';
$resourcesPath = $pluginSrcPath . '/Resources';
$filesystem = new Filesystem();
$pluginSrcPath = dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'src';

$toolsPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Tools', ROOT), '/');
$promptsPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Prompts', ROOT), '/');
$resourcesPath = rtrim($filesystem->makePathRelative($pluginSrcPath . DIRECTORY_SEPARATOR . 'Resources', ROOT), '/');
Comment thread
josbeir marked this conversation as resolved.
$this->assertContains($toolsPath, $builder->getScanDirs());
$this->assertContains($promptsPath, $builder->getScanDirs());
$this->assertContains($resourcesPath, $builder->getScanDirs());
Expand Down
33 changes: 3 additions & 30 deletions tests/TestCase/Documentation/Git/RepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Cake\TestSuite\TestCase;
use RuntimeException;
use Symfony\Component\Filesystem\Filesystem;
use Synapse\Documentation\Git\Repository;
use Synapse\TestSuite\MockGitAdapter;

Expand Down Expand Up @@ -34,9 +35,7 @@ protected function setUp(): void
$this->testDir = TMP . 'test_repos' . DS;
$this->gitAdapter = new MockGitAdapter();

if (!is_dir($this->testDir)) {
mkdir($this->testDir, 0755, true);
}
(new Filesystem())->mkdir($this->testDir);
}

/**
Expand All @@ -48,33 +47,7 @@ protected function tearDown(): void
$this->gitAdapter->reset();

// Clean up test directories
if (is_dir($this->testDir)) {
$this->removeDirectory($this->testDir);
}
}

/**
* Recursively remove a directory
*
* @param string $dir Directory path
*/
protected function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}

$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . DS . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}

rmdir($dir);
(new Filesystem())->remove($this->testDir);
}

/**
Expand Down
7 changes: 0 additions & 7 deletions tests/TestCase/Prompts/CodeReviewerPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public function testAll(): void
$this->assertInstanceOf(PromptMessage::class, $result[0]);
$this->assertSame(Role::User, $result[0]->role);
$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString($code, $content->text);
$this->assertStringContainsStringIgnoringCase('conventions', $content->text);
Expand All @@ -62,7 +61,6 @@ public function testConventionsFocus(): void
$result = $this->prompt->handle($code, 'conventions');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('5.x', $content->text);
$this->assertStringContainsString($code, $content->text);
Expand All @@ -74,7 +72,6 @@ public function testSecurityFocus(): void
$result = $this->prompt->handle($code, 'security');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('SQL injection', $content->text);
$this->assertStringContainsString($code, $content->text);
Expand All @@ -86,7 +83,6 @@ public function testPerformanceFocus(): void
$result = $this->prompt->handle($code, 'performance');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('N+1', $content->text);
$this->assertStringContainsString($code, $content->text);
Expand All @@ -100,7 +96,6 @@ public function testUsesConfiguredCakephpVersion(): void
$result = $prompt->handle('<?php echo $var; ?>');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('4.5', $content->text);
}
Expand All @@ -113,7 +108,6 @@ public function testUsesConfiguredPhpVersion(): void
$result = $prompt->handle('public function test() {}');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('PHP 8.3+', $content->text);
}
Expand All @@ -126,7 +120,6 @@ public function testReturnsValidStructure(): void
$this->assertInstanceOf(PromptMessage::class, $result[0]);
$this->assertSame(Role::User, $result[0]->role);
$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertNotEmpty($content->text);
}
Expand Down
6 changes: 0 additions & 6 deletions tests/TestCase/Prompts/DatabaseExplorerPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public function testAll(): void
$this->assertInstanceOf(PromptMessage::class, $result[0]);
$this->assertSame(Role::User, $result[0]->role);
$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('fetchTable', $content->text);
$this->assertStringContainsString('users', $content->text);
Expand All @@ -55,7 +54,6 @@ public function testSchema(): void
$result = $this->prompt->handle('posts', 'schema');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('schema', $content->text);
$this->assertStringContainsString('posts', $content->text);
Expand All @@ -66,7 +64,6 @@ public function testData(): void
$result = $this->prompt->handle('articles', 'data');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('data', $content->text);
$this->assertStringContainsString('find()->limit(5)', $content->text);
Expand All @@ -77,7 +74,6 @@ public function testRelationships(): void
$result = $this->prompt->handle('comments', 'relationships');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('relationships', $content->text);
$this->assertStringContainsString('associations', $content->text);
Expand All @@ -91,7 +87,6 @@ public function testUsesConfiguredCakephpVersion(): void
$result = $prompt->handle('products');

$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertStringContainsString('4.5', $content->text);
}
Expand All @@ -104,7 +99,6 @@ public function testReturnsValidStructure(): void
$this->assertInstanceOf(PromptMessage::class, $result[0]);
$this->assertSame(Role::User, $result[0]->role);
$this->assertInstanceOf(TextContent::class, $result[0]->content);
/** @var TextContent $content */
$content = $result[0]->content;
$this->assertNotEmpty($content->text);
}
Expand Down
Loading
Loading