Skip to content

Latest commit

 

History

History
1028 lines (846 loc) · 33.4 KB

File metadata and controls

1028 lines (846 loc) · 33.4 KB

Statamic AI Agent - Development Specification

Project Overview

I want to create a Statamic v6 addon called "Statamic AI Agent" (package name: reachweb/statamic-ai-agent) that provides an AI-powered terminal interface in the Control Panel for making site changes using natural language.

Core Concept

A Claude Code-like interface accessible via Cmd+K (or a toolbar button) that lets users type instructions like:

  • "Rename the 'days' field to 'days_before' in the cruises blueprint and update all entries"
  • "Translate all blog entries to French"
  • "Add a 'featured' toggle field to all collection blueprints"

The AI agent should execute these operations, show its progress, and ask for confirmations when needed.


Technical Requirements

Requirement Version/Details
Statamic v6
Laravel 12+
PHP 8.2+
Styling TailwindCSS v4
Frontend Vue 3 with Composition API
AI Backend Multi-driver architecture (Claude default, extensible to OpenAI, Google, etc.)
Default Driver Claude API (Anthropic) with tool use - default model: claude-sonnet-4-5
Streaming Server-Sent Events (SSE)
Testing Pest v4

Addon Structure

Create the addon with this structure:

statamic-ai-agent/
├── src/
│   ├── ServiceProvider.php
│   ├── UserConfig.php                       # User settings management (YAML-based)
│   ├── Http/
│   │   ├── Controllers/
│   │   │   └── AgentController.php          # Handles chat requests via SSE
│   │   └── Middleware/
│   │       └── CheckBudgetLimit.php         # Blocks requests if budget exceeded
│   ├── Agent/
│   │   ├── Agent.php                        # Main orchestrator - driver-agnostic
│   │   ├── Conversation.php                 # Manages conversation state/history
│   │   ├── ToolRegistry.php                 # Registers and manages available tools
│   │   ├── BudgetTracker.php                # Tracks API spending against limits (driver-aware)
│   │   ├── Drivers/                         # AI Driver implementations
│   │   │   ├── Contracts/
│   │   │   │   └── AiDriver.php             # Interface for all AI drivers
│   │   │   ├── DriverFactory.php            # Creates driver instances
│   │   │   ├── DriverResponse.php           # Unified response DTO
│   │   │   └── Claude/
│   │   │       └── ClaudeDriver.php         # Claude API implementation
│   │   └── Tools/                           # Individual tool implementations
│   │       ├── Contracts/
│   │       │   └── AgentTool.php            # Interface for all tools
│   │       ├── Blueprint/
│   │       │   ├── ListBlueprintsTool.php
│   │       │   ├── GetBlueprintTool.php
│   │       │   ├── UpdateBlueprintTool.php
│   │       │   └── RenameFieldTool.php
│   │       ├── Entry/
│   │       │   ├── ListEntriesTool.php
│   │       │   ├── GetEntryTool.php
│   │       │   ├── UpdateEntryTool.php
│   │       │   ├── BulkUpdateEntriesTool.php
│   │       │   └── TranslateEntryTool.php   # For multisite translation
│   │       ├── Collection/
│   │       │   ├── ListCollectionsTool.php
│   │       │   └── UpdateCollectionTool.php
│   │       ├── View/
│   │       │   ├── FindViewsTool.php        # Search Antlers/Blade for field usage
│   │       │   ├── UpdateViewTool.php
│   │       │   └── PreviewViewChangesTool.php
│   │       ├── Asset/
│   │       │   ├── ListAssetsTool.php
│   │       │   └── UpdateAssetTool.php
│   │       ├── Taxonomy/
│   │       │   ├── ListTaxonomiesTool.php
│   │       │   └── UpdateTermTool.php
│   │       ├── Global/
│   │       │   ├── ListGlobalsTool.php
│   │       │   └── UpdateGlobalTool.php
│   │       ├── Navigation/
│   │       │   └── ListNavsTool.php
│   │       └── Safety/
│   │           ├── GitCommitTool.php        # Create safety commit before changes
│   │           ├── GitDiffTool.php          # Show what changed
│   │           └── BackupTool.php
│   ├── Settings/
│   │   └── AgentSettings.php                # Addon configuration
│   ├── Console/
│   │   └── Commands/
│   │       └── PruneUsageFilesCommand.php   # Cleanup old usage JSON files
│   └── Facades/
│       └── Agent.php
├── resources/
│   ├── js/
│   │   ├── addon.js                         # Main entry point
│   │   └── components/
│   │       ├── AgentTerminal.vue            # Main terminal/chat UI
│   │       ├── AgentMessage.vue             # Individual message component
│   │       ├── ToolExecution.vue            # Shows tool being executed
│   │       ├── ConfirmationPrompt.vue       # Yes/No confirmations
│   │       ├── DiffPreview.vue              # Show file diffs before applying
│   │       ├── BudgetIndicator.vue          # Shows remaining budget
│   │       └── AgentToggle.vue              # Toolbar button to open terminal
│   ├── css/
│   │   └── agent.css
│   └── views/
│       └── settings.blade.php               # Addon settings page
├── routes/
│   └── cp.php
├── config/
│   └── statamic-ai-agent.php
├── storage/
│   └── ai-agent/                            # Flat-file storage for usage tracking
│       ├── usage/
│       │   └── 2024-01-15.json              # Daily usage files
│       └── config.json                      # Runtime config cache
├── lang/
│   └── en/
│       └── messages.php
├── composer.json
├── package.json
├── vite.config.js
└── README.md

