fgilio / agent-skill-foundation
Foundation package for PHP CLI skills: routing, analytics, docs, testing
Requires
- php: ^8.3
- illuminate/console: ^12.0
- illuminate/support: ^12.0
- nunomaduro/pao: ^0.1.7
- symfony/console: ^7.0
Requires (Dev)
- laravel-zero/framework: ^12.0
- laravel/pint: ^1.27
- pestphp/pest: ^4.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Foundation package for PHP CLI skills used by AI agents (Claude Code).
Installation
composer require fgilio/agent-skill-foundation
Features
- Native Artisan Routing - Use Laravel Zero's built-in command routing (recommended)
- Global
--jsonOption - Register--jsonas an application-level option - JSON Exception Renderer - Output JSON errors for parse-time failures
- Analytics - Local usage tracking via console events
- AgentCommand - Trait with JSON output helpers for commands
- OutputsJson - Standardized JSON output helpers
Quick Start (Native Routing)
Use Laravel Zero's native command routing. Commands self-describe via $signature:
// app/Commands/SearchCommand.php namespace App\Commands; use LaravelZero\Framework\Commands\Command; use Fgilio\AgentSkillFoundation\Console\AgentCommand; class SearchCommand extends Command { use AgentCommand; protected $signature = 'search {query* : Search terms} {--limit=10 : Max results}'; protected $description = 'Search the index'; public function handle(): int { $query = implode(' ', $this->argument('query')); $limit = (int) $this->option('limit'); $results = $this->performSearch($query, $limit); if ($this->wantsJson()) { return $this->outputJson(['results' => $results]); } $this->table(['Title', 'URL'], $results); return self::SUCCESS; } }
Global --json Option
The foundation's AgentSkillFoundationServiceProvider registers a global --json option via Artisan::starting() automatically. Nothing to wire up — as long as your skill uses the foundation service provider, every command supports --json without having to declare it in its signature.
Do NOT declare {--json} in a command's $signature when using the foundation — Symfony will throw LogicException on duplicate definitions.
JSON Exception Handling
Add JSON error rendering to your entrypoint for parse-time failures:
// my-skill (entrypoint) use Fgilio\AgentSkillFoundation\Console\JsonExceptionRenderer; try { $status = $kernel->handle($input, $output); } catch (Throwable $e) { if (JsonExceptionRenderer::render($e)) { exit(JsonExceptionRenderer::exitCode($e)); } throw $e; }
Analytics via Console Events
Analytics are automatically tracked via console events when you register the service provider. Track additional metadata by injecting Analytics into your commands:
use Fgilio\AgentSkillFoundation\Analytics\Analytics; public function handle(Analytics $analytics): int { $startTime = microtime(true); // ... command logic $analytics->track('search', self::SUCCESS, [ 'query' => $query, 'results' => count($results), ], $startTime); return self::SUCCESS; }
AgentCommand Trait
The AgentCommand trait provides JSON output helpers:
use Fgilio\AgentSkillFoundation\Console\AgentCommand; class MyCommand extends Command { use AgentCommand; public function handle(): int { // Check if --json flag is set if ($this->wantsJson()) { // Output JSON to stdout return $this->outputJson(['data' => $results]); } // Output JSON error to stderr return $this->jsonError('Not found', ['id' => $id]); } }
Command Naming
Use Artisan conventions:
| Pattern | Example | Usage |
|---|---|---|
| Single word | today, search |
Common commands |
| Namespaced | accounts:list, gmail:send |
Grouped commands |
// config/commands.php return [ 'default' => NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 'paths' => [app_path('Commands')], 'hidden' => [ // Hide internal/dev commands App\Commands\BuildCommand::class, ], ];
Analytics
Track command usage locally:
use Fgilio\AgentSkillFoundation\Analytics\Analytics; $analytics = new Analytics('my-skill'); $analytics->track('search', Command::SUCCESS, ['query' => 'foo', 'results' => 42], $startTime);
Disabled automatically in CI or when SKILL_ANALYTICS=off.
JSON Output (Static Helpers)
Standardized JSON responses for agent consumption:
use Fgilio\AgentSkillFoundation\Output\OutputsJson; // Success to stdout return OutputsJson::jsonOkPretty($ctx, ['results' => $data]); // Error to stderr return OutputsJson::jsonError($ctx, 'Not found', ['id' => $id]); // Conditional based on --json flag return OutputsJson::maybeJson($ctx, $this->option('json'), $data, fn($ctx, $d) => $ctx->table(['Name', 'Value'], $d) );
Testing
Contract tests with snapshot comparison:
use Fgilio\AgentSkillFoundation\Testing\CliOutputSnapshot; $snapshot = new CliOutputSnapshot('tests/snapshots'); $snapshot->assertMatchesSnapshot('help-output', $output);
For acceptance tests, use subprocess-based testing with Symfony Process:
use Symfony\Component\Process\Process; function mySkill(string ...$args): array { $process = new Process(['php', 'my-skill', ...$args], __DIR__.'/../../'); $process->run(); return [$process->getExitCode(), $process->getOutput(), $process->getErrorOutput()]; } it('shows help', function () { [$exitCode, $stdout] = mySkill('--help'); expect($exitCode)->toBe(0); expect($stdout)->toContain('USAGE'); });
Development
composer install
lefthook install # optional, requires https://github.com/evilmartians/lefthook
Git hooks (via lefthook) run PHPStan on pre-push. Install lefthook with brew install lefthook (macOS) or see installation docs.
License
MIT