From 4e43a7eab230228de837ee5d6caf7b63e125b235 Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Wed, 11 Mar 2026 16:18:06 +0200 Subject: [PATCH 1/5] Laravel/boost upgrade to v2. Remove WARP.md --- WARP.md | 72 -- _api_app/.agents/skills/pest-testing/SKILL.md | 117 ++ _api_app/.claude/skills/pest-testing/SKILL.md | 117 ++ _api_app/.codex/config.toml | 4 + _api_app/.cursor/skills/pest-testing/SKILL.md | 117 ++ _api_app/.github/skills/pest-testing/SKILL.md | 117 ++ _api_app/AGENTS.md | 222 ++-- _api_app/CLAUDE.md | 222 ++-- _api_app/boost.json | 17 + _api_app/composer.json | 2 +- _api_app/composer.lock | 1068 +++++++++-------- _api_app/opencode.json | 14 + 12 files changed, 1269 insertions(+), 820 deletions(-) delete mode 100644 WARP.md create mode 100644 _api_app/.agents/skills/pest-testing/SKILL.md create mode 100644 _api_app/.claude/skills/pest-testing/SKILL.md create mode 100644 _api_app/.codex/config.toml create mode 100644 _api_app/.cursor/skills/pest-testing/SKILL.md create mode 100644 _api_app/.github/skills/pest-testing/SKILL.md create mode 100644 _api_app/boost.json create mode 100644 _api_app/opencode.json diff --git a/WARP.md b/WARP.md deleted file mode 100644 index b4706d810..000000000 --- a/WARP.md +++ /dev/null @@ -1,72 +0,0 @@ -# WARP.md - -This file provides guidance to WARP (warp.dev) when working with code in this repository. - -## Development Commands - -### Root Project (Frontend Assets) -- **Install dependencies**: `npm install` -- **Development build with watch**: `npm run dev` (runs `gulp`) -- **Production build**: `npm run build` (runs `gulp build`) -- **Lint backend JS**: `gulp` includes JSHint for backend JS files - -### Angular Editor (_editor/_ directory) -- **Install dependencies**: `cd editor && npm install` -- **Development server**: `cd editor && npm start` (runs `ng serve` on http://localhost:4200) -- **Build for development**: `cd editor && npm run dev` (builds with watch) -- **Build for production**: `cd editor && npm run build` -- **Run tests**: `cd editor && npm test` - -### Laravel API (_api_app/_ directory) -- **Install dependencies**: `cd _api_app && composer install` -- **Development server**: `cd _api_app && npm run dev` (Vite dev server) -- **Build assets**: `cd _api_app && npm run build` -- **Run tests**: `cd _api_app && ./vendor/bin/pest` -- **Run tests (CI mode)**: `cd _api_app && ./vendor/bin/pest --ci` - -## Project Architecture - -### Multi-Component CMS System -Berta is a file-based CMS consisting of three main components that work together: - -1. **Legacy PHP Engine** (`engine/` directory) - Original Berta CMS core -2. **Angular Editor** (`editor/` directory) - Modern admin interface built with Angular 8 -3. **Laravel API** (`_api_app/` directory) - REST API backend using Laravel 12 - -### Key Architectural Components - -#### Frontend Build System (Gulp) -- Compiles SCSS to CSS for multiple templates -- Concatenates and minifies JS/CSS assets -- Builds separate bundles for frontend and backend -- Template-specific SCSS compilation for themes in `_templates/` - -#### Template System -- Templates located in `_templates/` directory (default, messy, white, mashup) -- Each template has its own SCSS files that are compiled separately -- Template CSS is built to template-specific directories - -#### Angular Editor -- NGXS state management for application state -- Component-based modular architecture with inline templates -- Outputs built files to `../engine/dist/` - -#### Laravel API Backend -- Modern Laravel 12 application -- Pest testing framework -- Vite for asset compilation -- RESTful API structure - -### Entry Points -- **Main site**: `index.php` (delegates to `engine/index.php`) -- **Editor interface**: Angular app served from `engine/dist/` -- **API endpoints**: Laravel routes in `_api_app/routes/` - -### Development Workflow -1. **Frontend assets**: Use `npm run dev` in root for CSS/JS compilation with watch -2. **Admin interface**: Use `npm start` in `editor/` for Angular development server -3. **API development**: Use Laravel's built-in server or Vite dev server in `_api_app/` - -### File Storage -- Content stored in files (not database) as per CMS design -- Storage directory contains user uploads and content files diff --git a/_api_app/.agents/skills/pest-testing/SKILL.md b/_api_app/.agents/skills/pest-testing/SKILL.md new file mode 100644 index 000000000..f6973277f --- /dev/null +++ b/_api_app/.agents/skills/pest-testing/SKILL.md @@ -0,0 +1,117 @@ +--- +name: pest-testing +description: "Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 3 + +## When to Apply + +Activate this skill when: +- Creating new tests (unit or feature) +- Modifying existing tests +- Debugging test failures +- Working with datasets, mocking, or test organization +- Writing architecture tests + +## Documentation + +Use `search-docs` for detailed Pest 3 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Tests live in the `tests/Feature` and `tests/Unit` directories. +- Do NOT remove tests without approval - these are core application code. +- Test happy paths, failure paths, and edge cases. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 3 Features + +### Architecture Testing + +Pest 3 includes architecture testing to enforce code conventions: + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); + +arch('models') + ->expect('App\Models') + ->toExtend('Illuminate\Database\Eloquent\Model'); + +arch('no debugging') + ->expect(['dd', 'dump', 'ray']) + ->not->toBeUsed(); +``` + +### Type Coverage + +Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval \ No newline at end of file diff --git a/_api_app/.claude/skills/pest-testing/SKILL.md b/_api_app/.claude/skills/pest-testing/SKILL.md new file mode 100644 index 000000000..f6973277f --- /dev/null +++ b/_api_app/.claude/skills/pest-testing/SKILL.md @@ -0,0 +1,117 @@ +--- +name: pest-testing +description: "Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 3 + +## When to Apply + +Activate this skill when: +- Creating new tests (unit or feature) +- Modifying existing tests +- Debugging test failures +- Working with datasets, mocking, or test organization +- Writing architecture tests + +## Documentation + +Use `search-docs` for detailed Pest 3 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Tests live in the `tests/Feature` and `tests/Unit` directories. +- Do NOT remove tests without approval - these are core application code. +- Test happy paths, failure paths, and edge cases. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 3 Features + +### Architecture Testing + +Pest 3 includes architecture testing to enforce code conventions: + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); + +arch('models') + ->expect('App\Models') + ->toExtend('Illuminate\Database\Eloquent\Model'); + +arch('no debugging') + ->expect(['dd', 'dump', 'ray']) + ->not->toBeUsed(); +``` + +### Type Coverage + +Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval \ No newline at end of file diff --git a/_api_app/.codex/config.toml b/_api_app/.codex/config.toml new file mode 100644 index 000000000..722d917ec --- /dev/null +++ b/_api_app/.codex/config.toml @@ -0,0 +1,4 @@ +[mcp_servers.laravel-boost] +command = "php" +args = ["artisan", "boost:mcp"] +cwd = "/Users/uldis/projects/berta/berta/_api_app" diff --git a/_api_app/.cursor/skills/pest-testing/SKILL.md b/_api_app/.cursor/skills/pest-testing/SKILL.md new file mode 100644 index 000000000..f6973277f --- /dev/null +++ b/_api_app/.cursor/skills/pest-testing/SKILL.md @@ -0,0 +1,117 @@ +--- +name: pest-testing +description: "Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 3 + +## When to Apply + +Activate this skill when: +- Creating new tests (unit or feature) +- Modifying existing tests +- Debugging test failures +- Working with datasets, mocking, or test organization +- Writing architecture tests + +## Documentation + +Use `search-docs` for detailed Pest 3 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Tests live in the `tests/Feature` and `tests/Unit` directories. +- Do NOT remove tests without approval - these are core application code. +- Test happy paths, failure paths, and edge cases. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 3 Features + +### Architecture Testing + +Pest 3 includes architecture testing to enforce code conventions: + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); + +arch('models') + ->expect('App\Models') + ->toExtend('Illuminate\Database\Eloquent\Model'); + +arch('no debugging') + ->expect(['dd', 'dump', 'ray']) + ->not->toBeUsed(); +``` + +### Type Coverage + +Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval \ No newline at end of file diff --git a/_api_app/.github/skills/pest-testing/SKILL.md b/_api_app/.github/skills/pest-testing/SKILL.md new file mode 100644 index 000000000..f6973277f --- /dev/null +++ b/_api_app/.github/skills/pest-testing/SKILL.md @@ -0,0 +1,117 @@ +--- +name: pest-testing +description: "Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works." +license: MIT +metadata: + author: laravel +--- + +# Pest Testing 3 + +## When to Apply + +Activate this skill when: +- Creating new tests (unit or feature) +- Modifying existing tests +- Debugging test failures +- Working with datasets, mocking, or test organization +- Writing architecture tests + +## Documentation + +Use `search-docs` for detailed Pest 3 patterns and documentation. + +## Basic Usage + +### Creating Tests + +All tests must be written using Pest. Use `php artisan make:test --pest {name}`. + +### Test Organization + +- Tests live in the `tests/Feature` and `tests/Unit` directories. +- Do NOT remove tests without approval - these are core application code. +- Test happy paths, failure paths, and edge cases. + +### Basic Test Structure + + +```php +it('is true', function () { + expect(true)->toBeTrue(); +}); +``` + +### Running Tests + +- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`. +- Run all tests: `php artisan test --compact`. +- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`. + +## Assertions + +Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`: + + +```php +it('returns all', function () { + $this->postJson('/api/docs', [])->assertSuccessful(); +}); +``` + +| Use | Instead of | +|-----|------------| +| `assertSuccessful()` | `assertStatus(200)` | +| `assertNotFound()` | `assertStatus(404)` | +| `assertForbidden()` | `assertStatus(403)` | + +## Mocking + +Import mock function before use: `use function Pest\Laravel\mock;` + +## Datasets + +Use datasets for repetitive tests (validation rules, etc.): + + +```php +it('has emails', function (string $email) { + expect($email)->not->toBeEmpty(); +})->with([ + 'james' => 'james@laravel.com', + 'taylor' => 'taylor@laravel.com', +]); +``` + +## Pest 3 Features + +### Architecture Testing + +Pest 3 includes architecture testing to enforce code conventions: + + +```php +arch('controllers') + ->expect('App\Http\Controllers') + ->toExtendNothing() + ->toHaveSuffix('Controller'); + +arch('models') + ->expect('App\Models') + ->toExtend('Illuminate\Database\Eloquent\Model'); + +arch('no debugging') + ->expect(['dd', 'dump', 'ray']) + ->not->toBeUsed(); +``` + +### Type Coverage + +Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. + +## Common Pitfalls + +- Not importing `use function Pest\Laravel\mock;` before using mock +- Using `assertStatus(200)` instead of `assertSuccessful()` +- Forgetting datasets for repetitive validation tests +- Deleting tests without approval \ No newline at end of file diff --git a/_api_app/AGENTS.md b/_api_app/AGENTS.md index c3ec68551..757ae9646 100644 --- a/_api_app/AGENTS.md +++ b/_api_app/AGENTS.md @@ -3,247 +3,235 @@ # Laravel Boost Guidelines -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. ## Foundational Context + This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.2.29 +- php - 8.2.30 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - laravel/sanctum (SANCTUM) - v4 +- laravel/boost (BOOST) - v2 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 - pestphp/pest (PEST) - v3 - phpunit/phpunit (PHPUNIT) - v11 +## Skills Activation + +This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +- `pest-testing` — Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works. ## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. - Check for existing components to reuse before writing a new one. ## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. ## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. + +- Stick to existing directory structure; don't create new base folders without approval. - Do not change the application's dependencies without approval. ## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. ## Documentation Files + - You must only create documentation files if explicitly requested by the user. +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. === boost rules === -## Laravel Boost +# Laravel Boost + - Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. ## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. + +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. ## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. + +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. ## Tinker / Debugging + - You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. - Use the `database-query` tool when you only need to read from the database. +- Use the `database-schema` tool to inspect table structure before writing migrations or models. ## Reading Browser Logs With the `browser-logs` Tool + - You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. - Only recent browser logs will be useful - ignore old logs. ## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. + +- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. - Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. +- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. ### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. === php rules === -## PHP +# PHP + +- Always use curly braces for control structures, even for single-line bodies. -- Always use curly braces for control structures, even if it has one line. +## Constructors -### Constructors - Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. + - `public function __construct(public GitHub $github) { }` +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +## Type Declarations -### Type Declarations - Always use explicit return type declarations for methods and functions. - Use appropriate PHP type hints for method parameters. - + +```php protected function isAccessible(User $user, ?string $path = null): bool { ... } - +``` + +## Enums + +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. ## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. + +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex. ## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. +- Add useful array shape type definitions when appropriate. + +=== tests rules === +# Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. === laravel/core rules === -## Do Things the Laravel Way +# Do Things the Laravel Way - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. +- If you're creating a generic PHP class, use `php artisan make:class`. - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. -### Database +## Database + - Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries +- Use Eloquent models and relationships before suggesting raw database queries. - Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. - Generate code that prevents N+1 query problems by using eager loading. - Use Laravel's query builder for very complex database operations. ### Model Creation + - When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. ### APIs & Eloquent Resources + - For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. -### Controllers & Validation +## Controllers & Validation + - Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. - Check sibling Form Requests to see if the application uses array or string based validation rules. -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. +## Authentication & Authorization -### Authentication & Authorization - Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). -### URL Generation +## URL Generation + - When generating links to other pages, prefer named routes and the `route()` function. -### Configuration +## Queues + +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +## Configuration + - Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. -### Testing +## Testing + - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. +## Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. === laravel/v12 rules === -## Laravel 12 +# Laravel 12 -- Use the `search-docs` tool to get version specific documentation. +- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples. - Since Laravel 11, Laravel has a new streamlined file structure which this project uses. -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. +## Laravel 12 Structure + +- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`. +- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`. - `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. - `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. +- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration. +- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration. + +## Database -### Database - When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. ### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. === pint/core rules === -## Laravel Pint Code Formatter +# Laravel Pint Code Formatter -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. - Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - === pest/core rules === ## Pest -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. +- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples. +- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task. - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. - \ No newline at end of file + diff --git a/_api_app/CLAUDE.md b/_api_app/CLAUDE.md index c3ec68551..757ae9646 100644 --- a/_api_app/CLAUDE.md +++ b/_api_app/CLAUDE.md @@ -3,247 +3,235 @@ # Laravel Boost Guidelines -The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications. ## Foundational Context + This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.2.29 +- php - 8.2.30 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - laravel/sanctum (SANCTUM) - v4 +- laravel/boost (BOOST) - v2 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 - pestphp/pest (PEST) - v3 - phpunit/phpunit (PHPUNIT) - v11 +## Skills Activation + +This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. + +- `pest-testing` — Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works. ## Conventions -- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming. + +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. - Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. - Check for existing components to reuse before writing a new one. ## Verification Scripts -- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important. ## Application Structure & Architecture -- Stick to existing directory structure - don't create new base folders without approval. + +- Stick to existing directory structure; don't create new base folders without approval. - Do not change the application's dependencies without approval. ## Frontend Bundling -- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. -## Replies -- Be concise in your explanations - focus on what's important rather than explaining obvious details. +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. ## Documentation Files + - You must only create documentation files if explicitly requested by the user. +## Replies + +- Be concise in your explanations - focus on what's important rather than explaining obvious details. === boost rules === -## Laravel Boost +# Laravel Boost + - Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. ## Artisan -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters. + +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. ## URLs -- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port. + +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. ## Tinker / Debugging + - You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. - Use the `database-query` tool when you only need to read from the database. +- Use the `database-schema` tool to inspect table structure before writing migrations or models. ## Reading Browser Logs With the `browser-logs` Tool + - You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. - Only recent browser logs will be useful - ignore old logs. ## Searching Documentation (Critically Important) -- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. -- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. -- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches. + +- Boost comes with a powerful `search-docs` tool you should use before trying other approaches when working with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. - Search the documentation before making code changes to ensure we are taking the correct approach. -- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. -- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. +- Use multiple, broad, simple, topic-based queries at once. For example: `['rate limiting', 'routing rate limiting', 'routing']`. The most relevant results will be returned first. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. ### Available Search Syntax -- You can and should pass multiple queries at once. The most relevant results will be returned first. - -1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth' -2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit" -3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order -4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit" -5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. === php rules === -## PHP +# PHP + +- Always use curly braces for control structures, even for single-line bodies. -- Always use curly braces for control structures, even if it has one line. +## Constructors -### Constructors - Use PHP 8 constructor property promotion in `__construct()`. - - public function __construct(public GitHub $github) { } -- Do not allow empty `__construct()` methods with zero parameters. + - `public function __construct(public GitHub $github) { }` +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +## Type Declarations -### Type Declarations - Always use explicit return type declarations for methods and functions. - Use appropriate PHP type hints for method parameters. - + +```php protected function isAccessible(User $user, ?string $path = null): bool { ... } - +``` + +## Enums + +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. ## Comments -- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on. + +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless the logic is exceptionally complex. ## PHPDoc Blocks -- Add useful array shape type definitions for arrays when appropriate. -## Enums -- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. +- Add useful array shape type definitions when appropriate. + +=== tests rules === +# Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. === laravel/core rules === -## Do Things the Laravel Way +# Do Things the Laravel Way - Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. -- If you're creating a generic PHP class, use `artisan make:class`. +- If you're creating a generic PHP class, use `php artisan make:class`. - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. -### Database +## Database + - Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. -- Use Eloquent models and relationships before suggesting raw database queries +- Use Eloquent models and relationships before suggesting raw database queries. - Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. - Generate code that prevents N+1 query problems by using eager loading. - Use Laravel's query builder for very complex database operations. ### Model Creation + - When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. ### APIs & Eloquent Resources + - For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. -### Controllers & Validation +## Controllers & Validation + - Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. - Check sibling Form Requests to see if the application uses array or string based validation rules. -### Queues -- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. +## Authentication & Authorization -### Authentication & Authorization - Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). -### URL Generation +## URL Generation + - When generating links to other pages, prefer named routes and the `route()` function. -### Configuration +## Queues + +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +## Configuration + - Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. -### Testing +## Testing + - When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. - Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. -- When creating tests, make use of `php artisan make:test [options] ` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. -### Vite Error -- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. +## Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. === laravel/v12 rules === -## Laravel 12 +# Laravel 12 -- Use the `search-docs` tool to get version specific documentation. +- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples. - Since Laravel 11, Laravel has a new streamlined file structure which this project uses. -### Laravel 12 Structure -- No middleware files in `app/Http/Middleware/`. +## Laravel 12 Structure + +- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`. +- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`. - `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. - `bootstrap/providers.php` contains application specific service providers. -- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration. -- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration. +- The `app\Console\Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration. +- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration. + +## Database -### Database - When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. ### Models -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. === pint/core rules === -## Laravel Pint Code Formatter +# Laravel Pint Code Formatter -- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. - Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. - === pest/core rules === ## Pest -### Testing -- If you need to verify a feature is working, write or update a Unit / Feature test. - -### Pest Tests -- All tests must be written using Pest. Use `php artisan make:test --pest `. -- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application. -- Tests should test all of the happy paths, failure paths, and weird paths. -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Pest tests look and behave like this: - -it('is true', function () { - expect(true)->toBeTrue(); -}); - - -### Running Tests -- Run the minimal number of tests using an appropriate filter before finalizing code edits. -- To run all tests: `php artisan test`. -- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`. -- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file). -- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing. - -### Pest Assertions -- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.: - -it('returns all', function () { - $response = $this->postJson('/api/docs', []); - - $response->assertSuccessful(); -}); - - -### Mocking -- Mocking can be very helpful when appropriate. -- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do. -- You can also create partial mocks using the same import or self method. - -### Datasets -- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules. - - -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); - +- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`. +- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`. +- Do NOT delete tests without approval. +- CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples. +- IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task. - -=== tests rules === - -## Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter. - \ No newline at end of file + diff --git a/_api_app/boost.json b/_api_app/boost.json new file mode 100644 index 000000000..1f17a8721 --- /dev/null +++ b/_api_app/boost.json @@ -0,0 +1,17 @@ +{ + "agents": [ + "cursor", + "claude_code", + "codex", + "copilot", + "opencode" + ], + "guidelines": true, + "herd_mcp": false, + "mcp": true, + "nightwatch_mcp": false, + "sail": false, + "skills": [ + "pest-testing" + ] +} diff --git a/_api_app/composer.json b/_api_app/composer.json index a21a8a346..2040f76d2 100644 --- a/_api_app/composer.json +++ b/_api_app/composer.json @@ -24,7 +24,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", - "laravel/boost": "^1.1", + "laravel/boost": "^2.0", "laravel/pint": "^1.21", "laravel/sail": "^1.41", "mockery/mockery": "^1.6", diff --git a/_api_app/composer.lock b/_api_app/composer.lock index 1cff27801..6f8a592ee 100644 --- a/_api_app/composer.lock +++ b/_api_app/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4c8e8fb68f05f9a8aed6bd1763eb523e", + "content-hash": "0db6c1107900b00f25f6a4e398f471c2", "packages": [ { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -379,29 +379,28 @@ }, { "name": "dragonmantank/cron-expression", - "version": "v3.4.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/dragonmantank/cron-expression.git", - "reference": "8c784d071debd117328803d86b2097615b457500" + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", - "reference": "8c784d071debd117328803d86b2097615b457500", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", "shasum": "" }, "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.0" + "php": "^8.2|^8.3|^8.4|^8.5" }, "replace": { "mtdowling/cron-expression": "^1.0" }, "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.0", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" }, "type": "library", "extra": { @@ -432,7 +431,7 @@ ], "support": { "issues": "https://github.com/dragonmantank/cron-expression/issues", - "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" }, "funding": [ { @@ -440,7 +439,7 @@ "type": "github" } ], - "time": "2024-10-09T13:47:03+00:00" + "time": "2025-10-31T18:51:33+00:00" }, { "name": "egulias/email-validator", @@ -574,31 +573,31 @@ }, { "name": "fruitcake/php-cors", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/fruitcake/php-cors.git", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", - "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", "shasum": "" }, "require": { - "php": "^7.4|^8.0", - "symfony/http-foundation": "^4.4|^5.4|^6|^7" + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" }, "require-dev": { - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^2", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "^3.5" + "squizlabs/php_codesniffer": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } }, "autoload": { @@ -629,7 +628,7 @@ ], "support": { "issues": "https://github.com/fruitcake/php-cors/issues", - "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" }, "funding": [ { @@ -641,28 +640,28 @@ "type": "github" } ], - "time": "2023-10-12T05:21:21+00:00" + "time": "2025-12-03T09:33:47+00:00" }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -691,7 +690,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -703,7 +702,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", @@ -916,16 +915,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -941,6 +940,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { @@ -1012,7 +1012,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -1028,7 +1028,7 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-03-10T16:41:02+00:00" }, { "name": "guzzlehttp/uri-template", @@ -1322,16 +1322,16 @@ }, { "name": "laravel/framework", - "version": "v12.33.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "124efc5f09d4668a4dc13f94a1018c524a58bcb1" + "reference": "325497463e7599cd14224c422c6e5dd2fe832868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/124efc5f09d4668a4dc13f94a1018c524a58bcb1", - "reference": "124efc5f09d4668a4dc13f94a1018c524a58bcb1", + "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868", "shasum": "" }, "require": { @@ -1352,7 +1352,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.7", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -1419,6 +1419,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -1443,13 +1444,13 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.6.5", + "orchestra/testbench-core": "^10.9.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -1483,7 +1484,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -1505,6 +1506,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -1513,7 +1515,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -1537,36 +1540,36 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-07T14:30:39+00:00" + "time": "2026-03-10T20:25:56+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.7", + "version": "v0.3.14", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc" + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc", - "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4", + "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", "phpstan/phpstan-mockery": "^1.1.3" }, @@ -1594,9 +1597,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.7" + "source": "https://github.com/laravel/prompts/tree/v0.3.14" }, - "time": "2025-09-19T13:47:56+00:00" + "time": "2026-03-01T09:02:38+00:00" }, { "name": "laravel/sanctum", @@ -1664,27 +1667,27 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.5", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed" + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed", - "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -1721,7 +1724,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-09-22T17:29:40+00:00" + "time": "2026-02-20T19:59:49+00:00" }, { "name": "laravel/tinker", @@ -1791,16 +1794,16 @@ }, { "name": "league/commonmark", - "version": "2.7.1", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + "reference": "84b1ca48347efdbe775426f108622a42735a6579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", - "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", + "reference": "84b1ca48347efdbe775426f108622a42735a6579", "shasum": "" }, "require": { @@ -1825,9 +1828,9 @@ "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" }, @@ -1837,7 +1840,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -1894,7 +1897,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2026-03-05T21:37:03+00:00" }, { "name": "league/config", @@ -1980,16 +1983,16 @@ }, { "name": "league/flysystem", - "version": "3.30.0", + "version": "3.32.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", - "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", "shasum": "" }, "require": { @@ -2057,22 +2060,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" }, - "time": "2025-06-25T13:29:59+00:00" + "time": "2026-02-25T17:01:41+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.31.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", "shasum": "" }, "require": { @@ -2106,9 +2109,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2026-01-23T15:30:45+00:00" }, { "name": "league/mime-type-detection", @@ -2168,33 +2171,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.8", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2222,6 +2230,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2234,9 +2243,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2246,7 +2257,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.8.0" }, "funding": [ { @@ -2254,26 +2265,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2026-01-14T17:24:56+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2281,6 +2291,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2305,7 +2316,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -2330,7 +2341,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" }, "funding": [ { @@ -2338,7 +2349,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "mobiledetect/mobiledetectlib", @@ -2407,16 +2418,16 @@ }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -2434,7 +2445,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -2494,7 +2505,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -2506,20 +2517,20 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "nesbot/carbon", - "version": "3.10.3", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + "reference": "57d696f4ec76d8560cc13b9d16ec01afc4379d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", - "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/57d696f4ec76d8560cc13b9d16ec01afc4379d04", + "reference": "57d696f4ec76d8560cc13b9d16ec01afc4379d04", "shasum": "" }, "require": { @@ -2527,9 +2538,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3.12 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -2543,7 +2554,7 @@ "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", - "squizlabs/php_codesniffer": "^3.13.4" + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -2586,14 +2597,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -2611,29 +2622,31 @@ "type": "tidelift" } ], - "time": "2025-09-06T13:39:36+00:00" + "time": "2026-03-10T21:43:48+00:00" }, { "name": "nette/schema", - "version": "v1.3.2", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", - "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", "shasum": "" }, "require": { "nette/utils": "^4.0", - "php": "8.1 - 8.4" + "php": "8.1 - 8.5" }, "require-dev": { - "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^1.0", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -2643,6 +2656,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -2671,26 +2687,26 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.2" + "source": "https://github.com/nette/schema/tree/v1.3.5" }, - "time": "2024-10-06T23:10:23+00:00" + "time": "2026-02-23T03:47:12+00:00" }, { "name": "nette/utils", - "version": "v4.0.8", + "version": "v4.1.3", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", - "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", "shasum": "" }, "require": { - "php": "8.0 - 8.5" + "php": "8.2 - 8.5" }, "conflict": { "nette/finder": "<3", @@ -2698,8 +2714,10 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", "nette/tester": "^2.5", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -2713,7 +2731,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -2760,9 +2778,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.8" + "source": "https://github.com/nette/utils/tree/v4.1.3" }, - "time": "2025-08-06T21:43:34+00:00" + "time": "2026-02-13T03:05:33+00:00" }, { "name": "nikic/php-parser", @@ -2824,31 +2842,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", - "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.2.6" + "symfony/console": "^7.4.4 || ^8.0.4" }, "require-dev": { - "illuminate/console": "^11.44.7", - "laravel/pint": "^1.22.0", + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.2", - "phpstan/phpstan": "^1.12.25", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.2.6", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -2880,7 +2898,7 @@ "email": "enunomaduro@gmail.com" } ], - "description": "Its like Tailwind CSS, but for the console.", + "description": "It's like Tailwind CSS, but for the console.", "keywords": [ "cli", "console", @@ -2891,7 +2909,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" }, "funding": [ { @@ -2907,7 +2925,7 @@ "type": "github" } ], - "time": "2025-05-08T08:14:37+00:00" + "time": "2026-02-16T23:10:27+00:00" }, { "name": "nyholm/psr7", @@ -3037,16 +3055,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -3096,7 +3114,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -3108,7 +3126,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "psr/cache", @@ -3771,20 +3789,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -3843,9 +3861,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "rcrowe/twigbridge", @@ -4194,16 +4212,16 @@ }, { "name": "symfony/clock", - "version": "v7.3.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", - "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", "shasum": "" }, "require": { @@ -4248,7 +4266,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.3.0" + "source": "https://github.com/symfony/clock/tree/v7.4.0" }, "funding": [ { @@ -4259,25 +4277,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/console", - "version": "v7.3.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", - "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { @@ -4285,7 +4307,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -4299,16 +4321,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4342,7 +4364,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.4" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -4362,20 +4384,20 @@ "type": "tidelift" } ], - "time": "2025-09-22T15:31:00+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/css-selector", - "version": "v7.3.0", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "2e7c52c647b406e2107dd867db424a4dbac91864" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2e7c52c647b406e2107dd867db424a4dbac91864", + "reference": "2e7c52c647b406e2107dd867db424a4dbac91864", "shasum": "" }, "require": { @@ -4411,7 +4433,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + "source": "https://github.com/symfony/css-selector/tree/v7.4.6" }, "funding": [ { @@ -4422,12 +4444,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-02-17T07:53:42+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4498,32 +4524,33 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.4", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4" + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", - "reference": "99f81bc944ab8e5dae4f21b4ca9972698bbad0e4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", "symfony/http-kernel": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", "symfony/webpack-encore-bundle": "^1.0|^2.0" }, "bin": [ @@ -4555,7 +4582,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.4" + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" }, "funding": [ { @@ -4575,20 +4602,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-01-20T16:42:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", "shasum": "" }, "require": { @@ -4605,13 +4632,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4639,7 +4667,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" }, "funding": [ { @@ -4659,7 +4687,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2026-01-05T11:45:34+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4739,23 +4767,23 @@ }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4783,7 +4811,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -4803,27 +4831,26 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.3.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6" + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/c061c7c18918b1b64268771aad04b40be41dd2e6", - "reference": "c061c7c18918b1b64268771aad04b40be41dd2e6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php83": "^1.27" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" }, "conflict": { "doctrine/dbal": "<3.6", @@ -4832,13 +4859,13 @@ "require-dev": { "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5", - "symfony/clock": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -4866,7 +4893,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.4" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" }, "funding": [ { @@ -4886,29 +4913,29 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2026-03-06T13:15:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b796dffea7821f035047235e076b60ca2446e3cf" + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b796dffea7821f035047235e076b60ca2446e3cf", - "reference": "b796dffea7821f035047235e076b60ca2446e3cf", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", "shasum": "" }, "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^7.3", - "symfony/http-foundation": "^7.3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { @@ -4918,6 +4945,7 @@ "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", "symfony/form": "<6.4", "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", @@ -4935,27 +4963,27 @@ }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0", - "symfony/clock": "^6.4|^7.0", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/css-selector": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^7.1", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/translation": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0", - "symfony/var-exporter": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "type": "library", @@ -4984,7 +5012,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" }, "funding": [ { @@ -5004,20 +5032,20 @@ "type": "tidelift" } ], - "time": "2025-09-27T12:32:17+00:00" + "time": "2026-03-06T16:33:18+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "ab97ef2f7acf0216955f5845484235113047a31d" + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/ab97ef2f7acf0216955f5845484235113047a31d", - "reference": "ab97ef2f7acf0216955f5845484235113047a31d", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", "shasum": "" }, "require": { @@ -5025,8 +5053,8 @@ "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -5037,10 +5065,10 @@ "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/twig-bridge": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -5068,7 +5096,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.4" + "source": "https://github.com/symfony/mailer/tree/v7.4.6" }, "funding": [ { @@ -5088,43 +5116,44 @@ "type": "tidelift" } ], - "time": "2025-09-17T05:51:54+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/mime", - "version": "v7.3.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35" + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b1b828f69cbaf887fa835a091869e55df91d0e35", - "reference": "b1b828f69cbaf887fa835a091869e55df91d0e35", + "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4.3|^7.0.3" + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" }, "type": "library", "autoload": { @@ -5156,7 +5185,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.3.4" + "source": "https://github.com/symfony/mime/tree/v7.4.7" }, "funding": [ { @@ -5176,7 +5205,7 @@ "type": "tidelift" } ], - "time": "2025-09-16T08:38:17+00:00" + "time": "2026-03-05T15:24:09+00:00" }, { "name": "symfony/options-resolver", @@ -6080,16 +6109,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -6121,7 +6150,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -6141,7 +6170,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -6228,16 +6257,16 @@ }, { "name": "symfony/routing", - "version": "v7.3.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c" + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/8dc648e159e9bac02b703b9fbd937f19ba13d07c", - "reference": "8dc648e159e9bac02b703b9fbd937f19ba13d07c", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", "shasum": "" }, "require": { @@ -6251,11 +6280,11 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6289,7 +6318,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.3.4" + "source": "https://github.com/symfony/routing/tree/v7.4.6" }, "funding": [ { @@ -6309,20 +6338,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -6376,7 +6405,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -6387,31 +6416,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", + "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-grapheme": "~1.33", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -6419,11 +6453,11 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6462,7 +6496,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v7.4.6" }, "funding": [ { @@ -6482,27 +6516,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "symfony/translation", - "version": "v7.3.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174" + "reference": "1888cf064399868af3784b9e043240f1d89d25ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ec25870502d0c7072d086e8ffba1420c85965174", - "reference": "ec25870502d0c7072d086e8ffba1420c85965174", + "url": "https://api.github.com/repos/symfony/translation/zipball/1888cf064399868af3784b9e043240f1d89d25ce", + "reference": "1888cf064399868af3784b9e043240f1d89d25ce", "shasum": "" }, "require": { "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^2.5|^3.0" + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", @@ -6521,17 +6555,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6562,7 +6596,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.4" + "source": "https://github.com/symfony/translation/tree/v7.4.6" }, "funding": [ { @@ -6582,20 +6616,20 @@ "type": "tidelift" } ], - "time": "2025-09-07T11:39:36+00:00" + "time": "2026-02-17T07:53:42+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", - "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", "shasum": "" }, "require": { @@ -6644,7 +6678,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" }, "funding": [ { @@ -6655,25 +6689,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-27T08:32:26+00:00" + "time": "2025-07-15T13:41:35+00:00" }, { "name": "symfony/uid", - "version": "v7.3.1", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", - "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", "shasum": "" }, "require": { @@ -6681,7 +6719,7 @@ "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6718,7 +6756,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.1" + "source": "https://github.com/symfony/uid/tree/v7.4.4" }, "funding": [ { @@ -6729,25 +6767,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", - "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", "shasum": "" }, "require": { @@ -6759,10 +6801,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -6801,7 +6843,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" }, "funding": [ { @@ -6821,27 +6863,27 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2026-02-15T10:53:20+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", - "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpstan/phpstan": "^2.0", @@ -6874,9 +6916,9 @@ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "time": "2024-12-21T16:25:41+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { "name": "twig/twig", @@ -6959,26 +7001,26 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -7027,7 +7069,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -7039,7 +7081,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -7114,64 +7156,6 @@ } ], "time": "2024-11-21T01:49:47+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" } ], "packages-dev": [ @@ -7270,29 +7254,29 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.5", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -7312,9 +7296,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "fakerphp/faker", @@ -7564,34 +7548,34 @@ }, { "name": "laravel/boost", - "version": "v1.3.0", + "version": "v2.2.3", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "ef8800843efc581965c38393adb63ba336dc3979" + "reference": "44ab65a5455c2d6fceb71d6145f8d5d89c02d889" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/ef8800843efc581965c38393adb63ba336dc3979", - "reference": "ef8800843efc581965c38393adb63ba336dc3979", + "url": "https://api.github.com/repos/laravel/boost/zipball/44ab65a5455c2d6fceb71d6145f8d5d89c02d889", + "reference": "44ab65a5455c2d6fceb71d6145f8d5d89c02d889", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.10", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "laravel/mcp": "^0.2.0", - "laravel/prompts": "0.1.25|^0.3.6", - "laravel/roster": "^0.2.8", - "php": "^8.1" + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" }, "require-dev": { - "laravel/pint": "1.20", + "laravel/pint": "^1.27.0", "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", - "pestphp/pest": "^2.36.0|^3.8.4", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.1" }, @@ -7626,41 +7610,41 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2025-09-30T09:34:43+00:00" + "time": "2026-03-06T20:20:28+00:00" }, { "name": "laravel/mcp", - "version": "v0.2.1", + "version": "v0.6.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "0ecf0c04b20e5946ae080e8d67984d5c555174b0" + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/0ecf0c04b20e5946ae080e8d67984d5c555174b0", - "reference": "0ecf0c04b20e5946ae080e8d67984d5c555174b0", + "url": "https://api.github.com/repos/laravel/mcp/zipball/f696e44735b95ff275392eab8ce5a3b4b42a2223", + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/json-schema": "^12.28.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.28.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1", - "php": "^8.1" + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" }, "require-dev": { - "laravel/pint": "1.20.0", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6.0", - "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", "phpstan/phpstan": "^2.1.27", - "rector/rector": "^2.1.7" + "rector/rector": "^2.2.4" }, "type": "library", "extra": { @@ -7699,7 +7683,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-09-24T15:48:16+00:00" + "time": "2026-03-10T20:00:23+00:00" }, { "name": "laravel/pint", @@ -7769,31 +7753,31 @@ }, { "name": "laravel/roster", - "version": "v0.2.8", + "version": "v0.5.1", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "832a6db43743bf08a58691da207f977ec8dc43aa" + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/832a6db43743bf08a58691da207f977ec8dc43aa", - "reference": "832a6db43743bf08a58691da207f977ec8dc43aa", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", "shasum": "" }, "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "php": "^8.1|^8.2", - "symfony/yaml": "^6.4|^7.2" + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" }, "require-dev": { "laravel/pint": "^1.14", "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", "phpstan/phpstan": "^2.0" }, "type": "library", @@ -7826,7 +7810,7 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2025-09-22T13:28:47+00:00" + "time": "2026-03-05T07:58:43+00:00" }, { "name": "laravel/sail", @@ -10355,28 +10339,28 @@ }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -10407,7 +10391,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.3" + "source": "https://github.com/symfony/yaml/tree/v7.4.6" }, "funding": [ { @@ -10427,7 +10411,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -10537,6 +10521,64 @@ } ], "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" } ], "aliases": [], @@ -10548,5 +10590,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/_api_app/opencode.json b/_api_app/opencode.json new file mode 100644 index 000000000..53e16f3d5 --- /dev/null +++ b/_api_app/opencode.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "laravel-boost": { + "type": "local", + "enabled": true, + "command": [ + "php", + "artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file From 298eca4c592e8cc3387a69cb6cf8949ae69dcea2 Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Wed, 11 Mar 2026 20:01:59 +0200 Subject: [PATCH 2/5] Claude project root guidelines --- CLAUDE.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..13a976ffa --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Berta is a file-based CMS (no database required for content storage). It has three distinct sub-applications that are developed and built independently: + +1. **`_api_app/`** — Laravel 12 API backend (PHP 8.2+) +2. **`editor/`** — Angular 20 admin editor (TypeScript) +3. **`engine/`** — Legacy PHP rendering engine with Gulp-built assets + +## Development Commands + +### Laravel API (`_api_app/`) +```bash +cd _api_app +composer install +php artisan test --compact # Run all tests +php artisan test --compact --filter=TestName # Run specific test +vendor/bin/pint --dirty # Format changed PHP files +npm run dev # Vite dev server +npm run build # Build assets +``` + +### Angular Editor (`editor/`) +```bash +cd editor +npm install +npm run dev # Watch mode (outputs to engine/dist) +npm run build # Production build (outputs to engine/dist) +npm test # Karma/Jasmine unit tests +``` + +### Legacy Engine Assets (root) +```bash +npm install +npm run dev # Gulp watch (compiles Sass for themes/templates) +npm run build # Gulp production build +``` + +## Architecture + +### How the Parts Connect + +- The **Angular editor** (`editor/`) compiles into `engine/dist/` — the legacy PHP engine serves these compiled assets to the browser. +- The **Laravel API** (`_api_app/`) exposes REST endpoints consumed by the Angular editor for CMS operations (site settings, sections, media, shop). +- The **legacy PHP engine** (`engine/`) handles frontend rendering of sites using file-based XML storage. It reads `.xml` files directly from the user's site directory. +- The Angular editor's **Twig templates** are bundled at build time via `editor/copy-twig-templates.mjs` and `editor/bundle-twig-templates.js` (prebuild step), then rendered client-side using the `twig` npm package. + +### Key Directories + +| Path | Purpose | +|------|---------| +| `_api_app/app/Sites/` | Site/section/entry management domain | +| `_api_app/app/Shop/` | E-commerce plugin | +| `_api_app/app/Plugins/` | Plugin system | +| `_api_app/app/Configuration/` | App configuration classes | +| `editor/src/` | Angular source (components, state, services) | +| `engine/_classes/` | Legacy PHP classes for site rendering | +| `engine/_lib/berta/` | CSS/JS assets bundled by Gulp | +| `_themes/` | Site themes (capetown, jaipur, kyoto, madrid, etc.) | +| `_templates/` | Email/system templates with SCSS | +| `_plugin_shop/` | Shop plugin PHP files | + +### State Management (Angular) + +The editor uses **NGXS** for state management. State files live in `editor/src/app/**/state/` alongside their actions. + +### Authentication + +Laravel Sanctum handles API auth. JWT tokens (Firebase JWT) are used for certain operations. + +## Laravel API Guidelines + +See `_api_app/CLAUDE.md` for detailed Laravel/PHP conventions, Pest testing rules, and Laravel Boost MCP tool usage. Those guidelines apply whenever working inside `_api_app/`. From 633f0971c9fd6eebffb3a440a7dd69c5729211d0 Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Wed, 18 Mar 2026 13:30:42 +0200 Subject: [PATCH 3/5] AI assistant chat widget --- .../skills/ai-sdk-development/SKILL.md | 413 ++++++++++++++++++ .../skills/ai-sdk-development/SKILL.md | 413 ++++++++++++++++++ _api_app/.codex/config.toml | 8 + .../skills/ai-sdk-development/SKILL.md | 413 ++++++++++++++++++ _api_app/.env.example | 2 + .../skills/ai-sdk-development/SKILL.md | 413 ++++++++++++++++++ _api_app/AGENTS.md | 31 +- _api_app/CLAUDE.md | 31 +- .../app/Http/Controllers/StateController.php | 10 +- _api_app/app/Http/Middleware/Authenticate.php | 16 - .../app/Http/Middleware/SetupMiddleware.php | 2 +- _api_app/app/User/UserModel.php | 1 + _api_app/boost.json | 8 +- _api_app/bootstrap/providers.php | 7 +- _api_app/composer.json | 5 +- _api_app/composer.lock | 183 +++++++- _api_app/config/ai.php | 129 ++++++ _api_app/config/auth.php | 4 +- _api_app/config/sanctum.php | 9 +- _api_app/config/twigbridge.php | 4 +- _api_app/database/factories/UserFactory.php | 3 +- _api_app/opencode.json | 11 + .../tests/Feature/AiChatControllerTest.php | 163 +++++++ _api_app/tests/Pest.php | 4 +- editor/package-lock.json | 13 + editor/package.json | 1 + .../app/ai-assistant/ai-assistant.actions.ts | 22 + .../ai-assistant/ai-assistant.component.ts | 359 +++++++++++++++ .../app/ai-assistant/ai-assistant.module.ts | 13 + .../app/ai-assistant/ai-assistant.service.ts | 37 ++ .../app/ai-assistant/ai-assistant.state.ts | 196 +++++++++ editor/src/app/app.component.ts | 1 + editor/src/app/app.module.ts | 4 + editor/src/app/header/header.component.ts | 17 + editor/src/app/pipes/markdown.pipe.ts | 18 + editor/src/app/pipes/pipes.module.ts | 9 + .../src/app/pipes/{pipe.ts => safe.pipe.ts} | 0 .../default-template-rerender.service.ts | 1 + .../mashup-template-rerender.service.ts | 1 + .../messy/messy-template-rerender.service.ts | 1 + .../app/rerender/template-rerender.service.ts | 6 + editor/src/app/rerender/types/components.ts | 1 + .../white/white-template-rerender.service.ts | 1 + .../sites/sections/site-sections.module.ts | 6 +- .../app/sites/settings/site-settings.state.ts | 1 + editor/src/app/user/user.state.ts | 2 + 46 files changed, 2921 insertions(+), 72 deletions(-) create mode 100644 _api_app/.agents/skills/ai-sdk-development/SKILL.md create mode 100644 _api_app/.claude/skills/ai-sdk-development/SKILL.md create mode 100644 _api_app/.cursor/skills/ai-sdk-development/SKILL.md create mode 100644 _api_app/.github/skills/ai-sdk-development/SKILL.md create mode 100644 _api_app/config/ai.php create mode 100644 _api_app/tests/Feature/AiChatControllerTest.php create mode 100644 editor/src/app/ai-assistant/ai-assistant.actions.ts create mode 100644 editor/src/app/ai-assistant/ai-assistant.component.ts create mode 100644 editor/src/app/ai-assistant/ai-assistant.module.ts create mode 100644 editor/src/app/ai-assistant/ai-assistant.service.ts create mode 100644 editor/src/app/ai-assistant/ai-assistant.state.ts create mode 100644 editor/src/app/pipes/markdown.pipe.ts create mode 100644 editor/src/app/pipes/pipes.module.ts rename editor/src/app/pipes/{pipe.ts => safe.pipe.ts} (100%) diff --git a/_api_app/.agents/skills/ai-sdk-development/SKILL.md b/_api_app/.agents/skills/ai-sdk-development/SKILL.md new file mode 100644 index 000000000..d74e342ec --- /dev/null +++ b/_api_app/.agents/skills/ai-sdk-development/SKILL.md @@ -0,0 +1,413 @@ +--- +name: ai-sdk-development +description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +--- + +# Developing with the Laravel AI SDK + +The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers. + +## Searching the Documentation + +This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth. + +- Use broad, simple queries that match the documentation section headings below. +- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`. +- Run multiple queries at once — the most relevant results are returned first. + +### Documentation Sections + +Use these section headings as query terms for accurate results: + +- Introduction, Installation, Configuration, Provider Support +- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration +- Images +- Audio (TTS) +- Transcription (STT) +- Embeddings: Querying Embeddings, Caching Embeddings +- Reranking +- Files +- Vector Stores: Adding Files to Stores +- Failover +- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores +- Events + +## Decision Workflow + +Determine the right entry point before writing code: + +Text generation or chat? → Agent class with `Promptable` trait +Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic) +Structured JSON output? → Agent + `HasStructuredOutput` interface +Image generation? → `Image::of()->generate()` +Audio synthesis? → `Audio::of()->generate()` +Transcription? → `Transcription::fromPath()->generate()` +Embeddings? → `Embeddings::for()->generate()` +Reranking? → `Reranking::of()->rerank()` +File storage? → `Document::fromPath()->put()` +Vector stores? → `Stores::create()` + +## Basic Usage Examples + +### Agents + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent +{ + use Promptable; + + public function instructions(): string + { + return 'You are a sales coach.'; + } +} + +// Prompting +$response = (new SalesCoach)->prompt('Analyze this transcript...'); +echo $response->text; + +// Streaming (returns SSE response from a route) +return (new SalesCoach)->stream('Analyze this transcript...'); + +// Queueing +(new SalesCoach)->queue('Analyze this transcript...') + ->then(fn ($response) => /* ... */); + +// Anonymous agents +use function Laravel\Ai\{agent}; + +$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello'); +``` + +### Conversation Context + +Manual conversation history via the `Conversational` interface: + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Messages\Message; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable; + + public function __construct(public User $user) {} + + public function instructions(): string { return 'You are a sales coach.'; } + + public function messages(): iterable + { + return History::where('user_id', $this->user->id) + ->latest()->limit(50)->get()->reverse() + ->map(fn ($m) => new Message($m->role, $m->content)) + ->all(); + } +} +``` + +Automatic conversation persistence via the `RemembersConversations` trait: + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable, RemembersConversations; + + public function instructions(): string { return 'You are a sales coach.'; } +} + +// Start a new conversation +$response = (new SalesCoach)->forUser($user)->prompt('Hello!'); +$conversationId = $response->conversationId; + +// Continue an existing conversation +$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.'); +``` + +### Structured Output + +```php +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\HasStructuredOutput; +use Laravel\Ai\Promptable; + +class Reviewer implements Agent, HasStructuredOutput +{ + use Promptable; + + public function instructions(): string { return 'Review and score content.'; } + + public function schema(JsonSchema $schema): array + { + return [ + 'feedback' => $schema->string()->required(), + 'score' => $schema->integer()->min(1)->max(10)->required(), + ]; + } +} + +$response = (new Reviewer)->prompt('Review this...'); +echo $response['score']; // Access like an array +``` + +### Images + +```php +use Laravel\Ai\Image; + +$image = Image::of('A sunset over mountains') + ->landscape() + ->quality('high') + ->generate(); + +$path = $image->store(); // Store to default disk +``` + +### Audio + +```php +use Laravel\Ai\Audio; + +$audio = Audio::of('Hello from Laravel.') + ->female() + ->instructions('Speak warmly') + ->generate(); + +$path = $audio->store(); +``` + +### Transcription + +```php +use Laravel\Ai\Transcription; + +$transcript = Transcription::fromStorage('audio.mp3') + ->diarize() + ->generate(); + +echo (string) $transcript; +``` + +### Embeddings + +```php +use Laravel\Ai\Embeddings; +use Illuminate\Support\Str; + +$response = Embeddings::for(['Text one', 'Text two']) + ->dimensions(1536) + ->cache() + ->generate(); + +// Single string via Stringable +$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); +``` + +### Reranking + +```php +use Laravel\Ai\Reranking; + +$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.']) + ->limit(5) + ->rerank('PHP frameworks'); + +$response->first()->document; // "Laravel is PHP." +``` + +### Files and Vector Stores + +```php +use Laravel\Ai\Files\Document; +use Laravel\Ai\Stores; + +// Store a file with the provider +$file = Document::fromPath('/path/to/doc.pdf')->put(); + +// Create a vector store and add files +$store = Stores::create('Knowledge Base'); +$store->add($file->id); +$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step +``` + +## Agent Configuration + +### PHP Attributes + +```php +use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; + +#[Provider('anthropic')] +#[MaxSteps(10)] +#[MaxTokens(4096)] +#[Temperature(0.7)] +#[Timeout(120)] +class MyAgent implements Agent +{ + use Promptable; + // ... +} +``` + +The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. + +### Tools + +Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: + +```php +use Laravel\Ai\Contracts\HasTools; + +class MyAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return [new MyCustomTool]; + } +} +``` + +### Provider Tools + +```php +use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch}; + +public function tools(): iterable +{ + return [ + (new WebSearch)->max(5)->allow(['laravel.com']), + new WebFetch, + new FileSearch(stores: ['store_id']), + ]; +} +``` + +### Conversation Memory + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Conversational; + +class ChatBot implements Agent, Conversational +{ + use Promptable, RemembersConversations; + // ... +} + +$response = (new ChatBot)->forUser($user)->prompt('Hello!'); +$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...'); +``` + +### Failover + +```php +$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +``` + +## Testing and Faking + +Each capability supports `fake()` with assertions: + +```php +use App\Ai\Agents\SalesCoach; +use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores}; + +// Agents +SalesCoach::fake(['Response 1', 'Response 2']); +SalesCoach::assertPrompted('query'); +SalesCoach::assertNotPrompted('query'); +SalesCoach::assertNeverPrompted(); +SalesCoach::fake()->preventStrayPrompts(); + +// Images +Image::fake(); +Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset')); +Image::assertNothingGenerated(); + +// Audio +Audio::fake(); +Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello')); + +// Transcription +Transcription::fake(['Transcribed text.']); +Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized()); + +// Embeddings +Embeddings::fake(); +Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel')); + +// Reranking +Reranking::fake(); +Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP')); + +// Files +Files::fake(); +Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain'); + +// Stores +Stores::fake(); +Stores::assertCreated('Knowledge Base'); +$store = Stores::get('id'); +$store->assertAdded('file_id'); +``` + +## Key Patterns + +- Namespace: `Laravel\Ai\` +- Package: `composer require laravel/ai` +- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait +- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` +- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Artisan commands: `php artisan make:agent`, `php artisan make:tool` +- Global helper: `agent()` for anonymous agents + +## Common Pitfalls + +### Wrong Namespace + +The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`. + +```php +// Correct +use Laravel\Ai\Image; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +// Wrong — these do not exist +use Illuminate\Ai\Image; +use Laravel\AI\Agent; +``` + +### Unsupported Provider Capability + +Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. + +### Never Use Prism Directly + +Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. + +## Provider Support + +| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | +| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | +| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | +| Anthropic | Y | - | - | - | - | - | Y | - | +| Gemini | Y | Y | - | - | Y | - | Y | Y | +| xAI | Y | Y | - | - | - | - | - | - | +| Groq | Y | - | - | - | - | - | - | - | +| OpenRouter | Y | - | - | - | - | - | - | - | +| ElevenLabs | - | - | Y | Y | - | - | - | - | +| Cohere | - | - | - | - | Y | Y | - | - | +| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file diff --git a/_api_app/.claude/skills/ai-sdk-development/SKILL.md b/_api_app/.claude/skills/ai-sdk-development/SKILL.md new file mode 100644 index 000000000..d74e342ec --- /dev/null +++ b/_api_app/.claude/skills/ai-sdk-development/SKILL.md @@ -0,0 +1,413 @@ +--- +name: ai-sdk-development +description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +--- + +# Developing with the Laravel AI SDK + +The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers. + +## Searching the Documentation + +This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth. + +- Use broad, simple queries that match the documentation section headings below. +- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`. +- Run multiple queries at once — the most relevant results are returned first. + +### Documentation Sections + +Use these section headings as query terms for accurate results: + +- Introduction, Installation, Configuration, Provider Support +- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration +- Images +- Audio (TTS) +- Transcription (STT) +- Embeddings: Querying Embeddings, Caching Embeddings +- Reranking +- Files +- Vector Stores: Adding Files to Stores +- Failover +- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores +- Events + +## Decision Workflow + +Determine the right entry point before writing code: + +Text generation or chat? → Agent class with `Promptable` trait +Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic) +Structured JSON output? → Agent + `HasStructuredOutput` interface +Image generation? → `Image::of()->generate()` +Audio synthesis? → `Audio::of()->generate()` +Transcription? → `Transcription::fromPath()->generate()` +Embeddings? → `Embeddings::for()->generate()` +Reranking? → `Reranking::of()->rerank()` +File storage? → `Document::fromPath()->put()` +Vector stores? → `Stores::create()` + +## Basic Usage Examples + +### Agents + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent +{ + use Promptable; + + public function instructions(): string + { + return 'You are a sales coach.'; + } +} + +// Prompting +$response = (new SalesCoach)->prompt('Analyze this transcript...'); +echo $response->text; + +// Streaming (returns SSE response from a route) +return (new SalesCoach)->stream('Analyze this transcript...'); + +// Queueing +(new SalesCoach)->queue('Analyze this transcript...') + ->then(fn ($response) => /* ... */); + +// Anonymous agents +use function Laravel\Ai\{agent}; + +$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello'); +``` + +### Conversation Context + +Manual conversation history via the `Conversational` interface: + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Messages\Message; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable; + + public function __construct(public User $user) {} + + public function instructions(): string { return 'You are a sales coach.'; } + + public function messages(): iterable + { + return History::where('user_id', $this->user->id) + ->latest()->limit(50)->get()->reverse() + ->map(fn ($m) => new Message($m->role, $m->content)) + ->all(); + } +} +``` + +Automatic conversation persistence via the `RemembersConversations` trait: + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable, RemembersConversations; + + public function instructions(): string { return 'You are a sales coach.'; } +} + +// Start a new conversation +$response = (new SalesCoach)->forUser($user)->prompt('Hello!'); +$conversationId = $response->conversationId; + +// Continue an existing conversation +$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.'); +``` + +### Structured Output + +```php +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\HasStructuredOutput; +use Laravel\Ai\Promptable; + +class Reviewer implements Agent, HasStructuredOutput +{ + use Promptable; + + public function instructions(): string { return 'Review and score content.'; } + + public function schema(JsonSchema $schema): array + { + return [ + 'feedback' => $schema->string()->required(), + 'score' => $schema->integer()->min(1)->max(10)->required(), + ]; + } +} + +$response = (new Reviewer)->prompt('Review this...'); +echo $response['score']; // Access like an array +``` + +### Images + +```php +use Laravel\Ai\Image; + +$image = Image::of('A sunset over mountains') + ->landscape() + ->quality('high') + ->generate(); + +$path = $image->store(); // Store to default disk +``` + +### Audio + +```php +use Laravel\Ai\Audio; + +$audio = Audio::of('Hello from Laravel.') + ->female() + ->instructions('Speak warmly') + ->generate(); + +$path = $audio->store(); +``` + +### Transcription + +```php +use Laravel\Ai\Transcription; + +$transcript = Transcription::fromStorage('audio.mp3') + ->diarize() + ->generate(); + +echo (string) $transcript; +``` + +### Embeddings + +```php +use Laravel\Ai\Embeddings; +use Illuminate\Support\Str; + +$response = Embeddings::for(['Text one', 'Text two']) + ->dimensions(1536) + ->cache() + ->generate(); + +// Single string via Stringable +$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); +``` + +### Reranking + +```php +use Laravel\Ai\Reranking; + +$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.']) + ->limit(5) + ->rerank('PHP frameworks'); + +$response->first()->document; // "Laravel is PHP." +``` + +### Files and Vector Stores + +```php +use Laravel\Ai\Files\Document; +use Laravel\Ai\Stores; + +// Store a file with the provider +$file = Document::fromPath('/path/to/doc.pdf')->put(); + +// Create a vector store and add files +$store = Stores::create('Knowledge Base'); +$store->add($file->id); +$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step +``` + +## Agent Configuration + +### PHP Attributes + +```php +use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; + +#[Provider('anthropic')] +#[MaxSteps(10)] +#[MaxTokens(4096)] +#[Temperature(0.7)] +#[Timeout(120)] +class MyAgent implements Agent +{ + use Promptable; + // ... +} +``` + +The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. + +### Tools + +Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: + +```php +use Laravel\Ai\Contracts\HasTools; + +class MyAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return [new MyCustomTool]; + } +} +``` + +### Provider Tools + +```php +use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch}; + +public function tools(): iterable +{ + return [ + (new WebSearch)->max(5)->allow(['laravel.com']), + new WebFetch, + new FileSearch(stores: ['store_id']), + ]; +} +``` + +### Conversation Memory + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Conversational; + +class ChatBot implements Agent, Conversational +{ + use Promptable, RemembersConversations; + // ... +} + +$response = (new ChatBot)->forUser($user)->prompt('Hello!'); +$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...'); +``` + +### Failover + +```php +$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +``` + +## Testing and Faking + +Each capability supports `fake()` with assertions: + +```php +use App\Ai\Agents\SalesCoach; +use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores}; + +// Agents +SalesCoach::fake(['Response 1', 'Response 2']); +SalesCoach::assertPrompted('query'); +SalesCoach::assertNotPrompted('query'); +SalesCoach::assertNeverPrompted(); +SalesCoach::fake()->preventStrayPrompts(); + +// Images +Image::fake(); +Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset')); +Image::assertNothingGenerated(); + +// Audio +Audio::fake(); +Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello')); + +// Transcription +Transcription::fake(['Transcribed text.']); +Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized()); + +// Embeddings +Embeddings::fake(); +Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel')); + +// Reranking +Reranking::fake(); +Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP')); + +// Files +Files::fake(); +Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain'); + +// Stores +Stores::fake(); +Stores::assertCreated('Knowledge Base'); +$store = Stores::get('id'); +$store->assertAdded('file_id'); +``` + +## Key Patterns + +- Namespace: `Laravel\Ai\` +- Package: `composer require laravel/ai` +- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait +- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` +- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Artisan commands: `php artisan make:agent`, `php artisan make:tool` +- Global helper: `agent()` for anonymous agents + +## Common Pitfalls + +### Wrong Namespace + +The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`. + +```php +// Correct +use Laravel\Ai\Image; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +// Wrong — these do not exist +use Illuminate\Ai\Image; +use Laravel\AI\Agent; +``` + +### Unsupported Provider Capability + +Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. + +### Never Use Prism Directly + +Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. + +## Provider Support + +| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | +| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | +| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | +| Anthropic | Y | - | - | - | - | - | Y | - | +| Gemini | Y | Y | - | - | Y | - | Y | Y | +| xAI | Y | Y | - | - | - | - | - | - | +| Groq | Y | - | - | - | - | - | - | - | +| OpenRouter | Y | - | - | - | - | - | - | - | +| ElevenLabs | - | - | Y | Y | - | - | - | - | +| Cohere | - | - | - | - | Y | Y | - | - | +| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file diff --git a/_api_app/.codex/config.toml b/_api_app/.codex/config.toml index 722d917ec..9f0141cdc 100644 --- a/_api_app/.codex/config.toml +++ b/_api_app/.codex/config.toml @@ -2,3 +2,11 @@ command = "php" args = ["artisan", "boost:mcp"] cwd = "/Users/uldis/projects/berta/berta/_api_app" + +[mcp_servers.herd] +command = "php" +args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"] +cwd = "/Users/uldis/projects/berta/berta/_api_app" + +[mcp_servers.herd.env] +SITE_PATH = "/Users/uldis/projects/berta/berta/_api_app" diff --git a/_api_app/.cursor/skills/ai-sdk-development/SKILL.md b/_api_app/.cursor/skills/ai-sdk-development/SKILL.md new file mode 100644 index 000000000..d74e342ec --- /dev/null +++ b/_api_app/.cursor/skills/ai-sdk-development/SKILL.md @@ -0,0 +1,413 @@ +--- +name: ai-sdk-development +description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +--- + +# Developing with the Laravel AI SDK + +The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers. + +## Searching the Documentation + +This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth. + +- Use broad, simple queries that match the documentation section headings below. +- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`. +- Run multiple queries at once — the most relevant results are returned first. + +### Documentation Sections + +Use these section headings as query terms for accurate results: + +- Introduction, Installation, Configuration, Provider Support +- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration +- Images +- Audio (TTS) +- Transcription (STT) +- Embeddings: Querying Embeddings, Caching Embeddings +- Reranking +- Files +- Vector Stores: Adding Files to Stores +- Failover +- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores +- Events + +## Decision Workflow + +Determine the right entry point before writing code: + +Text generation or chat? → Agent class with `Promptable` trait +Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic) +Structured JSON output? → Agent + `HasStructuredOutput` interface +Image generation? → `Image::of()->generate()` +Audio synthesis? → `Audio::of()->generate()` +Transcription? → `Transcription::fromPath()->generate()` +Embeddings? → `Embeddings::for()->generate()` +Reranking? → `Reranking::of()->rerank()` +File storage? → `Document::fromPath()->put()` +Vector stores? → `Stores::create()` + +## Basic Usage Examples + +### Agents + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent +{ + use Promptable; + + public function instructions(): string + { + return 'You are a sales coach.'; + } +} + +// Prompting +$response = (new SalesCoach)->prompt('Analyze this transcript...'); +echo $response->text; + +// Streaming (returns SSE response from a route) +return (new SalesCoach)->stream('Analyze this transcript...'); + +// Queueing +(new SalesCoach)->queue('Analyze this transcript...') + ->then(fn ($response) => /* ... */); + +// Anonymous agents +use function Laravel\Ai\{agent}; + +$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello'); +``` + +### Conversation Context + +Manual conversation history via the `Conversational` interface: + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Messages\Message; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable; + + public function __construct(public User $user) {} + + public function instructions(): string { return 'You are a sales coach.'; } + + public function messages(): iterable + { + return History::where('user_id', $this->user->id) + ->latest()->limit(50)->get()->reverse() + ->map(fn ($m) => new Message($m->role, $m->content)) + ->all(); + } +} +``` + +Automatic conversation persistence via the `RemembersConversations` trait: + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable, RemembersConversations; + + public function instructions(): string { return 'You are a sales coach.'; } +} + +// Start a new conversation +$response = (new SalesCoach)->forUser($user)->prompt('Hello!'); +$conversationId = $response->conversationId; + +// Continue an existing conversation +$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.'); +``` + +### Structured Output + +```php +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\HasStructuredOutput; +use Laravel\Ai\Promptable; + +class Reviewer implements Agent, HasStructuredOutput +{ + use Promptable; + + public function instructions(): string { return 'Review and score content.'; } + + public function schema(JsonSchema $schema): array + { + return [ + 'feedback' => $schema->string()->required(), + 'score' => $schema->integer()->min(1)->max(10)->required(), + ]; + } +} + +$response = (new Reviewer)->prompt('Review this...'); +echo $response['score']; // Access like an array +``` + +### Images + +```php +use Laravel\Ai\Image; + +$image = Image::of('A sunset over mountains') + ->landscape() + ->quality('high') + ->generate(); + +$path = $image->store(); // Store to default disk +``` + +### Audio + +```php +use Laravel\Ai\Audio; + +$audio = Audio::of('Hello from Laravel.') + ->female() + ->instructions('Speak warmly') + ->generate(); + +$path = $audio->store(); +``` + +### Transcription + +```php +use Laravel\Ai\Transcription; + +$transcript = Transcription::fromStorage('audio.mp3') + ->diarize() + ->generate(); + +echo (string) $transcript; +``` + +### Embeddings + +```php +use Laravel\Ai\Embeddings; +use Illuminate\Support\Str; + +$response = Embeddings::for(['Text one', 'Text two']) + ->dimensions(1536) + ->cache() + ->generate(); + +// Single string via Stringable +$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); +``` + +### Reranking + +```php +use Laravel\Ai\Reranking; + +$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.']) + ->limit(5) + ->rerank('PHP frameworks'); + +$response->first()->document; // "Laravel is PHP." +``` + +### Files and Vector Stores + +```php +use Laravel\Ai\Files\Document; +use Laravel\Ai\Stores; + +// Store a file with the provider +$file = Document::fromPath('/path/to/doc.pdf')->put(); + +// Create a vector store and add files +$store = Stores::create('Knowledge Base'); +$store->add($file->id); +$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step +``` + +## Agent Configuration + +### PHP Attributes + +```php +use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; + +#[Provider('anthropic')] +#[MaxSteps(10)] +#[MaxTokens(4096)] +#[Temperature(0.7)] +#[Timeout(120)] +class MyAgent implements Agent +{ + use Promptable; + // ... +} +``` + +The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. + +### Tools + +Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: + +```php +use Laravel\Ai\Contracts\HasTools; + +class MyAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return [new MyCustomTool]; + } +} +``` + +### Provider Tools + +```php +use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch}; + +public function tools(): iterable +{ + return [ + (new WebSearch)->max(5)->allow(['laravel.com']), + new WebFetch, + new FileSearch(stores: ['store_id']), + ]; +} +``` + +### Conversation Memory + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Conversational; + +class ChatBot implements Agent, Conversational +{ + use Promptable, RemembersConversations; + // ... +} + +$response = (new ChatBot)->forUser($user)->prompt('Hello!'); +$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...'); +``` + +### Failover + +```php +$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +``` + +## Testing and Faking + +Each capability supports `fake()` with assertions: + +```php +use App\Ai\Agents\SalesCoach; +use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores}; + +// Agents +SalesCoach::fake(['Response 1', 'Response 2']); +SalesCoach::assertPrompted('query'); +SalesCoach::assertNotPrompted('query'); +SalesCoach::assertNeverPrompted(); +SalesCoach::fake()->preventStrayPrompts(); + +// Images +Image::fake(); +Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset')); +Image::assertNothingGenerated(); + +// Audio +Audio::fake(); +Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello')); + +// Transcription +Transcription::fake(['Transcribed text.']); +Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized()); + +// Embeddings +Embeddings::fake(); +Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel')); + +// Reranking +Reranking::fake(); +Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP')); + +// Files +Files::fake(); +Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain'); + +// Stores +Stores::fake(); +Stores::assertCreated('Knowledge Base'); +$store = Stores::get('id'); +$store->assertAdded('file_id'); +``` + +## Key Patterns + +- Namespace: `Laravel\Ai\` +- Package: `composer require laravel/ai` +- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait +- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` +- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Artisan commands: `php artisan make:agent`, `php artisan make:tool` +- Global helper: `agent()` for anonymous agents + +## Common Pitfalls + +### Wrong Namespace + +The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`. + +```php +// Correct +use Laravel\Ai\Image; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +// Wrong — these do not exist +use Illuminate\Ai\Image; +use Laravel\AI\Agent; +``` + +### Unsupported Provider Capability + +Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. + +### Never Use Prism Directly + +Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. + +## Provider Support + +| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | +| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | +| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | +| Anthropic | Y | - | - | - | - | - | Y | - | +| Gemini | Y | Y | - | - | Y | - | Y | Y | +| xAI | Y | Y | - | - | - | - | - | - | +| Groq | Y | - | - | - | - | - | - | - | +| OpenRouter | Y | - | - | - | - | - | - | - | +| ElevenLabs | - | - | Y | Y | - | - | - | - | +| Cohere | - | - | - | - | Y | Y | - | - | +| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file diff --git a/_api_app/.env.example b/_api_app/.env.example index fcf24118d..e755a3387 100644 --- a/_api_app/.env.example +++ b/_api_app/.env.example @@ -6,3 +6,5 @@ APP_ID=[YOUR_APP_ID] API_PREFIX=_api SENTRY_DSN= SENTRY_FRONTEND_DSN= +AI_DEFAULT_PROVIDER=anthropic +ANTHROPIC_API_KEY= diff --git a/_api_app/.github/skills/ai-sdk-development/SKILL.md b/_api_app/.github/skills/ai-sdk-development/SKILL.md new file mode 100644 index 000000000..d74e342ec --- /dev/null +++ b/_api_app/.github/skills/ai-sdk-development/SKILL.md @@ -0,0 +1,413 @@ +--- +name: ai-sdk-development +description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +--- + +# Developing with the Laravel AI SDK + +The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers. + +## Searching the Documentation + +This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth. + +- Use broad, simple queries that match the documentation section headings below. +- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`. +- Run multiple queries at once — the most relevant results are returned first. + +### Documentation Sections + +Use these section headings as query terms for accurate results: + +- Introduction, Installation, Configuration, Provider Support +- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration +- Images +- Audio (TTS) +- Transcription (STT) +- Embeddings: Querying Embeddings, Caching Embeddings +- Reranking +- Files +- Vector Stores: Adding Files to Stores +- Failover +- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores +- Events + +## Decision Workflow + +Determine the right entry point before writing code: + +Text generation or chat? → Agent class with `Promptable` trait +Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic) +Structured JSON output? → Agent + `HasStructuredOutput` interface +Image generation? → `Image::of()->generate()` +Audio synthesis? → `Audio::of()->generate()` +Transcription? → `Transcription::fromPath()->generate()` +Embeddings? → `Embeddings::for()->generate()` +Reranking? → `Reranking::of()->rerank()` +File storage? → `Document::fromPath()->put()` +Vector stores? → `Stores::create()` + +## Basic Usage Examples + +### Agents + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent +{ + use Promptable; + + public function instructions(): string + { + return 'You are a sales coach.'; + } +} + +// Prompting +$response = (new SalesCoach)->prompt('Analyze this transcript...'); +echo $response->text; + +// Streaming (returns SSE response from a route) +return (new SalesCoach)->stream('Analyze this transcript...'); + +// Queueing +(new SalesCoach)->queue('Analyze this transcript...') + ->then(fn ($response) => /* ... */); + +// Anonymous agents +use function Laravel\Ai\{agent}; + +$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello'); +``` + +### Conversation Context + +Manual conversation history via the `Conversational` interface: + +```php +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Messages\Message; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable; + + public function __construct(public User $user) {} + + public function instructions(): string { return 'You are a sales coach.'; } + + public function messages(): iterable + { + return History::where('user_id', $this->user->id) + ->latest()->limit(50)->get()->reverse() + ->map(fn ($m) => new Message($m->role, $m->content)) + ->all(); + } +} +``` + +Automatic conversation persistence via the `RemembersConversations` trait: + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\Conversational; +use Laravel\Ai\Promptable; + +class SalesCoach implements Agent, Conversational +{ + use Promptable, RemembersConversations; + + public function instructions(): string { return 'You are a sales coach.'; } +} + +// Start a new conversation +$response = (new SalesCoach)->forUser($user)->prompt('Hello!'); +$conversationId = $response->conversationId; + +// Continue an existing conversation +$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.'); +``` + +### Structured Output + +```php +use Illuminate\Contracts\JsonSchema\JsonSchema; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Contracts\HasStructuredOutput; +use Laravel\Ai\Promptable; + +class Reviewer implements Agent, HasStructuredOutput +{ + use Promptable; + + public function instructions(): string { return 'Review and score content.'; } + + public function schema(JsonSchema $schema): array + { + return [ + 'feedback' => $schema->string()->required(), + 'score' => $schema->integer()->min(1)->max(10)->required(), + ]; + } +} + +$response = (new Reviewer)->prompt('Review this...'); +echo $response['score']; // Access like an array +``` + +### Images + +```php +use Laravel\Ai\Image; + +$image = Image::of('A sunset over mountains') + ->landscape() + ->quality('high') + ->generate(); + +$path = $image->store(); // Store to default disk +``` + +### Audio + +```php +use Laravel\Ai\Audio; + +$audio = Audio::of('Hello from Laravel.') + ->female() + ->instructions('Speak warmly') + ->generate(); + +$path = $audio->store(); +``` + +### Transcription + +```php +use Laravel\Ai\Transcription; + +$transcript = Transcription::fromStorage('audio.mp3') + ->diarize() + ->generate(); + +echo (string) $transcript; +``` + +### Embeddings + +```php +use Laravel\Ai\Embeddings; +use Illuminate\Support\Str; + +$response = Embeddings::for(['Text one', 'Text two']) + ->dimensions(1536) + ->cache() + ->generate(); + +// Single string via Stringable +$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings(); +``` + +### Reranking + +```php +use Laravel\Ai\Reranking; + +$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.']) + ->limit(5) + ->rerank('PHP frameworks'); + +$response->first()->document; // "Laravel is PHP." +``` + +### Files and Vector Stores + +```php +use Laravel\Ai\Files\Document; +use Laravel\Ai\Stores; + +// Store a file with the provider +$file = Document::fromPath('/path/to/doc.pdf')->put(); + +// Create a vector store and add files +$store = Stores::create('Knowledge Base'); +$store->add($file->id); +$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step +``` + +## Agent Configuration + +### PHP Attributes + +```php +use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; + +#[Provider('anthropic')] +#[MaxSteps(10)] +#[MaxTokens(4096)] +#[Temperature(0.7)] +#[Timeout(120)] +class MyAgent implements Agent +{ + use Promptable; + // ... +} +``` + +The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. + +### Tools + +Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: + +```php +use Laravel\Ai\Contracts\HasTools; + +class MyAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return [new MyCustomTool]; + } +} +``` + +### Provider Tools + +```php +use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch}; + +public function tools(): iterable +{ + return [ + (new WebSearch)->max(5)->allow(['laravel.com']), + new WebFetch, + new FileSearch(stores: ['store_id']), + ]; +} +``` + +### Conversation Memory + +```php +use Laravel\Ai\Concerns\RemembersConversations; +use Laravel\Ai\Contracts\Conversational; + +class ChatBot implements Agent, Conversational +{ + use Promptable, RemembersConversations; + // ... +} + +$response = (new ChatBot)->forUser($user)->prompt('Hello!'); +$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...'); +``` + +### Failover + +```php +$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +``` + +## Testing and Faking + +Each capability supports `fake()` with assertions: + +```php +use App\Ai\Agents\SalesCoach; +use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores}; + +// Agents +SalesCoach::fake(['Response 1', 'Response 2']); +SalesCoach::assertPrompted('query'); +SalesCoach::assertNotPrompted('query'); +SalesCoach::assertNeverPrompted(); +SalesCoach::fake()->preventStrayPrompts(); + +// Images +Image::fake(); +Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset')); +Image::assertNothingGenerated(); + +// Audio +Audio::fake(); +Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello')); + +// Transcription +Transcription::fake(['Transcribed text.']); +Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized()); + +// Embeddings +Embeddings::fake(); +Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel')); + +// Reranking +Reranking::fake(); +Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP')); + +// Files +Files::fake(); +Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain'); + +// Stores +Stores::fake(); +Stores::assertCreated('Knowledge Base'); +$store = Stores::get('id'); +$store->assertAdded('file_id'); +``` + +## Key Patterns + +- Namespace: `Laravel\Ai\` +- Package: `composer require laravel/ai` +- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait +- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` +- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Artisan commands: `php artisan make:agent`, `php artisan make:tool` +- Global helper: `agent()` for anonymous agents + +## Common Pitfalls + +### Wrong Namespace + +The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`. + +```php +// Correct +use Laravel\Ai\Image; +use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Promptable; + +// Wrong — these do not exist +use Illuminate\Ai\Image; +use Laravel\AI\Agent; +``` + +### Unsupported Provider Capability + +Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. + +### Never Use Prism Directly + +Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. + +## Provider Support + +| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | +| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | +| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | +| Anthropic | Y | - | - | - | - | - | Y | - | +| Gemini | Y | Y | - | - | Y | - | Y | Y | +| xAI | Y | Y | - | - | - | - | - | - | +| Groq | Y | - | - | - | - | - | - | - | +| OpenRouter | Y | - | - | - | - | - | - | - | +| ElevenLabs | - | - | Y | Y | - | - | - | - | +| Cohere | - | - | - | - | Y | Y | - | - | +| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file diff --git a/_api_app/AGENTS.md b/_api_app/AGENTS.md index 757ae9646..23eb4ca32 100644 --- a/_api_app/AGENTS.md +++ b/_api_app/AGENTS.md @@ -9,7 +9,8 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.2.30 +- php - 8.3.29 +- laravel/ai (AI) - v0 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - laravel/sanctum (SANCTUM) - v4 @@ -25,6 +26,7 @@ This application is a Laravel application and its main Laravel ecosystems packag This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. - `pest-testing` — Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works. +- `ai-sdk-development` — Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). ## Conventions @@ -59,19 +61,23 @@ This project has domain-specific skills available. You MUST activate the relevan - Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. -## Artisan +## Artisan Commands -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`). +- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. ## URLs - Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. -## Tinker / Debugging +## Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. - Use the `database-query` tool when you only need to read from the database. - Use the `database-schema` tool to inspect table structure before writing migrations or models. +- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly. +- To read configuration values, read the config files directly or run `php artisan config:show [key]`. +- To inspect routes, run `php artisan route:list` directly. +- To check environment variables, read the `.env` file directly. ## Reading Browser Logs With the `browser-logs` Tool @@ -141,7 +147,7 @@ protected function isAccessible(User $user, ?string $path = null): bool # Do Things the Laravel Way -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. - If you're creating a generic PHP class, use `php artisan make:class`. - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. @@ -155,7 +161,7 @@ protected function isAccessible(User $user, ?string $path = null): bool ### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. ### APIs & Eloquent Resources @@ -221,8 +227,8 @@ protected function isAccessible(User $user, ?string $path = null): bool # Laravel Pint Code Formatter -- If you have modified any PHP files, you must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. === pest/core rules === @@ -234,4 +240,11 @@ protected function isAccessible(User $user, ?string $path = null): bool - CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples. - IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task. +=== laravel/ai rules === + +## Laravel AI SDK + +- This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality. +- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). + diff --git a/_api_app/CLAUDE.md b/_api_app/CLAUDE.md index 757ae9646..23eb4ca32 100644 --- a/_api_app/CLAUDE.md +++ b/_api_app/CLAUDE.md @@ -9,7 +9,8 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.2.30 +- php - 8.3.29 +- laravel/ai (AI) - v0 - laravel/framework (LARAVEL) - v12 - laravel/prompts (PROMPTS) - v0 - laravel/sanctum (SANCTUM) - v4 @@ -25,6 +26,7 @@ This application is a Laravel application and its main Laravel ecosystems packag This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. - `pest-testing` — Tests applications using the Pest 3 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, architecture testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works. +- `ai-sdk-development` — Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). ## Conventions @@ -59,19 +61,23 @@ This project has domain-specific skills available. You MUST activate the relevan - Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. -## Artisan +## Artisan Commands -- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. +- Run Artisan commands directly via the command line (e.g., `php artisan route:list`, `php artisan tinker --execute "..."`). +- Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters. ## URLs - Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. -## Tinker / Debugging +## Debugging -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. - Use the `database-query` tool when you only need to read from the database. - Use the `database-schema` tool to inspect table structure before writing migrations or models. +- To execute PHP code for debugging, run `php artisan tinker --execute "your code here"` directly. +- To read configuration values, read the config files directly or run `php artisan config:show [key]`. +- To inspect routes, run `php artisan route:list` directly. +- To check environment variables, read the `.env` file directly. ## Reading Browser Logs With the `browser-logs` Tool @@ -141,7 +147,7 @@ protected function isAccessible(User $user, ?string $path = null): bool # Do Things the Laravel Way -- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`. - If you're creating a generic PHP class, use `php artisan make:class`. - Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. @@ -155,7 +161,7 @@ protected function isAccessible(User $user, ?string $path = null): bool ### Model Creation -- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options. ### APIs & Eloquent Resources @@ -221,8 +227,8 @@ protected function isAccessible(User $user, ?string $path = null): bool # Laravel Pint Code Formatter -- If you have modified any PHP files, you must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style. -- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues. +- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. === pest/core rules === @@ -234,4 +240,11 @@ protected function isAccessible(User $user, ?string $path = null): bool - CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples. - IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task. +=== laravel/ai rules === + +## Laravel AI SDK + +- This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality. +- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). + diff --git a/_api_app/app/Http/Controllers/StateController.php b/_api_app/app/Http/Controllers/StateController.php index fb063b167..f3ebf304e 100644 --- a/_api_app/app/Http/Controllers/StateController.php +++ b/_api_app/app/Http/Controllers/StateController.php @@ -14,7 +14,9 @@ use App\Sites\TemplateSettings\SiteTemplateSettingsDataService; use App\Sites\ThemesDataService; use App\User\UserModel; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; class StateController extends Controller { @@ -45,6 +47,10 @@ public function get($site = '') 'entryGallery' => route('entry_gallery'), 'entryGalleryUpload' => route('entry_gallery_upload'), ]; + + if (Route::has('ai_chat')) { + $state['urls']['aiChat'] = route('ai_chat'); + } $state['sites'] = $sitesDataService->getState(); $state['site_settings'] = []; $state['site_sections'] = []; @@ -126,10 +132,8 @@ public function getSentryDSN() /** * Returns translated settings for site localization: templates and settings config - * - * @return json */ - public function getLocaleSettings(Request $request) + public function getLocaleSettings(Request $request): JsonResponse { $lang = $request->query('language'); diff --git a/_api_app/app/Http/Middleware/Authenticate.php b/_api_app/app/Http/Middleware/Authenticate.php index 6cbec35d6..ca43c51f9 100644 --- a/_api_app/app/Http/Middleware/Authenticate.php +++ b/_api_app/app/Http/Middleware/Authenticate.php @@ -5,15 +5,9 @@ use Closure; use Illuminate\Contracts\Auth\Factory as Auth; use Illuminate\Http\Request; -use Symfony\Component\HttpFoundation\Response; class Authenticate { - /** - * The authentication guard factory instance. - * - * @var \Illuminate\Contracts\Auth\Factory - */ protected $auth; /** @@ -26,16 +20,6 @@ public function __construct(Auth $auth) $this->auth = $auth; } - /** - * Handle an incoming request. - * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next - */ - // public function handle(Request $request, Closure $next): Response - // { - // return $next($request); - // } - /** * Handle an incoming request. * diff --git a/_api_app/app/Http/Middleware/SetupMiddleware.php b/_api_app/app/Http/Middleware/SetupMiddleware.php index b6ac6dc89..2f4547dd4 100644 --- a/_api_app/app/Http/Middleware/SetupMiddleware.php +++ b/_api_app/app/Http/Middleware/SetupMiddleware.php @@ -12,7 +12,7 @@ class SetupMiddleware /** * Handle an incoming request. * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + * @param Closure(Request): (Response) $next */ public function handle(Request $request, Closure $next): Response { diff --git a/_api_app/app/User/UserModel.php b/_api_app/app/User/UserModel.php index 6e7205a59..d9797c59c 100644 --- a/_api_app/app/User/UserModel.php +++ b/_api_app/app/User/UserModel.php @@ -93,6 +93,7 @@ private function getFeatures() if (! $this->profile_url || $plan) { $features[] = 'custom_javascript'; + $features[] = 'ai_assistant'; } if ($is_trial || $plan > 1) { diff --git a/_api_app/boost.json b/_api_app/boost.json index 1f17a8721..1a418e377 100644 --- a/_api_app/boost.json +++ b/_api_app/boost.json @@ -7,11 +7,15 @@ "opencode" ], "guidelines": true, - "herd_mcp": false, + "herd_mcp": true, "mcp": true, "nightwatch_mcp": false, + "packages": [ + "laravel/ai" + ], "sail": false, "skills": [ - "pest-testing" + "pest-testing", + "ai-sdk-development" ] } diff --git a/_api_app/bootstrap/providers.php b/_api_app/bootstrap/providers.php index 6c4cd2685..f271445ed 100644 --- a/_api_app/bootstrap/providers.php +++ b/_api_app/bootstrap/providers.php @@ -1,6 +1,9 @@ env('AI_DEFAULT_PROVIDER', 'anthropic'), + 'default_for_images' => 'gemini', + 'default_for_audio' => 'openai', + 'default_for_transcription' => 'openai', + 'default_for_embeddings' => 'openai', + 'default_for_reranking' => 'cohere', + + /* + |-------------------------------------------------------------------------- + | Caching + |-------------------------------------------------------------------------- + | + | Below you may configure caching strategies for AI related operations + | such as embedding generation. You are free to adjust these values + | based on your application's available caching stores and needs. + | + */ + + 'caching' => [ + 'embeddings' => [ + 'cache' => false, + 'store' => env('CACHE_STORE', 'database'), + ], + ], + + /* + |-------------------------------------------------------------------------- + | AI Providers + |-------------------------------------------------------------------------- + | + | Below are each of your AI providers defined for this application. Each + | represents an AI provider and API key combination which can be used + | to perform tasks like text, image, and audio creation via agents. + | + */ + + 'providers' => [ + 'anthropic' => [ + 'driver' => 'anthropic', + 'key' => env('ANTHROPIC_API_KEY'), + ], + + 'azure' => [ + 'driver' => 'azure', + 'key' => env('AZURE_OPENAI_API_KEY'), + 'url' => env('AZURE_OPENAI_URL'), + 'api_version' => env('AZURE_OPENAI_API_VERSION', '2024-10-21'), + 'deployment' => env('AZURE_OPENAI_DEPLOYMENT', 'gpt-4o'), + 'embedding_deployment' => env('AZURE_OPENAI_EMBEDDING_DEPLOYMENT', 'text-embedding-3-small'), + ], + + 'cohere' => [ + 'driver' => 'cohere', + 'key' => env('COHERE_API_KEY'), + ], + + 'deepseek' => [ + 'driver' => 'deepseek', + 'key' => env('DEEPSEEK_API_KEY'), + ], + + 'eleven' => [ + 'driver' => 'eleven', + 'key' => env('ELEVENLABS_API_KEY'), + ], + + 'gemini' => [ + 'driver' => 'gemini', + 'key' => env('GEMINI_API_KEY'), + ], + + 'groq' => [ + 'driver' => 'groq', + 'key' => env('GROQ_API_KEY'), + ], + + 'jina' => [ + 'driver' => 'jina', + 'key' => env('JINA_API_KEY'), + ], + + 'mistral' => [ + 'driver' => 'mistral', + 'key' => env('MISTRAL_API_KEY'), + ], + + 'ollama' => [ + 'driver' => 'ollama', + 'key' => env('OLLAMA_API_KEY', ''), + 'url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'), + ], + + 'openai' => [ + 'driver' => 'openai', + 'key' => env('OPENAI_API_KEY'), + ], + + 'openrouter' => [ + 'driver' => 'openrouter', + 'key' => env('OPENROUTER_API_KEY'), + ], + + 'voyageai' => [ + 'driver' => 'voyageai', + 'key' => env('VOYAGEAI_API_KEY'), + ], + + 'xai' => [ + 'driver' => 'xai', + 'key' => env('XAI_API_KEY'), + ], + ], + +]; diff --git a/_api_app/config/auth.php b/_api_app/config/auth.php index 393071e20..ee9733fea 100644 --- a/_api_app/config/auth.php +++ b/_api_app/config/auth.php @@ -1,5 +1,7 @@ [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', App\Models\User::class), + 'model' => env('AUTH_MODEL', User::class), ], // 'users' => [ diff --git a/_api_app/config/sanctum.php b/_api_app/config/sanctum.php index 764a82fac..b6607039b 100644 --- a/_api_app/config/sanctum.php +++ b/_api_app/config/sanctum.php @@ -1,5 +1,8 @@ [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, ], ]; diff --git a/_api_app/config/twigbridge.php b/_api_app/config/twigbridge.php index 68957f823..eb66cb4b1 100644 --- a/_api_app/config/twigbridge.php +++ b/_api_app/config/twigbridge.php @@ -1,5 +1,7 @@ [ - \Illuminate\Contracts\Support\Htmlable::class => ['html'], + Htmlable::class => ['html'], ], /* diff --git a/_api_app/database/factories/UserFactory.php b/_api_app/database/factories/UserFactory.php index fb800423e..b9ced1082 100644 --- a/_api_app/database/factories/UserFactory.php +++ b/_api_app/database/factories/UserFactory.php @@ -2,13 +2,14 @@ namespace Database\Factories; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; // use Illuminate\Support\Facades\Hash; // use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> + * @extends Factory */ class UserFactory extends Factory { diff --git a/_api_app/opencode.json b/_api_app/opencode.json index 53e16f3d5..c43fb5c84 100644 --- a/_api_app/opencode.json +++ b/_api_app/opencode.json @@ -9,6 +9,17 @@ "artisan", "boost:mcp" ] + }, + "herd": { + "type": "local", + "enabled": true, + "command": [ + "php", + "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" + ], + "environment": { + "SITE_PATH": "/Users/uldis/projects/berta/berta/_api_app" + } } } } \ No newline at end of file diff --git a/_api_app/tests/Feature/AiChatControllerTest.php b/_api_app/tests/Feature/AiChatControllerTest.php new file mode 100644 index 000000000..f8d2cea32 --- /dev/null +++ b/_api_app/tests/Feature/AiChatControllerTest.php @@ -0,0 +1,163 @@ + 'Make the background blue', + 'site' => '', + 'template' => 'default', + ])->assertStatus(401); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('parses structured json response from anthropic', function () { + AssistantAgent::fake([ + '{"reply": "Changed background to blue.", "design_changes": [{"group": "background", "setting": "backgroundColor", "value": "#0000ff"}], "settings_changes": []}', + ]); + + $agent = new AssistantAgent('system prompt'); + $result = $agent->chat([['role' => 'user', 'content' => 'make background blue']]); + + expect($result['reply'])->toBe('Changed background to blue.') + ->and($result['design_changes'])->toHaveCount(1) + ->and($result['design_changes'][0]['group'])->toBe('background') + ->and($result['design_changes'][0]['setting'])->toBe('backgroundColor') + ->and($result['design_changes'][0]['value'])->toBe('#0000ff') + ->and($result['settings_changes'])->toBeEmpty(); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('returns empty changes when ai response has no json', function () { + AssistantAgent::fake([ + 'I cannot help with that.', + ]); + + $agent = new AssistantAgent('system prompt'); + $result = $agent->chat([['role' => 'user', 'content' => 'hello']]); + + expect($result['reply'])->toBe('I cannot help with that.') + ->and($result['design_changes'])->toBeEmpty() + ->and($result['settings_changes'])->toBeEmpty(); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('returns empty changes when json is embedded in prose', function () { + AssistantAgent::fake([ + 'Sure! Here is my response: {"reply": "Done!", "design_changes": [], "settings_changes": []} — let me know if you need more.', + ]); + + $agent = new AssistantAgent('system prompt'); + $result = $agent->chat([['role' => 'user', 'content' => 'reset']]); + + expect($result['reply'])->toBe('Done!') + ->and($result['design_changes'])->toBeEmpty() + ->and($result['settings_changes'])->toBeEmpty(); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('parses site settings changes from ai response', function () { + AssistantAgent::fake([ + '{"reply": "Updated the page title.", "design_changes": [], "settings_changes": [{"group": "texts", "setting": "pageTitle", "value": "My Site"}]}', + ]); + + $agent = new AssistantAgent('system prompt'); + $result = $agent->chat([['role' => 'user', 'content' => 'set the page title to My Site']]); + + expect($result['reply'])->toBe('Updated the page title.') + ->and($result['design_changes'])->toBeEmpty() + ->and($result['settings_changes'])->toHaveCount(1) + ->and($result['settings_changes'][0]['group'])->toBe('texts') + ->and($result['settings_changes'][0]['setting'])->toBe('pageTitle') + ->and($result['settings_changes'][0]['value'])->toBe('My Site'); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('parses is_undo from ai response', function () { + AssistantAgent::fake([ + '{"reply": "Reverted font size.", "is_undo": true, "design_changes": [{"group": "bodyText", "setting": "fontSize", "value": "12px"}], "settings_changes": []}', + ]); + + $agent = new AssistantAgent('system prompt'); + $result = $agent->chat([['role' => 'user', 'content' => 'undo']]); + + expect($result['is_undo'])->toBeTrue() + ->and($result['reply'])->toBe('Reverted font size.') + ->and($result['design_changes'])->toHaveCount(1) + ->and($result['design_changes'][0]['value'])->toBe('12px'); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('enriches changes with previous_value', function () { + $controller = new AiChatController; + $method = new ReflectionMethod($controller, 'enrichChangesWithPreviousValues'); + + $changes = [ + ['group' => 'bodyText', 'setting' => 'fontSize', 'value' => '16px'], + ['group' => 'bodyText', 'setting' => 'fontFamily', 'value' => 'Arial'], + ]; + $currentSettings = [ + 'bodyText' => ['fontSize' => '12px'], + ]; + + $result = $method->invoke($controller, $changes, $currentSettings); + + expect($result[0]['previous_value'])->toBe('12px') + ->and($result[1]['previous_value'])->toBeNull(); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('includes change history in system prompt', function () { + $controller = new AiChatController; + $method = new ReflectionMethod($controller, 'buildChangeHistorySection'); + + $changeHistory = [ + [ + 'user_message' => 'make the font bigger', + 'design_changes' => [ + ['group' => 'bodyText', 'setting' => 'fontSize', 'value' => '16px', 'previous_value' => '12px'], + ], + 'settings_changes' => [], + ], + [ + 'user_message' => 'make background dark', + 'design_changes' => [ + ['group' => 'background', 'setting' => 'backgroundColor', 'value' => '#000000', 'previous_value' => '#ffffff'], + ], + 'settings_changes' => [], + ], + ]; + + $result = $method->invoke($controller, $changeHistory); + + expect($result) + ->toContain('Change History') + ->toContain('make the font bigger') + ->toContain('bodyText > fontSize') + ->toContain('"12px" → "16px"') + ->toContain('make background dark') + ->toContain('background > backgroundColor') + ->toContain('"#ffffff" → "#000000"'); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('omits change history section when history is empty', function () { + $controller = new AiChatController; + $method = new ReflectionMethod($controller, 'buildChangeHistorySection'); + + $result = $method->invoke($controller, []); + + expect($result)->toBe(''); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); + +it('includes help articles in system prompt', function () { + $controller = new AiChatController; + $method = new ReflectionMethod($controller, 'buildHelpArticlesSection'); + + $result = $method->invoke($controller); + + expect($result) + ->toContain('Help Articles') + ->toContain('How to Add a Video') + ->toContain('https://support.berta.me/en/frequently-asked-questions/how-to-add-a-video') + ->toContain('support.berta.me') + ->toContain('Domains') + ->toContain('SSL Certificates and HTTPS'); +})->skip(! $pluginInstalled, 'AiAssistant plugin not installed'); diff --git a/_api_app/tests/Pest.php b/_api_app/tests/Pest.php index b239048cc..fbc6d9aee 100644 --- a/_api_app/tests/Pest.php +++ b/_api_app/tests/Pest.php @@ -1,5 +1,7 @@ extend(Tests\TestCase::class)->in('Feature'); +pest()->extend(TestCase::class)->in('Feature'); /* |-------------------------------------------------------------------------- diff --git a/editor/package-lock.json b/editor/package-lock.json index 133dc84dc..cd44c41d0 100644 --- a/editor/package-lock.json +++ b/editor/package-lock.json @@ -21,6 +21,7 @@ "@ngxs/store": "~20.1.0", "@sentry/angular": "^10.18.0", "lodash": "^4.17.21", + "marked": "^17.0.4", "ng-sortgrid": "^20.0.0", "ngx-color-picker": "^20.1.1", "ngx-image-cropper": "^9.1.5", @@ -7814,6 +7815,18 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/marked": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/editor/package.json b/editor/package.json index 4c1a08571..8f333fccb 100644 --- a/editor/package.json +++ b/editor/package.json @@ -28,6 +28,7 @@ "@ngxs/store": "~20.1.0", "@sentry/angular": "^10.18.0", "lodash": "^4.17.21", + "marked": "^17.0.4", "ng-sortgrid": "^20.0.0", "ngx-color-picker": "^20.1.1", "ngx-image-cropper": "^9.1.5", diff --git a/editor/src/app/ai-assistant/ai-assistant.actions.ts b/editor/src/app/ai-assistant/ai-assistant.actions.ts new file mode 100644 index 000000000..c97a42344 --- /dev/null +++ b/editor/src/app/ai-assistant/ai-assistant.actions.ts @@ -0,0 +1,22 @@ +export class ToggleAiAssistantAction { + static readonly type = 'AI_ASSISTANT:TOGGLE'; +} + +export class SendAiMessageAction { + static readonly type = 'AI_ASSISTANT:SEND_MESSAGE'; + constructor(public message: string) {} +} + +export class AiMessageReceivedAction { + static readonly type = 'AI_ASSISTANT:MESSAGE_RECEIVED'; + constructor( + public reply: string, + public designChanges: { group: string; setting: string; value: string; previous_value?: string | null }[], + public settingsChanges: { group: string; setting: string; value: string; previous_value?: string | null }[], + public isUndo: boolean = false, + ) {} +} + +export class ClearAiChatAction { + static readonly type = 'AI_ASSISTANT:CLEAR'; +} diff --git a/editor/src/app/ai-assistant/ai-assistant.component.ts b/editor/src/app/ai-assistant/ai-assistant.component.ts new file mode 100644 index 000000000..86a1405f8 --- /dev/null +++ b/editor/src/app/ai-assistant/ai-assistant.component.ts @@ -0,0 +1,359 @@ +import { + Component, + ViewChild, + ElementRef, + AfterViewChecked, + OnDestroy, +} from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; +import { Store } from '@ngxs/store'; + +import { AiAssistantState, AiMessage } from './ai-assistant.state'; +import { + SendAiMessageAction, + ClearAiChatAction, + ToggleAiAssistantAction, +} from './ai-assistant.actions'; + +@Component({ + selector: 'berta-ai-assistant', + template: ` + @if (isOpen$ | async) { +
+
+ AI Assistant +
+ Clear + +
+
+
+ @if ((messages$ | async)?.length === 0) { +

+ Ask me to change design or site settings.
+ e.g. "Make the background dark blue" or "Set the page title to My + Site" +

+ } + @for (msg of messages$ | async; track $index) { +
+ @if (msg.role === 'assistant') { + + } @else { + {{ msg.content }} + } +
+ } + @if (isLoading$ | async) { +
+ +
+ } +
+
+ + +
+
+ } + `, + styles: [ + ` + .ai-panel { + position: fixed; + top: 4.63em; + right: 0; + width: 320px; + bottom: 8.5em; + background: #fff; + border-left: 1px solid #ddd; + display: flex; + flex-direction: column; + z-index: 2; + box-shadow: -2px 2px 8px rgba(0, 0, 0, 0.1); + font-family: inherit; + font-size: 13px; + } + + .ai-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75em 1em; + border-bottom: 1px solid #ddd; + font-weight: bold; + flex-shrink: 0; + } + + .ai-panel-actions { + display: flex; + align-items: center; + gap: 0.75em; + } + + .ai-panel-actions a { + font-size: 12px; + color: #777; + text-decoration: none; + } + + .ai-panel-actions a:hover { + color: #333; + } + + .close-btn { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + line-height: 1; + color: #777; + padding: 0; + } + + .close-btn:hover { + color: #333; + } + + .ai-messages { + flex-grow: 1; + overflow-y: auto; + padding: 1em; + display: flex; + flex-direction: column; + gap: 0.5em; + } + + .ai-empty { + color: #aaa; + font-size: 12px; + text-align: center; + margin: auto; + line-height: 1.6; + } + + .ai-message { + max-width: 85%; + padding: 0.5em 0.75em; + border-radius: 8px; + line-height: 1.5; + word-break: break-word; + } + + .ai-message--user { + align-self: flex-end; + background: #333; + color: #fff; + } + + .ai-message--assistant { + align-self: flex-start; + background: #f0f0f0; + color: #333; + } + + .ai-message--loading { + display: flex; + gap: 4px; + align-items: center; + padding: 0.6em 0.75em; + } + + .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #999; + animation: bounce 1.2s infinite ease-in-out; + } + + .dot:nth-child(2) { + animation-delay: 0.2s; + } + + .dot:nth-child(3) { + animation-delay: 0.4s; + } + + @keyframes bounce { + 0%, + 60%, + 100% { + transform: translateY(0); + } + 30% { + transform: translateY(-4px); + } + } + + .ai-input-area { + padding: 0.75em; + border-top: 1px solid #ddd; + display: flex; + flex-direction: column; + gap: 0.5em; + flex-shrink: 0; + } + + .ai-input-area textarea { + flex-grow: 1; + resize: none; + border: 1px solid #ddd; + border-radius: 4px; + padding: 0.5em; + font-family: inherit; + font-size: 12px; + line-height: 1.4; + } + + .ai-input-area textarea:focus { + outline: none; + border-color: #999; + } + + .ai-input-area button { + align-self: flex-start; + padding: 0.4em 0.8em; + background: #333; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + white-space: nowrap; + } + + .ai-input-area button:disabled { + opacity: 0.4; + cursor: default; + } + + .ai-input-area button:hover:not(:disabled) { + background: #555; + } + + .ai-message--assistant p { + margin: 0 0 0.4em; + } + + .ai-message--assistant p:last-child { + margin-bottom: 0; + } + + .ai-message--assistant ul, + .ai-message--assistant ol { + margin: 0.3em 0; + padding-left: 1.4em; + } + + .ai-message--assistant code { + background: #e8e8e8; + border-radius: 3px; + padding: 0.1em 0.3em; + font-size: 11px; + } + + .ai-message--assistant strong { + font-weight: 600; + } + `, + ], + standalone: false, +}) +export class AiAssistantComponent implements AfterViewChecked, OnDestroy { + isOpen$: Observable; + messages$: Observable; + isLoading$: Observable; + inputText = ''; + + @ViewChild('messagesContainer') private messagesContainer: ElementRef; + @ViewChild('inputEl') private inputEl: ElementRef; + + private shouldFocus = false; + private shouldScroll = false; + private messageCount = 0; + private subs: Subscription[] = []; + + constructor(private store: Store) { + this.isOpen$ = this.store.select(AiAssistantState.isOpen); + this.messages$ = this.store.select(AiAssistantState.messages); + this.isLoading$ = this.store.select(AiAssistantState.isLoading); + this.subs.push( + this.isOpen$.subscribe((open) => { + if (open) this.shouldFocus = true; + }), + this.messages$.subscribe((msgs) => { + if (msgs.length > this.messageCount) this.shouldScroll = true; + this.messageCount = msgs.length; + }), + this.isLoading$.subscribe((loading) => { + if (loading) this.shouldScroll = true; + }), + ); + } + + ngAfterViewChecked() { + if (this.shouldScroll) { + this.scrollToBottom(); + this.shouldScroll = false; + } + if (this.shouldFocus && this.inputEl) { + this.inputEl.nativeElement.focus(); + this.shouldFocus = false; + } + } + + ngOnDestroy() { + this.subs.forEach((s) => s.unsubscribe()); + } + + send() { + const text = this.inputText.trim(); + if (!text) { + return; + } + this.inputText = ''; + this.store.dispatch(new SendAiMessageAction(text)); + } + + onEnter(event: KeyboardEvent) { + if (!event.shiftKey) { + event.preventDefault(); + this.send(); + } + } + + clearChat(event: Event) { + event.preventDefault(); + this.store.dispatch(new ClearAiChatAction()); + } + + close() { + this.store.dispatch(new ToggleAiAssistantAction()); + } + + private scrollToBottom() { + if (this.messagesContainer) { + const el = this.messagesContainer.nativeElement; + el.scrollTop = el.scrollHeight; + } + } +} diff --git a/editor/src/app/ai-assistant/ai-assistant.module.ts b/editor/src/app/ai-assistant/ai-assistant.module.ts new file mode 100644 index 000000000..9ee99f944 --- /dev/null +++ b/editor/src/app/ai-assistant/ai-assistant.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { PipesModule } from '../pipes/pipes.module'; +import { AiAssistantComponent } from './ai-assistant.component'; + +@NgModule({ + imports: [CommonModule, FormsModule, PipesModule], + declarations: [AiAssistantComponent], + exports: [AiAssistantComponent], +}) +export class AiAssistantModule {} diff --git a/editor/src/app/ai-assistant/ai-assistant.service.ts b/editor/src/app/ai-assistant/ai-assistant.service.ts new file mode 100644 index 000000000..06a586c86 --- /dev/null +++ b/editor/src/app/ai-assistant/ai-assistant.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { AppStateService } from '../app-state/app-state.service'; + +export interface AiChangeItem { + group: string; + setting: string; + value: string; + previous_value?: string | null; +} + +export interface AiChatResponse { + reply: string; + is_undo: boolean; + design_changes: AiChangeItem[]; + settings_changes: AiChangeItem[]; +} + +@Injectable({ + providedIn: 'root', +}) +export class AiAssistantService { + constructor(private appStateService: AppStateService) {} + + chat( + message: string, + history: { role: string; content: string }[], + site: string, + template: string, + changeHistory: { user_message: string; design_changes: AiChangeItem[]; settings_changes: AiChangeItem[] }[] = [], + ): Observable { + return this.appStateService + .sync('aiChat', { message, history, site, template, change_history: changeHistory }, 'POST') + .pipe(map((response: any) => response.data as AiChatResponse)); + } +} diff --git a/editor/src/app/ai-assistant/ai-assistant.state.ts b/editor/src/app/ai-assistant/ai-assistant.state.ts new file mode 100644 index 000000000..824bbd96d --- /dev/null +++ b/editor/src/app/ai-assistant/ai-assistant.state.ts @@ -0,0 +1,196 @@ +import { Injectable } from '@angular/core'; +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { tap, catchError } from 'rxjs/operators'; +import { EMPTY } from 'rxjs'; + +import { Store } from '@ngxs/store'; +import { AppState } from '../app-state/app.state'; +import { SiteSettingsState } from '../sites/settings/site-settings.state'; +import { UpdateSiteTemplateSettingsAction } from '../sites/template-settings/site-template-settings.actions'; +import { UpdateSiteSettingsAction } from '../sites/settings/site-settings.actions'; +import { AiAssistantService } from './ai-assistant.service'; +import { + ToggleAiAssistantAction, + SendAiMessageAction, + AiMessageReceivedAction, + ClearAiChatAction, +} from './ai-assistant.actions'; + +export interface AiMessage { + role: 'user' | 'assistant'; + content: string; +} + +export interface AiChangeEntry { + group: string; + setting: string; + value: string; + previousValue: string | null; +} + +export interface AiChangeHistoryEntry { + userMessage: string; + designChanges: AiChangeEntry[]; + settingsChanges: AiChangeEntry[]; +} + +export interface AiAssistantStateModel { + isOpen: boolean; + messages: AiMessage[]; + isLoading: boolean; + changeHistory: AiChangeHistoryEntry[]; +} + +const defaults: AiAssistantStateModel = { + isOpen: false, + messages: [], + isLoading: false, + changeHistory: [], +}; + +@State({ + name: 'aiAssistant', + defaults, +}) +@Injectable() +export class AiAssistantState { + @Selector() + static isOpen(state: AiAssistantStateModel) { + return state.isOpen; + } + + @Selector() + static messages(state: AiAssistantStateModel) { + return state.messages; + } + + @Selector() + static isLoading(state: AiAssistantStateModel) { + return state.isLoading; + } + + constructor( + private store: Store, + private aiAssistantService: AiAssistantService, + ) {} + + @Action(ToggleAiAssistantAction) + toggle({ patchState, getState }: StateContext) { + patchState({ isOpen: !getState().isOpen }); + } + + @Action(SendAiMessageAction) + sendMessage( + { patchState, getState, dispatch }: StateContext, + action: SendAiMessageAction, + ) { + const state = getState(); + const userMessage: AiMessage = { role: 'user', content: action.message }; + patchState({ + messages: [...state.messages, userMessage], + isLoading: true, + }); + + const site = this.store.selectSnapshot(AppState.getSite) || ''; + const template = + this.store.selectSnapshot(SiteSettingsState.getCurrentSiteTemplate) || + ''; + const history = state.messages.map((m) => ({ + role: m.role, + content: m.content, + })); + + const changeHistoryPayload = state.changeHistory.map((entry) => ({ + user_message: entry.userMessage, + design_changes: entry.designChanges.map((c) => ({ + group: c.group, + setting: c.setting, + value: c.value, + previous_value: c.previousValue, + })), + settings_changes: entry.settingsChanges.map((c) => ({ + group: c.group, + setting: c.setting, + value: c.value, + previous_value: c.previousValue, + })), + })); + + return this.aiAssistantService + .chat(action.message, history, site, template, changeHistoryPayload) + .pipe( + tap((response) => { + dispatch( + new AiMessageReceivedAction(response.reply, response.design_changes, response.settings_changes, response.is_undo), + ); + }), + catchError((error) => { + console.error('AI assistant error:', error); + patchState({ isLoading: false }); + return EMPTY; + }), + ); + } + + @Action(AiMessageReceivedAction) + messageReceived( + { patchState, getState, dispatch }: StateContext, + action: AiMessageReceivedAction, + ) { + const state = getState(); + const assistantMessage: AiMessage = { + role: 'assistant', + content: action.reply, + }; + + let changeHistory: AiChangeHistoryEntry[]; + if (action.isUndo) { + changeHistory = state.changeHistory.slice(0, -1); + } else { + const lastUserMessage = [...state.messages].reverse().find((m) => m.role === 'user'); + const newEntry: AiChangeHistoryEntry = { + userMessage: lastUserMessage?.content ?? '', + designChanges: action.designChanges.map((c) => ({ + group: c.group, + setting: c.setting, + value: c.value, + previousValue: c.previous_value ?? null, + })), + settingsChanges: action.settingsChanges.map((c) => ({ + group: c.group, + setting: c.setting, + value: c.value, + previousValue: c.previous_value ?? null, + })), + }; + changeHistory = [...state.changeHistory, newEntry]; + } + + patchState({ + messages: [...state.messages, assistantMessage], + isLoading: false, + changeHistory, + }); + + for (const change of action.designChanges) { + dispatch( + new UpdateSiteTemplateSettingsAction(change.group, { + [change.setting]: change.value, + }), + ); + } + + for (const change of action.settingsChanges) { + dispatch( + new UpdateSiteSettingsAction(change.group, { + [change.setting]: change.value, + }), + ); + } + } + + @Action(ClearAiChatAction) + clearChat({ setState }: StateContext) { + setState(defaults); + } +} diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts index 0ea2dc799..20a098fc5 100644 --- a/editor/src/app/app.component.ts +++ b/editor/src/app/app.component.ts @@ -53,6 +53,7 @@ import { AppStateService } from './app-state/app-state.service'; (click)="hideOverlay()" > + `, styles: [ ` diff --git a/editor/src/app/app.module.ts b/editor/src/app/app.module.ts index 2617d5e1a..e8dbd31de 100644 --- a/editor/src/app/app.module.ts +++ b/editor/src/app/app.module.ts @@ -36,6 +36,8 @@ import { MessyTemplateStyleService } from './preview/messy-template-style.servic import { SiteSectionsModule } from './sites/sections/site-sections.module'; import { ShopSettingsState } from './shop/settings/shop-settings.state'; import { ShopRegionalCostsState } from './shop/regional-costs/shop-regional-costs.state'; +import { AiAssistantModule } from './ai-assistant/ai-assistant.module'; +import { AiAssistantState } from './ai-assistant/ai-assistant.state'; import { SiteMediaModule } from './sites/media/site-media.module'; import { SentryConfigService } from './sentry/sentry-config.service'; import * as Sentry from '@sentry/angular'; @@ -67,6 +69,7 @@ import { sentryInitFactory } from './sentry/sentry-init.factory'; ErrorState, ShopSettingsState, ShopRegionalCostsState, + AiAssistantState, ], { developmentMode: !environment.production, @@ -78,6 +81,7 @@ import { sentryInitFactory } from './sentry/sentry-init.factory'; SitesSharedModule, SiteSectionsModule, SiteMediaModule, + AiAssistantModule, ], providers: [ SentryConfigService, diff --git a/editor/src/app/header/header.component.ts b/editor/src/app/header/header.component.ts index 570387ee6..f391c9597 100644 --- a/editor/src/app/header/header.component.ts +++ b/editor/src/app/header/header.component.ts @@ -4,6 +4,8 @@ import { Observable } from 'rxjs'; import { AppState } from '../app-state/app.state'; import { UserState } from '../user/user.state'; import { UserStateModel } from '../user/user.state.model'; +import { AiAssistantState } from '../ai-assistant/ai-assistant.state'; +import { ToggleAiAssistantAction } from '../ai-assistant/ai-assistant.actions'; @Component({ selector: 'berta-header', @@ -60,6 +62,14 @@ import { UserStateModel } from '../user/user.state.model'; Knowledge base + @if ((user$ | async).features.includes('ai_assistant')) { + AI + } } @@ -123,11 +133,18 @@ export class HeaderComponent { isLoggedIn$: Observable; isLoading$: Observable; isSetup$: Observable; + isAiOpen$: Observable; constructor(private store: Store) { this.user$ = this.store.select((state) => state.user); this.isLoggedIn$ = this.store.select(UserState.isLoggedIn); this.isLoading$ = this.store.select(AppState.getShowLoading); this.isSetup$ = this.store.select(AppState.isSetup); + this.isAiOpen$ = this.store.select(AiAssistantState.isOpen); + } + + toggleAiAssistant(event: Event) { + event.preventDefault(); + this.store.dispatch(new ToggleAiAssistantAction()); } } diff --git a/editor/src/app/pipes/markdown.pipe.ts b/editor/src/app/pipes/markdown.pipe.ts new file mode 100644 index 000000000..a03008d7b --- /dev/null +++ b/editor/src/app/pipes/markdown.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { marked, Renderer } from 'marked'; + +@Pipe({ name: 'markdown', standalone: false }) +export class MarkdownPipe implements PipeTransform { + constructor(private sanitizer: DomSanitizer) {} + + transform(value: string): SafeHtml { + const renderer = new Renderer(); + renderer.link = ({ href, title, text }: { href: string; title?: string | null; text: string }) => { + const titleAttr = title ? ` title="${title}"` : ''; + return `${text}`; + }; + const html = marked.parse(value, { renderer }) as string; + return this.sanitizer.bypassSecurityTrustHtml(html); + } +} diff --git a/editor/src/app/pipes/pipes.module.ts b/editor/src/app/pipes/pipes.module.ts new file mode 100644 index 000000000..2292ca80b --- /dev/null +++ b/editor/src/app/pipes/pipes.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { SafePipe } from './safe.pipe'; +import { MarkdownPipe } from './markdown.pipe'; + +@NgModule({ + declarations: [SafePipe, MarkdownPipe], + exports: [SafePipe, MarkdownPipe], +}) +export class PipesModule {} diff --git a/editor/src/app/pipes/pipe.ts b/editor/src/app/pipes/safe.pipe.ts similarity index 100% rename from editor/src/app/pipes/pipe.ts rename to editor/src/app/pipes/safe.pipe.ts diff --git a/editor/src/app/rerender/default-template-rerender.service.ts b/editor/src/app/rerender/default-template-rerender.service.ts index dfc53beee..89bf2f0b6 100644 --- a/editor/src/app/rerender/default-template-rerender.service.ts +++ b/editor/src/app/rerender/default-template-rerender.service.ts @@ -27,6 +27,7 @@ export class DefaultTemplateRerenderService extends TemplateRerenderService { banners: { id: 'siteBanners', dataKey: 'siteBanners' }, settings: { id: 'sectionFooter', dataKey: 'sectionFooter' }, entryLayout: { id: 'pageEntries', dataKey: 'entries' }, + siteTexts: { id: 'siteHeader', dataKey: 'siteHeader' }, }; constructor( diff --git a/editor/src/app/rerender/mashup/mashup-template-rerender.service.ts b/editor/src/app/rerender/mashup/mashup-template-rerender.service.ts index a3ab38fc6..24e73be7a 100644 --- a/editor/src/app/rerender/mashup/mashup-template-rerender.service.ts +++ b/editor/src/app/rerender/mashup/mashup-template-rerender.service.ts @@ -24,6 +24,7 @@ export class MashupTemplateRerenderService extends TemplateRerenderService { banners: { id: 'siteBanners', dataKey: 'siteBanners' }, settings: { id: 'sectionFooter', dataKey: 'sectionFooter' }, entryLayout: { id: 'pageEntries', dataKey: 'entries' }, + siteTexts: { id: 'siteHeader', dataKey: 'siteHeader' }, }; constructor( diff --git a/editor/src/app/rerender/messy/messy-template-rerender.service.ts b/editor/src/app/rerender/messy/messy-template-rerender.service.ts index c91a6478a..0a59683dd 100644 --- a/editor/src/app/rerender/messy/messy-template-rerender.service.ts +++ b/editor/src/app/rerender/messy/messy-template-rerender.service.ts @@ -25,6 +25,7 @@ export class MessyTemplateRerenderService extends TemplateRerenderService { banners: { id: 'siteBanners', dataKey: 'siteBanners' }, settings: { id: 'sectionFooter', dataKey: 'sectionFooter' }, entryLayout: { id: 'pageEntries', dataKey: 'entries' }, + siteTexts: { id: 'siteHeader', dataKey: 'siteHeader' }, }; constructor( diff --git a/editor/src/app/rerender/template-rerender.service.ts b/editor/src/app/rerender/template-rerender.service.ts index 2464c74ff..0dccb7e8b 100644 --- a/editor/src/app/rerender/template-rerender.service.ts +++ b/editor/src/app/rerender/template-rerender.service.ts @@ -59,12 +59,14 @@ export class TemplateRerenderService { private static readonly BANNERS_SETTINGS = 'banners'; private static readonly SETTINGS = 'settings'; private static readonly ENTRY_LAYOUT = 'entryLayout'; + private static readonly SITE_TEXTS = 'siteTexts'; protected static readonly COMMON_SETTING_GROUPS = [ TemplateRerenderService.SOCIAL_MEDIA_LINKS, TemplateRerenderService.SOCIAL_MEDIA_BTNS, TemplateRerenderService.BANNERS_SETTINGS, TemplateRerenderService.SETTINGS, TemplateRerenderService.ENTRY_LAYOUT, + TemplateRerenderService.SITE_TEXTS, ]; constructor( @@ -91,6 +93,10 @@ export class TemplateRerenderService { case TemplateRerenderService.SOCIAL_MEDIA_LINKS: case TemplateRerenderService.SOCIAL_MEDIA_BTNS: compList = info.socialMediaComp; + break; + case TemplateRerenderService.SITE_TEXTS: + if (info.siteTexts) { compList.push(info.siteTexts); } + break; } return compList; diff --git a/editor/src/app/rerender/types/components.ts b/editor/src/app/rerender/types/components.ts index 8018849de..f6f64027f 100644 --- a/editor/src/app/rerender/types/components.ts +++ b/editor/src/app/rerender/types/components.ts @@ -3,6 +3,7 @@ export interface SiteSettingChildrenHandler { banners: Component; settings: Component; entryLayout: Component; + siteTexts: Component; } export interface Component { diff --git a/editor/src/app/rerender/white/white-template-rerender.service.ts b/editor/src/app/rerender/white/white-template-rerender.service.ts index 7aa271ae1..6efcb05bb 100644 --- a/editor/src/app/rerender/white/white-template-rerender.service.ts +++ b/editor/src/app/rerender/white/white-template-rerender.service.ts @@ -24,6 +24,7 @@ export class WhiteTemplateRerenderService extends TemplateRerenderService { banners: { id: 'siteBanners', dataKey: 'siteBanners' }, settings: { id: 'sectionFooter', dataKey: 'sectionFooter' }, entryLayout: { id: 'pageEntries', dataKey: 'entries' }, + siteTexts: { id: 'siteHeader', dataKey: 'siteHeader' }, }; constructor( diff --git a/editor/src/app/sites/sections/site-sections.module.ts b/editor/src/app/sites/sections/site-sections.module.ts index 1f416eebd..7c4d84eff 100644 --- a/editor/src/app/sites/sections/site-sections.module.ts +++ b/editor/src/app/sites/sections/site-sections.module.ts @@ -4,7 +4,7 @@ import { RouterModule } from '@angular/router'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { NgxsModule } from '@ngxs/store'; import { NgsgModule } from 'ng-sortgrid'; -import { SafePipe } from '../../pipes/pipe'; +import { PipesModule } from '../../pipes/pipes.module'; import { SiteSectionsState } from './sections-state/site-sections.state'; import { SectionTagsState } from './tags/section-tags.state'; import { SitesSharedModule } from '../shared/sites-shared.module'; @@ -22,13 +22,13 @@ import { BackgroundGalleryEditorComponent } from './background-gallery-editor.co NgxsModule.forFeature([SiteSectionsState, SectionTagsState]), SectionEntriesModule, SitesSharedModule, + PipesModule, ], - exports: [SafePipe], + exports: [PipesModule], declarations: [ SiteSectionsComponent, SectionComponent, BackgroundGalleryEditorComponent, - SafePipe, ], }) export class SiteSectionsModule {} diff --git a/editor/src/app/sites/settings/site-settings.state.ts b/editor/src/app/sites/settings/site-settings.state.ts index d25271b03..27df297cf 100644 --- a/editor/src/app/sites/settings/site-settings.state.ts +++ b/editor/src/app/sites/settings/site-settings.state.ts @@ -167,6 +167,7 @@ export class SiteSettingsState implements NgxsOnInit { case 'navigation': dispatch(new UpdateNavigationSiteSettingsAction(action.payload)); break; + case 'siteTexts': case 'socialMediaLinks': case 'socialMediaButtons': case 'media': diff --git a/editor/src/app/user/user.state.ts b/editor/src/app/user/user.state.ts index 51628e943..9131eff5b 100644 --- a/editor/src/app/user/user.state.ts +++ b/editor/src/app/user/user.state.ts @@ -29,6 +29,7 @@ import { ResetSiteSettingsAction } from '../sites/settings/site-settings.actions import { ResetSiteSettingsConfigAction } from '../sites/settings/site-settings-config.actions'; import { ResetSiteTemplateSettingsAction } from '../sites/template-settings/site-template-settings.actions'; import { ResetSiteTemplatesAction } from '../sites/template-settings/site-templates.actions'; +import { ClearAiChatAction } from '../ai-assistant/ai-assistant.actions'; import { Injectable } from '@angular/core'; import { of } from 'rxjs'; @@ -151,6 +152,7 @@ export class UserState implements NgxsOnInit { new ResetSiteSettingsConfigAction(), new ResetSiteTemplateSettingsAction(), new ResetSiteTemplatesAction(), + new ClearAiChatAction(), ]); if (action.saveNextUrl) { From a68ff4e1d50873fb08c53e8541005d5fe473c92e Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Wed, 18 Mar 2026 13:37:38 +0200 Subject: [PATCH 4/5] PHP 8.3 for github workflow --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60240d7fb..ecd859362 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: Tests -on: ['push', 'pull_request'] +on: ["push", "pull_request"] jobs: laravel-tests: @@ -14,7 +14,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 + php-version: 8.3 tools: composer:v2 coverage: xdebug @@ -35,8 +35,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" cache-dependency-path: editor/package-lock.json - name: Install Dependencies From 6a7ecf2822bae595b33d43907bda35a68a699ac0 Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Wed, 18 Mar 2026 14:38:33 +0200 Subject: [PATCH 5/5] Gitignore local AI config files, add example files with placeholder paths Four AI tool config files contained an absolute machine-specific path. They are now gitignored and replaced with .example counterparts using /path/to/berta/_api_app. Co-Authored-By: Claude Sonnet 4.6 --- _api_app/.codex/{config.toml => config.toml.example} | 6 +++--- _api_app/.cursor/{mcp.json => mcp.json.example} | 4 ++-- _api_app/.gitignore | 4 ++++ _api_app/{.mcp.json => .mcp.json.example} | 4 ++-- _api_app/{opencode.json => opencode.json.example} | 4 ++-- 5 files changed, 13 insertions(+), 9 deletions(-) rename _api_app/.codex/{config.toml => config.toml.example} (56%) rename _api_app/.cursor/{mcp.json => mcp.json.example} (83%) rename _api_app/{.mcp.json => .mcp.json.example} (83%) rename _api_app/{opencode.json => opencode.json.example} (88%) diff --git a/_api_app/.codex/config.toml b/_api_app/.codex/config.toml.example similarity index 56% rename from _api_app/.codex/config.toml rename to _api_app/.codex/config.toml.example index 9f0141cdc..ac5c8d0de 100644 --- a/_api_app/.codex/config.toml +++ b/_api_app/.codex/config.toml.example @@ -1,12 +1,12 @@ [mcp_servers.laravel-boost] command = "php" args = ["artisan", "boost:mcp"] -cwd = "/Users/uldis/projects/berta/berta/_api_app" +cwd = "/path/to/berta/_api_app" [mcp_servers.herd] command = "php" args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"] -cwd = "/Users/uldis/projects/berta/berta/_api_app" +cwd = "/path/to/berta/_api_app" [mcp_servers.herd.env] -SITE_PATH = "/Users/uldis/projects/berta/berta/_api_app" +SITE_PATH = "/path/to/berta/_api_app" diff --git a/_api_app/.cursor/mcp.json b/_api_app/.cursor/mcp.json.example similarity index 83% rename from _api_app/.cursor/mcp.json rename to _api_app/.cursor/mcp.json.example index a9816a027..aac22b26a 100644 --- a/_api_app/.cursor/mcp.json +++ b/_api_app/.cursor/mcp.json.example @@ -13,8 +13,8 @@ "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" ], "env": { - "SITE_PATH": "/Users/uldis/projects/berta/berta/_api_app" + "SITE_PATH": "/path/to/berta/_api_app" } } } -} \ No newline at end of file +} diff --git a/_api_app/.gitignore b/_api_app/.gitignore index 46340a60a..fbb710106 100644 --- a/_api_app/.gitignore +++ b/_api_app/.gitignore @@ -18,3 +18,7 @@ yarn-error.log /.fleet /.idea /.vscode +.mcp.json +.cursor/mcp.json +.codex/config.toml +opencode.json diff --git a/_api_app/.mcp.json b/_api_app/.mcp.json.example similarity index 83% rename from _api_app/.mcp.json rename to _api_app/.mcp.json.example index a9816a027..aac22b26a 100644 --- a/_api_app/.mcp.json +++ b/_api_app/.mcp.json.example @@ -13,8 +13,8 @@ "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" ], "env": { - "SITE_PATH": "/Users/uldis/projects/berta/berta/_api_app" + "SITE_PATH": "/path/to/berta/_api_app" } } } -} \ No newline at end of file +} diff --git a/_api_app/opencode.json b/_api_app/opencode.json.example similarity index 88% rename from _api_app/opencode.json rename to _api_app/opencode.json.example index c43fb5c84..dd9e8a0b2 100644 --- a/_api_app/opencode.json +++ b/_api_app/opencode.json.example @@ -18,8 +18,8 @@ "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" ], "environment": { - "SITE_PATH": "/Users/uldis/projects/berta/berta/_api_app" + "SITE_PATH": "/path/to/berta/_api_app" } } } -} \ No newline at end of file +}