Permission Modes (Critical Feature)

The addon MUST have two operation modes configurable in settings:

1. Content Only Mode (Safe/Default)

Allowed Not Allowed
Modify entry content/data Modify blueprints
Translate entries Modify collection/taxonomy settings
Bulk update field values in entries Modify views/templates
Modify navigation structures

2. Full Access Mode (Power Mode)

Everything in Content Only, plus:

  • Can modify blueprints (add/remove/rename fields)
  • Can modify collection and taxonomy settings
  • Can search and modify Antlers/Blade views
  • Can modify navigation structures
  • Can modify global sets structure

Implementation Note: Store this setting in the config and check it before executing any tool. The Agent should be aware of its current mode and tell the user if they ask for something outside its permissions.


Budget/Wallet System (Critical Feature)

Implement a spending limit system to control API costs:

Budget Configuration

// config/statamic-ai-agent.php
return [
    // Default AI driver to use
    'default_driver' => env('AI_AGENT_DRIVER', 'claude'),

    // AI Driver configurations with per-model pricing
    'drivers' => [
        'claude' => [
            'api_key' => env('ANTHROPIC_API_KEY'),
            'default_model' => 'claude-sonnet-4-5',
            'models' => [
                'claude-haiku-4-5' => ['input' => 1.00, 'output' => 5.00],
                'claude-sonnet-4-5' => ['input' => 3.00, 'output' => 15.00],
                'claude-opus-4-5' => ['input' => 15.00, 'output' => 75.00],
            ],
        ],
        // Future drivers (OpenAI, Google, etc.) can be added here
    ],

    // Budget limits (in USD)
    'budget' => [
        // Maximum spend per day in USD
        'daily_limit' => env('AI_AGENT_DAILY_LIMIT', 10.00),

        // Maximum spend per month in USD (0 = unlimited)
        'monthly_limit' => env('AI_AGENT_MONTHLY_LIMIT', 100.00),

        // Maximum spend per single conversation/session (0 = unlimited)
        'per_session_limit' => env('AI_AGENT_SESSION_LIMIT', 5.00),

        // Warning threshold (percentage) - warn user when they hit this
        'warning_threshold' => env('AI_AGENT_WARNING_THRESHOLD', 80),

        // Action when limit reached: 'block' or 'warn'
        'limit_action' => env('AI_AGENT_LIMIT_ACTION', 'block'),
    ],
];

BudgetTracker Class (Flat-File Storage)

