feat(brain): recurse seed-memory scans
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
547a481d7b
commit
c8a2d62d27
2 changed files with 147 additions and 1 deletions
|
|
@ -40,7 +40,7 @@ class BrainSeedMemoryCommand extends Command
|
|||
$scanPath = $this->option('path')
|
||||
?? $this->expandHome('~/.claude/projects/*/memory/');
|
||||
|
||||
$files = glob($scanPath.'*.md');
|
||||
$files = $this->discoverMarkdownFiles($scanPath);
|
||||
if (empty($files)) {
|
||||
$this->info("No markdown files found in: {$scanPath}");
|
||||
|
||||
|
|
@ -227,4 +227,100 @@ class BrainSeedMemoryCommand extends Command
|
|||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover markdown files from a file path, directory, or glob.
|
||||
*
|
||||
* The command accepts a directory path like
|
||||
* `--path=/root/.claude/projects/workspace/memory/` as well as a single
|
||||
* file path like `--path=/root/.claude/projects/foo/memory/MEMORY.md`.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private function discoverMarkdownFiles(string $scanPath): array
|
||||
{
|
||||
$expandedPath = $this->expandHome($scanPath);
|
||||
if ($expandedPath === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = [];
|
||||
foreach ($this->expandScanTargets($expandedPath) as $target) {
|
||||
$files = array_merge($files, $this->collectMarkdownFiles($target));
|
||||
}
|
||||
|
||||
$files = array_values(array_unique($files));
|
||||
sort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a directory path or glob pattern into concrete scan targets.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private function expandScanTargets(string $scanPath): array
|
||||
{
|
||||
if ($this->hasGlobMeta($scanPath)) {
|
||||
return glob($scanPath) ?: [];
|
||||
}
|
||||
|
||||
return [$scanPath];
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect markdown files from a file or directory.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private function collectMarkdownFiles(string $path): array
|
||||
{
|
||||
if (! file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_file($path)) {
|
||||
return $this->isMarkdownFile($path) ? [$path] : [];
|
||||
}
|
||||
|
||||
if (! is_dir($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = [];
|
||||
$entries = scandir($path);
|
||||
if ($entries === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childPath = $path.'/'.$entry;
|
||||
if (is_dir($childPath)) {
|
||||
$files = array_merge($files, $this->collectMarkdownFiles($childPath));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isMarkdownFile($childPath)) {
|
||||
$files[] = $childPath;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function hasGlobMeta(string $path): bool
|
||||
{
|
||||
return str_contains($path, '*') || str_contains($path, '?') || str_contains($path, '[');
|
||||
}
|
||||
|
||||
private function isMarkdownFile(string $path): bool
|
||||
{
|
||||
return strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'md';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
php/tests/Feature/BrainSeedMemoryCommandTest.php
Normal file
50
php/tests/Feature/BrainSeedMemoryCommandTest.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Tests\Feature;
|
||||
|
||||
use Core\Mod\Agentic\Models\BrainMemory;
|
||||
use Core\Mod\Agentic\Services\BrainService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BrainSeedMemoryCommandTest extends TestCase
|
||||
{
|
||||
public function test_it_recursively_imports_markdown_files_from_a_directory(): void
|
||||
{
|
||||
$workspaceId = 42;
|
||||
$scanPath = $this->createSeedMemoryFixture();
|
||||
|
||||
$brain = Mockery::mock(BrainService::class);
|
||||
$brain->shouldReceive('ensureCollection')->once();
|
||||
$brain->shouldReceive('remember')
|
||||
->twice()
|
||||
->andReturnUsing(static fn (): BrainMemory => new BrainMemory());
|
||||
|
||||
$this->app->instance(BrainService::class, $brain);
|
||||
|
||||
$this->artisan('brain:seed-memory', [
|
||||
'--workspace' => $workspaceId,
|
||||
'--agent' => 'virgil',
|
||||
'--path' => $scanPath,
|
||||
])
|
||||
->expectsOutputToContain('Found 2 markdown file(s) to process.')
|
||||
->expectsOutputToContain('Imported 2 memories, skipped 0.')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
private function createSeedMemoryFixture(): string
|
||||
{
|
||||
$scanPath = sys_get_temp_dir().'/brain-seed-'.bin2hex(random_bytes(6));
|
||||
$nestedPath = $scanPath.'/nested';
|
||||
|
||||
mkdir($nestedPath, 0777, true);
|
||||
|
||||
file_put_contents($scanPath.'/MEMORY.md', "# Memory\n\n## Architecture\nUse Core.Process() for command execution.\n\n## Decision\nPrefer named actions.");
|
||||
file_put_contents($nestedPath.'/notes.md', "# Notes\n\n## Convention\nUse UK English in user-facing output.");
|
||||
file_put_contents($nestedPath.'/ignore.txt', 'This file should not be imported.');
|
||||
|
||||
return $scanPath;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue