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
27 changes: 27 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Lint code

on: ["push","pull_request"]

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: json, zip, pdo, pdo_sqlite
tools: composer:v2
coverage: none

- name: Install PHP dependencies
run: composer update --no-interaction --no-progress

- name: Run Linter
run: composer run lint -- --check
32 changes: 32 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Run Tests

on: ["push","pull_request"]

jobs:
tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: ["macos-latest","ubuntu-latest","windows-latest"]
php: ["8.5","8.4","8.3","8.2"]

name: PHP ${{ matrix.php }} - ${{ matrix.os }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: json, zip, pdo, pdo_sqlite
tools: composer:v2
coverage: xdebug

- name: Install PHP dependencies
run: composer update --no-interaction --no-progress

- name: Run Tests
run: composer run test -- --flags=coverage
93 changes: 93 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!-- markdownlint-disable no-inline-html -->
<p align="center">
<br><br>
<img src="https://leafphp.dev/logo-circle.png" height="100"/>
<h1 align="center">Leaf LLMs</h1>
<br><br>
</p>

[![Latest Stable Version](https://poser.pugx.org/leafs/llms/v/stable)](https://packagist.org/packages/leafs/llms)
[![Total Downloads](https://poser.pugx.org/leafs/llms/downloads)](https://packagist.org/packages/leafs/llms)
[![License](https://poser.pugx.org/leafs/llms/license)](https://packagist.org/packages/leafs/llms)

Coding agents do better work when a project tells them how it's built. `leafs/llms` reads your `composer.json` and folder layout, then writes the files coding agents such as Claude Code and Cursor look for. Your agent learns the commands your project really uses, so it doesn't have to guess.

It works with Leaf, Leaf MVC, Laravel, Symfony and plain PHP projects.

## Installation

```bash
composer require --dev leafs/llms
```

## Usage

From your project root:

```bash
vendor/bin/llms init
```

That writes these files:

| File | Read by |
| --- | --- |
| `AGENTS.md` | Most agents. It's the source of truth, covering your stack and commands plus the conventions to follow |
| `CLAUDE.md` | Claude Code. It imports `AGENTS.md` |
| `llms.txt` | Tools that follow [llmstxt.org](https://llmstxt.org), with links to your docs and your framework's docs |
| `.cursor/rules/project.mdc` | Cursor, on every request |
| `.cursor/rules/php.mdc` | Cursor, when a PHP file is involved |
| `.claude/skills/*/SKILL.md` | Claude Code skills |
| `.agents/skills/*/SKILL.md` | The same skills, for agents that read `.agents/` |

Each project gets a `php-tests` skill that uses your test runner, plus skills for its framework:

| Framework | Skills |
| --- | --- |
| Leaf | `leaf-routes` |
| Leaf MVC | `leaf-mvc-feature`, `leaf-database` (schema files, models, seeds) |
| Laravel | `laravel-feature`, `laravel-database` |
| Symfony | `symfony-controller`, `symfony-doctrine` |
| Plain PHP | `php-package` |

To see what was detected without writing anything:

```bash
vendor/bin/llms detect
vendor/bin/llms init --dry-run
```

If detection picks the wrong framework, set it yourself with `--framework=leaf-mvc`, `leaf`, `laravel`, `symfony` or `php`.

## What it detects

- The framework, from `composer.json`
- The PHP version constraint and PSR-4 namespaces
- The test runner (Pest, PHPUnit, or Pest through Alchemy) and the command that runs it
- Code style tools (Pint, PHP CS Fixer, PHP_CodeSniffer) and static analysis (PHPStan, Larastan, Psalm, Rector)
- Your Composer scripts, which it prefers over calling tools directly
- Folders that exist, described for your framework

## Re-running is safe

Generated text sits between two markers:

```md
<!-- leafs/llms:start -->
...
<!-- leafs/llms:end -->
```

Running `init` again only replaces what's between them. Anything you write above or below stays. If a file already exists without markers, such as the `AGENTS.md` that ships with Leaf MVC, the generated section is added at the end and your text is left alone.

Re-run it after you add a tool or change your scripts. You can add it to `composer.json` so it's easy to remember:

```json
"scripts": {
"llms": "llms init"
}
```

## License

MIT
60 changes: 60 additions & 0 deletions alchemy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
app:
- src

tests:
engine: pest
parallel: false
paths:
- tests
files:
- '*.test.php'
coverage:
processUncoveredFiles: true

lint:
preset: PSR12
rules:
single_quote: true
phpdoc_scalar: true
no_unused_imports: true
unary_operator_spaces: true
binary_operator_spaces: true
phpdoc_var_without_name: true
trailing_comma_in_multiline: true
phpdoc_single_line_var_spacing: true
single_trait_insert_per_statement: true
not_operator_with_successor_space: false
array_syntax:
syntax: short
ordered_imports:
sort_algorithm: alpha
method_argument_space:
on_multiline: ensure_fully_multiline
keep_multiple_spaces_after_comma: true
blank_line_before_statement:
statements:
- try
- break
- throw
- return
- declare
- continue

actions:
run:
- lint
- tests
os:
- macos-latest
- ubuntu-latest
- windows-latest
php:
extensions: json, zip, pdo, pdo_sqlite
versions: # library supports ^7.4|^8.0, but pest 3 needs 8.2+
- '8.5'
- '8.4'
- '8.3'
- '8.2'
events:
- push
- pull_request
14 changes: 14 additions & 0 deletions bin/llms
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php

foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $autoload) {
if (is_file($autoload)) {
require $autoload;
break;
}
}

$app = new Symfony\Component\Console\Application('leafs/llms', '0.1.0');
$app->add(new Leaf\Llms\Commands\InitCommand());
$app->add(new Leaf\Llms\Commands\DetectCommand());
$app->run();
57 changes: 57 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "leafs/llms",
"description": "Generate llms.txt, AGENTS.md, CLAUDE.md, Cursor rules and agent skills for any PHP project",
"type": "library",
"license": "MIT",
"keywords": [
"llms.txt",
"agents.md",
"claude",
"cursor",
"ai",
"php",
"leaf",
"laravel",
"symfony"
],
"homepage": "https://leafphp.dev",
"authors": [
{
"name": "Michael Darko",
"email": "mickdd22@gmail.com",
"homepage": "https://mychi.netlify.app",
"role": "Developer"
}
],
"require": {
"php": "^8.2",
"symfony/console": "^6.4 || ^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"leafs/alchemy": "^5.0",
"pestphp/pest": "*"
},
"autoload": {
"psr-4": {
"Leaf\\Llms\\": "src/"
}
},
"bin": [
"bin/llms"
],
"scripts": {
"test": "@php vendor/bin/alchemy test",
"lint": "@php vendor/bin/alchemy lint",
"fmt": "@php vendor/bin/alchemy fmt",
"ci": "@php vendor/bin/alchemy ci"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
},
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
4 changes: 4 additions & 0 deletions kata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
surfaces:
- "README.md"
rules:
preset: leaf-writing
47 changes: 47 additions & 0 deletions src/Commands/DetectCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Leaf\Llms\Commands;

use Leaf\Llms\Detector;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(name: 'detect', description: 'Show what llms detects about this project, without writing files')]
class DetectCommand extends Command
{
protected function configure(): void
{
$this->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Project root (defaults to the current directory)');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$project = (new Detector())->detect($input->getOption('path') ?: (string) getcwd());
} catch (\Throwable $e) {
$output->writeln("<error>{$e->getMessage()}</error>");

return Command::FAILURE;
}

$rows = [
'Name' => $project->name,
'Framework' => $project->framework,
'PHP' => $project->php ?: '(not set)',
'Tests' => $project->testRunner ?? '(none)',
];

foreach ($project->commands as $label => $command) {
$rows[ucfirst($label)] = $command;
}

foreach ($rows as $label => $value) {
$output->writeln(sprintf('<info>%-10s</info> %s', $label, $value));
}

return Command::SUCCESS;
}
}
Loading
Loading