Create src/Agent/BudgetTracker.php that uses JSON files instead of a database:

  1. Storage Structure:

    storage/statamic-ai-agent/
    ├── usage/
    │   ├── 2024-01-15.json      # One file per day
    │   ├── 2024-01-16.json
    │   └── ...
    └── monthly/
        ├── 2024-01.json          # Monthly aggregates (optional, for performance)
        └── ...
    
  2. Daily Usage File Format (2024-01-15.json):

    {
      "date": "2024-01-15",
      "total_cost": 2.45,
      "total_input_tokens": 150000,
      "total_output_tokens": 45000,
      "requests": [
        {
          "id": "req_abc123",
          "timestamp": "2024-01-15T10:30:00Z",
          "user_id": "1",
          "session_id": "sess_xyz",
          "input_tokens": 1500,
          "output_tokens": 800,
          "cost": 0.0165,
          "model": "claude-sonnet-4-5-20250514",
          "tools_used": ["list_collections", "get_blueprint"]
        }
      ]
    }
  3. Calculates costs based on driver pricing configuration:

    public function calculateCost(int $inputTokens, int $outputTokens, string $model, string $driver = 'claude'): float
    {
        // Pricing is now loaded from config per-driver
        $driverConfig = config("statamic-ai-agent.drivers.{$driver}", []);
        $models = $driverConfig['models'] ?? [];
    
        $rates = $models[$model] ?? [
            'input' => 3.00,   // Default: $3 per 1M input tokens
            'output' => 15.00, // Default: $15 per 1M output tokens
        ];
    
        return (($inputTokens / 1_000_000) * $rates['input'])
             + (($outputTokens / 1_000_000) * $rates['output']);
    }
  4. File-based storage methods:

    class BudgetTracker
    {
        protected string $storagePath;
    
        public function __construct()
        {
            $this->storagePath = storage_path('statamic-ai-agent');
            $this->ensureDirectoriesExist();
        }
    
        protected function ensureDirectoriesExist(): void
        {
            $directories = [
                $this->storagePath,
                $this->storagePath . '/usage',
                $this->storagePath . '/monthly',
            ];
    
            foreach ($directories as $dir) {
                if (!is_dir($dir)) {
                    mkdir($dir, 0755, true);
                }
            }
        }
    
        protected function getDailyFilePath(?string $date = null): string
        {
            $date = $date ?? now()->format('Y-m-d');
            return $this->storagePath . "/usage/{$date}.json";
        }
    
        protected function getDailyData(?string $date = null): array
        {
            $path = $this->getDailyFilePath($date);
    
            if (!file_exists($path)) {
                return [
                    'date' => $date ?? now()->format('Y-m-d'),
                    'total_cost' => 0,
                    'total_input_tokens' => 0,
                    'total_output_tokens' => 0,
                    'requests' => [],
                ];
            }
    
            return json_decode(file_get_contents($path), true);
        }
    
        protected function saveDailyData(array $data): void
        {
            $path = $this->getDailyFilePath($data['date']);
            file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT));
        }
    
        public function recordUsage(
            string $userId,
            string $sessionId,
            int $inputTokens,
            int $outputTokens,
            string $model,
            string $driver = 'claude',
            array $metadata = []
        ): void {
            $cost = $this->calculateCost($inputTokens, $outputTokens, $model, $driver);
            $data = $this->getDailyData();
    
            $data['requests'][] = [
                'id' => 'req_' . uniqid(),
                'timestamp' => now()->toISOString(),
                'user_id' => $userId,
                'session_id' => $sessionId,
                'input_tokens' => $inputTokens,
                'output_tokens' => $outputTokens,
                'cost' => $cost,
                'model' => $model,
                'driver' => $driver,
                'tools_used' => $metadata['tools_used'] ?? [],
            ];
    
            $data['total_cost'] += $cost;
            $data['total_input_tokens'] += $inputTokens;
            $data['total_output_tokens'] += $outputTokens;
    
            $this->saveDailyData($data);
        }
    
        public function getDailyUsage(): float
        {
            return $this->getDailyData()['total_cost'];
        }
    
        public function getMonthlyUsage(): float
        {
            $total = 0;
            $month = now()->format('Y-m');
            $pattern = $this->storagePath . "/usage/{$month}-*.json";
    
            foreach (glob($pattern) as $file) {
                $data = json_decode(file_get_contents($file), true);
                $total += $data['total_cost'] ?? 0;
            }
    
            return $total;
        }
    
        public function getSessionUsage(string $sessionId): float
        {
            $data = $this->getDailyData();
            $total = 0;
    
            foreach ($data['requests'] as $request) {
                if ($request['session_id'] === $sessionId) {
                    $total += $request['cost'];
                }
            }
    
            return $total;
        }
    
        public function canMakeRequest(): bool
        {
            $dailyLimit = config('statamic-ai-agent.budget.daily_limit');
    
            if ($dailyLimit <= 0) {
                return true; // Unlimited
            }
    
            return $this->getDailyUsage() < $dailyLimit;
        }
    
        public function getRemainingDailyBudget(): float
        {
            $limit = config('statamic-ai-agent.budget.daily_limit');
            return max(0, $limit - $this->getDailyUsage());
        }
    
        public function getUsagePercentage(): float
        {
            $limit = config('statamic-ai-agent.budget.daily_limit');
    
            if ($limit <= 0) {
                return 0;
            }
    
            return min(100, ($this->getDailyUsage() / $limit) * 100);
        }
    
        public function isApproachingLimit(): bool
        {
            $threshold = config('statamic-ai-agent.budget.warning_threshold', 80);
            return $this->getUsagePercentage() >= $threshold;
        }
    
        /**
         * Clean up old usage files (optional maintenance)
         */
        public function pruneOldUsageFiles(int $keepDays = 90): int
        {
            $cutoff = now()->subDays($keepDays)->format('Y-m-d');
            $deleted = 0;
    
            foreach (glob($this->storagePath . '/usage/*.json') as $file) {
                $date = basename($file, '.json');
                if ($date < $cutoff) {
                    unlink($file);
                    $deleted++;
                }
            }
    
            return $deleted;
        }
    }
  5. Provides checking methods:

    public function canMakeRequest(): bool;
    public function getDailyUsage(): float;
    public function getMonthlyUsage(): float;
    public function getSessionUsage(string $sessionId): float;
    public function getRemainingDailyBudget(): float;
    public function getRemainingMonthlyBudget(): float;
    public function getUsagePercentage(): float;
    public function isApproachingLimit(): bool; // True if > warning_threshold

