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 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/`. 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/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/.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/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/.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.example b/_api_app/.codex/config.toml.example new file mode 100644 index 000000000..ac5c8d0de --- /dev/null +++ b/_api_app/.codex/config.toml.example @@ -0,0 +1,12 @@ +[mcp_servers.laravel-boost] +command = "php" +args = ["artisan", "boost:mcp"] +cwd = "/path/to/berta/_api_app" + +[mcp_servers.herd] +command = "php" +args = ["/Applications/Herd.app/Contents/Resources/herd-mcp.phar"] +cwd = "/path/to/berta/_api_app" + +[mcp_servers.herd.env] +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/.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/.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/.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/.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/.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/AGENTS.md b/_api_app/AGENTS.md index c3ec68551..23eb4ca32 100644 --- a/_api_app/AGENTS.md +++ b/_api_app/AGENTS.md @@ -3,247 +3,248 @@ # 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.3.29 +- laravel/ai (AI) - v0 - 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. +- `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 -- 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. +## Artisan Commands + +- 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 -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- 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. + +## Debugging + - 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 + - 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 if it has one line. +- Always use curly braces for control structures, even for single-line bodies. + +## 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`. +- 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. -### 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`. + +- 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 + - 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 - -- 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. +# Laravel Pint Code Formatter +- 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 === ## 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. +=== laravel/ai rules === -=== tests rules === +## Laravel AI SDK -## Test Enforcement +- 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). -- 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..23eb4ca32 100644 --- a/_api_app/CLAUDE.md +++ b/_api_app/CLAUDE.md @@ -3,247 +3,248 @@ # 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.3.29 +- laravel/ai (AI) - v0 - 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. +- `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 -- 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. +## Artisan Commands + +- 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 -- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- 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. + +## Debugging + - 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 + - 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 if it has one line. +- Always use curly braces for control structures, even for single-line bodies. + +## 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`. +- 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. -### 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`. + +- 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 + - 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 - -- 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. +# Laravel Pint Code Formatter +- 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 === ## 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. +=== laravel/ai rules === -=== tests rules === +## Laravel AI SDK -## Test Enforcement +- 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). -- 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/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 new file mode 100644 index 000000000..1a418e377 --- /dev/null +++ b/_api_app/boost.json @@ -0,0 +1,21 @@ +{ + "agents": [ + "cursor", + "claude_code", + "codex", + "copilot", + "opencode" + ], + "guidelines": true, + "herd_mcp": true, + "mcp": true, + "nightwatch_mcp": false, + "packages": [ + "laravel/ai" + ], + "sail": false, + "skills": [ + "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 @@ =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 +1663,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 +1733,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 +1790,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 +1860,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 +1894,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 +1906,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.8-dev" + "dev-main": "2.9-dev" } }, "autoload": { @@ -1894,7 +1963,7 @@ "type": "tidelift" } ], - "time": "2025-07-20T12:47:49+00:00" + "time": "2026-03-05T21:37:03+00:00" }, { "name": "league/config", @@ -1980,16 +2049,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 +2126,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 +2175,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 +2237,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 +2296,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2234,9 +2309,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2246,7 +2323,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 +2331,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 +2357,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 +2382,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 +2407,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 +2415,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "mobiledetect/mobiledetectlib", @@ -2407,16 +2484,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 +2511,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 +2571,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 +2583,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 +2604,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 +2620,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 +2663,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 +2688,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 +2722,9 @@ } }, "autoload": { + "psr-4": { + "Nette\\": "src" + }, "classmap": [ "src/" ] @@ -2671,26 +2753,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 +2780,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 +2797,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -2760,9 +2844,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 +2908,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 +2964,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 +2975,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 +2991,7 @@ "type": "github" } ], - "time": "2025-05-08T08:14:37+00:00" + "time": "2026-02-16T23:10:27+00:00" }, { "name": "nyholm/psr7", @@ -3037,16 +3121,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 +3180,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 +3192,86 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "prism-php/prism", + "version": "v0.99.21", + "source": { + "type": "git", + "url": "https://github.com/prism-php/prism.git", + "reference": "95272567629a62831294f63b1b927b1e2e608daf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/prism-php/prism/zipball/95272567629a62831294f63b1b927b1e2e608daf", + "reference": "95272567629a62831294f63b1b927b1e2e608daf", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "laravel/framework": "^11.0|^12.0|^13.0", + "php": "^8.2" + }, + "require-dev": { + "brianium/paratest": "^7.8.4", + "laravel/mcp": "^0.6.0", + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^10", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-arch": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpdoc-parser": "^2.0", + "phpstan/phpstan": "2.1.34", + "phpstan/phpstan-deprecation-rules": "^2.0", + "projektgopher/whisky": "^0.7.0", + "rector/rector": "2.3.3", + "spatie/laravel-ray": "^1.39", + "symplify/rule-doc-generator-contracts": "^11.2" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PrismServer": "Prism\\Prism\\Facades\\PrismServer" + }, + "providers": [ + "Prism\\Prism\\PrismServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Prism\\Prism\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "TJ Miller", + "email": "hello@echolabs.dev" + } + ], + "description": "A powerful Laravel package for integrating Large Language Models (LLMs) into your applications.", + "support": { + "issues": "https://github.com/prism-php/prism/issues", + "source": "https://github.com/prism-php/prism/tree/v0.99.21" + }, + "funding": [ + { + "url": "https://github.com/sixlive", + "type": "github" + } + ], + "time": "2026-03-01T21:12:44+00:00" }, { "name": "psr/cache", @@ -3771,20 +3934,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 +4006,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 +4357,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 +4411,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 +4422,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 +4452,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 +4466,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 +4509,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 +4529,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 +4578,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 +4589,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 +4669,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 +4727,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 +4747,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 +4777,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 +4812,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 +4832,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 +4912,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 +4956,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 +4976,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 +5004,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 +5038,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 +5058,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 +5090,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 +5108,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 +5157,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 +5177,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 +5198,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 +5210,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 +5241,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 +5261,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 +5330,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 +5350,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 +6254,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 +6295,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 +6315,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 +6402,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 +6425,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 +6463,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 +6483,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 +6550,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 +6561,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 +6598,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 +6641,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 +6661,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 +6700,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 +6741,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 +6761,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 +6823,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 +6834,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 +6864,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 +6901,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 +6912,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 +6946,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 +6988,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 +7008,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 +7061,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 +7146,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 +7214,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 +7226,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 +7301,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 +7399,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 +7441,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 +7693,34 @@ }, { "name": "laravel/boost", - "version": "v1.3.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "ef8800843efc581965c38393adb63ba336dc3979" + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/ef8800843efc581965c38393adb63ba336dc3979", - "reference": "ef8800843efc581965c38393adb63ba336dc3979", + "url": "https://api.github.com/repos/laravel/boost/zipball/ba0a9e6497398b6ce8243f5517b67d6761509150", + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150", "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 +7755,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-12T09:06:47+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,20 +7828,20 @@ "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", - "version": "v1.25.1", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", - "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -7723,13 +7852,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.87.2", - "illuminate/view": "^11.46.0", - "larastan/larastan": "^3.7.1", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -7755,6 +7885,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -7765,35 +7896,35 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-09-19T02:57:12+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "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 +7957,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 +10486,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 +10538,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 +10558,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 +10668,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": [], @@ -10545,8 +10734,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.3" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/_api_app/config/ai.php b/_api_app/config/ai.php new file mode 100644 index 000000000..fe1cdf067 --- /dev/null +++ b/_api_app/config/ai.php @@ -0,0 +1,129 @@ + 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.example b/_api_app/opencode.json.example new file mode 100644 index 000000000..dd9e8a0b2 --- /dev/null +++ b/_api_app/opencode.json.example @@ -0,0 +1,25 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "laravel-boost": { + "type": "local", + "enabled": true, + "command": [ + "php", + "artisan", + "boost:mcp" + ] + }, + "herd": { + "type": "local", + "enabled": true, + "command": [ + "php", + "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" + ], + "environment": { + "SITE_PATH": "/path/to/berta/_api_app" + } + } + } +} 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) {