Artisan Command for Cleanup

Create a command to prune old usage files:

// src/Console/Commands/PruneUsageFilesCommand.php
<?php

namespace MyVendor\StatamicAiAgent\Console\Commands;

use Illuminate\Console\Command;
use MyVendor\StatamicAiAgent\Agent\BudgetTracker;

class PruneUsageFilesCommand extends Command
{
    protected $signature = 'ai-agent:prune-usage {--days=90 : Number of days to keep}';

    protected $description = 'Remove old AI Agent usage tracking files';

    public function handle(BudgetTracker $tracker)
    {
        $days = (int) $this->option('days');
        $deleted = $tracker->pruneOldUsageFiles($days);

        $this->info("Deleted {$deleted} old usage file(s).");

        return Command::SUCCESS;
    }
}

Register in ServiceProvider:

public function boot()
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            Commands\PruneUsageFilesCommand::class,
        ]);
    }
}

Budget UI Component

Create BudgetIndicator.vue that shows:

  • Daily spending: $X.XX / $10.00
  • Visual progress bar (green → yellow → red)
  • Warning message when approaching limit
  • "Budget exceeded" overlay when limit reached

Agent Integration

In the Agent.php class:

public function run(string $message, Conversation $conversation): Generator
{
    $budgetTracker = app(BudgetTracker::class);

    // Check budget before making request
    if (!$budgetTracker->canMakeRequest()) {
        yield [
            'type' => 'error',
            'code' => 'budget_exceeded',
            'message' => 'Daily API budget has been exceeded. Resets at midnight UTC.',
            'usage' => [
                'daily' => $budgetTracker->getDailyUsage(),
                'limit' => config('statamic-ai-agent.budget.daily_limit'),
            ]
        ];
        return;
    }

    // Warn if approaching limit
    if ($budgetTracker->isApproachingLimit()) {
        yield [
            'type' => 'warning',
            'code' => 'budget_warning',
            'message' => sprintf(
                'Warning: You have used %s%% of your daily budget ($%.2f remaining)',
                $budgetTracker->getUsagePercentage(),
                $budgetTracker->getRemainingDailyBudget()
            ),
        ];
    }

    // Make API call...
    $response = $this->claude->messages()->create([...]);

    // Record usage from response
    $budgetTracker->recordUsage(
        userId: auth()->id(),
        sessionId: $conversation->getId(),
        inputTokens: $response->usage->input_tokens,
        outputTokens: $response->usage->output_tokens,
        model: $this->model,
        metadata: ['tools_used' => $this->extractToolNames($response)]
    );

    // ... rest of the loop
}

Settings Page Budget Section

Add to the settings page:

<div class="budget-settings">
    <h2>Budget & Spending Limits</h2>

    <div class="form-group">
        <label>Daily Limit (USD)</label>
        <input type="number" step="0.01" min="0" name="daily_limit" value="{{ $daily_limit }}">
        <p class="help-text">Maximum amount to spend per day. Set to 0 for unlimited.</p>
    </div>

    <div class="form-group">
        <label>Monthly Limit (USD)</label>
        <input type="number" step="0.01" min="0" name="monthly_limit" value="{{ $monthly_limit }}">
    </div>

    <div class="form-group">
        <label>Per-Session Limit (USD)</label>
        <input type="number" step="0.01" min="0" name="per_session_limit" value="{{ $per_session_limit }}">
        <p class="help-text">Maximum spend for a single conversation session.</p>
    </div>

    <div class="form-group">
        <label>Warning Threshold (%)</label>
        <input type="number" min="0" max="100" name="warning_threshold" value="{{ $warning_threshold }}">
        <p class="help-text">Show warning when usage exceeds this percentage of daily limit.</p>
    </div>

    <div class="current-usage">
        <h3>Current Usage</h3>
        <p>Today: ${{ number_format($daily_usage, 2) }} / ${{ number_format($daily_limit, 2) }}</p>
        <p>This Month: ${{ number_format($monthly_usage, 2) }} / ${{ number_format($monthly_limit, 2) }}</p>
    </div>
</div>

Multisite Translation Feature

When Statamic multisite is enabled, the agent should be able to:

  1. Detect available sites/locales
  2. Translate entry content from one locale to another
  3. Support bulk translation (e.g., "Translate all blog posts to French and German")
  4. Use Claude itself for translation (no external translation API needed)

TranslateEntryTool Implementation

The TranslateEntryTool should:

  • Check if multisite is enabled
  • Get the source entry content
  • Ask Claude to translate (can be done in a nested call or separate tool)
  • Save to the target site's entry file
class TranslateEntryTool implements AgentTool
{
    public function name(): string
    {
        return 'translate_entry';
    }

    public function description(): string
    {
        return 'Translates an entry from one locale to another. Uses AI to translate the content while preserving structure and formatting.';
    }

    public function inputSchema(): array
    {
        return [
            'type' => 'object',
            'properties' => [
                'collection' => [
                    'type' => 'string',
                    'description' => 'The collection handle'
                ],
                'entry_id' => [
                    'type' => 'string',
                    'description' => 'The entry ID or slug'
                ],
                'source_site' => [
                    'type' => 'string',
                    'description' => 'Source site handle (e.g., "default", "english")'
                ],
                'target_site' => [
                    'type' => 'string',
                    'description' => 'Target site handle (e.g., "french", "german")'
                ],
                'fields_to_translate' => [
                    'type' => 'array',
                    'items' => ['type' => 'string'],
                    'description' => 'Specific fields to translate. If empty, translates all text fields.'
                ]
            ],
            'required' => ['collection', 'entry_id', 'target_site']
        ];
    }
}

Agent System Prompt

Create a comprehensive system prompt that explains:

  1. Statamic's Architecture:

    • Collections, blueprints, entries
    • Taxonomies and terms
    • Globals and global sets
    • Navigation structures
    • Assets and asset containers
    • The flat-file structure and how content is stored in YAML/Markdown
  2. Available Tools and When to Use Them

  3. Behavioral Instructions:

    • Always explain what it's about to do before doing it
    • Ask for confirmation before destructive/bulk operations
    • Create a git commit before making changes (if git is available)
    • After renaming fields in blueprints, proactively ask about updating views
    • When translating, preserve the structure and only translate text content
    • Be aware of its permission mode and explain limitations
    • Handle errors gracefully and suggest fixes
    • Be mindful of the budget and warn about expensive operations
  4. Budget Awareness:

    • Inform user about remaining budget when relevant
    • Suggest more efficient approaches for expensive operations
    • Warn before bulk operations that might consume significant budget

Vue 3 Terminal Component Requirements

The AgentTerminal.vue component should:

  1. Open via Cmd+K (Mac) / Ctrl+K (Windows) keyboard shortcut
  2. Also be accessible via a button in the CP toolbar
  3. Have a dark theme terminal-like aesthetic (similar to Claude Code)
  4. Support markdown rendering in responses
  5. Show typing/thinking indicators during streaming
  6. Display tool executions in a collapsible format showing:
    • Tool name
    • Input parameters
    • Output/result
    • Time taken
  7. Handle confirmation prompts inline with Yes/No buttons
  8. Show diffs in a readable format before applying file changes
  9. Maintain conversation history within the session
  10. Have a "Stop" button to cancel ongoing operations
  11. Support clearing conversation with a command or button
  12. Show budget indicator in the header/footer

Component Template Structure

<template>
  <Teleport to="body">
    <Transition name="slide-up">
      <div v-if="isOpen" class="ai-agent-terminal">
        <!-- Header with budget indicator -->
        <div class="terminal-header">
          <h3>AI Agent</h3>
          <BudgetIndicator
            :daily-usage="budget.daily"
            :daily-limit="budget.dailyLimit"
            :warning="budget.isWarning"
          />
          <button @click="close" class="close-btn">×</button>
        </div>

        <!-- Messages area -->
        <div class="terminal-messages" ref="messagesContainer">
          <AgentMessage
            v-for="msg in messages"
            :key="msg.id"
            :message="msg"
            @confirm="handleConfirmation"
          />
          <div v-if="isThinking" class="thinking-indicator">
            <span class="pulse">●</span> Thinking...
          </div>
        </div>

        <!-- Input area -->
        <div class="terminal-input">
          <input
            v-model="input"
            @keydown.enter="send"
            @keydown.escape="close"
            placeholder="Ask the AI to make changes..."
            :disabled="isThinking || budgetExceeded"
            ref="inputField"
          />
          <button
            v-if="isThinking"
            @click="stop"
            class="stop-btn"
          >
            Stop
          </button>
          <button
            v-else
            @click="send"
            :disabled="!input.trim() || budgetExceeded"
            class="send-btn"
          >
            Send
          </button>
        </div>

        <!-- Budget exceeded overlay -->
        <div v-if="budgetExceeded" class="budget-exceeded-overlay">
          <p>Daily budget exceeded</p>
          <p>Resets at midnight UTC</p>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

Settings Page

Create a settings page at /cp/statamic-ai-agent/settings with:

Setting Type Description
AI Driver Select Choose AI provider (Claude, OpenAI, etc.)
Model Select Model selection (dynamic per driver)
Permission Mode Toggle Content Only / Full Access
Git Integration Toggle Auto-commit before changes
Default Confirmation Select Always ask / Only destructive / Never
Keyboard Shortcut Input Customize the trigger shortcut
Daily Limit Number Max USD per day
Monthly Limit Number Max USD per month
Per-Session Limit Number Max USD per conversation
Warning Threshold Number Percentage to trigger warning

Note: API keys are configured via environment variables in .env, not in the UI for security.


SSE Streaming Implementation

The AgentController should stream responses using SSE:

public function chat(Request $request)
{
    // Check budget via middleware or here
    $budgetTracker = app(BudgetTracker::class);

    if (!$budgetTracker->canMakeRequest()) {
        return response()->json([
            'error' => 'budget_exceeded',
            'message' => 'Daily API budget exceeded',
            'usage' => $budgetTracker->getDailyUsage(),
            'limit' => config('statamic-ai-agent.budget.daily_limit'),
        ], 429);
    }

    return response()->stream(function () use ($request, $budgetTracker) {
        $agent = new Agent($budgetTracker);

        foreach ($agent->run($request->input('message'), $request->session()) as $event) {
            echo "data: " . json_encode($event) . "\n\n";
            ob_flush();
            flush();
        }
    }, 200, [
        'Content-Type' => 'text/event-stream',
        'Cache-Control' => 'no-cache',
        'Connection' => 'keep-alive',
        'X-Accel-Buffering' => 'no',
    ]);
}

Safety Requirements

Requirement Implementation
Git Integration Before any write operation, offer to create a git commit
Dry Run For bulk operations, first show what WOULD change
Confirmation Flow Agent yields a 'confirm' event and waits for user response
Rate Limiting Prevent accidental loops or runaway operations
Budget Limits Block requests when spending limits are reached
Audit Log Log all operations to storage/logs/ai-agent.log

Example Tool Implementation

Here's how a tool should be structured:

<?php

namespace MyVendor\StatamicAiAgent\Agent\Tools\Blueprint;

use MyVendor\StatamicAiAgent\Agent\Tools\Contracts\AgentTool;
use Statamic\Facades\Blueprint;

class RenameFieldTool implements AgentTool
{
    public function name(): string
    {
        return 'rename_blueprint_field';
    }

    public function description(): string
    {
        return 'Renames a field in a blueprint. This will update the blueprint YAML file. Note: This does NOT automatically update entries or views - use separate tools for that.';
    }

    public function requiredMode(): string
    {
        return 'full'; // 'content' or 'full'
    }

    public function requiresConfirmation(): bool
    {
        return true;
    }

    public function inputSchema(): array
    {
        return [
            'type' => 'object',
            'properties' => [
                'blueprint_handle' => [
                    'type' => 'string',
                    'description' => 'The handle of the blueprint (e.g., "article" for a collection blueprint)'
                ],
                'blueprint_namespace' => [
                    'type' => 'string',
                    'description' => 'The namespace - usually "collections.{collection_handle}" or "taxonomies.{taxonomy_handle}"'
                ],
                'old_field_handle' => [
                    'type' => 'string',
                    'description' => 'The current field handle to rename'
                ],
                'new_field_handle' => [
                    'type' => 'string',
                    'description' => 'The new field handle'
                ],
            ],
            'required' => ['blueprint_handle', 'blueprint_namespace', 'old_field_handle', 'new_field_handle']
        ];
    }

    public function execute(array $input): array
    {
        $blueprint = Blueprint::find($input['blueprint_namespace'] . '.' . $input['blueprint_handle']);

        if (!$blueprint) {
            return ['success' => false, 'error' => 'Blueprint not found'];
        }

        // Implementation here...

        return [
            'success' => true,
            'message' => "Renamed field '{$input['old_field_handle']}' to '{$input['new_field_handle']}' in blueprint '{$input['blueprint_handle']}'",
            'next_steps' => [
                'You should now update all entries that use this field',
                'You may also want to update any views that reference this field'
            ]
        ];
    }
}

Reference Package

Look at https://github.com/cboxdk/statamic-mcp for reference on how to interact with Statamic's APIs for various operations. You can borrow patterns from there but implement them as tools for the Claude agent.


Getting Started - Implementation Order

  1. Scaffold the addon structure with all the files
  2. Implement the ServiceProvider with proper registration
  3. Create the BudgetTracker class with JSON file storage and all budget logic
  4. Create the Agent class with the Claude API loop and budget integration
  5. Implement 4-5 core tools to prove the concept:
    • ListCollectionsTool
    • ListBlueprintsTool
    • GetBlueprintTool
    • UpdateEntryTool
    • TranslateEntryTool (if multisite)
  6. Create the Vue terminal component with basic chat and budget indicator
  7. Add SSE streaming
  8. Create the settings page with all configuration options
  9. Expand with more tools and polish the UI

Important Notes

  • Use Statamic v6 APIs and patterns (check their upgrade guide if needed)
  • Use Vue 3 Composition API with <script setup> syntax
  • Use Statamic's built-in permission system for CP access control
  • Store the Claude API key securely using Laravel's encryption
  • The conversation state should be stored in the session, not persisted
  • Usage/budget data is stored as JSON files in storage/statamic-ai-agent/ (no database required!)
  • Make sure the terminal works well on both desktop and tablet CP views
  • All monetary values should be stored and calculated in USD
  • Consider timezone handling for daily resets (use UTC as default)
  • Add a scheduled command or manual button to prune old usage files (keep last 90 days)

Environment Variables

Add these to the .env.example:

# Statamic AI Agent Configuration
AI_AGENT_DRIVER=claude           # AI driver: claude, openai, google (when implemented)
AI_AGENT_MODE=content            # 'content' or 'full'

# Claude API (default driver)
ANTHROPIC_API_KEY=

# Future drivers (add when implementing)
# OPENAI_API_KEY=
# GOOGLE_AI_API_KEY=

# Budget Limits (in USD)
AI_AGENT_DAILY_LIMIT=10.00
AI_AGENT_MONTHLY_LIMIT=100.00
AI_AGENT_SESSION_LIMIT=5.00
AI_AGENT_WARNING_THRESHOLD=80
AI_AGENT_LIMIT_ACTION=block      # 'block' or 'warn'

Start Building

Please start by creating the complete addon structure with all files, then implement the core functionality. Focus on getting a working proof-of-concept with:

  1. The budget tracking system using JSON flat-file storage (no database!)
  2. 4-5 working tools
  3. The Vue terminal with budget indicator
  4. SSE streaming

Remember: Statamic is a flat-file CMS, so this addon should also be flat-file friendly. All usage tracking is stored in storage/statamic-ai-agent/ as JSON files.

Then we can expand from there.