diff --git a/claude/core/skills/app-split/SKILL.md b/claude/core/skills/app-split/SKILL.md new file mode 100644 index 0000000..65855b4 --- /dev/null +++ b/claude/core/skills/app-split/SKILL.md @@ -0,0 +1,139 @@ +--- +name: app-split +description: This skill should be used when the user asks to "split an app", "fork an app", "create a new app from host.uk.com", "de-hostuk", "copy app to new domain", or needs to extract a Website module from the host.uk.com monolith into a standalone CorePHP application. Covers the full copy-strip-rebrand process. +--- + +# App Split — Extract CorePHP App from Monolith + +Split a Website module from the host.uk.com monolith into a standalone CorePHP application. The approach is copy-everything-then-strip rather than build-from-scratch. + +## When to Use + +- Extracting a domain-specific app (lthn.ai, bio.host.uk.com, etc.) from host.uk.com +- Creating a new standalone CorePHP app from the existing platform +- Any "fork and specialise" operation on the host.uk.com codebase + +## Process + +### 1. Inventory — Decide What Stays and Goes + +Before copying, map which modules belong to the target app. + +**Inputs needed from user:** +- Target domain (e.g. `lthn.ai`) +- Which `Website/*` modules to keep (check `$domains` in each Boot.php) +- Which `Mod/*` modules to keep (product modules vs platform modules) +- Which `Service/*` providers to keep (depends on kept Mod modules) + +Run the inventory script to see all modules and their domain bindings: + +```bash +!`scripts/inventory.sh /Users/snider/Code/lab/host.uk.com` +``` + +Consult `references/module-classification.md` for the standard keep/remove classification. + +### 2. Copy — Wholesale Clone + +```bash +rsync -a \ + --exclude='vendor/' \ + --exclude='node_modules/' \ + --exclude='.git/' \ + --exclude='storage/logs/*' \ + --exclude='storage/framework/cache/*' \ + --exclude='storage/framework/sessions/*' \ + --exclude='storage/framework/views/*' \ + SOURCE/ TARGET/ +``` + +Copy everything. Do not cherry-pick — the framework has deep cross-references and it is faster to remove than to reconstruct. + +### 3. Strip — Remove Unwanted Modules + +Delete removed module directories: +```bash +# Website modules +rm -rf TARGET/app/Website/{Host,Html,Lab,Service} + +# Mod modules +rm -rf TARGET/app/Mod/{Links,Social,Trees,Front,Hub} + +# Service providers that depend on removed Mod modules +rm -rf TARGET/app/Service/Hub +``` + +### 4. Update Boot.php Providers + +Edit `TARGET/app/Boot.php`: +- Remove all `\Website\*\Boot::class` entries for deleted Website modules +- Remove all `\Mod\*\Boot::class` entries for deleted Mod modules +- Remove all `\Service\*\Boot::class` entries for deleted Service providers +- Update class docblock (name, description) +- Update `guestRedirectUrl()` — change fallback login host from `host.uk.com` to target domain + +### 5. Rebrand — Domain References + +Run the domain scan script to find all references: + +```bash +!`scripts/domain-scan.sh TARGET` +``` + +**Critical files to update** (in priority order): + +| File | What to Change | +|------|----------------| +| `composer.json` | name, description, licence | +| `config/app.php` | `base_domain` default | +| `.env.example` | APP_URL, SESSION_DOMAIN, MCP_DOMAIN, DB_DATABASE, mail | +| `vite.config.js` | dev server host + HMR host | +| `app/Boot.php` | providers, guest redirect, comments | +| `CLAUDE.md` | Full rewrite for new app | +| `.gitignore` | Add any env files with secrets | +| `robots.txt` | Sitemap URL, allowed paths | +| `public/errors/*.html` | Support contact links | +| `public/js/*.js` | API base URLs in embed widgets | +| `config/cdn.php` | default_domain, apex URL | +| `config/mail.php` | contact_recipient | +| `database/seeders/` | email, domains, branding | + +**Leave alone** (shared infrastructure): +- `analytics.host.uk.com` references in CSP headers and tracking pixels +- CDN storage zone names (same Hetzner/BunnyCDN buckets) +- External links to host.uk.com in footers (legitimate cross-links) + +### 6. Secure — Check for Secrets + +Scan for env files with real credentials before committing: + +```bash +# Find env files that might have secrets +find TARGET -name ".env*" -not -name ".env.example" | while read f; do + if grep -qE '(KEY|SECRET|PASSWORD|TOKEN)=.{8,}' "$f"; then + echo "SECRETS: $f — add to .gitignore" + fi +done +``` + +### 7. Init Git and Verify + +```bash +cd TARGET +git init +git add -A +git status # Review what's being committed +``` + +Check for: +- No `.env` files with real secrets staged +- No `auth.json` staged +- No `vendor/` or `node_modules/` staged + +## Gotchas + +- **Service providers reference Mod modules**: If `Service/Hub` depends on `Mod/Hub` and you remove `Mod/Hub`, also remove `Service/Hub` — otherwise the app crashes on boot. +- **Boot.php $providers is the master list**: Every module must be listed here. Missing entries = module doesn't load. Extra entries for deleted modules = crash. +- **Seeders reference removed services**: SystemUserSeeder sets up analytics, trust, push, bio etc. The seeder uses `class_exists()` checks so it gracefully skips missing services, but domain references still need updating. +- **Composer deps for removed modules**: Packages like `core/php-plug-social` are only needed for removed modules. Safe to remove from composer.json but not urgent — they're just unused. +- **The `.env.lthn-ai` pattern**: Production env files often live in the repo for reference but MUST be gitignored since they contain real credentials. diff --git a/claude/core/skills/app-split/references/module-classification.md b/claude/core/skills/app-split/references/module-classification.md new file mode 100644 index 0000000..aaf3f3a --- /dev/null +++ b/claude/core/skills/app-split/references/module-classification.md @@ -0,0 +1,100 @@ +# Module Classification Guide + +When splitting an app from host.uk.com, classify each module as **keep** or **remove** based on domain ownership. + +## Website Modules + +Website modules have `$domains` arrays that define which domains they respond to. Check the regex patterns to determine ownership. + +| Module | Domains | Classification | +|--------|---------|----------------| +| Host | `host.uk.com`, `host.test` | host.uk.com only | +| Lthn | `lthn.ai`, `lthn.test`, `lthn.sh` | lthn.ai only | +| App | `app.lthn.*`, `hub.lthn.*` | lthn.ai (client dashboard) | +| Api | `api.lthn.*`, `api.host.*` | Shared — check domain patterns | +| Mcp | `mcp.lthn.*`, `mcp.host.*` | Shared — check domain patterns | +| Docs | `docs.lthn.*`, `docs.host.*` | Shared — check domain patterns | +| Html | Static HTML pages | host.uk.com only | +| Lab | `lab.host.*` | host.uk.com only | +| Service | `*.host.uk.com` service subdomains | host.uk.com only | + +**Rule**: If the module's `$domains` patterns match the target domain, keep it. If they only match host.uk.com patterns, remove it. For shared modules (Api, Mcp, Docs), strip the host.uk.com domain patterns. + +## Mod Modules (Products) + +Mod modules are product-level features. Classify by which platform they serve. + +### host.uk.com Products (Remove for lthn.ai) + +| Module | Product | Why Remove | +|--------|---------|------------| +| Links | BioHost (link-in-bio) | host.uk.com SaaS product | +| Social | SocialHost (scheduling) | host.uk.com SaaS product | +| Front | Frontend chrome/nav | host.uk.com-specific UI | +| Hub | Admin dashboard | host.uk.com admin panel | +| Trees | Trees for Agents | host.uk.com feature | + +### lthn.ai Products (Keep for lthn.ai) + +| Module | Product | Why Keep | +|--------|---------|----------| +| Agentic | AI agent orchestration | Core lthn.ai feature | +| Lem | LEM model management | Core lthn.ai feature | +| Mcp | MCP tool registry | Core lthn.ai feature | +| Studio | Multimedia pipeline | lthn.ai content creation | +| Uptelligence | Server monitoring | Cross-platform, lthn.ai relevant | + +## Service Providers + +Service providers in `app/Service/` are the product layer — they register ServiceDefinition contracts. They depend on their corresponding Mod module. + +**Rule**: If the Mod module is removed, the Service provider MUST also be removed. Otherwise the app crashes on boot when it tries to resolve the missing module's classes. + +| Service | Depends On | Action | +|---------|-----------|--------| +| Hub | Mod/Hub | Remove with Hub | +| Commerce | Core\Mod\Commerce (package) | Keep — it's a core package | +| Agentic | Core\Mod\Agentic (package) | Keep — it's a core package | + +## Core Framework Providers + +These are from CorePHP packages (`core/php`, `core/php-admin`, etc.) and should always be kept — they're the framework itself. + +- `Core\Storage\CacheResilienceProvider` +- `Core\LifecycleEventProvider` +- `Core\Website\Boot` +- `Core\Bouncer\Boot` +- `Core\Config\Boot` +- `Core\Tenant\Boot` +- `Core\Cdn\Boot`, `Core\Mail\Boot`, `Core\Front\Boot` +- `Core\Headers\Boot`, `Core\Helpers\Boot` +- `Core\Media\Boot`, `Core\Search\Boot`, `Core\Seo\Boot` +- `Core\Webhook\Boot` +- `Core\Api\Boot` +- `Core\Mod\Agentic\Boot`, `Core\Mod\Commerce\Boot` +- `Core\Mod\Uptelligence\Boot`, `Core\Mod\Content\Boot` + +## Shared Infrastructure + +Some host.uk.com references are shared infrastructure that ALL apps use. These should NOT be changed during the split: + +| Reference | Why Keep | +|-----------|----------| +| `analytics.host.uk.com` | Shared analytics service (CSP headers, tracking pixel) | +| `cdn.host.uk.com` | Shared CDN delivery URL | +| Hetzner S3 bucket names (`hostuk`, `host-uk`) | Shared storage | +| BunnyCDN storage zones | Shared CDN zones | +| Footer link to host.uk.com | Legitimate external link | + +## Composer Dependencies + +After removing modules, review composer.json for packages only needed by removed modules: + +| Package | Used By | Action | +|---------|---------|--------| +| `core/php-plug-social` | Mod/Social | Remove | +| `core/php-plug-stock` | Stock photo integration | Keep if any module uses it | +| `webklex/php-imap` | Mod/Support (if removed) | Safe to remove | +| `minishlink/web-push` | Mod/Notify (if removed) | Safe to remove | + +**Conservative approach**: Leave deps in place. They don't hurt — they're just unused. Remove later during a cleanup pass. diff --git a/claude/core/skills/app-split/scripts/domain-scan.sh b/claude/core/skills/app-split/scripts/domain-scan.sh new file mode 100755 index 0000000..3d8d0a2 --- /dev/null +++ b/claude/core/skills/app-split/scripts/domain-scan.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# domain-scan.sh — Find all host.uk.com / host.test references in a CorePHP app. +# Usage: ./domain-scan.sh /path/to/app [domain_pattern] +# Default domain pattern: host\.uk\.com|host\.test + +APP_DIR="${1:-.}" +PATTERN="${2:-host\.uk\.com|host\.test}" + +echo "=== Domain Reference Scan ===" +echo "Directory: $APP_DIR" +echo "Pattern: $PATTERN" +echo "" + +echo "--- By Directory ---" +for dir in app config database public resources routes; do + [ -d "$APP_DIR/$dir" ] || continue + count=$(grep -rlE "$PATTERN" "$APP_DIR/$dir" 2>/dev/null | wc -l | tr -d ' ') + [ "$count" -gt 0 ] && printf "%-20s %s files\n" "$dir/" "$count" +done + +# Root files +echo "" +echo "--- Root Files ---" +for f in .env.example vite.config.js CLAUDE.md robots.txt Makefile playwright.config.ts; do + [ -f "$APP_DIR/$f" ] && grep -qE "$PATTERN" "$APP_DIR/$f" 2>/dev/null && printf " %s\n" "$f" +done + +echo "" +echo "--- Critical Files (app code, not docs) ---" +grep -rnE "$PATTERN" \ + "$APP_DIR/app/" \ + "$APP_DIR/config/" \ + "$APP_DIR/database/seeders/" \ + "$APP_DIR/public/js/" \ + "$APP_DIR/public/errors/" \ + "$APP_DIR/public/robots.txt" \ + "$APP_DIR/vite.config.js" \ + "$APP_DIR/.env.example" \ + 2>/dev/null \ + | grep -v '/docs/' \ + | grep -v '/plans/' \ + | grep -v 'node_modules' \ + | grep -v 'vendor/' \ + || echo "(none found)" + +echo "" +echo "--- Shared Infra References (review — may be intentional) ---" +grep -rnE 'analytics\.host\.uk\.com|cdn\.host\.uk\.com' \ + "$APP_DIR/app/" \ + "$APP_DIR/config/" \ + 2>/dev/null \ + || echo "(none found)" + +exit 0 diff --git a/claude/core/skills/app-split/scripts/inventory.sh b/claude/core/skills/app-split/scripts/inventory.sh new file mode 100755 index 0000000..0ce0ad9 --- /dev/null +++ b/claude/core/skills/app-split/scripts/inventory.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# inventory.sh — List all modules and their domain bindings in a CorePHP app. +# Usage: ./inventory.sh /path/to/app +set -euo pipefail + +APP_DIR="${1:-.}" + +echo "=== Website Modules ===" +echo "" +for boot in "$APP_DIR"/app/Website/*/Boot.php; do + [ -f "$boot" ] || continue + mod=$(basename "$(dirname "$boot")") + # Extract domain patterns from $domains array + domains=$(grep -E "'/\^" "$boot" 2>/dev/null | sed "s/.*'\(.*\)'.*/\1/" | tr '\n' ' ' || echo "(no domain pattern)") + # Extract event class names from $listens array + listens=$(grep '::class' "$boot" 2>/dev/null | grep -oE '[A-Za-z]+::class' | sed 's/::class//' | tr '\n' ', ' | sed 's/,$//' || echo "none") + printf "%-15s domains: %s\n" "$mod" "$domains" + printf "%-15s listens: %s\n" "" "$listens" + echo "" +done + +echo "=== Mod Modules ===" +echo "" +for boot in "$APP_DIR"/app/Mod/*/Boot.php; do + [ -f "$boot" ] || continue + mod=$(basename "$(dirname "$boot")") + listens=$(grep '::class' "$boot" 2>/dev/null | grep -oE '[A-Za-z]+::class' | sed 's/::class//' | tr '\n' ', ' | sed 's/,$//' || echo "none") + printf "%-15s listens: %s\n" "$mod" "$listens" +done + +echo "" +echo "=== Service Providers ===" +echo "" +for boot in "$APP_DIR"/app/Service/*/Boot.php; do + [ -f "$boot" ] || continue + mod=$(basename "$(dirname "$boot")") + code=$(grep -oE "'code'\s*=>\s*'[^']+'" "$boot" 2>/dev/null | head -1 || echo "") + printf "%-15s %s\n" "$mod" "$code" +done + +echo "" +echo "=== Boot.php Provider List ===" +grep '::class' "$APP_DIR/app/Boot.php" 2>/dev/null | grep -v '//' | sed 's/^[[:space:]]*/ /' | sed 's/,$//' diff --git a/claude/core/skills/deploy-homelab/SKILL.md b/claude/core/skills/deploy-homelab/SKILL.md new file mode 100644 index 0000000..8f42b09 --- /dev/null +++ b/claude/core/skills/deploy-homelab/SKILL.md @@ -0,0 +1,125 @@ +--- +name: deploy-homelab +description: This skill should be used when the user asks to "deploy to homelab", "deploy to lthn.sh", "ship to homelab", "build and deploy", "push image to homelab", or needs to build a Docker image locally and transfer it to the homelab server at 10.69.69.165. Covers the full build-locally → transfer-tarball → deploy pipeline for CorePHP apps. +--- + +# Deploy to Homelab + +Build a CorePHP app Docker image locally (required for paid package auth), transfer via tarball to the homelab (no registry), and deploy. + +## When to Use + +- Deploying any CorePHP app to the homelab (*.lthn.sh) +- Building images that need `auth.json` for Flux Pro or other paid packages +- Shipping a new version of an app to 10.69.69.165 + +## Prerequisites + +- Docker Desktop running locally +- `auth.json` in the app root (for Flux Pro licence) +- Homelab accessible at 10.69.69.165 (SSH: claude/claude) +- **NEVER ssh directly** — use the deploy script or Ansible from `~/Code/DevOps` + +## Process + +### 1. Build Locally + +Run from the app directory (e.g. `/Users/snider/Code/lab/lthn.ai`): + +```bash +# Install deps (auth.json provides paid package access) +composer install --no-dev --optimize-autoloader +npm ci +npm run build + +# Build the Docker image for linux/amd64 (homelab is x86_64) +docker build --platform linux/amd64 -t IMAGE_NAME:latest . +``` + +The image name follows the pattern: `lthn-sh`, `lthn-ai`, etc. + +### 2. Transfer to Homelab + +```bash +# Save image as compressed tarball +docker save IMAGE_NAME:latest | gzip > /tmp/IMAGE_NAME.tar.gz + +# SCP to homelab +sshpass -p claude scp -P 22 /tmp/IMAGE_NAME.tar.gz claude@10.69.69.165:/tmp/ + +# Load image on homelab +sshpass -p claude ssh -p 22 claude@10.69.69.165 'echo claude | sudo -S docker load < /tmp/IMAGE_NAME.tar.gz' +``` + +**Note:** Homelab SSH is port 22 (NOT port 4819 — that's production servers). Credentials: claude/claude. + +### 3. Deploy on Homelab + +```bash +# Restart container with new image +sshpass -p claude ssh -p 22 claude@10.69.69.165 'echo claude | sudo -S docker restart CONTAINER_NAME' + +# Or if using docker-compose +sshpass -p claude ssh -p 22 claude@10.69.69.165 'cd /opt/services/APP_DIR && echo claude | sudo -S docker compose up -d' +``` + +### 4. Post-Deploy Checks + +```bash +# Run migrations +sshpass -p claude ssh -p 22 claude@10.69.69.165 'echo claude | sudo -S docker exec CONTAINER_NAME php artisan migrate --force' + +# Clear and rebuild caches +sshpass -p claude ssh -p 22 claude@10.69.69.165 'echo claude | sudo -S docker exec CONTAINER_NAME php artisan config:cache && sudo docker exec CONTAINER_NAME php artisan route:cache && sudo docker exec CONTAINER_NAME php artisan view:cache && sudo docker exec CONTAINER_NAME php artisan event:cache' + +# Health check +curl -sf https://APP_DOMAIN/up && echo "OK" || echo "FAILED" +``` + +### One-Shot Script + +Use the bundled script for the full pipeline: + +```bash +scripts/build-and-ship.sh APP_DIR IMAGE_NAME CONTAINER_NAME +``` + +Example: +```bash +scripts/build-and-ship.sh /Users/snider/Code/lab/host.uk.com lthn-sh lthn-sh-hub +scripts/build-and-ship.sh /Users/snider/Code/lab/lthn.ai lthn-ai lthn-ai +``` + +## Or Use Ansible (Preferred) + +The Ansible playbooks handle all of this automatically: + +```bash +cd ~/Code/DevOps +ansible-playbook playbooks/deploy/website/lthn_sh.yml -i inventory/linux_snider_dev.yml +``` + +Available playbooks: +- `lthn_sh.yml` — host.uk.com app to homelab +- `lthn_ai.yml` — lthn.ai app to homelab/prod + +## Known Apps on Homelab + +| App | Image | Container | Port | Data Dir | +|-----|-------|-----------|------|----------| +| host.uk.com | lthn-sh:latest | lthn-sh-hub | 8088 | /opt/services/lthn-lan | +| lthn.ai | lthn-ai:latest | lthn-ai | 80 | /opt/services/lthn-ai | + +## Gotchas + +- **Platform flag required**: Mac builds ARM images by default. Always use `--platform linux/amd64` — homelab is x86_64 Ryzen 9. +- **auth.json stays local**: The Dockerfile copies the entire app directory. The `.dockerignore` should exclude `auth.json` to avoid leaking licence keys into the image. If it doesn't, add it. +- **Tarball size**: Full images are 500MB–1GB compressed. Ensure `/tmp` has space on both ends. +- **Homelab SSH is port 22**: Unlike production servers (port 4819 + Endlessh on 22), the homelab uses standard port 22. +- **No `sudo` password prompt**: Use `echo claude | sudo -S` pattern for sudo commands over SSH. +- **Redis is embedded**: The FrankenPHP image includes supervisord running Redis. No separate Redis container needed on homelab. +- **GPU services**: The homelab has Ollama (11434), Whisper (9150), TTS (9200), ComfyUI (8188) running natively — the app container connects to them via `127.0.0.1` with `--network host`. + +## Consult References + +- `references/environments.md` — Environment variables and service mapping for each deployment target diff --git a/claude/core/skills/deploy-homelab/references/environments.md b/claude/core/skills/deploy-homelab/references/environments.md new file mode 100644 index 0000000..506a8fa --- /dev/null +++ b/claude/core/skills/deploy-homelab/references/environments.md @@ -0,0 +1,115 @@ +# Environment Reference + +## Homelab (lthn.sh) + +**Host:** 10.69.69.165 (Ryzen 9 + 128GB RAM + RX 7800 XT) +**SSH:** claude:claude (port 22) +**Domains:** *.lthn.sh → 10.69.69.165 + +### host.uk.com on homelab + +| Setting | Value | +|---------|-------| +| Container | lthn-sh-hub | +| Image | lthn-sh:latest | +| Port | 8088 (Octane/FrankenPHP) | +| Network | --network host | +| Data | /opt/services/lthn-lan | +| DB | MariaDB 127.0.0.1:3306, db=lthn_sh | +| Redis | Embedded (supervisord in container) | +| APP_URL | https://lthn.sh | +| SESSION_DOMAIN | .lthn.sh | + +### lthn.ai on homelab + +| Setting | Value | +|---------|-------| +| Container | lthn-ai | +| Image | lthn-ai:latest | +| Port | 80 (via docker-compose) | +| Network | proxy + lthn-ai bridge | +| Data | /opt/services/lthn-ai | +| DB | MariaDB lthn-ai-db:3306, db=lthn_ai | +| Redis | Embedded | +| APP_URL | https://lthn.sh (homelab) | +| SESSION_DOMAIN | .lthn.sh | + +### GPU Services (native on homelab) + +| Service | Port | Used By | +|---------|------|---------| +| Ollama | 11434 | LEM scoring (lem-4b model) | +| Whisper | 9150 | Studio speech-to-text | +| Kokoro TTS | 9200 | Studio text-to-speech | +| ComfyUI | 8188 | Studio image generation | +| InfluxDB | via https://influx.infra.lthn.sh | LEM metrics | + +### Key .env differences from production + +```env +# Homelab-specific +APP_ENV=production +APP_URL=https://lthn.sh +SESSION_DOMAIN=.lthn.sh + +# Local GPU services (--network host) +STUDIO_WHISPER_URL=http://127.0.0.1:9150 +STUDIO_OLLAMA_URL=http://127.0.0.1:11434 +STUDIO_TTS_URL=http://127.0.0.1:9200 +STUDIO_COMFYUI_URL=http://127.0.0.1:8188 + +# Local Redis (embedded in container via supervisord) +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +``` + +## Production (de1 — Falkenstein) + +**Host:** eu-prd-01.lthn.io (Hetzner AX102) +**SSH:** Port 4819 only (port 22 = Endlessh tarpit) +**Deploy:** ONLY via Ansible from ~/Code/DevOps + +### Port Map + +| Port | Service | +|------|---------| +| 80/443 | Traefik (TLS termination) | +| 2223/3000 | Forgejo | +| 3306 | Galera (MariaDB cluster) | +| 5432 | PostgreSQL | +| 6379 | Dragonfly (Redis-compatible) | +| 8000-8001 | host.uk.com | +| 8003 | lthn.io | +| 8004 | bugseti.app | +| 8005-8006 | lthn.ai | +| 8007 | api.lthn.ai | +| 8008 | mcp.lthn.ai | +| 8083 | 66Biolinks | +| 8084 | Blesta | +| 8085 | Analytics | +| 8086 | Push Notifications | +| 8087 | Social Proof | +| 3900 | Garage S3 | +| 9000/9443 | Authentik | + +### Ansible Playbooks + +```bash +cd ~/Code/DevOps + +# Homelab +ansible-playbook playbooks/deploy/website/lthn_sh.yml -i inventory/linux_snider_dev.yml + +# Production (de1) +ansible-playbook playbooks/deploy/website/lthn_ai.yml -i inventory/production.yml +``` + +## Dockerfile Base + +All CorePHP apps use the same Dockerfile pattern: + +- Base: `dunglas/frankenphp:1-php8.5-trixie` +- PHP extensions: pcntl, pdo_mysql, redis, gd, intl, zip, opcache, bcmath, exif, sockets +- System packages: redis-server, supervisor, curl, mariadb-client +- Runtime: Supervisord (FrankenPHP + Redis + Horizon + Scheduler) +- Healthcheck: `curl -f http://localhost:${OCTANE_PORT}/up` diff --git a/claude/core/skills/deploy-homelab/scripts/build-and-ship.sh b/claude/core/skills/deploy-homelab/scripts/build-and-ship.sh new file mode 100755 index 0000000..b52f542 --- /dev/null +++ b/claude/core/skills/deploy-homelab/scripts/build-and-ship.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# build-and-ship.sh — Build Docker image locally and ship to homelab. +# +# Usage: ./build-and-ship.sh APP_DIR IMAGE_NAME [CONTAINER_NAME] +# +# Examples: +# ./build-and-ship.sh ~/Code/lab/host.uk.com lthn-sh lthn-sh-hub +# ./build-and-ship.sh ~/Code/lab/lthn.ai lthn-ai lthn-ai +set -euo pipefail + +APP_DIR="${1:?Usage: build-and-ship.sh APP_DIR IMAGE_NAME [CONTAINER_NAME]}" +IMAGE_NAME="${2:?Usage: build-and-ship.sh APP_DIR IMAGE_NAME [CONTAINER_NAME]}" +CONTAINER_NAME="${3:-$IMAGE_NAME}" + +HOMELAB_HOST="10.69.69.165" +HOMELAB_USER="claude" +HOMELAB_PASS="claude" +TARBALL="/tmp/${IMAGE_NAME}.tar.gz" + +ssh_cmd() { + sshpass -p "$HOMELAB_PASS" ssh -o StrictHostKeyChecking=no "$HOMELAB_USER@$HOMELAB_HOST" "$@" +} + +scp_cmd() { + sshpass -p "$HOMELAB_PASS" scp -o StrictHostKeyChecking=no "$@" +} + +sudo_cmd() { + ssh_cmd "echo $HOMELAB_PASS | sudo -S $*" +} + +echo "=== Build & Ship to Homelab ===" +echo "App: $APP_DIR" +echo "Image: $IMAGE_NAME:latest" +echo "Container: $CONTAINER_NAME" +echo "Target: $HOMELAB_USER@$HOMELAB_HOST" +echo "" + +# Step 1: Build dependencies +echo "--- Step 1: Dependencies ---" +cd "$APP_DIR" +composer install --no-dev --optimize-autoloader --quiet +npm ci --silent +npm run build + +# Step 2: Docker build +echo "" +echo "--- Step 2: Docker Build (linux/amd64) ---" +docker build --platform linux/amd64 -t "${IMAGE_NAME}:latest" . + +# Step 3: Save and transfer +echo "" +echo "--- Step 3: Save & Transfer ---" +echo "Saving image..." +docker save "${IMAGE_NAME}:latest" | gzip > "$TARBALL" +SIZE=$(du -h "$TARBALL" | cut -f1) +echo "Tarball: $TARBALL ($SIZE)" + +echo "Transferring to homelab..." +scp_cmd "$TARBALL" "${HOMELAB_USER}@${HOMELAB_HOST}:/tmp/" + +# Step 4: Load on homelab +echo "" +echo "--- Step 4: Load Image ---" +sudo_cmd "docker load < /tmp/${IMAGE_NAME}.tar.gz" + +# Step 5: Restart container +echo "" +echo "--- Step 5: Restart Container ---" +sudo_cmd "docker restart $CONTAINER_NAME" 2>/dev/null || echo "Container $CONTAINER_NAME not running — start manually" + +# Step 6: Post-deploy +echo "" +echo "--- Step 6: Post-Deploy ---" +sleep 3 +sudo_cmd "docker exec $CONTAINER_NAME php artisan migrate --force" 2>/dev/null || echo "Migration skipped (container may not be running)" +sudo_cmd "docker exec $CONTAINER_NAME php artisan config:cache" 2>/dev/null || true +sudo_cmd "docker exec $CONTAINER_NAME php artisan route:cache" 2>/dev/null || true +sudo_cmd "docker exec $CONTAINER_NAME php artisan view:cache" 2>/dev/null || true + +# Step 7: Health check +echo "" +echo "--- Step 7: Health Check ---" +sleep 2 +if sudo_cmd "curl -sf http://localhost:8088/up" >/dev/null 2>&1; then + echo "Health check: OK" +else + echo "Health check: FAILED (may need manual start)" +fi + +# Cleanup +rm -f "$TARBALL" +echo "" +echo "=== Deploy Complete ===" diff --git a/claude/core/skills/deploy-production/SKILL.md b/claude/core/skills/deploy-production/SKILL.md new file mode 100644 index 0000000..8fae9e5 --- /dev/null +++ b/claude/core/skills/deploy-production/SKILL.md @@ -0,0 +1,104 @@ +--- +name: deploy-production +description: This skill should be used when the user asks to "deploy to production", "deploy to de1", "push to prod", "deploy lthn.ai", "deploy host.uk.com", or needs to deploy any website or service to the production fleet. Covers the full Ansible-based deployment pipeline. NEVER ssh directly to production. +--- + +# Deploy to Production + +All production deployments go through Ansible from `~/Code/DevOps`. NEVER ssh directly. + +## Quick Reference + +```bash +cd ~/Code/DevOps + +# Websites +ansible-playbook playbooks/deploy/website/lthn_ai.yml -l primary -e ansible_port=4819 +ansible-playbook playbooks/deploy/website/saas.yml -l primary -e ansible_port=4819 +ansible-playbook playbooks/deploy/website/core_help.yml -l primary -e ansible_port=4819 + +# Homelab (different inventory) +ansible-playbook playbooks/deploy/website/lthn_sh.yml -i inventory/linux_snider_dev.yml + +# Services +ansible-playbook playbooks/deploy/service/forgejo.yml -l primary -e ansible_port=4819 +ansible-playbook playbooks/deploy/service/authentik.yml -l primary -e ansible_port=4819 + +# Infrastructure +ansible-playbook playbooks/deploy/server/base.yml -l primary -e ansible_port=4819 --tags traefik +``` + +## Production Fleet + +| Host | IP | DC | SSH | +|------|----|----|-----| +| eu-prd-01.lthn.io (de1) | 116.202.82.115 | Falkenstein | Port 4819 | +| eu-prd-noc.lthn.io | 77.42.42.205 | Helsinki | Port 4819 | +| ap-au-syd1.lthn.io | 139.99.131.177 | Sydney | Port 4819 | + +Port 22 = Endlessh honeypot. ALWAYS use `-e ansible_port=4819`. + +## Website Deploy Pattern (Build + Ship) + +For Laravel/CorePHP apps that need local build: + +1. **Local build** (needs auth.json for paid packages): + ```bash + cd ~/Code/lab/APP_DIR + composer install --no-dev --optimize-autoloader + npm ci && npm run build + docker build --platform linux/amd64 -t IMAGE_NAME:latest . + docker save IMAGE_NAME:latest | gzip > /tmp/IMAGE_NAME.tar.gz + ``` + +2. **Ship to server**: + ```bash + scp -P 4819 /tmp/IMAGE_NAME.tar.gz root@116.202.82.115:/tmp/ + ``` + Or let the Ansible playbook handle the transfer. + +3. **Deploy via Ansible**: + ```bash + cd ~/Code/DevOps + ansible-playbook playbooks/deploy/website/PLAYBOOK.yml -l primary -e ansible_port=4819 + ``` + +4. **Verify**: + ```bash + curl -sf https://DOMAIN/up + ``` + +## Containers on de1 + +| Website | Container | Port | Domain | +|---------|-----------|------|--------| +| lthn.ai | lthn-ai | 8005/8006 | lthn.ai, api.lthn.ai, mcp.lthn.ai | +| bugseti.app | bugseti-app | 8004 | bugseti.app | +| core.help | core-help | — | core.help | +| SaaS analytics | saas-analytics | 8085 | analytics.host.uk.com | +| SaaS biolinks | saas-biolinks | 8083 | link.host.uk.com | +| SaaS pusher | saas-pusher | 8086 | notify.host.uk.com | +| SaaS socialproof | saas-socialproof | 8087 | trust.host.uk.com | +| SaaS blesta | saas-blesta | 8084 | order.host.uk.com | + +## Traefik Routing + +De1 uses Docker labels for routing (Traefik Docker provider). Each container declares its own Traefik labels in its docker-compose. Traefik auto-discovers via Docker socket. + +Homelab uses file-based routing at `/opt/noc/traefik/config/dynamic.yml`. + +## Key Rules + +- **NEVER ssh directly** — ALL operations through Ansible or ad-hoc commands +- **Port 4819** — always pass `-e ansible_port=4819` for production hosts +- **Credentials** — stored in `inventory/.credentials/` via Ansible password lookup +- **Dry run** — test with `--check` before applying +- **Existing playbooks** — ALWAYS check `playbooks/deploy/` before creating new ones +- **CLAUDE.md files** — read them at `DevOps/CLAUDE.md`, `playbooks/CLAUDE.md`, `playbooks/deploy/CLAUDE.md`, `playbooks/deploy/website/CLAUDE.md`, `roles/CLAUDE.md` + +## Gotchas + +- The lthn.ai container on de1 previously ran the FULL host.uk.com app (serving both host.uk.com and lthn.ai domains). Now lthn.ai is a separate split app. +- The host.uk.com SaaS products (analytics, biolinks, pusher, socialproof, blesta) are separate AltumCode containers, NOT part of the CorePHP app. +- host.uk.com itself does NOT have a separate container on de1 yet — it was served by the lthn-ai container. After the split, host.uk.com needs its own container or the lthn-ai playbook needs updating. +- Galera replication: de1 is bootstrap node. Don't run galera playbooks unless you understand the cluster state. diff --git a/cmd/core-agent/main.go b/go/cmd/core-agent/main.go similarity index 96% rename from cmd/core-agent/main.go rename to go/cmd/core-agent/main.go index 726b9a7..17c09d9 100644 --- a/cmd/core-agent/main.go +++ b/go/cmd/core-agent/main.go @@ -6,9 +6,9 @@ import ( "os" "path/filepath" - "forge.lthn.ai/core/agent/pkg/agentic" - "forge.lthn.ai/core/agent/pkg/brain" - "forge.lthn.ai/core/agent/pkg/monitor" + "forge.lthn.ai/core/agent/agentic" + "forge.lthn.ai/core/agent/brain" + "forge.lthn.ai/core/agent/monitor" "forge.lthn.ai/core/cli/pkg/cli" "forge.lthn.ai/core/go-process" "forge.lthn.ai/core/go/pkg/core" diff --git a/go.mod b/go/go.mod similarity index 99% rename from go.mod rename to go/go.mod index 35188b2..b15ccbb 100644 --- a/go.mod +++ b/go/go.mod @@ -1,4 +1,4 @@ -module forge.lthn.ai/core/agent +module dAppCo.re/go/agent go 1.26.0 diff --git a/go.sum b/go/go.sum similarity index 100% rename from go.sum rename to go/go.sum diff --git a/pkg/agentic/auto_pr.go b/go/pkg/agentic/auto_pr.go similarity index 100% rename from pkg/agentic/auto_pr.go rename to go/pkg/agentic/auto_pr.go diff --git a/pkg/agentic/dispatch.go b/go/pkg/agentic/dispatch.go similarity index 100% rename from pkg/agentic/dispatch.go rename to go/pkg/agentic/dispatch.go diff --git a/pkg/agentic/epic.go b/go/pkg/agentic/epic.go similarity index 100% rename from pkg/agentic/epic.go rename to go/pkg/agentic/epic.go diff --git a/pkg/agentic/events.go b/go/pkg/agentic/events.go similarity index 100% rename from pkg/agentic/events.go rename to go/pkg/agentic/events.go diff --git a/pkg/agentic/ingest.go b/go/pkg/agentic/ingest.go similarity index 100% rename from pkg/agentic/ingest.go rename to go/pkg/agentic/ingest.go diff --git a/pkg/agentic/mirror.go b/go/pkg/agentic/mirror.go similarity index 100% rename from pkg/agentic/mirror.go rename to go/pkg/agentic/mirror.go diff --git a/pkg/agentic/paths.go b/go/pkg/agentic/paths.go similarity index 100% rename from pkg/agentic/paths.go rename to go/pkg/agentic/paths.go diff --git a/pkg/agentic/paths_test.go b/go/pkg/agentic/paths_test.go similarity index 100% rename from pkg/agentic/paths_test.go rename to go/pkg/agentic/paths_test.go diff --git a/pkg/agentic/plan.go b/go/pkg/agentic/plan.go similarity index 100% rename from pkg/agentic/plan.go rename to go/pkg/agentic/plan.go diff --git a/pkg/agentic/pr.go b/go/pkg/agentic/pr.go similarity index 100% rename from pkg/agentic/pr.go rename to go/pkg/agentic/pr.go diff --git a/pkg/agentic/prep.go b/go/pkg/agentic/prep.go similarity index 99% rename from pkg/agentic/prep.go rename to go/pkg/agentic/prep.go index 48695ad..732c502 100644 --- a/pkg/agentic/prep.go +++ b/go/pkg/agentic/prep.go @@ -17,7 +17,7 @@ import ( "strings" "time" - "forge.lthn.ai/core/agent/pkg/lib" + "forge.lthn.ai/core/agent/lib" coreio "forge.lthn.ai/core/go-io" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" diff --git a/pkg/agentic/queue.go b/go/pkg/agentic/queue.go similarity index 100% rename from pkg/agentic/queue.go rename to go/pkg/agentic/queue.go diff --git a/pkg/agentic/remote.go b/go/pkg/agentic/remote.go similarity index 100% rename from pkg/agentic/remote.go rename to go/pkg/agentic/remote.go diff --git a/pkg/agentic/remote_client.go b/go/pkg/agentic/remote_client.go similarity index 100% rename from pkg/agentic/remote_client.go rename to go/pkg/agentic/remote_client.go diff --git a/pkg/agentic/remote_status.go b/go/pkg/agentic/remote_status.go similarity index 100% rename from pkg/agentic/remote_status.go rename to go/pkg/agentic/remote_status.go diff --git a/pkg/agentic/resume.go b/go/pkg/agentic/resume.go similarity index 100% rename from pkg/agentic/resume.go rename to go/pkg/agentic/resume.go diff --git a/pkg/agentic/review_queue.go b/go/pkg/agentic/review_queue.go similarity index 100% rename from pkg/agentic/review_queue.go rename to go/pkg/agentic/review_queue.go diff --git a/pkg/agentic/scan.go b/go/pkg/agentic/scan.go similarity index 100% rename from pkg/agentic/scan.go rename to go/pkg/agentic/scan.go diff --git a/pkg/agentic/status.go b/go/pkg/agentic/status.go similarity index 100% rename from pkg/agentic/status.go rename to go/pkg/agentic/status.go diff --git a/pkg/agentic/verify.go b/go/pkg/agentic/verify.go similarity index 100% rename from pkg/agentic/verify.go rename to go/pkg/agentic/verify.go diff --git a/pkg/agentic/watch.go b/go/pkg/agentic/watch.go similarity index 100% rename from pkg/agentic/watch.go rename to go/pkg/agentic/watch.go diff --git a/pkg/brain/brain.go b/go/pkg/brain/brain.go similarity index 100% rename from pkg/brain/brain.go rename to go/pkg/brain/brain.go diff --git a/pkg/brain/brain_test.go b/go/pkg/brain/brain_test.go similarity index 100% rename from pkg/brain/brain_test.go rename to go/pkg/brain/brain_test.go diff --git a/pkg/brain/direct.go b/go/pkg/brain/direct.go similarity index 99% rename from pkg/brain/direct.go rename to go/pkg/brain/direct.go index 16140f8..7bbef9c 100644 --- a/pkg/brain/direct.go +++ b/go/pkg/brain/direct.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "forge.lthn.ai/core/agent/pkg/agentic" + "forge.lthn.ai/core/agent/agentic" coreio "forge.lthn.ai/core/go-io" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" diff --git a/pkg/brain/messaging.go b/go/pkg/brain/messaging.go similarity index 100% rename from pkg/brain/messaging.go rename to go/pkg/brain/messaging.go diff --git a/pkg/brain/provider.go b/go/pkg/brain/provider.go similarity index 100% rename from pkg/brain/provider.go rename to go/pkg/brain/provider.go diff --git a/pkg/brain/tools.go b/go/pkg/brain/tools.go similarity index 95% rename from pkg/brain/tools.go rename to go/pkg/brain/tools.go index 47d1e02..93a8b31 100644 --- a/pkg/brain/tools.go +++ b/go/pkg/brain/tools.go @@ -33,17 +33,17 @@ type RememberOutput struct { // RecallInput is the input for brain_recall. type RecallInput struct { - Query string `json:"query"` - TopK int `json:"top_k,omitempty"` + Query string `json:"query"` + TopK int `json:"top_k,omitempty"` Filter RecallFilter `json:"filter,omitempty"` } // RecallFilter holds optional filter criteria for brain_recall. type RecallFilter struct { - Project string `json:"project,omitempty"` - Type any `json:"type,omitempty"` - AgentID string `json:"agent_id,omitempty"` - MinConfidence float64 `json:"min_confidence,omitempty"` + Project string `json:"project,omitempty"` + Type any `json:"type,omitempty"` + AgentID string `json:"agent_id,omitempty"` + MinConfidence float64 `json:"min_confidence,omitempty"` } // RecallOutput is the output for brain_recall. diff --git a/pkg/lib/flow/cpp.md b/go/pkg/lib/flow/cpp.md similarity index 100% rename from pkg/lib/flow/cpp.md rename to go/pkg/lib/flow/cpp.md diff --git a/pkg/lib/flow/docker.md b/go/pkg/lib/flow/docker.md similarity index 100% rename from pkg/lib/flow/docker.md rename to go/pkg/lib/flow/docker.md diff --git a/pkg/lib/flow/git.md b/go/pkg/lib/flow/git.md similarity index 100% rename from pkg/lib/flow/git.md rename to go/pkg/lib/flow/git.md diff --git a/pkg/lib/flow/go.md b/go/pkg/lib/flow/go.md similarity index 100% rename from pkg/lib/flow/go.md rename to go/pkg/lib/flow/go.md diff --git a/pkg/lib/flow/npm.md b/go/pkg/lib/flow/npm.md similarity index 100% rename from pkg/lib/flow/npm.md rename to go/pkg/lib/flow/npm.md diff --git a/pkg/lib/flow/php.md b/go/pkg/lib/flow/php.md similarity index 100% rename from pkg/lib/flow/php.md rename to go/pkg/lib/flow/php.md diff --git a/pkg/lib/flow/prod-push-polish.md b/go/pkg/lib/flow/prod-push-polish.md similarity index 100% rename from pkg/lib/flow/prod-push-polish.md rename to go/pkg/lib/flow/prod-push-polish.md diff --git a/pkg/lib/flow/py.md b/go/pkg/lib/flow/py.md similarity index 100% rename from pkg/lib/flow/py.md rename to go/pkg/lib/flow/py.md diff --git a/pkg/lib/flow/release.md b/go/pkg/lib/flow/release.md similarity index 100% rename from pkg/lib/flow/release.md rename to go/pkg/lib/flow/release.md diff --git a/pkg/lib/flow/ts.md b/go/pkg/lib/flow/ts.md similarity index 100% rename from pkg/lib/flow/ts.md rename to go/pkg/lib/flow/ts.md diff --git a/pkg/lib/lib.go b/go/pkg/lib/lib.go similarity index 100% rename from pkg/lib/lib.go rename to go/pkg/lib/lib.go diff --git a/pkg/lib/persona/ads/auditor.md b/go/pkg/lib/persona/ads/auditor.md similarity index 100% rename from pkg/lib/persona/ads/auditor.md rename to go/pkg/lib/persona/ads/auditor.md diff --git a/pkg/lib/persona/ads/creative-strategist.md b/go/pkg/lib/persona/ads/creative-strategist.md similarity index 100% rename from pkg/lib/persona/ads/creative-strategist.md rename to go/pkg/lib/persona/ads/creative-strategist.md diff --git a/pkg/lib/persona/ads/paid-social-strategist.md b/go/pkg/lib/persona/ads/paid-social-strategist.md similarity index 100% rename from pkg/lib/persona/ads/paid-social-strategist.md rename to go/pkg/lib/persona/ads/paid-social-strategist.md diff --git a/pkg/lib/persona/ads/ppc-strategist.md b/go/pkg/lib/persona/ads/ppc-strategist.md similarity index 100% rename from pkg/lib/persona/ads/ppc-strategist.md rename to go/pkg/lib/persona/ads/ppc-strategist.md diff --git a/pkg/lib/persona/ads/programmatic-buyer.md b/go/pkg/lib/persona/ads/programmatic-buyer.md similarity index 100% rename from pkg/lib/persona/ads/programmatic-buyer.md rename to go/pkg/lib/persona/ads/programmatic-buyer.md diff --git a/pkg/lib/persona/ads/search-query-analyst.md b/go/pkg/lib/persona/ads/search-query-analyst.md similarity index 100% rename from pkg/lib/persona/ads/search-query-analyst.md rename to go/pkg/lib/persona/ads/search-query-analyst.md diff --git a/pkg/lib/persona/ads/tracking-specialist.md b/go/pkg/lib/persona/ads/tracking-specialist.md similarity index 100% rename from pkg/lib/persona/ads/tracking-specialist.md rename to go/pkg/lib/persona/ads/tracking-specialist.md diff --git a/pkg/lib/persona/blockchain/identity-graph-operator.md b/go/pkg/lib/persona/blockchain/identity-graph-operator.md similarity index 100% rename from pkg/lib/persona/blockchain/identity-graph-operator.md rename to go/pkg/lib/persona/blockchain/identity-graph-operator.md diff --git a/pkg/lib/persona/blockchain/identity-trust.md b/go/pkg/lib/persona/blockchain/identity-trust.md similarity index 100% rename from pkg/lib/persona/blockchain/identity-trust.md rename to go/pkg/lib/persona/blockchain/identity-trust.md diff --git a/pkg/lib/persona/blockchain/security-auditor.md b/go/pkg/lib/persona/blockchain/security-auditor.md similarity index 100% rename from pkg/lib/persona/blockchain/security-auditor.md rename to go/pkg/lib/persona/blockchain/security-auditor.md diff --git a/pkg/lib/persona/blockchain/zk-steward.md b/go/pkg/lib/persona/blockchain/zk-steward.md similarity index 100% rename from pkg/lib/persona/blockchain/zk-steward.md rename to go/pkg/lib/persona/blockchain/zk-steward.md diff --git a/pkg/lib/persona/code/agents-orchestrator.md b/go/pkg/lib/persona/code/agents-orchestrator.md similarity index 100% rename from pkg/lib/persona/code/agents-orchestrator.md rename to go/pkg/lib/persona/code/agents-orchestrator.md diff --git a/pkg/lib/persona/code/ai-engineer.md b/go/pkg/lib/persona/code/ai-engineer.md similarity index 100% rename from pkg/lib/persona/code/ai-engineer.md rename to go/pkg/lib/persona/code/ai-engineer.md diff --git a/pkg/lib/persona/code/autonomous-optimization-architect.md b/go/pkg/lib/persona/code/autonomous-optimization-architect.md similarity index 100% rename from pkg/lib/persona/code/autonomous-optimization-architect.md rename to go/pkg/lib/persona/code/autonomous-optimization-architect.md diff --git a/pkg/lib/persona/code/backend-architect.md b/go/pkg/lib/persona/code/backend-architect.md similarity index 100% rename from pkg/lib/persona/code/backend-architect.md rename to go/pkg/lib/persona/code/backend-architect.md diff --git a/pkg/lib/persona/code/data-engineer.md b/go/pkg/lib/persona/code/data-engineer.md similarity index 100% rename from pkg/lib/persona/code/data-engineer.md rename to go/pkg/lib/persona/code/data-engineer.md diff --git a/pkg/lib/persona/code/developer-advocate.md b/go/pkg/lib/persona/code/developer-advocate.md similarity index 100% rename from pkg/lib/persona/code/developer-advocate.md rename to go/pkg/lib/persona/code/developer-advocate.md diff --git a/pkg/lib/persona/code/frontend-developer.md b/go/pkg/lib/persona/code/frontend-developer.md similarity index 100% rename from pkg/lib/persona/code/frontend-developer.md rename to go/pkg/lib/persona/code/frontend-developer.md diff --git a/pkg/lib/persona/code/lsp-index-engineer.md b/go/pkg/lib/persona/code/lsp-index-engineer.md similarity index 100% rename from pkg/lib/persona/code/lsp-index-engineer.md rename to go/pkg/lib/persona/code/lsp-index-engineer.md diff --git a/pkg/lib/persona/code/rapid-prototyper.md b/go/pkg/lib/persona/code/rapid-prototyper.md similarity index 100% rename from pkg/lib/persona/code/rapid-prototyper.md rename to go/pkg/lib/persona/code/rapid-prototyper.md diff --git a/pkg/lib/persona/code/senior-developer.md b/go/pkg/lib/persona/code/senior-developer.md similarity index 100% rename from pkg/lib/persona/code/senior-developer.md rename to go/pkg/lib/persona/code/senior-developer.md diff --git a/pkg/lib/persona/code/technical-writer.md b/go/pkg/lib/persona/code/technical-writer.md similarity index 100% rename from pkg/lib/persona/code/technical-writer.md rename to go/pkg/lib/persona/code/technical-writer.md diff --git a/pkg/lib/persona/design/brand-guardian.md b/go/pkg/lib/persona/design/brand-guardian.md similarity index 100% rename from pkg/lib/persona/design/brand-guardian.md rename to go/pkg/lib/persona/design/brand-guardian.md diff --git a/pkg/lib/persona/design/image-prompt-engineer.md b/go/pkg/lib/persona/design/image-prompt-engineer.md similarity index 100% rename from pkg/lib/persona/design/image-prompt-engineer.md rename to go/pkg/lib/persona/design/image-prompt-engineer.md diff --git a/pkg/lib/persona/design/inclusive-visuals-specialist.md b/go/pkg/lib/persona/design/inclusive-visuals-specialist.md similarity index 100% rename from pkg/lib/persona/design/inclusive-visuals-specialist.md rename to go/pkg/lib/persona/design/inclusive-visuals-specialist.md diff --git a/pkg/lib/persona/design/security-developer.md b/go/pkg/lib/persona/design/security-developer.md similarity index 100% rename from pkg/lib/persona/design/security-developer.md rename to go/pkg/lib/persona/design/security-developer.md diff --git a/pkg/lib/persona/design/ui-designer.md b/go/pkg/lib/persona/design/ui-designer.md similarity index 100% rename from pkg/lib/persona/design/ui-designer.md rename to go/pkg/lib/persona/design/ui-designer.md diff --git a/pkg/lib/persona/design/ux-architect.md b/go/pkg/lib/persona/design/ux-architect.md similarity index 100% rename from pkg/lib/persona/design/ux-architect.md rename to go/pkg/lib/persona/design/ux-architect.md diff --git a/pkg/lib/persona/design/ux-researcher.md b/go/pkg/lib/persona/design/ux-researcher.md similarity index 100% rename from pkg/lib/persona/design/ux-researcher.md rename to go/pkg/lib/persona/design/ux-researcher.md diff --git a/pkg/lib/persona/design/visual-storyteller.md b/go/pkg/lib/persona/design/visual-storyteller.md similarity index 100% rename from pkg/lib/persona/design/visual-storyteller.md rename to go/pkg/lib/persona/design/visual-storyteller.md diff --git a/pkg/lib/persona/design/whimsy-injector.md b/go/pkg/lib/persona/design/whimsy-injector.md similarity index 100% rename from pkg/lib/persona/design/whimsy-injector.md rename to go/pkg/lib/persona/design/whimsy-injector.md diff --git a/pkg/lib/persona/devops/automator.md b/go/pkg/lib/persona/devops/automator.md similarity index 100% rename from pkg/lib/persona/devops/automator.md rename to go/pkg/lib/persona/devops/automator.md diff --git a/pkg/lib/persona/devops/junior.md b/go/pkg/lib/persona/devops/junior.md similarity index 100% rename from pkg/lib/persona/devops/junior.md rename to go/pkg/lib/persona/devops/junior.md diff --git a/pkg/lib/persona/devops/security-developer.md b/go/pkg/lib/persona/devops/security-developer.md similarity index 100% rename from pkg/lib/persona/devops/security-developer.md rename to go/pkg/lib/persona/devops/security-developer.md diff --git a/pkg/lib/persona/devops/senior.md b/go/pkg/lib/persona/devops/senior.md similarity index 100% rename from pkg/lib/persona/devops/senior.md rename to go/pkg/lib/persona/devops/senior.md diff --git a/pkg/lib/persona/plan/EXECUTIVE-BRIEF.md b/go/pkg/lib/persona/plan/EXECUTIVE-BRIEF.md similarity index 100% rename from pkg/lib/persona/plan/EXECUTIVE-BRIEF.md rename to go/pkg/lib/persona/plan/EXECUTIVE-BRIEF.md diff --git a/pkg/lib/persona/plan/QUICKSTART.md b/go/pkg/lib/persona/plan/QUICKSTART.md similarity index 100% rename from pkg/lib/persona/plan/QUICKSTART.md rename to go/pkg/lib/persona/plan/QUICKSTART.md diff --git a/pkg/lib/persona/plan/coordination/agent-activation-prompts.md b/go/pkg/lib/persona/plan/coordination/agent-activation-prompts.md similarity index 100% rename from pkg/lib/persona/plan/coordination/agent-activation-prompts.md rename to go/pkg/lib/persona/plan/coordination/agent-activation-prompts.md diff --git a/pkg/lib/persona/plan/coordination/handoff-templates.md b/go/pkg/lib/persona/plan/coordination/handoff-templates.md similarity index 100% rename from pkg/lib/persona/plan/coordination/handoff-templates.md rename to go/pkg/lib/persona/plan/coordination/handoff-templates.md diff --git a/pkg/lib/persona/plan/experiment-tracker.md b/go/pkg/lib/persona/plan/experiment-tracker.md similarity index 100% rename from pkg/lib/persona/plan/experiment-tracker.md rename to go/pkg/lib/persona/plan/experiment-tracker.md diff --git a/pkg/lib/persona/plan/nexus-strategy.md b/go/pkg/lib/persona/plan/nexus-strategy.md similarity index 100% rename from pkg/lib/persona/plan/nexus-strategy.md rename to go/pkg/lib/persona/plan/nexus-strategy.md diff --git a/pkg/lib/persona/plan/playbooks/phase-0-discovery.md b/go/pkg/lib/persona/plan/playbooks/phase-0-discovery.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-0-discovery.md rename to go/pkg/lib/persona/plan/playbooks/phase-0-discovery.md diff --git a/pkg/lib/persona/plan/playbooks/phase-1-strategy.md b/go/pkg/lib/persona/plan/playbooks/phase-1-strategy.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-1-strategy.md rename to go/pkg/lib/persona/plan/playbooks/phase-1-strategy.md diff --git a/pkg/lib/persona/plan/playbooks/phase-2-foundation.md b/go/pkg/lib/persona/plan/playbooks/phase-2-foundation.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-2-foundation.md rename to go/pkg/lib/persona/plan/playbooks/phase-2-foundation.md diff --git a/pkg/lib/persona/plan/playbooks/phase-3-build.md b/go/pkg/lib/persona/plan/playbooks/phase-3-build.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-3-build.md rename to go/pkg/lib/persona/plan/playbooks/phase-3-build.md diff --git a/pkg/lib/persona/plan/playbooks/phase-4-hardening.md b/go/pkg/lib/persona/plan/playbooks/phase-4-hardening.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-4-hardening.md rename to go/pkg/lib/persona/plan/playbooks/phase-4-hardening.md diff --git a/pkg/lib/persona/plan/playbooks/phase-5-launch.md b/go/pkg/lib/persona/plan/playbooks/phase-5-launch.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-5-launch.md rename to go/pkg/lib/persona/plan/playbooks/phase-5-launch.md diff --git a/pkg/lib/persona/plan/playbooks/phase-6-operate.md b/go/pkg/lib/persona/plan/playbooks/phase-6-operate.md similarity index 100% rename from pkg/lib/persona/plan/playbooks/phase-6-operate.md rename to go/pkg/lib/persona/plan/playbooks/phase-6-operate.md diff --git a/pkg/lib/persona/plan/project-shepherd.md b/go/pkg/lib/persona/plan/project-shepherd.md similarity index 100% rename from pkg/lib/persona/plan/project-shepherd.md rename to go/pkg/lib/persona/plan/project-shepherd.md diff --git a/pkg/lib/persona/plan/runbooks/scenario-enterprise-feature.md b/go/pkg/lib/persona/plan/runbooks/scenario-enterprise-feature.md similarity index 100% rename from pkg/lib/persona/plan/runbooks/scenario-enterprise-feature.md rename to go/pkg/lib/persona/plan/runbooks/scenario-enterprise-feature.md diff --git a/pkg/lib/persona/plan/runbooks/scenario-incident-response.md b/go/pkg/lib/persona/plan/runbooks/scenario-incident-response.md similarity index 100% rename from pkg/lib/persona/plan/runbooks/scenario-incident-response.md rename to go/pkg/lib/persona/plan/runbooks/scenario-incident-response.md diff --git a/pkg/lib/persona/plan/runbooks/scenario-marketing-campaign.md b/go/pkg/lib/persona/plan/runbooks/scenario-marketing-campaign.md similarity index 100% rename from pkg/lib/persona/plan/runbooks/scenario-marketing-campaign.md rename to go/pkg/lib/persona/plan/runbooks/scenario-marketing-campaign.md diff --git a/pkg/lib/persona/plan/runbooks/scenario-startup-mvp.md b/go/pkg/lib/persona/plan/runbooks/scenario-startup-mvp.md similarity index 100% rename from pkg/lib/persona/plan/runbooks/scenario-startup-mvp.md rename to go/pkg/lib/persona/plan/runbooks/scenario-startup-mvp.md diff --git a/pkg/lib/persona/plan/senior.md b/go/pkg/lib/persona/plan/senior.md similarity index 100% rename from pkg/lib/persona/plan/senior.md rename to go/pkg/lib/persona/plan/senior.md diff --git a/pkg/lib/persona/plan/studio-operations.md b/go/pkg/lib/persona/plan/studio-operations.md similarity index 100% rename from pkg/lib/persona/plan/studio-operations.md rename to go/pkg/lib/persona/plan/studio-operations.md diff --git a/pkg/lib/persona/plan/studio-producer.md b/go/pkg/lib/persona/plan/studio-producer.md similarity index 100% rename from pkg/lib/persona/plan/studio-producer.md rename to go/pkg/lib/persona/plan/studio-producer.md diff --git a/pkg/lib/persona/product/behavioral-nudge-engine.md b/go/pkg/lib/persona/product/behavioral-nudge-engine.md similarity index 100% rename from pkg/lib/persona/product/behavioral-nudge-engine.md rename to go/pkg/lib/persona/product/behavioral-nudge-engine.md diff --git a/pkg/lib/persona/product/feedback-synthesizer.md b/go/pkg/lib/persona/product/feedback-synthesizer.md similarity index 100% rename from pkg/lib/persona/product/feedback-synthesizer.md rename to go/pkg/lib/persona/product/feedback-synthesizer.md diff --git a/pkg/lib/persona/product/security-developer.md b/go/pkg/lib/persona/product/security-developer.md similarity index 100% rename from pkg/lib/persona/product/security-developer.md rename to go/pkg/lib/persona/product/security-developer.md diff --git a/pkg/lib/persona/product/sprint-prioritizer.md b/go/pkg/lib/persona/product/sprint-prioritizer.md similarity index 100% rename from pkg/lib/persona/product/sprint-prioritizer.md rename to go/pkg/lib/persona/product/sprint-prioritizer.md diff --git a/pkg/lib/persona/product/trend-researcher.md b/go/pkg/lib/persona/product/trend-researcher.md similarity index 100% rename from pkg/lib/persona/product/trend-researcher.md rename to go/pkg/lib/persona/product/trend-researcher.md diff --git a/pkg/lib/persona/sales/account-strategist.md b/go/pkg/lib/persona/sales/account-strategist.md similarity index 100% rename from pkg/lib/persona/sales/account-strategist.md rename to go/pkg/lib/persona/sales/account-strategist.md diff --git a/pkg/lib/persona/sales/coach.md b/go/pkg/lib/persona/sales/coach.md similarity index 100% rename from pkg/lib/persona/sales/coach.md rename to go/pkg/lib/persona/sales/coach.md diff --git a/pkg/lib/persona/sales/deal-strategist.md b/go/pkg/lib/persona/sales/deal-strategist.md similarity index 100% rename from pkg/lib/persona/sales/deal-strategist.md rename to go/pkg/lib/persona/sales/deal-strategist.md diff --git a/pkg/lib/persona/sales/discovery-coach.md b/go/pkg/lib/persona/sales/discovery-coach.md similarity index 100% rename from pkg/lib/persona/sales/discovery-coach.md rename to go/pkg/lib/persona/sales/discovery-coach.md diff --git a/pkg/lib/persona/sales/engineer.md b/go/pkg/lib/persona/sales/engineer.md similarity index 100% rename from pkg/lib/persona/sales/engineer.md rename to go/pkg/lib/persona/sales/engineer.md diff --git a/pkg/lib/persona/sales/outbound-strategist.md b/go/pkg/lib/persona/sales/outbound-strategist.md similarity index 100% rename from pkg/lib/persona/sales/outbound-strategist.md rename to go/pkg/lib/persona/sales/outbound-strategist.md diff --git a/pkg/lib/persona/sales/pipeline-analyst.md b/go/pkg/lib/persona/sales/pipeline-analyst.md similarity index 100% rename from pkg/lib/persona/sales/pipeline-analyst.md rename to go/pkg/lib/persona/sales/pipeline-analyst.md diff --git a/pkg/lib/persona/sales/proposal-strategist.md b/go/pkg/lib/persona/sales/proposal-strategist.md similarity index 100% rename from pkg/lib/persona/sales/proposal-strategist.md rename to go/pkg/lib/persona/sales/proposal-strategist.md diff --git a/pkg/lib/persona/secops/architect.md b/go/pkg/lib/persona/secops/architect.md similarity index 100% rename from pkg/lib/persona/secops/architect.md rename to go/pkg/lib/persona/secops/architect.md diff --git a/pkg/lib/persona/secops/developer.md b/go/pkg/lib/persona/secops/developer.md similarity index 100% rename from pkg/lib/persona/secops/developer.md rename to go/pkg/lib/persona/secops/developer.md diff --git a/pkg/lib/persona/secops/devops.md b/go/pkg/lib/persona/secops/devops.md similarity index 100% rename from pkg/lib/persona/secops/devops.md rename to go/pkg/lib/persona/secops/devops.md diff --git a/pkg/lib/persona/secops/incident-commander.md b/go/pkg/lib/persona/secops/incident-commander.md similarity index 100% rename from pkg/lib/persona/secops/incident-commander.md rename to go/pkg/lib/persona/secops/incident-commander.md diff --git a/pkg/lib/persona/secops/junior.md b/go/pkg/lib/persona/secops/junior.md similarity index 100% rename from pkg/lib/persona/secops/junior.md rename to go/pkg/lib/persona/secops/junior.md diff --git a/pkg/lib/persona/secops/operations.md b/go/pkg/lib/persona/secops/operations.md similarity index 100% rename from pkg/lib/persona/secops/operations.md rename to go/pkg/lib/persona/secops/operations.md diff --git a/pkg/lib/persona/secops/senior.md b/go/pkg/lib/persona/secops/senior.md similarity index 100% rename from pkg/lib/persona/secops/senior.md rename to go/pkg/lib/persona/secops/senior.md diff --git a/pkg/lib/persona/smm/carousel-growth-engine.md b/go/pkg/lib/persona/smm/carousel-growth-engine.md similarity index 100% rename from pkg/lib/persona/smm/carousel-growth-engine.md rename to go/pkg/lib/persona/smm/carousel-growth-engine.md diff --git a/pkg/lib/persona/smm/content-creator.md b/go/pkg/lib/persona/smm/content-creator.md similarity index 100% rename from pkg/lib/persona/smm/content-creator.md rename to go/pkg/lib/persona/smm/content-creator.md diff --git a/pkg/lib/persona/smm/cultural-intelligence.md b/go/pkg/lib/persona/smm/cultural-intelligence.md similarity index 100% rename from pkg/lib/persona/smm/cultural-intelligence.md rename to go/pkg/lib/persona/smm/cultural-intelligence.md diff --git a/pkg/lib/persona/smm/growth-hacker.md b/go/pkg/lib/persona/smm/growth-hacker.md similarity index 100% rename from pkg/lib/persona/smm/growth-hacker.md rename to go/pkg/lib/persona/smm/growth-hacker.md diff --git a/pkg/lib/persona/smm/instagram-curator.md b/go/pkg/lib/persona/smm/instagram-curator.md similarity index 100% rename from pkg/lib/persona/smm/instagram-curator.md rename to go/pkg/lib/persona/smm/instagram-curator.md diff --git a/pkg/lib/persona/smm/linkedin-content-creator.md b/go/pkg/lib/persona/smm/linkedin-content-creator.md similarity index 100% rename from pkg/lib/persona/smm/linkedin-content-creator.md rename to go/pkg/lib/persona/smm/linkedin-content-creator.md diff --git a/pkg/lib/persona/smm/reddit-community-builder.md b/go/pkg/lib/persona/smm/reddit-community-builder.md similarity index 100% rename from pkg/lib/persona/smm/reddit-community-builder.md rename to go/pkg/lib/persona/smm/reddit-community-builder.md diff --git a/pkg/lib/persona/smm/security-developer.md b/go/pkg/lib/persona/smm/security-developer.md similarity index 100% rename from pkg/lib/persona/smm/security-developer.md rename to go/pkg/lib/persona/smm/security-developer.md diff --git a/pkg/lib/persona/smm/security-secops.md b/go/pkg/lib/persona/smm/security-secops.md similarity index 100% rename from pkg/lib/persona/smm/security-secops.md rename to go/pkg/lib/persona/smm/security-secops.md diff --git a/pkg/lib/persona/smm/seo-specialist.md b/go/pkg/lib/persona/smm/seo-specialist.md similarity index 100% rename from pkg/lib/persona/smm/seo-specialist.md rename to go/pkg/lib/persona/smm/seo-specialist.md diff --git a/pkg/lib/persona/smm/social-media-strategist.md b/go/pkg/lib/persona/smm/social-media-strategist.md similarity index 100% rename from pkg/lib/persona/smm/social-media-strategist.md rename to go/pkg/lib/persona/smm/social-media-strategist.md diff --git a/pkg/lib/persona/smm/tiktok-strategist.md b/go/pkg/lib/persona/smm/tiktok-strategist.md similarity index 100% rename from pkg/lib/persona/smm/tiktok-strategist.md rename to go/pkg/lib/persona/smm/tiktok-strategist.md diff --git a/pkg/lib/persona/smm/twitter-engager.md b/go/pkg/lib/persona/smm/twitter-engager.md similarity index 100% rename from pkg/lib/persona/smm/twitter-engager.md rename to go/pkg/lib/persona/smm/twitter-engager.md diff --git a/pkg/lib/persona/spatial/macos-spatial-metal-engineer.md b/go/pkg/lib/persona/spatial/macos-spatial-metal-engineer.md similarity index 100% rename from pkg/lib/persona/spatial/macos-spatial-metal-engineer.md rename to go/pkg/lib/persona/spatial/macos-spatial-metal-engineer.md diff --git a/pkg/lib/persona/spatial/terminal-integration-specialist.md b/go/pkg/lib/persona/spatial/terminal-integration-specialist.md similarity index 100% rename from pkg/lib/persona/spatial/terminal-integration-specialist.md rename to go/pkg/lib/persona/spatial/terminal-integration-specialist.md diff --git a/pkg/lib/persona/support/accounts-payable.md b/go/pkg/lib/persona/support/accounts-payable.md similarity index 100% rename from pkg/lib/persona/support/accounts-payable.md rename to go/pkg/lib/persona/support/accounts-payable.md diff --git a/pkg/lib/persona/support/analytics-reporter.md b/go/pkg/lib/persona/support/analytics-reporter.md similarity index 100% rename from pkg/lib/persona/support/analytics-reporter.md rename to go/pkg/lib/persona/support/analytics-reporter.md diff --git a/pkg/lib/persona/support/compliance-auditor.md b/go/pkg/lib/persona/support/compliance-auditor.md similarity index 100% rename from pkg/lib/persona/support/compliance-auditor.md rename to go/pkg/lib/persona/support/compliance-auditor.md diff --git a/pkg/lib/persona/support/executive-summary-generator.md b/go/pkg/lib/persona/support/executive-summary-generator.md similarity index 100% rename from pkg/lib/persona/support/executive-summary-generator.md rename to go/pkg/lib/persona/support/executive-summary-generator.md diff --git a/pkg/lib/persona/support/finance-tracker.md b/go/pkg/lib/persona/support/finance-tracker.md similarity index 100% rename from pkg/lib/persona/support/finance-tracker.md rename to go/pkg/lib/persona/support/finance-tracker.md diff --git a/pkg/lib/persona/support/infrastructure-maintainer.md b/go/pkg/lib/persona/support/infrastructure-maintainer.md similarity index 100% rename from pkg/lib/persona/support/infrastructure-maintainer.md rename to go/pkg/lib/persona/support/infrastructure-maintainer.md diff --git a/pkg/lib/persona/support/legal-compliance-checker.md b/go/pkg/lib/persona/support/legal-compliance-checker.md similarity index 100% rename from pkg/lib/persona/support/legal-compliance-checker.md rename to go/pkg/lib/persona/support/legal-compliance-checker.md diff --git a/pkg/lib/persona/support/responder.md b/go/pkg/lib/persona/support/responder.md similarity index 100% rename from pkg/lib/persona/support/responder.md rename to go/pkg/lib/persona/support/responder.md diff --git a/pkg/lib/persona/support/security-developer.md b/go/pkg/lib/persona/support/security-developer.md similarity index 100% rename from pkg/lib/persona/support/security-developer.md rename to go/pkg/lib/persona/support/security-developer.md diff --git a/pkg/lib/persona/support/security-secops.md b/go/pkg/lib/persona/support/security-secops.md similarity index 100% rename from pkg/lib/persona/support/security-secops.md rename to go/pkg/lib/persona/support/security-secops.md diff --git a/pkg/lib/persona/testing/accessibility-auditor.md b/go/pkg/lib/persona/testing/accessibility-auditor.md similarity index 100% rename from pkg/lib/persona/testing/accessibility-auditor.md rename to go/pkg/lib/persona/testing/accessibility-auditor.md diff --git a/pkg/lib/persona/testing/api-tester.md b/go/pkg/lib/persona/testing/api-tester.md similarity index 100% rename from pkg/lib/persona/testing/api-tester.md rename to go/pkg/lib/persona/testing/api-tester.md diff --git a/pkg/lib/persona/testing/evidence-collector.md b/go/pkg/lib/persona/testing/evidence-collector.md similarity index 100% rename from pkg/lib/persona/testing/evidence-collector.md rename to go/pkg/lib/persona/testing/evidence-collector.md diff --git a/pkg/lib/persona/testing/model-qa.md b/go/pkg/lib/persona/testing/model-qa.md similarity index 100% rename from pkg/lib/persona/testing/model-qa.md rename to go/pkg/lib/persona/testing/model-qa.md diff --git a/pkg/lib/persona/testing/performance-benchmarker.md b/go/pkg/lib/persona/testing/performance-benchmarker.md similarity index 100% rename from pkg/lib/persona/testing/performance-benchmarker.md rename to go/pkg/lib/persona/testing/performance-benchmarker.md diff --git a/pkg/lib/persona/testing/reality-checker.md b/go/pkg/lib/persona/testing/reality-checker.md similarity index 100% rename from pkg/lib/persona/testing/reality-checker.md rename to go/pkg/lib/persona/testing/reality-checker.md diff --git a/pkg/lib/persona/testing/security-developer.md b/go/pkg/lib/persona/testing/security-developer.md similarity index 100% rename from pkg/lib/persona/testing/security-developer.md rename to go/pkg/lib/persona/testing/security-developer.md diff --git a/pkg/lib/persona/testing/test-results-analyzer.md b/go/pkg/lib/persona/testing/test-results-analyzer.md similarity index 100% rename from pkg/lib/persona/testing/test-results-analyzer.md rename to go/pkg/lib/persona/testing/test-results-analyzer.md diff --git a/pkg/lib/persona/testing/tool-evaluator.md b/go/pkg/lib/persona/testing/tool-evaluator.md similarity index 100% rename from pkg/lib/persona/testing/tool-evaluator.md rename to go/pkg/lib/persona/testing/tool-evaluator.md diff --git a/pkg/lib/persona/testing/workflow-optimizer.md b/go/pkg/lib/persona/testing/workflow-optimizer.md similarity index 100% rename from pkg/lib/persona/testing/workflow-optimizer.md rename to go/pkg/lib/persona/testing/workflow-optimizer.md diff --git a/pkg/lib/prompt/coding.md b/go/pkg/lib/prompt/coding.md similarity index 100% rename from pkg/lib/prompt/coding.md rename to go/pkg/lib/prompt/coding.md diff --git a/pkg/lib/prompt/conventions.md b/go/pkg/lib/prompt/conventions.md similarity index 100% rename from pkg/lib/prompt/conventions.md rename to go/pkg/lib/prompt/conventions.md diff --git a/pkg/lib/prompt/default.md b/go/pkg/lib/prompt/default.md similarity index 100% rename from pkg/lib/prompt/default.md rename to go/pkg/lib/prompt/default.md diff --git a/pkg/lib/prompt/security.md b/go/pkg/lib/prompt/security.md similarity index 100% rename from pkg/lib/prompt/security.md rename to go/pkg/lib/prompt/security.md diff --git a/pkg/lib/prompt/verify.md b/go/pkg/lib/prompt/verify.md similarity index 100% rename from pkg/lib/prompt/verify.md rename to go/pkg/lib/prompt/verify.md diff --git a/pkg/lib/task/api-consistency.yaml b/go/pkg/lib/task/api-consistency.yaml similarity index 100% rename from pkg/lib/task/api-consistency.yaml rename to go/pkg/lib/task/api-consistency.yaml diff --git a/pkg/lib/task/bug-fix.yaml b/go/pkg/lib/task/bug-fix.yaml similarity index 100% rename from pkg/lib/task/bug-fix.yaml rename to go/pkg/lib/task/bug-fix.yaml diff --git a/pkg/lib/task/code/dead-code.md b/go/pkg/lib/task/code/dead-code.md similarity index 100% rename from pkg/lib/task/code/dead-code.md rename to go/pkg/lib/task/code/dead-code.md diff --git a/pkg/lib/task/code/refactor.md b/go/pkg/lib/task/code/refactor.md similarity index 100% rename from pkg/lib/task/code/refactor.md rename to go/pkg/lib/task/code/refactor.md diff --git a/pkg/lib/task/code/review.md b/go/pkg/lib/task/code/review.md similarity index 100% rename from pkg/lib/task/code/review.md rename to go/pkg/lib/task/code/review.md diff --git a/pkg/lib/task/code/review/conventions.md b/go/pkg/lib/task/code/review/conventions.md similarity index 100% rename from pkg/lib/task/code/review/conventions.md rename to go/pkg/lib/task/code/review/conventions.md diff --git a/pkg/lib/task/code/review/plan.yaml b/go/pkg/lib/task/code/review/plan.yaml similarity index 100% rename from pkg/lib/task/code/review/plan.yaml rename to go/pkg/lib/task/code/review/plan.yaml diff --git a/pkg/lib/task/code/review/severity.md b/go/pkg/lib/task/code/review/severity.md similarity index 100% rename from pkg/lib/task/code/review/severity.md rename to go/pkg/lib/task/code/review/severity.md diff --git a/pkg/lib/task/code/simplifier.md b/go/pkg/lib/task/code/simplifier.md similarity index 100% rename from pkg/lib/task/code/simplifier.md rename to go/pkg/lib/task/code/simplifier.md diff --git a/pkg/lib/task/code/simplifier/patterns.md b/go/pkg/lib/task/code/simplifier/patterns.md similarity index 100% rename from pkg/lib/task/code/simplifier/patterns.md rename to go/pkg/lib/task/code/simplifier/patterns.md diff --git a/pkg/lib/task/code/test-gaps.md b/go/pkg/lib/task/code/test-gaps.md similarity index 100% rename from pkg/lib/task/code/test-gaps.md rename to go/pkg/lib/task/code/test-gaps.md diff --git a/pkg/lib/task/dependency-audit.yaml b/go/pkg/lib/task/dependency-audit.yaml similarity index 100% rename from pkg/lib/task/dependency-audit.yaml rename to go/pkg/lib/task/dependency-audit.yaml diff --git a/pkg/lib/task/doc-sync.yaml b/go/pkg/lib/task/doc-sync.yaml similarity index 100% rename from pkg/lib/task/doc-sync.yaml rename to go/pkg/lib/task/doc-sync.yaml diff --git a/pkg/lib/task/feature-port.yaml b/go/pkg/lib/task/feature-port.yaml similarity index 100% rename from pkg/lib/task/feature-port.yaml rename to go/pkg/lib/task/feature-port.yaml diff --git a/pkg/lib/task/new-feature.yaml b/go/pkg/lib/task/new-feature.yaml similarity index 100% rename from pkg/lib/task/new-feature.yaml rename to go/pkg/lib/task/new-feature.yaml diff --git a/pkg/lib/workspace/default/.gitignore b/go/pkg/lib/workspace/default/.gitignore similarity index 100% rename from pkg/lib/workspace/default/.gitignore rename to go/pkg/lib/workspace/default/.gitignore diff --git a/pkg/lib/workspace/default/CLAUDE.md.tmpl b/go/pkg/lib/workspace/default/CLAUDE.md.tmpl similarity index 100% rename from pkg/lib/workspace/default/CLAUDE.md.tmpl rename to go/pkg/lib/workspace/default/CLAUDE.md.tmpl diff --git a/pkg/lib/workspace/default/CONTEXT.md.tmpl b/go/pkg/lib/workspace/default/CONTEXT.md.tmpl similarity index 100% rename from pkg/lib/workspace/default/CONTEXT.md.tmpl rename to go/pkg/lib/workspace/default/CONTEXT.md.tmpl diff --git a/pkg/lib/workspace/default/PROMPT.md.tmpl b/go/pkg/lib/workspace/default/PROMPT.md.tmpl similarity index 100% rename from pkg/lib/workspace/default/PROMPT.md.tmpl rename to go/pkg/lib/workspace/default/PROMPT.md.tmpl diff --git a/pkg/lib/workspace/default/TODO.md.tmpl b/go/pkg/lib/workspace/default/TODO.md.tmpl similarity index 100% rename from pkg/lib/workspace/default/TODO.md.tmpl rename to go/pkg/lib/workspace/default/TODO.md.tmpl diff --git a/pkg/lib/workspace/review/CLAUDE.md.tmpl b/go/pkg/lib/workspace/review/CLAUDE.md.tmpl similarity index 100% rename from pkg/lib/workspace/review/CLAUDE.md.tmpl rename to go/pkg/lib/workspace/review/CLAUDE.md.tmpl diff --git a/pkg/lib/workspace/review/REVIEW.md.tmpl b/go/pkg/lib/workspace/review/REVIEW.md.tmpl similarity index 100% rename from pkg/lib/workspace/review/REVIEW.md.tmpl rename to go/pkg/lib/workspace/review/REVIEW.md.tmpl diff --git a/pkg/lib/workspace/security/CLAUDE.md.tmpl b/go/pkg/lib/workspace/security/CLAUDE.md.tmpl similarity index 100% rename from pkg/lib/workspace/security/CLAUDE.md.tmpl rename to go/pkg/lib/workspace/security/CLAUDE.md.tmpl diff --git a/pkg/lib/workspace/security/FINDINGS.md.tmpl b/go/pkg/lib/workspace/security/FINDINGS.md.tmpl similarity index 100% rename from pkg/lib/workspace/security/FINDINGS.md.tmpl rename to go/pkg/lib/workspace/security/FINDINGS.md.tmpl diff --git a/pkg/lib/workspace/security/TODO.md.tmpl b/go/pkg/lib/workspace/security/TODO.md.tmpl similarity index 100% rename from pkg/lib/workspace/security/TODO.md.tmpl rename to go/pkg/lib/workspace/security/TODO.md.tmpl diff --git a/pkg/monitor/monitor.go b/go/pkg/monitor/monitor.go similarity index 99% rename from pkg/monitor/monitor.go rename to go/pkg/monitor/monitor.go index e1f607a..1531498 100644 --- a/pkg/monitor/monitor.go +++ b/go/pkg/monitor/monitor.go @@ -20,7 +20,7 @@ import ( "sync" "time" - "forge.lthn.ai/core/agent/pkg/agentic" + "forge.lthn.ai/core/agent/agentic" coreio "forge.lthn.ai/core/go-io" coreerr "forge.lthn.ai/core/go-log" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -378,4 +378,3 @@ func (m *Subsystem) agentStatusResource(ctx context.Context, req *mcp.ReadResour }, }, nil } - diff --git a/pkg/monitor/sync.go b/go/pkg/monitor/sync.go similarity index 98% rename from pkg/monitor/sync.go rename to go/pkg/monitor/sync.go index 846ff45..7e3da0e 100644 --- a/pkg/monitor/sync.go +++ b/go/pkg/monitor/sync.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "forge.lthn.ai/core/agent/pkg/agentic" + "forge.lthn.ai/core/agent/agentic" coreio "forge.lthn.ai/core/go-io" ) diff --git a/go/pkg/setup/config.go b/go/pkg/setup/config.go new file mode 100644 index 0000000..e3d289e --- /dev/null +++ b/go/pkg/setup/config.go @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package setup + +import ( + "os/exec" + "path/filepath" + "strings" + + "forge.lthn.ai/core/agent/lib" +) + +// ConfigData holds the data passed to config templates. +type ConfigData struct { + Name string + Description string + Type string + Module string + Repository string + GoVersion string + Targets []Target + Commands []Command + Env map[string]string +} + +// Target is a build target (os/arch pair). +type Target struct { + OS string + Arch string +} + +// Command is a named runnable command. +type Command struct { + Name string + Run string +} + +// GenerateBuildConfig renders a build.yaml for the detected project type. +func GenerateBuildConfig(path string, projType ProjectType) (string, error) { + name := filepath.Base(path) + data := map[string]any{ + "Comment": name + " build configuration", + "Sections": []map[string]any{ + { + "Key": "project", + "Values": []map[string]any{ + {"Key": "name", "Value": name}, + {"Key": "type", "Value": string(projType)}, + }, + }, + }, + } + + switch projType { + case TypeGo, TypeWails: + data["Sections"] = append(data["Sections"].([]map[string]any), + map[string]any{ + "Key": "build", + "Values": []map[string]any{ + {"Key": "main", "Value": "./cmd/" + name}, + {"Key": "binary", "Value": name}, + {"Key": "cgo", "Value": "false"}, + }, + }, + ) + case TypePHP: + data["Sections"] = append(data["Sections"].([]map[string]any), + map[string]any{ + "Key": "build", + "Values": []map[string]any{ + {"Key": "dockerfile", "Value": "Dockerfile"}, + {"Key": "image", "Value": name}, + }, + }, + ) + case TypeNode: + data["Sections"] = append(data["Sections"].([]map[string]any), + map[string]any{ + "Key": "build", + "Values": []map[string]any{ + {"Key": "script", "Value": "npm run build"}, + {"Key": "output", "Value": "dist"}, + }, + }, + ) + } + + return lib.RenderFile("yaml/config", data) +} + +// GenerateTestConfig renders a test.yaml for the detected project type. +func GenerateTestConfig(projType ProjectType) (string, error) { + data := map[string]any{ + "Comment": "Test configuration", + } + + switch projType { + case TypeGo, TypeWails: + data["Sections"] = []map[string]any{ + { + "Key": "commands", + "Values": []map[string]any{ + {"Key": "unit", "Value": "go test ./..."}, + {"Key": "coverage", "Value": "go test -coverprofile=coverage.out ./..."}, + {"Key": "race", "Value": "go test -race ./..."}, + }, + }, + } + case TypePHP: + data["Sections"] = []map[string]any{ + { + "Key": "commands", + "Values": []map[string]any{ + {"Key": "unit", "Value": "vendor/bin/pest --parallel"}, + {"Key": "lint", "Value": "vendor/bin/pint --test"}, + }, + }, + } + case TypeNode: + data["Sections"] = []map[string]any{ + { + "Key": "commands", + "Values": []map[string]any{ + {"Key": "unit", "Value": "npm test"}, + {"Key": "lint", "Value": "npm run lint"}, + }, + }, + } + } + + return lib.RenderFile("yaml/config", data) +} + +// detectGitRemote extracts owner/repo from git remote origin. +func detectGitRemote() string { + cmd := exec.Command("git", "remote", "get-url", "origin") + output, err := cmd.Output() + if err != nil { + return "" + } + url := strings.TrimSpace(string(output)) + + // SSH: git@github.com:owner/repo.git or ssh://git@forge.lthn.ai:2223/core/agent.git + if strings.Contains(url, ":") { + parts := strings.SplitN(url, ":", 2) + if len(parts) == 2 { + repo := parts[1] + repo = strings.TrimSuffix(repo, ".git") + // Handle port in SSH URL (ssh://git@host:port/path) + if strings.Contains(repo, "/") { + segments := strings.SplitN(repo, "/", 2) + if len(segments) == 2 && strings.ContainsAny(segments[0], "0123456789") { + repo = segments[1] + } + } + return repo + } + } + + // HTTPS: https://github.com/owner/repo.git + for _, host := range []string{"github.com/", "forge.lthn.ai/"} { + if idx := strings.Index(url, host); idx >= 0 { + repo := url[idx+len(host):] + return strings.TrimSuffix(repo, ".git") + } + } + + return "" +} diff --git a/go/pkg/setup/detect.go b/go/pkg/setup/detect.go new file mode 100644 index 0000000..6aaaf31 --- /dev/null +++ b/go/pkg/setup/detect.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// Package setup provides workspace setup and scaffolding using lib templates. +package setup + +import ( + "os" + "path/filepath" +) + +// ProjectType identifies what kind of project lives at a path. +type ProjectType string + +const ( + TypeGo ProjectType = "go" + TypePHP ProjectType = "php" + TypeNode ProjectType = "node" + TypeWails ProjectType = "wails" + TypeUnknown ProjectType = "unknown" +) + +// Detect identifies the project type from files present at the given path. +func Detect(path string) ProjectType { + checks := []struct { + file string + projType ProjectType + }{ + {"wails.json", TypeWails}, + {"go.mod", TypeGo}, + {"composer.json", TypePHP}, + {"package.json", TypeNode}, + } + for _, c := range checks { + if _, err := os.Stat(filepath.Join(path, c.file)); err == nil { + return c.projType + } + } + return TypeUnknown +} + +// DetectAll returns all project types found at the path (polyglot repos). +func DetectAll(path string) []ProjectType { + var types []ProjectType + all := []struct { + file string + projType ProjectType + }{ + {"go.mod", TypeGo}, + {"composer.json", TypePHP}, + {"package.json", TypeNode}, + {"wails.json", TypeWails}, + } + for _, c := range all { + if _, err := os.Stat(filepath.Join(path, c.file)); err == nil { + types = append(types, c.projType) + } + } + return types +} diff --git a/go/pkg/setup/setup.go b/go/pkg/setup/setup.go new file mode 100644 index 0000000..4297336 --- /dev/null +++ b/go/pkg/setup/setup.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package setup + +import ( + "fmt" + "os" + "path/filepath" + + "forge.lthn.ai/core/agent/lib" +) + +// Options controls setup behaviour. +type Options struct { + Path string // Target directory (default: cwd) + DryRun bool // Preview only, don't write + Force bool // Overwrite existing files + Template string // Dir template to use (agent, php, go, gui) +} + +// Run performs the workspace setup at the given path. +// It detects the project type, generates .core/ configs, +// and optionally scaffolds a workspace from a dir template. +func Run(opts Options) error { + if opts.Path == "" { + var err error + opts.Path, err = os.Getwd() + if err != nil { + return fmt.Errorf("setup: %w", err) + } + } + + projType := Detect(opts.Path) + allTypes := DetectAll(opts.Path) + + fmt.Printf("Project: %s\n", filepath.Base(opts.Path)) + fmt.Printf("Type: %s\n", projType) + if len(allTypes) > 1 { + fmt.Printf("Also: %v (polyglot)\n", allTypes) + } + + // Generate .core/ config files + if err := setupCoreDir(opts, projType); err != nil { + return err + } + + // Scaffold from dir template if requested + if opts.Template != "" { + return scaffoldTemplate(opts, projType) + } + + return nil +} + +// setupCoreDir creates .core/ with build.yaml and test.yaml. +func setupCoreDir(opts Options, projType ProjectType) error { + coreDir := filepath.Join(opts.Path, ".core") + + if opts.DryRun { + fmt.Printf("\nWould create %s/\n", coreDir) + } else { + if err := os.MkdirAll(coreDir, 0755); err != nil { + return fmt.Errorf("setup: create .core: %w", err) + } + } + + // build.yaml + buildConfig, err := GenerateBuildConfig(opts.Path, projType) + if err != nil { + return fmt.Errorf("setup: build config: %w", err) + } + if err := writeConfig(filepath.Join(coreDir, "build.yaml"), buildConfig, opts); err != nil { + return err + } + + // test.yaml + testConfig, err := GenerateTestConfig(projType) + if err != nil { + return fmt.Errorf("setup: test config: %w", err) + } + if err := writeConfig(filepath.Join(coreDir, "test.yaml"), testConfig, opts); err != nil { + return err + } + + return nil +} + +// scaffoldTemplate extracts a dir template into the target path. +func scaffoldTemplate(opts Options, projType ProjectType) error { + tmplName := opts.Template + if tmplName == "auto" { + switch projType { + case TypeGo, TypeWails: + tmplName = "go" + case TypePHP: + tmplName = "php" + case TypeNode: + tmplName = "gui" + default: + tmplName = "agent" + } + } + + fmt.Printf("Template: %s\n", tmplName) + + data := map[string]any{ + "Name": filepath.Base(opts.Path), + "Module": detectGitRemote(), + "Namespace": "App", + "ViewNamespace": filepath.Base(opts.Path), + "RouteName": filepath.Base(opts.Path), + "GoVersion": "1.26", + "HasAdmin": true, + "HasApi": true, + "HasConsole": true, + } + + if opts.DryRun { + fmt.Printf("Would extract template/%s to %s\n", tmplName, opts.Path) + files := lib.ListDirTemplates() + for _, f := range files { + if f == tmplName { + fmt.Printf(" Template found: %s\n", f) + } + } + return nil + } + + return lib.ExtractDir(tmplName, opts.Path, data) +} + +func writeConfig(path, content string, opts Options) error { + if opts.DryRun { + fmt.Printf(" %s\n", path) + return nil + } + + if !opts.Force { + if _, err := os.Stat(path); err == nil { + fmt.Printf(" skip %s (exists, use --force to overwrite)\n", filepath.Base(path)) + return nil + } + } + + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + return fmt.Errorf("setup: write %s: %w", filepath.Base(path), err) + } + fmt.Printf(" created %s\n", path) + return nil +} diff --git a/src/php/AGENTS.md b/php/AGENTS.md similarity index 100% rename from src/php/AGENTS.md rename to php/AGENTS.md diff --git a/src/php/Actions/Brain/ForgetKnowledge.php b/php/Actions/Brain/ForgetKnowledge.php similarity index 100% rename from src/php/Actions/Brain/ForgetKnowledge.php rename to php/Actions/Brain/ForgetKnowledge.php diff --git a/src/php/Actions/Brain/ListKnowledge.php b/php/Actions/Brain/ListKnowledge.php similarity index 100% rename from src/php/Actions/Brain/ListKnowledge.php rename to php/Actions/Brain/ListKnowledge.php diff --git a/src/php/Actions/Brain/RecallKnowledge.php b/php/Actions/Brain/RecallKnowledge.php similarity index 100% rename from src/php/Actions/Brain/RecallKnowledge.php rename to php/Actions/Brain/RecallKnowledge.php diff --git a/src/php/Actions/Brain/RememberKnowledge.php b/php/Actions/Brain/RememberKnowledge.php similarity index 100% rename from src/php/Actions/Brain/RememberKnowledge.php rename to php/Actions/Brain/RememberKnowledge.php diff --git a/src/php/Actions/Forge/AssignAgent.php b/php/Actions/Forge/AssignAgent.php similarity index 100% rename from src/php/Actions/Forge/AssignAgent.php rename to php/Actions/Forge/AssignAgent.php diff --git a/src/php/Actions/Forge/CreatePlanFromIssue.php b/php/Actions/Forge/CreatePlanFromIssue.php similarity index 100% rename from src/php/Actions/Forge/CreatePlanFromIssue.php rename to php/Actions/Forge/CreatePlanFromIssue.php diff --git a/src/php/Actions/Forge/ManagePullRequest.php b/php/Actions/Forge/ManagePullRequest.php similarity index 100% rename from src/php/Actions/Forge/ManagePullRequest.php rename to php/Actions/Forge/ManagePullRequest.php diff --git a/src/php/Actions/Forge/ReportToIssue.php b/php/Actions/Forge/ReportToIssue.php similarity index 100% rename from src/php/Actions/Forge/ReportToIssue.php rename to php/Actions/Forge/ReportToIssue.php diff --git a/src/php/Actions/Forge/ScanForWork.php b/php/Actions/Forge/ScanForWork.php similarity index 100% rename from src/php/Actions/Forge/ScanForWork.php rename to php/Actions/Forge/ScanForWork.php diff --git a/src/php/Actions/Issue/AddIssueComment.php b/php/Actions/Issue/AddIssueComment.php similarity index 100% rename from src/php/Actions/Issue/AddIssueComment.php rename to php/Actions/Issue/AddIssueComment.php diff --git a/src/php/Actions/Issue/ArchiveIssue.php b/php/Actions/Issue/ArchiveIssue.php similarity index 100% rename from src/php/Actions/Issue/ArchiveIssue.php rename to php/Actions/Issue/ArchiveIssue.php diff --git a/src/php/Actions/Issue/CreateIssue.php b/php/Actions/Issue/CreateIssue.php similarity index 100% rename from src/php/Actions/Issue/CreateIssue.php rename to php/Actions/Issue/CreateIssue.php diff --git a/src/php/Actions/Issue/GetIssue.php b/php/Actions/Issue/GetIssue.php similarity index 100% rename from src/php/Actions/Issue/GetIssue.php rename to php/Actions/Issue/GetIssue.php diff --git a/src/php/Actions/Issue/ListIssues.php b/php/Actions/Issue/ListIssues.php similarity index 100% rename from src/php/Actions/Issue/ListIssues.php rename to php/Actions/Issue/ListIssues.php diff --git a/src/php/Actions/Issue/UpdateIssue.php b/php/Actions/Issue/UpdateIssue.php similarity index 100% rename from src/php/Actions/Issue/UpdateIssue.php rename to php/Actions/Issue/UpdateIssue.php diff --git a/src/php/Actions/Phase/AddCheckpoint.php b/php/Actions/Phase/AddCheckpoint.php similarity index 100% rename from src/php/Actions/Phase/AddCheckpoint.php rename to php/Actions/Phase/AddCheckpoint.php diff --git a/src/php/Actions/Phase/GetPhase.php b/php/Actions/Phase/GetPhase.php similarity index 100% rename from src/php/Actions/Phase/GetPhase.php rename to php/Actions/Phase/GetPhase.php diff --git a/src/php/Actions/Phase/UpdatePhaseStatus.php b/php/Actions/Phase/UpdatePhaseStatus.php similarity index 100% rename from src/php/Actions/Phase/UpdatePhaseStatus.php rename to php/Actions/Phase/UpdatePhaseStatus.php diff --git a/src/php/Actions/Plan/ArchivePlan.php b/php/Actions/Plan/ArchivePlan.php similarity index 100% rename from src/php/Actions/Plan/ArchivePlan.php rename to php/Actions/Plan/ArchivePlan.php diff --git a/src/php/Actions/Plan/CreatePlan.php b/php/Actions/Plan/CreatePlan.php similarity index 100% rename from src/php/Actions/Plan/CreatePlan.php rename to php/Actions/Plan/CreatePlan.php diff --git a/src/php/Actions/Plan/GetPlan.php b/php/Actions/Plan/GetPlan.php similarity index 100% rename from src/php/Actions/Plan/GetPlan.php rename to php/Actions/Plan/GetPlan.php diff --git a/src/php/Actions/Plan/ListPlans.php b/php/Actions/Plan/ListPlans.php similarity index 100% rename from src/php/Actions/Plan/ListPlans.php rename to php/Actions/Plan/ListPlans.php diff --git a/src/php/Actions/Plan/UpdatePlanStatus.php b/php/Actions/Plan/UpdatePlanStatus.php similarity index 100% rename from src/php/Actions/Plan/UpdatePlanStatus.php rename to php/Actions/Plan/UpdatePlanStatus.php diff --git a/src/php/Actions/Session/ContinueSession.php b/php/Actions/Session/ContinueSession.php similarity index 100% rename from src/php/Actions/Session/ContinueSession.php rename to php/Actions/Session/ContinueSession.php diff --git a/src/php/Actions/Session/EndSession.php b/php/Actions/Session/EndSession.php similarity index 100% rename from src/php/Actions/Session/EndSession.php rename to php/Actions/Session/EndSession.php diff --git a/src/php/Actions/Session/GetSession.php b/php/Actions/Session/GetSession.php similarity index 100% rename from src/php/Actions/Session/GetSession.php rename to php/Actions/Session/GetSession.php diff --git a/src/php/Actions/Session/ListSessions.php b/php/Actions/Session/ListSessions.php similarity index 100% rename from src/php/Actions/Session/ListSessions.php rename to php/Actions/Session/ListSessions.php diff --git a/src/php/Actions/Session/StartSession.php b/php/Actions/Session/StartSession.php similarity index 100% rename from src/php/Actions/Session/StartSession.php rename to php/Actions/Session/StartSession.php diff --git a/src/php/Actions/Sprint/ArchiveSprint.php b/php/Actions/Sprint/ArchiveSprint.php similarity index 100% rename from src/php/Actions/Sprint/ArchiveSprint.php rename to php/Actions/Sprint/ArchiveSprint.php diff --git a/src/php/Actions/Sprint/CreateSprint.php b/php/Actions/Sprint/CreateSprint.php similarity index 100% rename from src/php/Actions/Sprint/CreateSprint.php rename to php/Actions/Sprint/CreateSprint.php diff --git a/src/php/Actions/Sprint/GetSprint.php b/php/Actions/Sprint/GetSprint.php similarity index 100% rename from src/php/Actions/Sprint/GetSprint.php rename to php/Actions/Sprint/GetSprint.php diff --git a/src/php/Actions/Sprint/ListSprints.php b/php/Actions/Sprint/ListSprints.php similarity index 100% rename from src/php/Actions/Sprint/ListSprints.php rename to php/Actions/Sprint/ListSprints.php diff --git a/src/php/Actions/Sprint/UpdateSprint.php b/php/Actions/Sprint/UpdateSprint.php similarity index 100% rename from src/php/Actions/Sprint/UpdateSprint.php rename to php/Actions/Sprint/UpdateSprint.php diff --git a/src/php/Actions/Task/ToggleTask.php b/php/Actions/Task/ToggleTask.php similarity index 100% rename from src/php/Actions/Task/ToggleTask.php rename to php/Actions/Task/ToggleTask.php diff --git a/src/php/Actions/Task/UpdateTask.php b/php/Actions/Task/UpdateTask.php similarity index 100% rename from src/php/Actions/Task/UpdateTask.php rename to php/Actions/Task/UpdateTask.php diff --git a/src/php/Boot.php b/php/Boot.php similarity index 100% rename from src/php/Boot.php rename to php/Boot.php diff --git a/src/php/Configs/AIConfig.php b/php/Configs/AIConfig.php similarity index 100% rename from src/php/Configs/AIConfig.php rename to php/Configs/AIConfig.php diff --git a/src/php/Console/Commands/BrainIngestCommand.php b/php/Console/Commands/BrainIngestCommand.php similarity index 100% rename from src/php/Console/Commands/BrainIngestCommand.php rename to php/Console/Commands/BrainIngestCommand.php diff --git a/src/php/Console/Commands/BrainSeedMemoryCommand.php b/php/Console/Commands/BrainSeedMemoryCommand.php similarity index 100% rename from src/php/Console/Commands/BrainSeedMemoryCommand.php rename to php/Console/Commands/BrainSeedMemoryCommand.php diff --git a/src/php/Console/Commands/DispatchCommand.php b/php/Console/Commands/DispatchCommand.php similarity index 100% rename from src/php/Console/Commands/DispatchCommand.php rename to php/Console/Commands/DispatchCommand.php diff --git a/src/php/Console/Commands/GenerateCommand.php b/php/Console/Commands/GenerateCommand.php similarity index 100% rename from src/php/Console/Commands/GenerateCommand.php rename to php/Console/Commands/GenerateCommand.php diff --git a/src/php/Console/Commands/PlanCommand.php b/php/Console/Commands/PlanCommand.php similarity index 100% rename from src/php/Console/Commands/PlanCommand.php rename to php/Console/Commands/PlanCommand.php diff --git a/src/php/Console/Commands/PlanRetentionCommand.php b/php/Console/Commands/PlanRetentionCommand.php similarity index 100% rename from src/php/Console/Commands/PlanRetentionCommand.php rename to php/Console/Commands/PlanRetentionCommand.php diff --git a/src/php/Console/Commands/PrManageCommand.php b/php/Console/Commands/PrManageCommand.php similarity index 100% rename from src/php/Console/Commands/PrManageCommand.php rename to php/Console/Commands/PrManageCommand.php diff --git a/src/php/Console/Commands/PrepWorkspaceCommand.php b/php/Console/Commands/PrepWorkspaceCommand.php similarity index 100% rename from src/php/Console/Commands/PrepWorkspaceCommand.php rename to php/Console/Commands/PrepWorkspaceCommand.php diff --git a/src/php/Console/Commands/ScanCommand.php b/php/Console/Commands/ScanCommand.php similarity index 100% rename from src/php/Console/Commands/ScanCommand.php rename to php/Console/Commands/ScanCommand.php diff --git a/src/php/Console/Commands/TaskCommand.php b/php/Console/Commands/TaskCommand.php similarity index 100% rename from src/php/Console/Commands/TaskCommand.php rename to php/Console/Commands/TaskCommand.php diff --git a/src/php/Controllers/AgentApiController.php b/php/Controllers/AgentApiController.php similarity index 100% rename from src/php/Controllers/AgentApiController.php rename to php/Controllers/AgentApiController.php diff --git a/src/php/Controllers/Api/BrainController.php b/php/Controllers/Api/BrainController.php similarity index 100% rename from src/php/Controllers/Api/BrainController.php rename to php/Controllers/Api/BrainController.php diff --git a/src/php/Controllers/Api/CheckinController.php b/php/Controllers/Api/CheckinController.php similarity index 100% rename from src/php/Controllers/Api/CheckinController.php rename to php/Controllers/Api/CheckinController.php diff --git a/src/php/Controllers/Api/GitHubWebhookController.php b/php/Controllers/Api/GitHubWebhookController.php similarity index 100% rename from src/php/Controllers/Api/GitHubWebhookController.php rename to php/Controllers/Api/GitHubWebhookController.php diff --git a/src/php/Controllers/Api/IssueController.php b/php/Controllers/Api/IssueController.php similarity index 100% rename from src/php/Controllers/Api/IssueController.php rename to php/Controllers/Api/IssueController.php diff --git a/src/php/Controllers/Api/MessageController.php b/php/Controllers/Api/MessageController.php similarity index 100% rename from src/php/Controllers/Api/MessageController.php rename to php/Controllers/Api/MessageController.php diff --git a/src/php/Controllers/Api/PhaseController.php b/php/Controllers/Api/PhaseController.php similarity index 100% rename from src/php/Controllers/Api/PhaseController.php rename to php/Controllers/Api/PhaseController.php diff --git a/src/php/Controllers/Api/PlanController.php b/php/Controllers/Api/PlanController.php similarity index 100% rename from src/php/Controllers/Api/PlanController.php rename to php/Controllers/Api/PlanController.php diff --git a/src/php/Controllers/Api/SessionController.php b/php/Controllers/Api/SessionController.php similarity index 100% rename from src/php/Controllers/Api/SessionController.php rename to php/Controllers/Api/SessionController.php diff --git a/src/php/Controllers/Api/SprintController.php b/php/Controllers/Api/SprintController.php similarity index 100% rename from src/php/Controllers/Api/SprintController.php rename to php/Controllers/Api/SprintController.php diff --git a/src/php/Controllers/Api/TaskController.php b/php/Controllers/Api/TaskController.php similarity index 100% rename from src/php/Controllers/Api/TaskController.php rename to php/Controllers/Api/TaskController.php diff --git a/src/php/Controllers/ForAgentsController.php b/php/Controllers/ForAgentsController.php similarity index 100% rename from src/php/Controllers/ForAgentsController.php rename to php/Controllers/ForAgentsController.php diff --git a/src/php/FINDINGS.md b/php/FINDINGS.md similarity index 100% rename from src/php/FINDINGS.md rename to php/FINDINGS.md diff --git a/src/php/Facades/Agentic.php b/php/Facades/Agentic.php similarity index 100% rename from src/php/Facades/Agentic.php rename to php/Facades/Agentic.php diff --git a/src/php/GEMINI.md b/php/GEMINI.md similarity index 100% rename from src/php/GEMINI.md rename to php/GEMINI.md diff --git a/src/php/Jobs/BatchContentGeneration.php b/php/Jobs/BatchContentGeneration.php similarity index 100% rename from src/php/Jobs/BatchContentGeneration.php rename to php/Jobs/BatchContentGeneration.php diff --git a/src/php/Jobs/ProcessContentTask.php b/php/Jobs/ProcessContentTask.php similarity index 100% rename from src/php/Jobs/ProcessContentTask.php rename to php/Jobs/ProcessContentTask.php diff --git a/src/php/Lang/en_GB/agentic.php b/php/Lang/en_GB/agentic.php similarity index 100% rename from src/php/Lang/en_GB/agentic.php rename to php/Lang/en_GB/agentic.php diff --git a/src/php/Middleware/AgentApiAuth.php b/php/Middleware/AgentApiAuth.php similarity index 100% rename from src/php/Middleware/AgentApiAuth.php rename to php/Middleware/AgentApiAuth.php diff --git a/src/php/Migrations/0001_01_01_000001_create_agentic_tables.php b/php/Migrations/0001_01_01_000001_create_agentic_tables.php similarity index 100% rename from src/php/Migrations/0001_01_01_000001_create_agentic_tables.php rename to php/Migrations/0001_01_01_000001_create_agentic_tables.php diff --git a/src/php/Migrations/0001_01_01_000002_add_ip_whitelist_to_agent_api_keys.php b/php/Migrations/0001_01_01_000002_add_ip_whitelist_to_agent_api_keys.php similarity index 100% rename from src/php/Migrations/0001_01_01_000002_add_ip_whitelist_to_agent_api_keys.php rename to php/Migrations/0001_01_01_000002_add_ip_whitelist_to_agent_api_keys.php diff --git a/src/php/Migrations/0001_01_01_000003_create_agent_plans_tables.php b/php/Migrations/0001_01_01_000003_create_agent_plans_tables.php similarity index 100% rename from src/php/Migrations/0001_01_01_000003_create_agent_plans_tables.php rename to php/Migrations/0001_01_01_000003_create_agent_plans_tables.php diff --git a/src/php/Migrations/0001_01_01_000004_create_prompt_tables.php b/php/Migrations/0001_01_01_000004_create_prompt_tables.php similarity index 100% rename from src/php/Migrations/0001_01_01_000004_create_prompt_tables.php rename to php/Migrations/0001_01_01_000004_create_prompt_tables.php diff --git a/src/php/Migrations/0001_01_01_000005_add_performance_indexes.php b/php/Migrations/0001_01_01_000005_add_performance_indexes.php similarity index 100% rename from src/php/Migrations/0001_01_01_000005_add_performance_indexes.php rename to php/Migrations/0001_01_01_000005_add_performance_indexes.php diff --git a/src/php/Migrations/0001_01_01_000006_add_soft_deletes_to_agent_plans.php b/php/Migrations/0001_01_01_000006_add_soft_deletes_to_agent_plans.php similarity index 100% rename from src/php/Migrations/0001_01_01_000006_add_soft_deletes_to_agent_plans.php rename to php/Migrations/0001_01_01_000006_add_soft_deletes_to_agent_plans.php diff --git a/src/php/Migrations/0001_01_01_000007_add_template_versions.php b/php/Migrations/0001_01_01_000007_add_template_versions.php similarity index 100% rename from src/php/Migrations/0001_01_01_000007_add_template_versions.php rename to php/Migrations/0001_01_01_000007_add_template_versions.php diff --git a/src/php/Migrations/0001_01_01_000008_create_brain_memories_table.php b/php/Migrations/0001_01_01_000008_create_brain_memories_table.php similarity index 100% rename from src/php/Migrations/0001_01_01_000008_create_brain_memories_table.php rename to php/Migrations/0001_01_01_000008_create_brain_memories_table.php diff --git a/src/php/Migrations/0001_01_01_000009_drop_brain_memories_workspace_fk.php b/php/Migrations/0001_01_01_000009_drop_brain_memories_workspace_fk.php similarity index 100% rename from src/php/Migrations/0001_01_01_000009_drop_brain_memories_workspace_fk.php rename to php/Migrations/0001_01_01_000009_drop_brain_memories_workspace_fk.php diff --git a/src/php/Migrations/0001_01_01_000010_add_source_to_brain_memories.php b/php/Migrations/0001_01_01_000010_add_source_to_brain_memories.php similarity index 100% rename from src/php/Migrations/0001_01_01_000010_add_source_to_brain_memories.php rename to php/Migrations/0001_01_01_000010_add_source_to_brain_memories.php diff --git a/src/php/Migrations/0001_01_01_000010_rename_session_columns.php b/php/Migrations/0001_01_01_000010_rename_session_columns.php similarity index 100% rename from src/php/Migrations/0001_01_01_000010_rename_session_columns.php rename to php/Migrations/0001_01_01_000010_rename_session_columns.php diff --git a/src/php/Migrations/0001_01_01_000011_create_issue_tracker_tables.php b/php/Migrations/0001_01_01_000011_create_issue_tracker_tables.php similarity index 100% rename from src/php/Migrations/0001_01_01_000011_create_issue_tracker_tables.php rename to php/Migrations/0001_01_01_000011_create_issue_tracker_tables.php diff --git a/src/php/Migrations/0001_01_01_000012_create_agent_messages_table.php b/php/Migrations/0001_01_01_000012_create_agent_messages_table.php similarity index 100% rename from src/php/Migrations/0001_01_01_000012_create_agent_messages_table.php rename to php/Migrations/0001_01_01_000012_create_agent_messages_table.php diff --git a/src/php/Migrations/2026_03_17_000001_create_github_tracking_tables.php b/php/Migrations/2026_03_17_000001_create_github_tracking_tables.php similarity index 100% rename from src/php/Migrations/2026_03_17_000001_create_github_tracking_tables.php rename to php/Migrations/2026_03_17_000001_create_github_tracking_tables.php diff --git a/src/php/Models/AgentApiKey.php b/php/Models/AgentApiKey.php similarity index 100% rename from src/php/Models/AgentApiKey.php rename to php/Models/AgentApiKey.php diff --git a/src/php/Models/AgentMessage.php b/php/Models/AgentMessage.php similarity index 100% rename from src/php/Models/AgentMessage.php rename to php/Models/AgentMessage.php diff --git a/src/php/Models/AgentPhase.php b/php/Models/AgentPhase.php similarity index 100% rename from src/php/Models/AgentPhase.php rename to php/Models/AgentPhase.php diff --git a/src/php/Models/AgentPlan.php b/php/Models/AgentPlan.php similarity index 100% rename from src/php/Models/AgentPlan.php rename to php/Models/AgentPlan.php diff --git a/src/php/Models/AgentSession.php b/php/Models/AgentSession.php similarity index 100% rename from src/php/Models/AgentSession.php rename to php/Models/AgentSession.php diff --git a/src/php/Models/BrainMemory.php b/php/Models/BrainMemory.php similarity index 100% rename from src/php/Models/BrainMemory.php rename to php/Models/BrainMemory.php diff --git a/src/php/Models/Issue.php b/php/Models/Issue.php similarity index 100% rename from src/php/Models/Issue.php rename to php/Models/Issue.php diff --git a/src/php/Models/IssueComment.php b/php/Models/IssueComment.php similarity index 100% rename from src/php/Models/IssueComment.php rename to php/Models/IssueComment.php diff --git a/src/php/Models/PlanTemplateVersion.php b/php/Models/PlanTemplateVersion.php similarity index 100% rename from src/php/Models/PlanTemplateVersion.php rename to php/Models/PlanTemplateVersion.php diff --git a/src/php/Models/Prompt.php b/php/Models/Prompt.php similarity index 100% rename from src/php/Models/Prompt.php rename to php/Models/Prompt.php diff --git a/src/php/Models/PromptVersion.php b/php/Models/PromptVersion.php similarity index 100% rename from src/php/Models/PromptVersion.php rename to php/Models/PromptVersion.php diff --git a/src/php/Models/Sprint.php b/php/Models/Sprint.php similarity index 100% rename from src/php/Models/Sprint.php rename to php/Models/Sprint.php diff --git a/src/php/Models/Task.php b/php/Models/Task.php similarity index 100% rename from src/php/Models/Task.php rename to php/Models/Task.php diff --git a/src/php/Models/WorkspaceState.php b/php/Models/WorkspaceState.php similarity index 100% rename from src/php/Models/WorkspaceState.php rename to php/Models/WorkspaceState.php diff --git a/src/php/Routes/admin.php b/php/Routes/admin.php similarity index 100% rename from src/php/Routes/admin.php rename to php/Routes/admin.php diff --git a/src/php/Routes/api.php b/php/Routes/api.php similarity index 100% rename from src/php/Routes/api.php rename to php/Routes/api.php diff --git a/src/php/Routes/console.php b/php/Routes/console.php similarity index 100% rename from src/php/Routes/console.php rename to php/Routes/console.php diff --git a/src/php/Routes/web.php b/php/Routes/web.php similarity index 100% rename from src/php/Routes/web.php rename to php/Routes/web.php diff --git a/src/php/Service/Boot.php b/php/Service/Boot.php similarity index 100% rename from src/php/Service/Boot.php rename to php/Service/Boot.php diff --git a/src/php/Services/AgentApiKeyService.php b/php/Services/AgentApiKeyService.php similarity index 100% rename from src/php/Services/AgentApiKeyService.php rename to php/Services/AgentApiKeyService.php diff --git a/src/php/Services/AgentDetection.php b/php/Services/AgentDetection.php similarity index 100% rename from src/php/Services/AgentDetection.php rename to php/Services/AgentDetection.php diff --git a/src/php/Services/AgentSessionService.php b/php/Services/AgentSessionService.php similarity index 100% rename from src/php/Services/AgentSessionService.php rename to php/Services/AgentSessionService.php diff --git a/src/php/Services/AgentToolRegistry.php b/php/Services/AgentToolRegistry.php similarity index 100% rename from src/php/Services/AgentToolRegistry.php rename to php/Services/AgentToolRegistry.php diff --git a/src/php/Services/AgenticManager.php b/php/Services/AgenticManager.php similarity index 100% rename from src/php/Services/AgenticManager.php rename to php/Services/AgenticManager.php diff --git a/src/php/Services/AgenticProviderInterface.php b/php/Services/AgenticProviderInterface.php similarity index 100% rename from src/php/Services/AgenticProviderInterface.php rename to php/Services/AgenticProviderInterface.php diff --git a/src/php/Services/AgenticResponse.php b/php/Services/AgenticResponse.php similarity index 100% rename from src/php/Services/AgenticResponse.php rename to php/Services/AgenticResponse.php diff --git a/src/php/Services/BrainService.php b/php/Services/BrainService.php similarity index 100% rename from src/php/Services/BrainService.php rename to php/Services/BrainService.php diff --git a/src/php/Services/ClaudeService.php b/php/Services/ClaudeService.php similarity index 100% rename from src/php/Services/ClaudeService.php rename to php/Services/ClaudeService.php diff --git a/src/php/Services/Concerns/HasRetry.php b/php/Services/Concerns/HasRetry.php similarity index 100% rename from src/php/Services/Concerns/HasRetry.php rename to php/Services/Concerns/HasRetry.php diff --git a/src/php/Services/Concerns/HasStreamParsing.php b/php/Services/Concerns/HasStreamParsing.php similarity index 100% rename from src/php/Services/Concerns/HasStreamParsing.php rename to php/Services/Concerns/HasStreamParsing.php diff --git a/src/php/Services/ContentService.php b/php/Services/ContentService.php similarity index 100% rename from src/php/Services/ContentService.php rename to php/Services/ContentService.php diff --git a/src/php/Services/ForgejoService.php b/php/Services/ForgejoService.php similarity index 100% rename from src/php/Services/ForgejoService.php rename to php/Services/ForgejoService.php diff --git a/src/php/Services/GeminiService.php b/php/Services/GeminiService.php similarity index 100% rename from src/php/Services/GeminiService.php rename to php/Services/GeminiService.php diff --git a/src/php/Services/IpRestrictionService.php b/php/Services/IpRestrictionService.php similarity index 100% rename from src/php/Services/IpRestrictionService.php rename to php/Services/IpRestrictionService.php diff --git a/src/php/Services/OpenAIService.php b/php/Services/OpenAIService.php similarity index 100% rename from src/php/Services/OpenAIService.php rename to php/Services/OpenAIService.php diff --git a/src/php/Services/PlanTemplateService.php b/php/Services/PlanTemplateService.php similarity index 100% rename from src/php/Services/PlanTemplateService.php rename to php/Services/PlanTemplateService.php diff --git a/src/php/Support/AgentIdentity.php b/php/Support/AgentIdentity.php similarity index 100% rename from src/php/Support/AgentIdentity.php rename to php/Support/AgentIdentity.php diff --git a/src/php/TODO.md b/php/TODO.md similarity index 100% rename from src/php/TODO.md rename to php/TODO.md diff --git a/src/php/View/Blade/admin/api-key-manager.blade.php b/php/View/Blade/admin/api-key-manager.blade.php similarity index 100% rename from src/php/View/Blade/admin/api-key-manager.blade.php rename to php/View/Blade/admin/api-key-manager.blade.php diff --git a/src/php/View/Blade/admin/api-keys.blade.php b/php/View/Blade/admin/api-keys.blade.php similarity index 100% rename from src/php/View/Blade/admin/api-keys.blade.php rename to php/View/Blade/admin/api-keys.blade.php diff --git a/src/php/View/Blade/admin/dashboard.blade.php b/php/View/Blade/admin/dashboard.blade.php similarity index 100% rename from src/php/View/Blade/admin/dashboard.blade.php rename to php/View/Blade/admin/dashboard.blade.php diff --git a/src/php/View/Blade/admin/plan-detail.blade.php b/php/View/Blade/admin/plan-detail.blade.php similarity index 100% rename from src/php/View/Blade/admin/plan-detail.blade.php rename to php/View/Blade/admin/plan-detail.blade.php diff --git a/src/php/View/Blade/admin/plans.blade.php b/php/View/Blade/admin/plans.blade.php similarity index 100% rename from src/php/View/Blade/admin/plans.blade.php rename to php/View/Blade/admin/plans.blade.php diff --git a/src/php/View/Blade/admin/playground.blade.php b/php/View/Blade/admin/playground.blade.php similarity index 100% rename from src/php/View/Blade/admin/playground.blade.php rename to php/View/Blade/admin/playground.blade.php diff --git a/src/php/View/Blade/admin/request-log.blade.php b/php/View/Blade/admin/request-log.blade.php similarity index 100% rename from src/php/View/Blade/admin/request-log.blade.php rename to php/View/Blade/admin/request-log.blade.php diff --git a/src/php/View/Blade/admin/session-detail.blade.php b/php/View/Blade/admin/session-detail.blade.php similarity index 100% rename from src/php/View/Blade/admin/session-detail.blade.php rename to php/View/Blade/admin/session-detail.blade.php diff --git a/src/php/View/Blade/admin/sessions.blade.php b/php/View/Blade/admin/sessions.blade.php similarity index 100% rename from src/php/View/Blade/admin/sessions.blade.php rename to php/View/Blade/admin/sessions.blade.php diff --git a/src/php/View/Blade/admin/templates.blade.php b/php/View/Blade/admin/templates.blade.php similarity index 100% rename from src/php/View/Blade/admin/templates.blade.php rename to php/View/Blade/admin/templates.blade.php diff --git a/src/php/View/Blade/admin/tool-analytics.blade.php b/php/View/Blade/admin/tool-analytics.blade.php similarity index 100% rename from src/php/View/Blade/admin/tool-analytics.blade.php rename to php/View/Blade/admin/tool-analytics.blade.php diff --git a/src/php/View/Blade/admin/tool-calls.blade.php b/php/View/Blade/admin/tool-calls.blade.php similarity index 100% rename from src/php/View/Blade/admin/tool-calls.blade.php rename to php/View/Blade/admin/tool-calls.blade.php diff --git a/src/php/View/Modal/Admin/ApiKeyManager.php b/php/View/Modal/Admin/ApiKeyManager.php similarity index 100% rename from src/php/View/Modal/Admin/ApiKeyManager.php rename to php/View/Modal/Admin/ApiKeyManager.php diff --git a/src/php/View/Modal/Admin/ApiKeys.php b/php/View/Modal/Admin/ApiKeys.php similarity index 100% rename from src/php/View/Modal/Admin/ApiKeys.php rename to php/View/Modal/Admin/ApiKeys.php diff --git a/src/php/View/Modal/Admin/Dashboard.php b/php/View/Modal/Admin/Dashboard.php similarity index 100% rename from src/php/View/Modal/Admin/Dashboard.php rename to php/View/Modal/Admin/Dashboard.php diff --git a/src/php/View/Modal/Admin/PlanDetail.php b/php/View/Modal/Admin/PlanDetail.php similarity index 100% rename from src/php/View/Modal/Admin/PlanDetail.php rename to php/View/Modal/Admin/PlanDetail.php diff --git a/src/php/View/Modal/Admin/Plans.php b/php/View/Modal/Admin/Plans.php similarity index 100% rename from src/php/View/Modal/Admin/Plans.php rename to php/View/Modal/Admin/Plans.php diff --git a/src/php/View/Modal/Admin/Playground.php b/php/View/Modal/Admin/Playground.php similarity index 100% rename from src/php/View/Modal/Admin/Playground.php rename to php/View/Modal/Admin/Playground.php diff --git a/src/php/View/Modal/Admin/RequestLog.php b/php/View/Modal/Admin/RequestLog.php similarity index 100% rename from src/php/View/Modal/Admin/RequestLog.php rename to php/View/Modal/Admin/RequestLog.php diff --git a/src/php/View/Modal/Admin/SessionDetail.php b/php/View/Modal/Admin/SessionDetail.php similarity index 100% rename from src/php/View/Modal/Admin/SessionDetail.php rename to php/View/Modal/Admin/SessionDetail.php diff --git a/src/php/View/Modal/Admin/Sessions.php b/php/View/Modal/Admin/Sessions.php similarity index 100% rename from src/php/View/Modal/Admin/Sessions.php rename to php/View/Modal/Admin/Sessions.php diff --git a/src/php/View/Modal/Admin/Templates.php b/php/View/Modal/Admin/Templates.php similarity index 100% rename from src/php/View/Modal/Admin/Templates.php rename to php/View/Modal/Admin/Templates.php diff --git a/src/php/View/Modal/Admin/ToolAnalytics.php b/php/View/Modal/Admin/ToolAnalytics.php similarity index 100% rename from src/php/View/Modal/Admin/ToolAnalytics.php rename to php/View/Modal/Admin/ToolAnalytics.php diff --git a/src/php/View/Modal/Admin/ToolCalls.php b/php/View/Modal/Admin/ToolCalls.php similarity index 100% rename from src/php/View/Modal/Admin/ToolCalls.php rename to php/View/Modal/Admin/ToolCalls.php diff --git a/src/php/agentic.php b/php/agentic.php similarity index 100% rename from src/php/agentic.php rename to php/agentic.php diff --git a/src/php/config.php b/php/config.php similarity index 100% rename from src/php/config.php rename to php/config.php diff --git a/src/php/docs/api-keys.md b/php/docs/api-keys.md similarity index 100% rename from src/php/docs/api-keys.md rename to php/docs/api-keys.md diff --git a/src/php/docs/architecture.md b/php/docs/architecture.md similarity index 100% rename from src/php/docs/architecture.md rename to php/docs/architecture.md diff --git a/src/php/docs/mcp-tools.md b/php/docs/mcp-tools.md similarity index 100% rename from src/php/docs/mcp-tools.md rename to php/docs/mcp-tools.md diff --git a/src/php/docs/security.md b/php/docs/security.md similarity index 100% rename from src/php/docs/security.md rename to php/docs/security.md diff --git a/src/php/tests/Feature/.gitkeep b/php/tests/Feature/.gitkeep similarity index 100% rename from src/php/tests/Feature/.gitkeep rename to php/tests/Feature/.gitkeep diff --git a/src/php/tests/Feature/AgentApiKeyServiceTest.php b/php/tests/Feature/AgentApiKeyServiceTest.php similarity index 100% rename from src/php/tests/Feature/AgentApiKeyServiceTest.php rename to php/tests/Feature/AgentApiKeyServiceTest.php diff --git a/src/php/tests/Feature/AgentApiKeyTest.php b/php/tests/Feature/AgentApiKeyTest.php similarity index 100% rename from src/php/tests/Feature/AgentApiKeyTest.php rename to php/tests/Feature/AgentApiKeyTest.php diff --git a/src/php/tests/Feature/AgentPhaseTest.php b/php/tests/Feature/AgentPhaseTest.php similarity index 100% rename from src/php/tests/Feature/AgentPhaseTest.php rename to php/tests/Feature/AgentPhaseTest.php diff --git a/src/php/tests/Feature/AgentPlanTest.php b/php/tests/Feature/AgentPlanTest.php similarity index 100% rename from src/php/tests/Feature/AgentPlanTest.php rename to php/tests/Feature/AgentPlanTest.php diff --git a/src/php/tests/Feature/AgentSessionTest.php b/php/tests/Feature/AgentSessionTest.php similarity index 100% rename from src/php/tests/Feature/AgentSessionTest.php rename to php/tests/Feature/AgentSessionTest.php diff --git a/src/php/tests/Feature/ApiKeyManagerTest.php b/php/tests/Feature/ApiKeyManagerTest.php similarity index 100% rename from src/php/tests/Feature/ApiKeyManagerTest.php rename to php/tests/Feature/ApiKeyManagerTest.php diff --git a/src/php/tests/Feature/ContentServiceTest.php b/php/tests/Feature/ContentServiceTest.php similarity index 100% rename from src/php/tests/Feature/ContentServiceTest.php rename to php/tests/Feature/ContentServiceTest.php diff --git a/src/php/tests/Feature/CreatePlanFromIssueTest.php b/php/tests/Feature/CreatePlanFromIssueTest.php similarity index 100% rename from src/php/tests/Feature/CreatePlanFromIssueTest.php rename to php/tests/Feature/CreatePlanFromIssueTest.php diff --git a/src/php/tests/Feature/ForAgentsControllerTest.php b/php/tests/Feature/ForAgentsControllerTest.php similarity index 100% rename from src/php/tests/Feature/ForAgentsControllerTest.php rename to php/tests/Feature/ForAgentsControllerTest.php diff --git a/src/php/tests/Feature/ForgeActionsTest.php b/php/tests/Feature/ForgeActionsTest.php similarity index 100% rename from src/php/tests/Feature/ForgeActionsTest.php rename to php/tests/Feature/ForgeActionsTest.php diff --git a/src/php/tests/Feature/ForgejoServiceTest.php b/php/tests/Feature/ForgejoServiceTest.php similarity index 100% rename from src/php/tests/Feature/ForgejoServiceTest.php rename to php/tests/Feature/ForgejoServiceTest.php diff --git a/src/php/tests/Feature/IpRestrictionServiceTest.php b/php/tests/Feature/IpRestrictionServiceTest.php similarity index 100% rename from src/php/tests/Feature/IpRestrictionServiceTest.php rename to php/tests/Feature/IpRestrictionServiceTest.php diff --git a/src/php/tests/Feature/IssueTest.php b/php/tests/Feature/IssueTest.php similarity index 100% rename from src/php/tests/Feature/IssueTest.php rename to php/tests/Feature/IssueTest.php diff --git a/src/php/tests/Feature/Jobs/BatchContentGenerationTest.php b/php/tests/Feature/Jobs/BatchContentGenerationTest.php similarity index 100% rename from src/php/tests/Feature/Jobs/BatchContentGenerationTest.php rename to php/tests/Feature/Jobs/BatchContentGenerationTest.php diff --git a/src/php/tests/Feature/Jobs/ProcessContentTaskTest.php b/php/tests/Feature/Jobs/ProcessContentTaskTest.php similarity index 100% rename from src/php/tests/Feature/Jobs/ProcessContentTaskTest.php rename to php/tests/Feature/Jobs/ProcessContentTaskTest.php diff --git a/src/php/tests/Feature/Livewire/ApiKeyManagerTest.php b/php/tests/Feature/Livewire/ApiKeyManagerTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/ApiKeyManagerTest.php rename to php/tests/Feature/Livewire/ApiKeyManagerTest.php diff --git a/src/php/tests/Feature/Livewire/ApiKeysTest.php b/php/tests/Feature/Livewire/ApiKeysTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/ApiKeysTest.php rename to php/tests/Feature/Livewire/ApiKeysTest.php diff --git a/src/php/tests/Feature/Livewire/DashboardTest.php b/php/tests/Feature/Livewire/DashboardTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/DashboardTest.php rename to php/tests/Feature/Livewire/DashboardTest.php diff --git a/src/php/tests/Feature/Livewire/LivewireTestCase.php b/php/tests/Feature/Livewire/LivewireTestCase.php similarity index 100% rename from src/php/tests/Feature/Livewire/LivewireTestCase.php rename to php/tests/Feature/Livewire/LivewireTestCase.php diff --git a/src/php/tests/Feature/Livewire/PlanDetailTest.php b/php/tests/Feature/Livewire/PlanDetailTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/PlanDetailTest.php rename to php/tests/Feature/Livewire/PlanDetailTest.php diff --git a/src/php/tests/Feature/Livewire/PlansTest.php b/php/tests/Feature/Livewire/PlansTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/PlansTest.php rename to php/tests/Feature/Livewire/PlansTest.php diff --git a/src/php/tests/Feature/Livewire/PlaygroundTest.php b/php/tests/Feature/Livewire/PlaygroundTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/PlaygroundTest.php rename to php/tests/Feature/Livewire/PlaygroundTest.php diff --git a/src/php/tests/Feature/Livewire/RequestLogTest.php b/php/tests/Feature/Livewire/RequestLogTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/RequestLogTest.php rename to php/tests/Feature/Livewire/RequestLogTest.php diff --git a/src/php/tests/Feature/Livewire/SessionDetailTest.php b/php/tests/Feature/Livewire/SessionDetailTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/SessionDetailTest.php rename to php/tests/Feature/Livewire/SessionDetailTest.php diff --git a/src/php/tests/Feature/Livewire/SessionsTest.php b/php/tests/Feature/Livewire/SessionsTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/SessionsTest.php rename to php/tests/Feature/Livewire/SessionsTest.php diff --git a/src/php/tests/Feature/Livewire/TemplatesTest.php b/php/tests/Feature/Livewire/TemplatesTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/TemplatesTest.php rename to php/tests/Feature/Livewire/TemplatesTest.php diff --git a/src/php/tests/Feature/Livewire/ToolAnalyticsTest.php b/php/tests/Feature/Livewire/ToolAnalyticsTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/ToolAnalyticsTest.php rename to php/tests/Feature/Livewire/ToolAnalyticsTest.php diff --git a/src/php/tests/Feature/Livewire/ToolCallsTest.php b/php/tests/Feature/Livewire/ToolCallsTest.php similarity index 100% rename from src/php/tests/Feature/Livewire/ToolCallsTest.php rename to php/tests/Feature/Livewire/ToolCallsTest.php diff --git a/src/php/tests/Feature/PlanRetentionTest.php b/php/tests/Feature/PlanRetentionTest.php similarity index 100% rename from src/php/tests/Feature/PlanRetentionTest.php rename to php/tests/Feature/PlanRetentionTest.php diff --git a/src/php/tests/Feature/PlanTemplateServiceTest.php b/php/tests/Feature/PlanTemplateServiceTest.php similarity index 100% rename from src/php/tests/Feature/PlanTemplateServiceTest.php rename to php/tests/Feature/PlanTemplateServiceTest.php diff --git a/src/php/tests/Feature/PromptVersionTest.php b/php/tests/Feature/PromptVersionTest.php similarity index 100% rename from src/php/tests/Feature/PromptVersionTest.php rename to php/tests/Feature/PromptVersionTest.php diff --git a/src/php/tests/Feature/ScanForWorkTest.php b/php/tests/Feature/ScanForWorkTest.php similarity index 100% rename from src/php/tests/Feature/ScanForWorkTest.php rename to php/tests/Feature/ScanForWorkTest.php diff --git a/src/php/tests/Feature/SecurityTest.php b/php/tests/Feature/SecurityTest.php similarity index 100% rename from src/php/tests/Feature/SecurityTest.php rename to php/tests/Feature/SecurityTest.php diff --git a/src/php/tests/Feature/SprintTest.php b/php/tests/Feature/SprintTest.php similarity index 100% rename from src/php/tests/Feature/SprintTest.php rename to php/tests/Feature/SprintTest.php diff --git a/src/php/tests/Feature/TemplateVersionManagementTest.php b/php/tests/Feature/TemplateVersionManagementTest.php similarity index 100% rename from src/php/tests/Feature/TemplateVersionManagementTest.php rename to php/tests/Feature/TemplateVersionManagementTest.php diff --git a/src/php/tests/Feature/WorkspaceStateTest.php b/php/tests/Feature/WorkspaceStateTest.php similarity index 100% rename from src/php/tests/Feature/WorkspaceStateTest.php rename to php/tests/Feature/WorkspaceStateTest.php diff --git a/src/php/tests/Fixtures/HadesUser.php b/php/tests/Fixtures/HadesUser.php similarity index 100% rename from src/php/tests/Fixtures/HadesUser.php rename to php/tests/Fixtures/HadesUser.php diff --git a/src/php/tests/Pest.php b/php/tests/Pest.php similarity index 100% rename from src/php/tests/Pest.php rename to php/tests/Pest.php diff --git a/src/php/tests/TestCase.php b/php/tests/TestCase.php similarity index 100% rename from src/php/tests/TestCase.php rename to php/tests/TestCase.php diff --git a/src/php/tests/Unit/.gitkeep b/php/tests/Unit/.gitkeep similarity index 100% rename from src/php/tests/Unit/.gitkeep rename to php/tests/Unit/.gitkeep diff --git a/src/php/tests/Unit/AgentDetectionTest.php b/php/tests/Unit/AgentDetectionTest.php similarity index 100% rename from src/php/tests/Unit/AgentDetectionTest.php rename to php/tests/Unit/AgentDetectionTest.php diff --git a/src/php/tests/Unit/AgentToolRegistryTest.php b/php/tests/Unit/AgentToolRegistryTest.php similarity index 100% rename from src/php/tests/Unit/AgentToolRegistryTest.php rename to php/tests/Unit/AgentToolRegistryTest.php diff --git a/src/php/tests/Unit/AgenticManagerTest.php b/php/tests/Unit/AgenticManagerTest.php similarity index 100% rename from src/php/tests/Unit/AgenticManagerTest.php rename to php/tests/Unit/AgenticManagerTest.php diff --git a/src/php/tests/Unit/ClaudeServiceTest.php b/php/tests/Unit/ClaudeServiceTest.php similarity index 100% rename from src/php/tests/Unit/ClaudeServiceTest.php rename to php/tests/Unit/ClaudeServiceTest.php diff --git a/src/php/tests/Unit/Concerns/HasRetryTest.php b/php/tests/Unit/Concerns/HasRetryTest.php similarity index 100% rename from src/php/tests/Unit/Concerns/HasRetryTest.php rename to php/tests/Unit/Concerns/HasRetryTest.php diff --git a/src/php/tests/Unit/Concerns/HasStreamParsingTest.php b/php/tests/Unit/Concerns/HasStreamParsingTest.php similarity index 100% rename from src/php/tests/Unit/Concerns/HasStreamParsingTest.php rename to php/tests/Unit/Concerns/HasStreamParsingTest.php diff --git a/src/php/tests/Unit/GeminiServiceTest.php b/php/tests/Unit/GeminiServiceTest.php similarity index 100% rename from src/php/tests/Unit/GeminiServiceTest.php rename to php/tests/Unit/GeminiServiceTest.php diff --git a/src/php/tests/Unit/OpenAIServiceTest.php b/php/tests/Unit/OpenAIServiceTest.php similarity index 100% rename from src/php/tests/Unit/OpenAIServiceTest.php rename to php/tests/Unit/OpenAIServiceTest.php diff --git a/src/php/tests/Unit/ProcessContentTaskTest.php b/php/tests/Unit/ProcessContentTaskTest.php similarity index 100% rename from src/php/tests/Unit/ProcessContentTaskTest.php rename to php/tests/Unit/ProcessContentTaskTest.php diff --git a/src/php/tests/UseCase/AdminPanelBasic.php b/php/tests/UseCase/AdminPanelBasic.php similarity index 100% rename from src/php/tests/UseCase/AdminPanelBasic.php rename to php/tests/UseCase/AdminPanelBasic.php diff --git a/src/php/tests/views/admin/api-keys.blade.php b/php/tests/views/admin/api-keys.blade.php similarity index 100% rename from src/php/tests/views/admin/api-keys.blade.php rename to php/tests/views/admin/api-keys.blade.php diff --git a/src/php/tests/views/admin/dashboard.blade.php b/php/tests/views/admin/dashboard.blade.php similarity index 100% rename from src/php/tests/views/admin/dashboard.blade.php rename to php/tests/views/admin/dashboard.blade.php diff --git a/src/php/tests/views/admin/plan-detail.blade.php b/php/tests/views/admin/plan-detail.blade.php similarity index 100% rename from src/php/tests/views/admin/plan-detail.blade.php rename to php/tests/views/admin/plan-detail.blade.php diff --git a/src/php/tests/views/admin/plans.blade.php b/php/tests/views/admin/plans.blade.php similarity index 100% rename from src/php/tests/views/admin/plans.blade.php rename to php/tests/views/admin/plans.blade.php diff --git a/src/php/tests/views/admin/playground.blade.php b/php/tests/views/admin/playground.blade.php similarity index 100% rename from src/php/tests/views/admin/playground.blade.php rename to php/tests/views/admin/playground.blade.php diff --git a/src/php/tests/views/admin/session-detail.blade.php b/php/tests/views/admin/session-detail.blade.php similarity index 100% rename from src/php/tests/views/admin/session-detail.blade.php rename to php/tests/views/admin/session-detail.blade.php diff --git a/src/php/tests/views/admin/sessions.blade.php b/php/tests/views/admin/sessions.blade.php similarity index 100% rename from src/php/tests/views/admin/sessions.blade.php rename to php/tests/views/admin/sessions.blade.php diff --git a/src/php/tests/views/admin/templates.blade.php b/php/tests/views/admin/templates.blade.php similarity index 100% rename from src/php/tests/views/admin/templates.blade.php rename to php/tests/views/admin/templates.blade.php diff --git a/src/php/tests/views/admin/tool-analytics.blade.php b/php/tests/views/admin/tool-analytics.blade.php similarity index 100% rename from src/php/tests/views/admin/tool-analytics.blade.php rename to php/tests/views/admin/tool-analytics.blade.php diff --git a/src/php/tests/views/admin/tool-calls.blade.php b/php/tests/views/admin/tool-calls.blade.php similarity index 100% rename from src/php/tests/views/admin/tool-calls.blade.php rename to php/tests/views/admin/tool-calls.blade.php diff --git a/src/php/Mcp/Prompts/AnalysePerformancePrompt.php b/src/php/Mcp/Prompts/AnalysePerformancePrompt.php deleted file mode 100644 index e657fa3..0000000 --- a/src/php/Mcp/Prompts/AnalysePerformancePrompt.php +++ /dev/null @@ -1,207 +0,0 @@ - - */ - public function arguments(): array - { - return [ - new Argument( - name: 'biolink_id', - description: 'The ID of the biolink to analyse', - required: true - ), - new Argument( - name: 'period', - description: 'Analysis period: 7d, 30d, 90d (default: 30d)', - required: false - ), - ]; - } - - public function handle(): Response - { - return Response::text(<<<'PROMPT' -# Analyse Bio Link Performance - -This workflow helps you analyse a biolink's performance and provide actionable recommendations. - -## Step 1: Gather Analytics Data - -Fetch detailed analytics: -```json -{ - "action": "get_analytics_detailed", - "biolink_id": , - "period": "30d", - "include": ["geo", "devices", "referrers", "utm", "blocks"] -} -``` - -Also get basic biolink info: -```json -{ - "action": "get", - "biolink_id": -} -``` - -## Step 2: Analyse the Data - -Review these key metrics: - -### Traffic Overview -- **Total clicks**: Overall engagement -- **Unique clicks**: Individual visitors -- **Click rate trend**: Is traffic growing or declining? - -### Geographic Insights -Look at the `geo.countries` data: -- Where is traffic coming from? -- Are target markets represented? -- Any unexpected sources? - -### Device Breakdown -Examine `devices` data: -- Mobile vs desktop ratio -- Browser distribution -- Operating systems - -**Optimisation tip:** If mobile traffic is high (>60%), ensure blocks are mobile-friendly. - -### Traffic Sources -Analyse `referrers`: -- Direct traffic (typed URL, QR codes) -- Social media sources -- Search engines -- Other websites - -### UTM Campaign Performance -If using UTM tracking, review `utm`: -- Which campaigns drive traffic? -- Which sources convert best? - -### Block Performance -The `blocks` data shows: -- Which links get the most clicks -- Click-through rate per block -- Underperforming content - -## Step 3: Identify Issues - -Common issues to look for: - -### Low Click-Through Rate -If total clicks are high but block clicks are low: -- Consider reordering blocks (most important first) -- Review link text clarity -- Check if call-to-action is compelling - -### High Bounce Rate -If unique clicks are close to total clicks with low block engagement: -- Page may not match visitor expectations -- Loading issues on certain devices -- Content not relevant to traffic source - -### Geographic Mismatch -If traffic is from unexpected regions: -- Review where links are being shared -- Consider language/localisation -- Check for bot traffic - -### Mobile Performance Issues -If mobile traffic shows different patterns: -- Test page on mobile devices -- Ensure buttons are tap-friendly -- Check image loading - -## Step 4: Generate Recommendations - -Based on analysis, suggest: - -### Quick Wins -- Reorder blocks by popularity -- Update underperforming link text -- Add missing social platforms - -### Medium-Term Improvements -- Create targeted content for top traffic sources -- Implement A/B testing for key links -- Add tracking for better attribution - -### Strategic Changes -- Adjust marketing spend based on source performance -- Consider custom domains for branding -- Set up notification alerts for engagement milestones - -## Step 5: Present Findings - -Summarise for the user: - -```markdown -## Performance Summary for [Biolink Name] - -### Key Metrics (Last 30 Days) -- Total Clicks: X,XXX -- Unique Visitors: X,XXX -- Top Performing Block: [Name] (XX% of clicks) - -### Traffic Sources -1. [Source 1] - XX% -2. [Source 2] - XX% -3. [Source 3] - XX% - -### Geographic Distribution -- [Country 1] - XX% -- [Country 2] - XX% -- [Country 3] - XX% - -### Recommendations -1. [High Priority Action] -2. [Medium Priority Action] -3. [Low Priority Action] - -### Next Steps -- [Specific action item] -- Schedule follow-up analysis in [timeframe] -``` - ---- - -**Analytics Periods:** -- `7d` - Last 7 days (quick check) -- `30d` - Last 30 days (standard analysis) -- `90d` - Last 90 days (trend analysis) - -**Note:** Analytics retention may be limited based on the workspace's subscription tier. - -**Pro Tips:** -- Compare week-over-week for seasonal patterns -- Cross-reference with marketing calendar -- Export submission data for lead quality analysis -PROMPT - ); - } -} diff --git a/src/php/Mcp/Prompts/ConfigureNotificationsPrompt.php b/src/php/Mcp/Prompts/ConfigureNotificationsPrompt.php deleted file mode 100644 index edd88e1..0000000 --- a/src/php/Mcp/Prompts/ConfigureNotificationsPrompt.php +++ /dev/null @@ -1,239 +0,0 @@ - - */ - public function arguments(): array - { - return [ - new Argument( - name: 'biolink_id', - description: 'The ID of the biolink to configure notifications for', - required: true - ), - new Argument( - name: 'notification_type', - description: 'Type of notification: webhook, email, slack, discord, or telegram', - required: false - ), - ]; - } - - public function handle(): Response - { - return Response::text(<<<'PROMPT' -# Configure Biolink Notifications - -Set up real-time notifications when visitors interact with your biolink page. - -## Available Event Types - -| Event | Description | -|-------|-------------| -| `click` | Page view or link click | -| `block_click` | Specific block clicked | -| `form_submit` | Email/phone/contact form submission | -| `payment` | Payment received (if applicable) | - -## Available Handler Types - -### 1. Webhook (Custom Integration) - -Send HTTP POST requests to your own endpoint: -```json -{ - "action": "create_notification_handler", - "biolink_id": , - "name": "My Webhook", - "type": "webhook", - "events": ["form_submit", "payment"], - "settings": { - "url": "https://your-server.com/webhook", - "secret": "optional-hmac-secret" - } -} -``` - -Webhook payload includes: -- Event type and timestamp -- Biolink and block details -- Visitor data (country, device type) -- Form data (for submissions) -- HMAC signature header if secret is set - -### 2. Email Notifications - -Send email alerts: -```json -{ - "action": "create_notification_handler", - "biolink_id": , - "name": "Email Alerts", - "type": "email", - "events": ["form_submit"], - "settings": { - "recipients": ["alerts@example.com", "team@example.com"], - "subject_prefix": "[BioLink]" - } -} -``` - -### 3. Slack Integration - -Post to a Slack channel: -```json -{ - "action": "create_notification_handler", - "biolink_id": , - "name": "Slack Notifications", - "type": "slack", - "events": ["form_submit", "click"], - "settings": { - "webhook_url": "https://hooks.slack.com/services/T.../B.../xxx", - "channel": "#leads", - "username": "BioLink Bot" - } -} -``` - -To get a Slack webhook URL: -1. Go to https://api.slack.com/apps -2. Create or select an app -3. Enable "Incoming Webhooks" -4. Add a webhook to your workspace - -### 4. Discord Integration - -Post to a Discord channel: -```json -{ - "action": "create_notification_handler", - "biolink_id": , - "name": "Discord Notifications", - "type": "discord", - "events": ["form_submit"], - "settings": { - "webhook_url": "https://discord.com/api/webhooks/xxx/yyy", - "username": "BioLink" - } -} -``` - -To get a Discord webhook URL: -1. Open channel settings -2. Go to Integrations > Webhooks -3. Create a new webhook - -### 5. Telegram Integration - -Send messages to a Telegram chat: -```json -{ - "action": "create_notification_handler", - "biolink_id": , - "name": "Telegram Alerts", - "type": "telegram", - "events": ["form_submit"], - "settings": { - "bot_token": "123456:ABC-DEF...", - "chat_id": "-1001234567890" - } -} -``` - -To set up Telegram: -1. Message @BotFather to create a bot -2. Get the bot token -3. Add the bot to your group/channel -4. Get the chat ID (use @userinfobot or API) - -## Managing Handlers - -### List Existing Handlers -```json -{ - "action": "list_notification_handlers", - "biolink_id": -} -``` - -### Update a Handler -```json -{ - "action": "update_notification_handler", - "handler_id": , - "events": ["form_submit"], - "is_enabled": true -} -``` - -### Test a Handler -```json -{ - "action": "test_notification_handler", - "handler_id": -} -``` - -### Disable or Delete -```json -{ - "action": "update_notification_handler", - "handler_id": , - "is_enabled": false -} -``` - -```json -{ - "action": "delete_notification_handler", - "handler_id": -} -``` - -## Auto-Disable Behaviour - -Handlers are automatically disabled after 5 consecutive failures. To re-enable: -```json -{ - "action": "update_notification_handler", - "handler_id": , - "is_enabled": true -} -``` - -This resets the failure counter. - ---- - -**Tips:** -- Use form_submit events for lead generation alerts -- Combine multiple handlers for redundancy -- Test handlers after creation to verify configuration -- Monitor trigger_count and consecutive_failures in list output -PROMPT - ); - } -} diff --git a/src/php/Mcp/Prompts/SetupQrCampaignPrompt.php b/src/php/Mcp/Prompts/SetupQrCampaignPrompt.php deleted file mode 100644 index b296f92..0000000 --- a/src/php/Mcp/Prompts/SetupQrCampaignPrompt.php +++ /dev/null @@ -1,205 +0,0 @@ - - */ - public function arguments(): array - { - return [ - new Argument( - name: 'destination_url', - description: 'The URL where the QR code should redirect to', - required: true - ), - new Argument( - name: 'campaign_name', - description: 'A name for this campaign (e.g., "Summer Flyer 2024")', - required: true - ), - new Argument( - name: 'tracking_platform', - description: 'Analytics platform to use (google_analytics, facebook, etc.)', - required: false - ), - ]; - } - - public function handle(): Response - { - return Response::text(<<<'PROMPT' -# Set Up a QR Code Campaign - -This workflow creates a trackable short link with a QR code for print materials, packaging, or any offline-to-online campaign. - -## Step 1: Gather Campaign Details - -Ask the user for: -- **Destination URL**: Where should the QR code redirect? -- **Campaign name**: For organisation (e.g., "Spring 2024 Flyers") -- **UTM parameters**: Optional tracking parameters -- **QR code style**: Colour preferences, size requirements - -## Step 2: Create a Short Link - -Create a redirect-type biolink: -```json -{ - "action": "create", - "user_id": , - "url": "", - "type": "link", - "location_url": "?utm_source=qr&utm_campaign=" -} -``` - -**Tip:** Include UTM parameters in the destination URL for better attribution in Google Analytics. - -## Step 3: Set Up Tracking Pixel (Optional) - -If the user wants conversion tracking, create a pixel: -```json -{ - "action": "create_pixel", - "user_id": , - "type": "google_analytics", - "pixel_id": "G-XXXXXXXXXX", - "name": " Tracking" -} -``` - -Available pixel types: -- `google_analytics` - GA4 measurement -- `google_tag_manager` - GTM container -- `facebook` - Meta Pixel -- `tiktok` - TikTok Pixel -- `linkedin` - LinkedIn Insight Tag -- `twitter` - Twitter Pixel - -Attach the pixel to the link: -```json -{ - "action": "attach_pixel", - "biolink_id": , - "pixel_id": -} -``` - -## Step 4: Organise in a Project - -Create or use a campaign project: -```json -{ - "action": "create_project", - "user_id": , - "name": "QR Campaigns 2024", - "color": "#6366f1" -} -``` - -Move the link to the project: -```json -{ - "action": "move_to_project", - "biolink_id": , - "project_id": -} -``` - -## Step 5: Generate the QR Code - -Generate with default settings (black on white, 400px): -```json -{ - "action": "generate_qr", - "biolink_id": -} -``` - -Generate with custom styling: -```json -{ - "action": "generate_qr", - "biolink_id": , - "size": 600, - "foreground_colour": "#1a1a1a", - "background_colour": "#ffffff", - "module_style": "rounded", - "ecc_level": "H" -} -``` - -**QR Code Options:** -- `size`: 100-1000 pixels (default: 400) -- `format`: "png" or "svg" -- `foreground_colour`: Hex colour for QR modules (default: #000000) -- `background_colour`: Hex colour for background (default: #ffffff) -- `module_style`: "square", "rounded", or "dots" -- `ecc_level`: Error correction - "L", "M", "Q", or "H" (higher = more resilient but denser) - -The response includes a `data_uri` that can be used directly in HTML or saved as an image. - -## Step 6: Set Up Notifications (Optional) - -Get notified when someone scans the QR code: -```json -{ - "action": "create_notification_handler", - "biolink_id": , - "name": " Alerts", - "type": "slack", - "events": ["click"], - "settings": { - "webhook_url": "https://hooks.slack.com/services/..." - } -} -``` - -## Step 7: Review and Deliver - -Get the final link details: -```json -{ - "action": "get", - "biolink_id": -} -``` - -Provide the user with: -1. The short URL for reference -2. The QR code image (data URI or downloadable) -3. Instructions for the print designer - ---- - -**Best Practices:** -- Use error correction level "H" for QR codes on curved surfaces or small prints -- Keep foreground/background contrast high for reliable scanning -- Test the QR code on multiple devices before printing -- Include the short URL as text near the QR code as a fallback -- Use different short links for each print run to track effectiveness -PROMPT - ); - } -} diff --git a/src/php/Mcp/Servers/HostHub.php b/src/php/Mcp/Servers/HostHub.php deleted file mode 100644 index 35f1ca7..0000000 --- a/src/php/Mcp/Servers/HostHub.php +++ /dev/null @@ -1,184 +0,0 @@ -: Get detailed tool information - - utility_tools action=execute tool= input={...}: Execute a tool - - Available tool categories: Marketing, Development, Design, Security, Network, Text, Converters, Generators, Link Generators, Miscellaneous - - ## Available Prompts - - create_biolink_page: Step-by-step biolink page creation - - setup_qr_campaign: Create QR code campaign with tracking - - configure_notifications: Set up notification handlers - - analyse_performance: Analyse biolink performance with recommendations - - ## Available Resources - - config://app: Application configuration - - schema://database: Full database schema - - content://{workspace}/{slug}: Content item as markdown - - biolink://{workspace}/{slug}: Biolink page as markdown - MARKDOWN; - - protected array $tools = [ - ListSites::class, - GetStats::class, - ListRoutes::class, - QueryDatabase::class, - ListTables::class, - // Commerce tools - GetBillingStatus::class, - ListInvoices::class, - CreateCoupon::class, - UpgradePlan::class, - // Content tools - ContentTools::class, - // BioHost tools - \Mod\Bio\Mcp\Tools\BioLinkTools::class, - \Mod\Bio\Mcp\Tools\AnalyticsTools::class, - \Mod\Bio\Mcp\Tools\DomainTools::class, - \Mod\Bio\Mcp\Tools\ProjectTools::class, - \Mod\Bio\Mcp\Tools\PixelTools::class, - \Mod\Bio\Mcp\Tools\QrTools::class, - \Mod\Bio\Mcp\Tools\ThemeTools::class, - \Mod\Bio\Mcp\Tools\NotificationTools::class, - \Mod\Bio\Mcp\Tools\SubmissionTools::class, - \Mod\Bio\Mcp\Tools\TemplateTools::class, - \Mod\Bio\Mcp\Tools\StaticPageTools::class, - \Mod\Bio\Mcp\Tools\PwaTools::class, - // TrustHost tools - \Mod\Trust\Mcp\Tools\CampaignTools::class, - \Mod\Trust\Mcp\Tools\NotificationTools::class, - \Mod\Trust\Mcp\Tools\AnalyticsTools::class, - // Utility tools - \Mod\Tools\Mcp\Tools\UtilityTools::class, - ]; - - protected array $resources = [ - AppConfig::class, - DatabaseSchema::class, - ContentResource::class, - BioResource::class, - ]; - - protected array $prompts = [ - CreateBioPagePrompt::class, - SetupQrCampaignPrompt::class, - ConfigureNotificationsPrompt::class, - AnalysePerformancePrompt::class, - ]; -} diff --git a/src/php/Mcp/Servers/Marketing.php b/src/php/Mcp/Servers/Marketing.php deleted file mode 100644 index 50938dd..0000000 --- a/src/php/Mcp/Servers/Marketing.php +++ /dev/null @@ -1,114 +0,0 @@ - - */ - protected array $scopes = ['read']; - - /** - * Tool-specific timeout override (null uses config default). - */ - protected ?int $timeout = null; - - /** - * Get the tool category. - */ - public function category(): string - { - return $this->category; - } - - /** - * Get required scopes. - */ - public function requiredScopes(): array - { - return $this->scopes; - } - - /** - * Get the timeout for this tool in seconds. - */ - public function getTimeout(): int - { - // Check tool-specific override - if ($this->timeout !== null) { - return $this->timeout; - } - - // Check per-tool config - $perToolTimeout = config('mcp.timeouts.per_tool.'.$this->name()); - if ($perToolTimeout !== null) { - return (int) $perToolTimeout; - } - - // Use default timeout - return (int) config('mcp.timeouts.default', 30); - } - - /** - * Convert to MCP tool definition format. - */ - public function toMcpDefinition(): array - { - return [ - 'name' => $this->name(), - 'description' => $this->description(), - 'inputSchema' => $this->inputSchema(), - ]; - } - - /** - * Create a success response. - */ - protected function success(array $data): array - { - return array_merge(['success' => true], $data); - } - - /** - * Create an error response. - */ - protected function error(string $message, ?string $code = null): array - { - $response = ['error' => $message]; - - if ($code !== null) { - $response['code'] = $code; - } - - return $response; - } - - /** - * Get a required argument or return error. - */ - protected function require(array $args, string $key, ?string $label = null): mixed - { - if (! isset($args[$key]) || $args[$key] === '') { - throw new \InvalidArgumentException( - sprintf('%s is required', $label ?? $key) - ); - } - - return $args[$key]; - } - - /** - * Get an optional argument with default. - */ - protected function optional(array $args, string $key, mixed $default = null): mixed - { - return $args[$key] ?? $default; - } - - /** - * Validate and get a required string argument. - * - * @throws \InvalidArgumentException - */ - protected function requireString(array $args, string $key, ?int $maxLength = null, ?string $label = null): string - { - $value = $this->require($args, $key, $label); - - if (! is_string($value)) { - throw new \InvalidArgumentException( - sprintf('%s must be a string', $label ?? $key) - ); - } - - if ($maxLength !== null && strlen($value) > $maxLength) { - throw new \InvalidArgumentException( - sprintf('%s exceeds maximum length of %d characters', $label ?? $key, $maxLength) - ); - } - - return $value; - } - - /** - * Validate and get a required integer argument. - * - * @throws \InvalidArgumentException - */ - protected function requireInt(array $args, string $key, ?int $min = null, ?int $max = null, ?string $label = null): int - { - $value = $this->require($args, $key, $label); - - if (! is_int($value) && ! (is_numeric($value) && (int) $value == $value)) { - throw new \InvalidArgumentException( - sprintf('%s must be an integer', $label ?? $key) - ); - } - - $intValue = (int) $value; - - if ($min !== null && $intValue < $min) { - throw new \InvalidArgumentException( - sprintf('%s must be at least %d', $label ?? $key, $min) - ); - } - - if ($max !== null && $intValue > $max) { - throw new \InvalidArgumentException( - sprintf('%s must be at most %d', $label ?? $key, $max) - ); - } - - return $intValue; - } - - /** - * Validate and get an optional string argument. - */ - protected function optionalString(array $args, string $key, ?string $default = null, ?int $maxLength = null): ?string - { - $value = $args[$key] ?? $default; - - if ($value === null) { - return null; - } - - if (! is_string($value)) { - throw new \InvalidArgumentException( - sprintf('%s must be a string', $key) - ); - } - - if ($maxLength !== null && strlen($value) > $maxLength) { - throw new \InvalidArgumentException( - sprintf('%s exceeds maximum length of %d characters', $key, $maxLength) - ); - } - - return $value; - } - - /** - * Validate and get an optional integer argument. - */ - protected function optionalInt(array $args, string $key, ?int $default = null, ?int $min = null, ?int $max = null): ?int - { - if (! isset($args[$key])) { - return $default; - } - - $value = $args[$key]; - - if (! is_int($value) && ! (is_numeric($value) && (int) $value == $value)) { - throw new \InvalidArgumentException( - sprintf('%s must be an integer', $key) - ); - } - - $intValue = (int) $value; - - if ($min !== null && $intValue < $min) { - throw new \InvalidArgumentException( - sprintf('%s must be at least %d', $key, $min) - ); - } - - if ($max !== null && $intValue > $max) { - throw new \InvalidArgumentException( - sprintf('%s must be at most %d', $key, $max) - ); - } - - return $intValue; - } - - /** - * Validate and get a required array argument. - * - * @throws \InvalidArgumentException - */ - protected function requireArray(array $args, string $key, ?string $label = null): array - { - $value = $this->require($args, $key, $label); - - if (! is_array($value)) { - throw new \InvalidArgumentException( - sprintf('%s must be an array', $label ?? $key) - ); - } - - return $value; - } - - /** - * Validate a value is one of the allowed values. - * - * @throws \InvalidArgumentException - */ - protected function requireEnum(array $args, string $key, array $allowed, ?string $label = null): string - { - $value = $this->requireString($args, $key, null, $label); - - if (! in_array($value, $allowed, true)) { - throw new \InvalidArgumentException( - sprintf('%s must be one of: %s', $label ?? $key, implode(', ', $allowed)) - ); - } - - return $value; - } - - /** - * Validate an optional enum value. - */ - protected function optionalEnum(array $args, string $key, array $allowed, ?string $default = null): ?string - { - if (! isset($args[$key])) { - return $default; - } - - $value = $args[$key]; - - if (! is_string($value)) { - throw new \InvalidArgumentException( - sprintf('%s must be a string', $key) - ); - } - - if (! in_array($value, $allowed, true)) { - throw new \InvalidArgumentException( - sprintf('%s must be one of: %s', $key, implode(', ', $allowed)) - ); - } - - return $value; - } - - /** - * Execute an operation with circuit breaker protection. - * - * Wraps calls to external modules (Agentic, Content, etc.) with fault tolerance. - * If the service fails repeatedly, the circuit opens and returns the fallback. - * - * @param string $service Service identifier (e.g., 'agentic', 'content') - * @param Closure $operation The operation to execute - * @param Closure|null $fallback Optional fallback when circuit is open - * @return mixed The operation result or fallback value - */ - protected function withCircuitBreaker(string $service, Closure $operation, ?Closure $fallback = null): mixed - { - $breaker = app(CircuitBreaker::class); - - try { - return $breaker->call($service, $operation, $fallback); - } catch (CircuitOpenException $e) { - // If no fallback was provided and circuit is open, return error response - return $this->error($e->getMessage(), 'service_unavailable'); - } - } - - /** - * Check if an external service is available. - * - * @param string $service Service identifier (e.g., 'agentic', 'content') - */ - protected function isServiceAvailable(string $service): bool - { - return app(CircuitBreaker::class)->isAvailable($service); - } -} diff --git a/src/php/Mcp/Tools/Agent/Brain/BrainForget.php b/src/php/Mcp/Tools/Agent/Brain/BrainForget.php deleted file mode 100644 index 6f3cafb..0000000 --- a/src/php/Mcp/Tools/Agent/Brain/BrainForget.php +++ /dev/null @@ -1,78 +0,0 @@ - 'object', - 'properties' => [ - 'id' => [ - 'type' => 'string', - 'format' => 'uuid', - 'description' => 'UUID of the memory to remove', - ], - 'reason' => [ - 'type' => 'string', - 'description' => 'Optional reason for forgetting this memory', - 'maxLength' => 500, - ], - ], - 'required' => ['id'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai'); - } - - $id = $args['id'] ?? ''; - $reason = $this->optionalString($args, 'reason', null, 500); - $agentId = $context['agent_id'] ?? $context['session_id'] ?? 'anonymous'; - - return $this->withCircuitBreaker('brain', function () use ($id, $workspaceId, $agentId, $reason) { - $result = ForgetKnowledge::run($id, (int) $workspaceId, $agentId, $reason); - - return $this->success($result); - }, fn () => $this->error('Brain service temporarily unavailable. Memory could not be removed.', 'service_unavailable')); - } -} diff --git a/src/php/Mcp/Tools/Agent/Brain/BrainList.php b/src/php/Mcp/Tools/Agent/Brain/BrainList.php deleted file mode 100644 index bffaf6e..0000000 --- a/src/php/Mcp/Tools/Agent/Brain/BrainList.php +++ /dev/null @@ -1,81 +0,0 @@ - 'object', - 'properties' => [ - 'project' => [ - 'type' => 'string', - 'description' => 'Filter by project scope', - ], - 'type' => [ - 'type' => 'string', - 'description' => 'Filter by memory type', - 'enum' => BrainMemory::VALID_TYPES, - ], - 'agent_id' => [ - 'type' => 'string', - 'description' => 'Filter by originating agent', - ], - 'limit' => [ - 'type' => 'integer', - 'description' => 'Maximum results to return (default: 20, max: 100)', - 'minimum' => 1, - 'maximum' => 100, - 'default' => 20, - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai'); - } - - $result = ListKnowledge::run((int) $workspaceId, $args); - - return $this->success($result); - } -} diff --git a/src/php/Mcp/Tools/Agent/Brain/BrainRecall.php b/src/php/Mcp/Tools/Agent/Brain/BrainRecall.php deleted file mode 100644 index f2b67fd..0000000 --- a/src/php/Mcp/Tools/Agent/Brain/BrainRecall.php +++ /dev/null @@ -1,119 +0,0 @@ - 'object', - 'properties' => [ - 'query' => [ - 'type' => 'string', - 'description' => 'Natural language search query (max 2,000 characters)', - 'maxLength' => 2000, - ], - 'top_k' => [ - 'type' => 'integer', - 'description' => 'Number of results to return (default: 5, max: 20)', - 'minimum' => 1, - 'maximum' => 20, - 'default' => 5, - ], - 'filter' => [ - 'type' => 'object', - 'description' => 'Optional filters to narrow results', - 'properties' => [ - 'project' => [ - 'type' => 'string', - 'description' => 'Filter by project scope', - ], - 'type' => [ - 'oneOf' => [ - ['type' => 'string', 'enum' => BrainMemory::VALID_TYPES], - [ - 'type' => 'array', - 'items' => ['type' => 'string', 'enum' => BrainMemory::VALID_TYPES], - ], - ], - 'description' => 'Filter by memory type (single or array)', - ], - 'agent_id' => [ - 'type' => 'string', - 'description' => 'Filter by originating agent', - ], - 'min_confidence' => [ - 'type' => 'number', - 'description' => 'Minimum confidence threshold (0.0-1.0)', - 'minimum' => 0.0, - 'maximum' => 1.0, - ], - ], - ], - ], - 'required' => ['query'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai'); - } - - $query = $args['query'] ?? ''; - $topK = $this->optionalInt($args, 'top_k', 5, 1, 20); - $filter = $this->optional($args, 'filter', []); - - if (! is_array($filter)) { - return $this->error('filter must be an object'); - } - - return $this->withCircuitBreaker('brain', function () use ($query, $workspaceId, $filter, $topK) { - $result = RecallKnowledge::run($query, (int) $workspaceId, $filter, $topK); - - return $this->success([ - 'count' => $result['count'], - 'memories' => $result['memories'], - 'scores' => $result['scores'], - ]); - }, fn () => $this->error('Brain service temporarily unavailable. Recall failed.', 'service_unavailable')); - } -} diff --git a/src/php/Mcp/Tools/Agent/Brain/BrainRemember.php b/src/php/Mcp/Tools/Agent/Brain/BrainRemember.php deleted file mode 100644 index 9cc84a2..0000000 --- a/src/php/Mcp/Tools/Agent/Brain/BrainRemember.php +++ /dev/null @@ -1,103 +0,0 @@ - 'object', - 'properties' => [ - 'content' => [ - 'type' => 'string', - 'description' => 'The knowledge to remember (max 50,000 characters)', - 'maxLength' => 50000, - ], - 'type' => [ - 'type' => 'string', - 'description' => 'Memory type classification', - 'enum' => BrainMemory::VALID_TYPES, - ], - 'tags' => [ - 'type' => 'array', - 'items' => ['type' => 'string'], - 'description' => 'Optional tags for categorisation', - ], - 'project' => [ - 'type' => 'string', - 'description' => 'Optional project scope (e.g. repo name)', - ], - 'confidence' => [ - 'type' => 'number', - 'description' => 'Confidence level from 0.0 to 1.0 (default: 0.8)', - 'minimum' => 0.0, - 'maximum' => 1.0, - ], - 'supersedes' => [ - 'type' => 'string', - 'format' => 'uuid', - 'description' => 'UUID of an older memory this one replaces', - ], - 'expires_in' => [ - 'type' => 'integer', - 'description' => 'Hours until this memory expires (null = never)', - 'minimum' => 1, - ], - ], - 'required' => ['content', 'type'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai'); - } - - $agentId = $context['agent_id'] ?? $context['session_id'] ?? 'anonymous'; - - return $this->withCircuitBreaker('brain', function () use ($args, $workspaceId, $agentId) { - $memory = RememberKnowledge::run($args, (int) $workspaceId, $agentId); - - return $this->success([ - 'memory' => $memory->toMcpContext(), - ]); - }, fn () => $this->error('Brain service temporarily unavailable. Memory could not be stored.', 'service_unavailable')); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentBatchGenerate.php b/src/php/Mcp/Tools/Agent/Content/ContentBatchGenerate.php deleted file mode 100644 index a1773c7..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentBatchGenerate.php +++ /dev/null @@ -1,85 +0,0 @@ - 'object', - 'properties' => [ - 'limit' => [ - 'type' => 'integer', - 'description' => 'Maximum briefs to process (default: 5)', - ], - 'mode' => [ - 'type' => 'string', - 'description' => 'Generation mode', - 'enum' => ['draft', 'refine', 'full'], - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $limit = $this->optionalInt($args, 'limit', 5, 1, 50); - $mode = $this->optionalEnum($args, 'mode', ['draft', 'refine', 'full'], 'full'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $query = ContentBrief::readyToProcess(); - - // Scope to workspace if provided - if (! empty($context['workspace_id'])) { - $query->where('workspace_id', $context['workspace_id']); - } - - $briefs = $query->limit($limit)->get(); - - if ($briefs->isEmpty()) { - return $this->success([ - 'message' => 'No briefs ready for processing', - 'queued' => 0, - ]); - } - - foreach ($briefs as $brief) { - GenerateContentJob::dispatch($brief, $mode); - } - - return $this->success([ - 'queued' => $briefs->count(), - 'mode' => $mode, - 'brief_ids' => $briefs->pluck('id')->all(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentBriefCreate.php b/src/php/Mcp/Tools/Agent/Content/ContentBriefCreate.php deleted file mode 100644 index e922a0b..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentBriefCreate.php +++ /dev/null @@ -1,128 +0,0 @@ - 'object', - 'properties' => [ - 'title' => [ - 'type' => 'string', - 'description' => 'Content title', - ], - 'content_type' => [ - 'type' => 'string', - 'description' => 'Type of content', - 'enum' => BriefContentType::values(), - ], - 'service' => [ - 'type' => 'string', - 'description' => 'Service context (e.g., BioHost, QRHost)', - ], - 'keywords' => [ - 'type' => 'array', - 'description' => 'SEO keywords to include', - 'items' => ['type' => 'string'], - ], - 'target_word_count' => [ - 'type' => 'integer', - 'description' => 'Target word count (default: 800)', - ], - 'description' => [ - 'type' => 'string', - 'description' => 'Brief description of what to write about', - ], - 'difficulty' => [ - 'type' => 'string', - 'description' => 'Target audience level', - 'enum' => ['beginner', 'intermediate', 'advanced'], - ], - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Link to an existing plan', - ], - ], - 'required' => ['title', 'content_type'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $title = $this->requireString($args, 'title', 255); - $contentType = $this->requireEnum($args, 'content_type', BriefContentType::values()); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $plan = null; - if (! empty($args['plan_slug'])) { - $plan = AgentPlan::where('slug', $args['plan_slug'])->first(); - if (! $plan) { - return $this->error("Plan not found: {$args['plan_slug']}"); - } - } - - // Determine workspace_id from context - $workspaceId = $context['workspace_id'] ?? null; - - $brief = ContentBrief::create([ - 'workspace_id' => $workspaceId, - 'title' => $title, - 'slug' => Str::slug($title).'-'.Str::random(6), - 'content_type' => $contentType, - 'service' => $args['service'] ?? null, - 'description' => $args['description'] ?? null, - 'keywords' => $args['keywords'] ?? null, - 'target_word_count' => $args['target_word_count'] ?? 800, - 'difficulty' => $args['difficulty'] ?? null, - 'status' => ContentBrief::STATUS_PENDING, - 'metadata' => $plan ? [ - 'plan_id' => $plan->id, - 'plan_slug' => $plan->slug, - ] : null, - ]); - - return $this->success([ - 'brief' => [ - 'id' => $brief->id, - 'title' => $brief->title, - 'slug' => $brief->slug, - 'status' => $brief->status, - 'content_type' => $brief->content_type instanceof BriefContentType - ? $brief->content_type->value - : $brief->content_type, - ], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentBriefGet.php b/src/php/Mcp/Tools/Agent/Content/ContentBriefGet.php deleted file mode 100644 index 72fd152..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentBriefGet.php +++ /dev/null @@ -1,92 +0,0 @@ - 'object', - 'properties' => [ - 'id' => [ - 'type' => 'integer', - 'description' => 'Brief ID', - ], - ], - 'required' => ['id'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $id = $this->requireInt($args, 'id', 1); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $brief = ContentBrief::find($id); - - if (! $brief) { - return $this->error("Brief not found: {$id}"); - } - - // Optional workspace scoping for multi-tenant security - if (! empty($context['workspace_id']) && $brief->workspace_id !== $context['workspace_id']) { - return $this->error('Access denied: brief belongs to a different workspace'); - } - - return $this->success([ - 'brief' => [ - 'id' => $brief->id, - 'title' => $brief->title, - 'slug' => $brief->slug, - 'status' => $brief->status, - 'content_type' => $brief->content_type instanceof BriefContentType - ? $brief->content_type->value - : $brief->content_type, - 'service' => $brief->service, - 'description' => $brief->description, - 'keywords' => $brief->keywords, - 'target_word_count' => $brief->target_word_count, - 'difficulty' => $brief->difficulty, - 'draft_output' => $brief->draft_output, - 'refined_output' => $brief->refined_output, - 'final_content' => $brief->final_content, - 'error_message' => $brief->error_message, - 'generation_log' => $brief->generation_log, - 'metadata' => $brief->metadata, - 'total_cost' => $brief->total_cost, - 'created_at' => $brief->created_at->toIso8601String(), - 'updated_at' => $brief->updated_at->toIso8601String(), - 'generated_at' => $brief->generated_at?->toIso8601String(), - 'refined_at' => $brief->refined_at?->toIso8601String(), - 'published_at' => $brief->published_at?->toIso8601String(), - ], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentBriefList.php b/src/php/Mcp/Tools/Agent/Content/ContentBriefList.php deleted file mode 100644 index 6c0f9d2..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentBriefList.php +++ /dev/null @@ -1,86 +0,0 @@ - 'object', - 'properties' => [ - 'status' => [ - 'type' => 'string', - 'description' => 'Filter by status', - 'enum' => ['pending', 'queued', 'generating', 'review', 'published', 'failed'], - ], - 'limit' => [ - 'type' => 'integer', - 'description' => 'Maximum results (default: 20)', - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $limit = $this->optionalInt($args, 'limit', 20, 1, 100); - $status = $this->optionalEnum($args, 'status', [ - 'pending', 'queued', 'generating', 'review', 'published', 'failed', - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $query = ContentBrief::query()->orderBy('created_at', 'desc'); - - // Scope to workspace if provided - if (! empty($context['workspace_id'])) { - $query->where('workspace_id', $context['workspace_id']); - } - - if ($status) { - $query->where('status', $status); - } - - $briefs = $query->limit($limit)->get(); - - return $this->success([ - 'briefs' => $briefs->map(fn ($brief) => [ - 'id' => $brief->id, - 'title' => $brief->title, - 'status' => $brief->status, - 'content_type' => $brief->content_type instanceof BriefContentType - ? $brief->content_type->value - : $brief->content_type, - 'service' => $brief->service, - 'created_at' => $brief->created_at->toIso8601String(), - ])->all(), - 'total' => $briefs->count(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentFromPlan.php b/src/php/Mcp/Tools/Agent/Content/ContentFromPlan.php deleted file mode 100644 index c1c257b..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentFromPlan.php +++ /dev/null @@ -1,163 +0,0 @@ - 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug to generate content from', - ], - 'content_type' => [ - 'type' => 'string', - 'description' => 'Type of content to generate', - 'enum' => BriefContentType::values(), - ], - 'service' => [ - 'type' => 'string', - 'description' => 'Service context', - ], - 'limit' => [ - 'type' => 'integer', - 'description' => 'Maximum briefs to create (default: 5)', - ], - 'target_word_count' => [ - 'type' => 'integer', - 'description' => 'Target word count per article', - ], - ], - 'required' => ['plan_slug'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $planSlug = $this->requireString($args, 'plan_slug', 255); - $limit = $this->optionalInt($args, 'limit', 5, 1, 50); - $wordCount = $this->optionalInt($args, 'target_word_count', 800, 100, 10000); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $plan = AgentPlan::with('agentPhases') - ->where('slug', $planSlug) - ->first(); - - if (! $plan) { - return $this->error("Plan not found: {$planSlug}"); - } - - $contentType = $args['content_type'] ?? 'help_article'; - $service = $args['service'] ?? ($plan->context['service'] ?? null); - - // Get workspace_id from context - $workspaceId = $context['workspace_id'] ?? $plan->workspace_id; - - $phases = $plan->agentPhases() - ->whereIn('status', ['pending', 'in_progress']) - ->get(); - - if ($phases->isEmpty()) { - return $this->success([ - 'message' => 'No pending phases in plan', - 'created' => 0, - ]); - } - - $briefsCreated = []; - - foreach ($phases as $phase) { - $tasks = $phase->tasks ?? []; - - foreach ($tasks as $index => $task) { - if (count($briefsCreated) >= $limit) { - break 2; - } - - $taskName = is_string($task) ? $task : ($task['name'] ?? ''); - $taskStatus = is_array($task) ? ($task['status'] ?? 'pending') : 'pending'; - - // Skip completed tasks - if ($taskStatus === 'completed' || empty($taskName)) { - continue; - } - - // Create brief from task - $brief = ContentBrief::create([ - 'workspace_id' => $workspaceId, - 'title' => $taskName, - 'slug' => Str::slug($taskName).'-'.Str::random(6), - 'content_type' => $contentType, - 'service' => $service, - 'target_word_count' => $wordCount, - 'status' => ContentBrief::STATUS_QUEUED, - 'metadata' => [ - 'plan_id' => $plan->id, - 'plan_slug' => $plan->slug, - 'phase_order' => $phase->order, - 'phase_name' => $phase->name, - 'task_index' => $index, - ], - ]); - - // Queue for generation - GenerateContentJob::dispatch($brief, 'full'); - - $briefsCreated[] = [ - 'id' => $brief->id, - 'title' => $brief->title, - 'phase' => $phase->name, - ]; - } - } - - if (empty($briefsCreated)) { - return $this->success([ - 'message' => 'No eligible tasks found (all completed or empty)', - 'created' => 0, - ]); - } - - return $this->success([ - 'created' => count($briefsCreated), - 'content_type' => $contentType, - 'service' => $service, - 'briefs' => $briefsCreated, - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentGenerate.php b/src/php/Mcp/Tools/Agent/Content/ContentGenerate.php deleted file mode 100644 index 3529403..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentGenerate.php +++ /dev/null @@ -1,172 +0,0 @@ - Claude refine)'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'brief_id' => [ - 'type' => 'integer', - 'description' => 'Brief ID to generate content for', - ], - 'mode' => [ - 'type' => 'string', - 'description' => 'Generation mode', - 'enum' => ['draft', 'refine', 'full'], - ], - 'sync' => [ - 'type' => 'boolean', - 'description' => 'Run synchronously (wait for result) vs queue for async processing', - ], - ], - 'required' => ['brief_id'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $briefId = $this->requireInt($args, 'brief_id', 1); - $mode = $this->optionalEnum($args, 'mode', ['draft', 'refine', 'full'], 'full'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $brief = ContentBrief::find($briefId); - - if (! $brief) { - return $this->error("Brief not found: {$briefId}"); - } - - // Optional workspace scoping - if (! empty($context['workspace_id']) && $brief->workspace_id !== $context['workspace_id']) { - return $this->error('Access denied: brief belongs to a different workspace'); - } - - $gateway = app(AIGatewayService::class); - - if (! $gateway->isAvailable()) { - return $this->error('AI providers not configured. Set GOOGLE_AI_API_KEY and ANTHROPIC_API_KEY.'); - } - - $sync = $args['sync'] ?? false; - - if ($sync) { - return $this->generateSync($brief, $gateway, $mode); - } - - // Queue for async processing - $brief->markQueued(); - GenerateContentJob::dispatch($brief, $mode); - - return $this->success([ - 'brief_id' => $brief->id, - 'status' => 'queued', - 'mode' => $mode, - 'message' => 'Content generation queued for async processing', - ]); - } - - /** - * Run generation synchronously and return results. - */ - protected function generateSync(ContentBrief $brief, AIGatewayService $gateway, string $mode): array - { - try { - if ($mode === 'full') { - $result = $gateway->generateAndRefine($brief); - - return $this->success([ - 'brief_id' => $brief->id, - 'status' => $brief->fresh()->status, - 'draft' => [ - 'model' => $result['draft']->model, - 'tokens' => $result['draft']->totalTokens(), - 'cost' => $result['draft']->estimateCost(), - ], - 'refined' => [ - 'model' => $result['refined']->model, - 'tokens' => $result['refined']->totalTokens(), - 'cost' => $result['refined']->estimateCost(), - ], - ]); - } - - if ($mode === 'draft') { - $response = $gateway->generateDraft($brief); - $brief->markDraftComplete($response->content); - - return $this->success([ - 'brief_id' => $brief->id, - 'status' => $brief->fresh()->status, - 'draft' => [ - 'model' => $response->model, - 'tokens' => $response->totalTokens(), - 'cost' => $response->estimateCost(), - ], - ]); - } - - if ($mode === 'refine') { - if (! $brief->isGenerated()) { - return $this->error('No draft to refine. Generate draft first.'); - } - - $response = $gateway->refineDraft($brief, $brief->draft_output); - $brief->markRefined($response->content); - - return $this->success([ - 'brief_id' => $brief->id, - 'status' => $brief->fresh()->status, - 'refined' => [ - 'model' => $response->model, - 'tokens' => $response->totalTokens(), - 'cost' => $response->estimateCost(), - ], - ]); - } - - return $this->error("Invalid mode: {$mode}"); - } catch (\Exception $e) { - $brief->markFailed($e->getMessage()); - - return $this->error("Generation failed: {$e->getMessage()}"); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentStatus.php b/src/php/Mcp/Tools/Agent/Content/ContentStatus.php deleted file mode 100644 index fa88735..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentStatus.php +++ /dev/null @@ -1,60 +0,0 @@ - 'object', - 'properties' => (object) [], - ]; - } - - public function handle(array $args, array $context = []): array - { - $gateway = app(AIGatewayService::class); - - return $this->success([ - 'providers' => [ - 'gemini' => $gateway->isGeminiAvailable(), - 'claude' => $gateway->isClaudeAvailable(), - ], - 'pipeline_available' => $gateway->isAvailable(), - 'briefs' => [ - 'pending' => ContentBrief::pending()->count(), - 'queued' => ContentBrief::where('status', ContentBrief::STATUS_QUEUED)->count(), - 'generating' => ContentBrief::where('status', ContentBrief::STATUS_GENERATING)->count(), - 'review' => ContentBrief::needsReview()->count(), - 'published' => ContentBrief::where('status', ContentBrief::STATUS_PUBLISHED)->count(), - 'failed' => ContentBrief::where('status', ContentBrief::STATUS_FAILED)->count(), - ], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Content/ContentUsageStats.php b/src/php/Mcp/Tools/Agent/Content/ContentUsageStats.php deleted file mode 100644 index 9d6e3ee..0000000 --- a/src/php/Mcp/Tools/Agent/Content/ContentUsageStats.php +++ /dev/null @@ -1,68 +0,0 @@ - 'object', - 'properties' => [ - 'period' => [ - 'type' => 'string', - 'description' => 'Time period for stats', - 'enum' => ['day', 'week', 'month', 'year'], - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $period = $this->optionalEnum($args, 'period', ['day', 'week', 'month', 'year'], 'month'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - // Use workspace_id from context if available (null returns system-wide stats) - $workspaceId = $context['workspace_id'] ?? null; - - $stats = AIUsage::statsForWorkspace($workspaceId, $period); - - return $this->success([ - 'period' => $period, - 'total_requests' => $stats['total_requests'], - 'total_input_tokens' => (int) $stats['total_input_tokens'], - 'total_output_tokens' => (int) $stats['total_output_tokens'], - 'total_cost' => number_format((float) $stats['total_cost'], 4), - 'by_provider' => $stats['by_provider'], - 'by_purpose' => $stats['by_purpose'], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Contracts/AgentToolInterface.php b/src/php/Mcp/Tools/Agent/Contracts/AgentToolInterface.php deleted file mode 100644 index 8e15ec7..0000000 --- a/src/php/Mcp/Tools/Agent/Contracts/AgentToolInterface.php +++ /dev/null @@ -1,50 +0,0 @@ - List of required scopes - */ - public function requiredScopes(): array; - - /** - * Get the tool category for grouping. - */ - public function category(): string; -} diff --git a/src/php/Mcp/Tools/Agent/Messaging/AgentConversation.php b/src/php/Mcp/Tools/Agent/Messaging/AgentConversation.php deleted file mode 100644 index 3d7c7f6..0000000 --- a/src/php/Mcp/Tools/Agent/Messaging/AgentConversation.php +++ /dev/null @@ -1,78 +0,0 @@ - 'object', - 'properties' => [ - 'me' => [ - 'type' => 'string', - 'description' => 'Your agent name (e.g. "cladius")', - 'maxLength' => 100, - ], - 'agent' => [ - 'type' => 'string', - 'description' => 'The other agent to view conversation with (e.g. "charon")', - 'maxLength' => 100, - ], - ], - 'required' => ['me', 'agent'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - $me = $this->requireString($args, 'me', 100); - $agent = $this->requireString($args, 'agent', 100); - - $messages = AgentMessage::where('workspace_id', $workspaceId) - ->conversation($me, $agent) - ->limit(50) - ->get() - ->map(fn (AgentMessage $m) => [ - 'id' => $m->id, - 'from' => $m->from_agent, - 'to' => $m->to_agent, - 'subject' => $m->subject, - 'content' => $m->content, - 'read' => $m->read_at !== null, - 'created_at' => $m->created_at->toIso8601String(), - ]); - - return $this->success([ - 'count' => $messages->count(), - 'messages' => $messages->toArray(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Messaging/AgentInbox.php b/src/php/Mcp/Tools/Agent/Messaging/AgentInbox.php deleted file mode 100644 index b97538e..0000000 --- a/src/php/Mcp/Tools/Agent/Messaging/AgentInbox.php +++ /dev/null @@ -1,72 +0,0 @@ - 'object', - 'properties' => [ - 'agent' => [ - 'type' => 'string', - 'description' => 'Your agent name (e.g. "cladius", "charon")', - 'maxLength' => 100, - ], - ], - 'required' => ['agent'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - $agent = $this->requireString($args, 'agent', 100); - - $messages = AgentMessage::where('workspace_id', $workspaceId) - ->inbox($agent) - ->limit(20) - ->get() - ->map(fn (AgentMessage $m) => [ - 'id' => $m->id, - 'from' => $m->from_agent, - 'to' => $m->to_agent, - 'subject' => $m->subject, - 'content' => $m->content, - 'read' => $m->read_at !== null, - 'created_at' => $m->created_at->toIso8601String(), - ]); - - return $this->success([ - 'count' => $messages->count(), - 'messages' => $messages->toArray(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Messaging/AgentSend.php b/src/php/Mcp/Tools/Agent/Messaging/AgentSend.php deleted file mode 100644 index 23a4385..0000000 --- a/src/php/Mcp/Tools/Agent/Messaging/AgentSend.php +++ /dev/null @@ -1,89 +0,0 @@ - 'object', - 'properties' => [ - 'to' => [ - 'type' => 'string', - 'description' => 'Recipient agent name (e.g. "charon", "cladius")', - 'maxLength' => 100, - ], - 'from' => [ - 'type' => 'string', - 'description' => 'Sender agent name (e.g. "cladius")', - 'maxLength' => 100, - ], - 'content' => [ - 'type' => 'string', - 'description' => 'Message content', - 'maxLength' => 10000, - ], - 'subject' => [ - 'type' => 'string', - 'description' => 'Optional subject line', - 'maxLength' => 255, - ], - ], - 'required' => ['to', 'from', 'content'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - $to = $this->requireString($args, 'to', 100); - $from = $this->requireString($args, 'from', 100); - $content = $this->requireString($args, 'content', 10000); - $subject = $this->optionalString($args, 'subject', null, 255); - - $message = AgentMessage::create([ - 'workspace_id' => $workspaceId, - 'from_agent' => $from, - 'to_agent' => $to, - 'content' => $content, - 'subject' => $subject, - ]); - - return $this->success([ - 'id' => $message->id, - 'from' => $message->from_agent, - 'to' => $message->to_agent, - 'created_at' => $message->created_at->toIso8601String(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Phase/PhaseAddCheckpoint.php b/src/php/Mcp/Tools/Agent/Phase/PhaseAddCheckpoint.php deleted file mode 100644 index a2d8e84..0000000 --- a/src/php/Mcp/Tools/Agent/Phase/PhaseAddCheckpoint.php +++ /dev/null @@ -1,78 +0,0 @@ - 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'phase' => [ - 'type' => 'string', - 'description' => 'Phase identifier (number or name)', - ], - 'note' => [ - 'type' => 'string', - 'description' => 'Checkpoint note', - ], - 'context' => [ - 'type' => 'object', - 'description' => 'Additional context data', - ], - ], - 'required' => ['plan_slug', 'phase', 'note'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $phase = AddCheckpoint::run( - $args['plan_slug'] ?? '', - $args['phase'] ?? '', - $args['note'] ?? '', - (int) $workspaceId, - $args['context'] ?? [], - ); - - return $this->success([ - 'checkpoints' => $phase->getCheckpoints(), - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Phase/PhaseGet.php b/src/php/Mcp/Tools/Agent/Phase/PhaseGet.php deleted file mode 100644 index 1afc535..0000000 --- a/src/php/Mcp/Tools/Agent/Phase/PhaseGet.php +++ /dev/null @@ -1,76 +0,0 @@ - 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'phase' => [ - 'type' => 'string', - 'description' => 'Phase identifier (number or name)', - ], - ], - 'required' => ['plan_slug', 'phase'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $phase = GetPhase::run( - $args['plan_slug'] ?? '', - $args['phase'] ?? '', - (int) $workspaceId, - ); - - return $this->success([ - 'phase' => [ - 'order' => $phase->order, - 'name' => $phase->name, - 'description' => $phase->description, - 'status' => $phase->status, - 'tasks' => $phase->tasks, - 'checkpoints' => $phase->getCheckpoints(), - 'dependencies' => $phase->dependencies, - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Phase/PhaseUpdateStatus.php b/src/php/Mcp/Tools/Agent/Phase/PhaseUpdateStatus.php deleted file mode 100644 index ef4bff1..0000000 --- a/src/php/Mcp/Tools/Agent/Phase/PhaseUpdateStatus.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::entityExists('plan', 'Plan must exist', ['arg_key' => 'plan_slug']), - ]; - } - - public function name(): string - { - return 'phase_update_status'; - } - - public function description(): string - { - return 'Update the status of a phase'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'phase' => [ - 'type' => 'string', - 'description' => 'Phase identifier (number or name)', - ], - 'status' => [ - 'type' => 'string', - 'description' => 'New status', - 'enum' => ['pending', 'in_progress', 'completed', 'blocked', 'skipped'], - ], - 'notes' => [ - 'type' => 'string', - 'description' => 'Optional notes about the status change', - ], - ], - 'required' => ['plan_slug', 'phase', 'status'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $phase = UpdatePhaseStatus::run( - $args['plan_slug'] ?? '', - $args['phase'] ?? '', - $args['status'] ?? '', - (int) $workspaceId, - $args['notes'] ?? null, - ); - - return $this->success([ - 'phase' => [ - 'order' => $phase->order, - 'name' => $phase->name, - 'status' => $phase->status, - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Plan/PlanArchive.php b/src/php/Mcp/Tools/Agent/Plan/PlanArchive.php deleted file mode 100644 index 3eedd6f..0000000 --- a/src/php/Mcp/Tools/Agent/Plan/PlanArchive.php +++ /dev/null @@ -1,72 +0,0 @@ - 'object', - 'properties' => [ - 'slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'reason' => [ - 'type' => 'string', - 'description' => 'Reason for archiving', - ], - ], - 'required' => ['slug'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $plan = ArchivePlan::run( - $args['slug'] ?? '', - (int) $workspaceId, - $args['reason'] ?? null, - ); - - return $this->success([ - 'plan' => [ - 'slug' => $plan->slug, - 'status' => 'archived', - 'archived_at' => $plan->archived_at?->toIso8601String(), - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Plan/PlanCreate.php b/src/php/Mcp/Tools/Agent/Plan/PlanCreate.php deleted file mode 100644 index dfd877a..0000000 --- a/src/php/Mcp/Tools/Agent/Plan/PlanCreate.php +++ /dev/null @@ -1,105 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required'), - ]; - } - - public function name(): string - { - return 'plan_create'; - } - - public function description(): string - { - return 'Create a new work plan with phases and tasks'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'title' => [ - 'type' => 'string', - 'description' => 'Plan title', - ], - 'slug' => [ - 'type' => 'string', - 'description' => 'URL-friendly identifier (auto-generated if not provided)', - ], - 'description' => [ - 'type' => 'string', - 'description' => 'Plan description', - ], - 'context' => [ - 'type' => 'object', - 'description' => 'Additional context (related files, dependencies, etc.)', - ], - 'phases' => [ - 'type' => 'array', - 'description' => 'Array of phase definitions with name, description, and tasks', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'name' => ['type' => 'string'], - 'description' => ['type' => 'string'], - 'tasks' => [ - 'type' => 'array', - 'items' => ['type' => 'string'], - ], - ], - ], - ], - ], - 'required' => ['title'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session. See: https://host.uk.com/ai'); - } - - try { - $plan = CreatePlan::run($args, (int) $workspaceId); - - return $this->success([ - 'plan' => [ - 'slug' => $plan->slug, - 'title' => $plan->title, - 'status' => $plan->status, - 'phases' => $plan->agentPhases->count(), - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Plan/PlanGet.php b/src/php/Mcp/Tools/Agent/Plan/PlanGet.php deleted file mode 100644 index ce1f77c..0000000 --- a/src/php/Mcp/Tools/Agent/Plan/PlanGet.php +++ /dev/null @@ -1,84 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required for plan operations'), - ]; - } - - public function name(): string - { - return 'plan_get'; - } - - public function description(): string - { - return 'Get detailed information about a specific plan'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'format' => [ - 'type' => 'string', - 'description' => 'Output format: json or markdown', - 'enum' => ['json', 'markdown'], - ], - ], - 'required' => ['slug'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session. See: https://host.uk.com/ai'); - } - - try { - $plan = GetPlan::run($args['slug'] ?? '', (int) $workspaceId); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $format = $args['format'] ?? 'json'; - - if ($format === 'markdown') { - return $this->success(['markdown' => $plan->toMarkdown()]); - } - - return $this->success(['plan' => $plan->toMcpContext()]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Plan/PlanList.php b/src/php/Mcp/Tools/Agent/Plan/PlanList.php deleted file mode 100644 index c003669..0000000 --- a/src/php/Mcp/Tools/Agent/Plan/PlanList.php +++ /dev/null @@ -1,90 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required for plan operations'), - ]; - } - - public function name(): string - { - return 'plan_list'; - } - - public function description(): string - { - return 'List all work plans with their current status and progress'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'status' => [ - 'type' => 'string', - 'description' => 'Filter by status (draft, active, paused, completed, archived)', - 'enum' => ['draft', 'active', 'paused', 'completed', 'archived'], - ], - 'include_archived' => [ - 'type' => 'boolean', - 'description' => 'Include archived plans (default: false)', - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session. See: https://host.uk.com/ai'); - } - - try { - $plans = ListPlans::run( - (int) $workspaceId, - $args['status'] ?? null, - (bool) ($args['include_archived'] ?? false), - ); - - return $this->success([ - 'plans' => $plans->map(fn ($plan) => [ - 'slug' => $plan->slug, - 'title' => $plan->title, - 'status' => $plan->status, - 'progress' => $plan->getProgress(), - 'updated_at' => $plan->updated_at->toIso8601String(), - ])->all(), - 'total' => $plans->count(), - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Plan/PlanUpdateStatus.php b/src/php/Mcp/Tools/Agent/Plan/PlanUpdateStatus.php deleted file mode 100644 index 6a4c917..0000000 --- a/src/php/Mcp/Tools/Agent/Plan/PlanUpdateStatus.php +++ /dev/null @@ -1,72 +0,0 @@ - 'object', - 'properties' => [ - 'slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'status' => [ - 'type' => 'string', - 'description' => 'New status', - 'enum' => ['draft', 'active', 'paused', 'completed'], - ], - ], - 'required' => ['slug', 'status'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $plan = UpdatePlanStatus::run( - $args['slug'] ?? '', - $args['status'] ?? '', - (int) $workspaceId, - ); - - return $this->success([ - 'plan' => [ - 'slug' => $plan->slug, - 'status' => $plan->status, - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/README.md b/src/php/Mcp/Tools/Agent/README.md deleted file mode 100644 index 8112c3e..0000000 --- a/src/php/Mcp/Tools/Agent/README.md +++ /dev/null @@ -1,279 +0,0 @@ -# MCP Agent Tools - -This directory contains MCP (Model Context Protocol) tool implementations for the agent orchestration system. All tools extend `AgentTool` and integrate with the `ToolDependency` system to declare and validate their execution prerequisites. - -## Directory Structure - -``` -Mcp/Tools/Agent/ -├── AgentTool.php # Base class — extend this for all new tools -├── Contracts/ -│ └── AgentToolInterface.php # Tool contract -├── Content/ # Content generation tools -├── Phase/ # Plan phase management tools -├── Plan/ # Work plan CRUD tools -├── Session/ # Agent session lifecycle tools -├── State/ # Shared workspace state tools -├── Task/ # Task status and tracking tools -└── Template/ # Template listing and application tools -``` - -## ToolDependency System - -`ToolDependency` (from `Core\Mcp\Dependencies\ToolDependency`) lets a tool declare what must be true in the execution context before it runs. The `AgentToolRegistry` validates these automatically — the tool's `handle()` method is never called if a dependency is unmet. - -### How It Works - -1. A tool declares its dependencies in a `dependencies()` method returning `ToolDependency[]`. -2. When the tool is registered, `AgentToolRegistry::register()` passes those dependencies to `ToolDependencyService`. -3. On each call, `AgentToolRegistry::execute()` calls `ToolDependencyService::validateDependencies()` before invoking `handle()`. -4. If any required dependency fails, a `MissingDependencyException` is thrown and the tool is never called. -5. After a successful call, `ToolDependencyService::recordToolCall()` logs the execution for audit purposes. - -### Dependency Types - -#### `contextExists` — Require a context field - -Validates that a key is present in the `$context` array passed at execution time. Use this for multi-tenant isolation fields like `workspace_id` that come from API key authentication. - -```php -ToolDependency::contextExists('workspace_id', 'Workspace context required') -``` - -Mark a dependency optional with `->asOptional()` when the tool can work without it (e.g. the value can be inferred from another argument): - -```php -// SessionStart: workspace can be inferred from the plan if plan_slug is provided -ToolDependency::contextExists('workspace_id', 'Workspace context required (or provide plan_slug)') - ->asOptional() -``` - -#### `sessionState` — Require an active session - -Validates that a session is active. Use this for tools that must run within an established session context. - -```php -ToolDependency::sessionState('session_id', 'Active session required. Call session_start first.') -``` - -#### `entityExists` — Require a database entity - -Validates that an entity exists in the database before the tool runs. The `arg_key` maps to the tool argument that holds the entity identifier. - -```php -ToolDependency::entityExists('plan', 'Plan must exist', ['arg_key' => 'plan_slug']) -``` - -## Context Requirements - -The `$context` array is injected into every tool's `handle(array $args, array $context)` call. Context is set by API key authentication middleware — tools should never hardcode or fall back to default values. - -| Key | Type | Set by | Used by | -|-----|------|--------|---------| -| `workspace_id` | `string\|int` | API key auth middleware | All workspace-scoped tools | -| `session_id` | `string` | Client (from `session_start` response) | Session-dependent tools | - -**Multi-tenant safety:** Always validate `workspace_id` in `handle()` as a defence-in-depth measure, even when a `contextExists` dependency is declared. Use `forWorkspace($workspaceId)` scopes on all queries. - -```php -$workspaceId = $context['workspace_id'] ?? null; -if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai'); -} - -$plan = AgentPlan::forWorkspace($workspaceId)->where('slug', $slug)->first(); -``` - -## Creating a New Tool - -### 1. Create the class - -Place the file in the appropriate subdirectory and extend `AgentTool`: - -```php - 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - ], - 'required' => ['plan_slug'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $planSlug = $this->requireString($args, 'plan_slug', 255); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. See: https://host.uk.com/ai'); - } - - $plan = AgentPlan::forWorkspace($workspaceId)->where('slug', $planSlug)->first(); - - if (! $plan) { - return $this->error("Plan not found: {$planSlug}"); - } - - $plan->update(['status' => 'active']); - - return $this->success(['plan' => ['slug' => $plan->slug, 'status' => $plan->status]]); - } -} -``` - -### 2. Register the tool - -Add it to the tool registration list in the package boot sequence (see `Boot.php` and the `McpToolsRegistering` event handler). - -### 3. Write tests - -Add a Pest test file under `Tests/` covering success and failure paths, including missing dependency scenarios. - -## AgentTool Base Class Reference - -### Properties - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `$category` | `string` | `'general'` | Groups tools in the registry | -| `$scopes` | `string[]` | `['read']` | API key scopes required to call this tool | -| `$timeout` | `?int` | `null` | Per-tool timeout override in seconds (null uses config default of 30s) | - -### Argument Helpers - -All helpers throw `\InvalidArgumentException` on failure. Catch it in `handle()` and return `$this->error()`. - -| Method | Description | -|--------|-------------| -| `requireString($args, $key, $maxLength, $label)` | Required string with optional max length | -| `requireInt($args, $key, $min, $max, $label)` | Required integer with optional bounds | -| `requireArray($args, $key, $label)` | Required array | -| `requireEnum($args, $key, $allowed, $label)` | Required string constrained to allowed values | -| `optionalString($args, $key, $default, $maxLength)` | Optional string | -| `optionalInt($args, $key, $default, $min, $max)` | Optional integer | -| `optionalEnum($args, $key, $allowed, $default)` | Optional enum string | -| `optional($args, $key, $default)` | Optional value of any type | - -### Response Helpers - -```php -return $this->success(['key' => 'value']); // merges ['success' => true] -return $this->error('Something went wrong'); -return $this->error('Resource locked', 'resource_locked'); // with error code -``` - -### Circuit Breaker - -Wrap calls to external services with `withCircuitBreaker()` for fault tolerance: - -```php -return $this->withCircuitBreaker( - 'agentic', // service name - fn () => $this->doWork(), // operation - fn () => $this->error('Service unavailable', 'service_unavailable') // fallback -); -``` - -If no fallback is provided and the circuit is open, `error()` is returned automatically. - -### Timeout Override - -For long-running tools (e.g. content generation), override the timeout: - -```php -protected ?int $timeout = 300; // 5 minutes -``` - -## Dependency Resolution Order - -Dependencies are validated in the order they are returned from `dependencies()`. All required dependencies must pass before the tool runs. Optional dependencies are checked but do not block execution. - -Recommended declaration order: - -1. `contextExists('workspace_id', ...)` — tenant isolation first -2. `sessionState('session_id', ...)` — session presence second -3. `entityExists(...)` — entity existence last (may query DB) - -## Troubleshooting - -### "Workspace context required" - -The `workspace_id` key is missing from the execution context. This is injected by the API key authentication middleware. Causes: - -- Request is unauthenticated or the API key is invalid. -- The API key has no workspace association. -- Dependency validation was bypassed but the tool checks it internally. - -**Fix:** Authenticate with a valid API key. See https://host.uk.com/ai. - -### "Active session required. Call session_start first." - -The `session_id` context key is missing. The tool requires an active session. - -**Fix:** Call `session_start` before calling session-dependent tools. Pass the returned `session_id` in the context of all subsequent calls. - -### "Plan must exist" / "Plan not found" - -The `plan_slug` argument does not match any plan. Either the plan was never created, the slug is misspelled, or the plan belongs to a different workspace. - -**Fix:** Call `plan_list` to find valid slugs, then retry. - -### "Permission denied: API key missing scope" - -The API key does not have the required scope (`read` or `write`) for the tool. - -**Fix:** Issue a new API key with the correct scopes, or use an existing key that has the required permissions. - -### "Unknown tool: {name}" - -The tool name does not match any registered tool. - -**Fix:** Check `plan_list` / MCP tool discovery endpoint for the exact tool name. Names are snake_case. - -### `MissingDependencyException` in logs - -A required dependency was not met and the framework threw before calling `handle()`. The exception message will identify which dependency failed. - -**Fix:** Inspect the `context` passed to `execute()`. Ensure required keys are present and the relevant entity exists. diff --git a/src/php/Mcp/Tools/Agent/Session/SessionArtifact.php b/src/php/Mcp/Tools/Agent/Session/SessionArtifact.php deleted file mode 100644 index 9f2b0c9..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionArtifact.php +++ /dev/null @@ -1,81 +0,0 @@ - 'object', - 'properties' => [ - 'path' => [ - 'type' => 'string', - 'description' => 'File or resource path', - ], - 'action' => [ - 'type' => 'string', - 'description' => 'Action performed', - 'enum' => ['created', 'modified', 'deleted', 'reviewed'], - ], - 'description' => [ - 'type' => 'string', - 'description' => 'Description of changes', - ], - ], - 'required' => ['path', 'action'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $path = $this->require($args, 'path'); - $action = $this->require($args, 'action'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $sessionId = $context['session_id'] ?? null; - - if (! $sessionId) { - return $this->error('No active session. Call session_start first.'); - } - - $session = AgentSession::where('session_id', $sessionId)->first(); - - if (! $session) { - return $this->error('Session not found'); - } - - $session->addArtifact( - $path, - $action, - $this->optional($args, 'description') - ); - - return $this->success(['artifact' => $path]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionContinue.php b/src/php/Mcp/Tools/Agent/Session/SessionContinue.php deleted file mode 100644 index 712088d..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionContinue.php +++ /dev/null @@ -1,73 +0,0 @@ - 'object', - 'properties' => [ - 'previous_session_id' => [ - 'type' => 'string', - 'description' => 'Session ID to continue from', - ], - 'agent_type' => [ - 'type' => 'string', - 'description' => 'New agent type taking over', - ], - ], - 'required' => ['previous_session_id', 'agent_type'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $session = ContinueSession::run( - $args['previous_session_id'] ?? '', - $args['agent_type'] ?? '', - ); - - $inheritedContext = $session->context_summary ?? []; - - return $this->success([ - 'session' => [ - 'session_id' => $session->session_id, - 'agent_type' => $session->agent_type, - 'status' => $session->status, - 'plan' => $session->plan?->slug, - ], - 'continued_from' => $inheritedContext['continued_from'] ?? null, - 'previous_agent' => $inheritedContext['previous_agent'] ?? null, - 'handoff_notes' => $inheritedContext['handoff_notes'] ?? null, - 'inherited_context' => $inheritedContext['inherited_context'] ?? null, - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionEnd.php b/src/php/Mcp/Tools/Agent/Session/SessionEnd.php deleted file mode 100644 index 34f57e5..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionEnd.php +++ /dev/null @@ -1,73 +0,0 @@ - 'object', - 'properties' => [ - 'status' => [ - 'type' => 'string', - 'description' => 'Final session status', - 'enum' => ['completed', 'handed_off', 'paused', 'failed'], - ], - 'summary' => [ - 'type' => 'string', - 'description' => 'Final summary', - ], - ], - 'required' => ['status'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $sessionId = $context['session_id'] ?? null; - if (! $sessionId) { - return $this->error('No active session'); - } - - try { - $session = EndSession::run( - $sessionId, - $args['status'] ?? '', - $args['summary'] ?? null, - ); - - return $this->success([ - 'session' => [ - 'session_id' => $session->session_id, - 'status' => $session->status, - 'duration' => $session->getDurationFormatted(), - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionHandoff.php b/src/php/Mcp/Tools/Agent/Session/SessionHandoff.php deleted file mode 100644 index ad59a65..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionHandoff.php +++ /dev/null @@ -1,88 +0,0 @@ - 'object', - 'properties' => [ - 'summary' => [ - 'type' => 'string', - 'description' => 'Summary of work done', - ], - 'next_steps' => [ - 'type' => 'array', - 'description' => 'Recommended next steps', - 'items' => ['type' => 'string'], - ], - 'blockers' => [ - 'type' => 'array', - 'description' => 'Any blockers encountered', - 'items' => ['type' => 'string'], - ], - 'context_for_next' => [ - 'type' => 'object', - 'description' => 'Context to pass to next agent', - ], - ], - 'required' => ['summary'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $summary = $this->require($args, 'summary'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $sessionId = $context['session_id'] ?? null; - - if (! $sessionId) { - return $this->error('No active session. Call session_start first.'); - } - - $session = AgentSession::where('session_id', $sessionId)->first(); - - if (! $session) { - return $this->error('Session not found'); - } - - $session->prepareHandoff( - $summary, - $this->optional($args, 'next_steps', []), - $this->optional($args, 'blockers', []), - $this->optional($args, 'context_for_next', []) - ); - - return $this->success([ - 'handoff_context' => $session->getHandoffContext(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionList.php b/src/php/Mcp/Tools/Agent/Session/SessionList.php deleted file mode 100644 index 551147c..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionList.php +++ /dev/null @@ -1,83 +0,0 @@ - 'object', - 'properties' => [ - 'status' => [ - 'type' => 'string', - 'description' => 'Filter by status', - 'enum' => ['active', 'paused', 'completed', 'failed'], - ], - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Filter by plan slug', - ], - 'limit' => [ - 'type' => 'integer', - 'description' => 'Maximum number of sessions to return', - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $sessions = ListSessions::run( - (int) $workspaceId, - $args['status'] ?? null, - $args['plan_slug'] ?? null, - isset($args['limit']) ? (int) $args['limit'] : null, - ); - - return $this->success([ - 'sessions' => $sessions->map(fn ($session) => [ - 'session_id' => $session->session_id, - 'agent_type' => $session->agent_type, - 'status' => $session->status, - 'plan' => $session->plan?->slug, - 'duration' => $session->getDurationFormatted(), - 'started_at' => $session->started_at->toIso8601String(), - 'last_active_at' => $session->last_active_at->toIso8601String(), - 'has_handoff' => ! empty($session->handoff_notes), - ])->all(), - 'total' => $sessions->count(), - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionLog.php b/src/php/Mcp/Tools/Agent/Session/SessionLog.php deleted file mode 100644 index 54e1f58..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionLog.php +++ /dev/null @@ -1,93 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::sessionState('session_id', 'Active session required. Call session_start first.'), - ]; - } - - public function name(): string - { - return 'session_log'; - } - - public function description(): string - { - return 'Log an entry in the current session'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'message' => [ - 'type' => 'string', - 'description' => 'Log message', - ], - 'type' => [ - 'type' => 'string', - 'description' => 'Log type', - 'enum' => ['info', 'progress', 'decision', 'error', 'checkpoint'], - ], - 'data' => [ - 'type' => 'object', - 'description' => 'Additional data to log', - ], - ], - 'required' => ['message'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $message = $this->require($args, 'message'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $sessionId = $context['session_id'] ?? null; - - if (! $sessionId) { - return $this->error('No active session. Call session_start first.'); - } - - $session = AgentSession::where('session_id', $sessionId)->first(); - - if (! $session) { - return $this->error('Session not found'); - } - - $session->addWorkLogEntry( - $message, - $this->optional($args, 'type', 'info'), - $this->optional($args, 'data', []) - ); - - return $this->success(['logged' => $message]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionReplay.php b/src/php/Mcp/Tools/Agent/Session/SessionReplay.php deleted file mode 100644 index fe0f46b..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionReplay.php +++ /dev/null @@ -1,101 +0,0 @@ - 'object', - 'properties' => [ - 'session_id' => [ - 'type' => 'string', - 'description' => 'Session ID to replay from', - ], - 'agent_type' => [ - 'type' => 'string', - 'description' => 'Agent type for the new session (defaults to original session\'s agent type)', - ], - 'context_only' => [ - 'type' => 'boolean', - 'description' => 'If true, only return the replay context without creating a new session', - ], - ], - 'required' => ['session_id'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $sessionId = $this->require($args, 'session_id'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $agentType = $this->optional($args, 'agent_type'); - $contextOnly = $this->optional($args, 'context_only', false); - - return $this->withCircuitBreaker('agentic', function () use ($sessionId, $agentType, $contextOnly) { - $sessionService = app(AgentSessionService::class); - - // If only context requested, return the replay context - if ($contextOnly) { - $replayContext = $sessionService->getReplayContext($sessionId); - - if (! $replayContext) { - return $this->error("Session not found: {$sessionId}"); - } - - return $this->success([ - 'replay_context' => $replayContext, - ]); - } - - // Create a new replay session - $newSession = $sessionService->replay($sessionId, $agentType); - - if (! $newSession) { - return $this->error("Session not found: {$sessionId}"); - } - - return $this->success([ - 'session' => [ - 'session_id' => $newSession->session_id, - 'agent_type' => $newSession->agent_type, - 'status' => $newSession->status, - 'plan' => $newSession->plan?->slug, - ], - 'replayed_from' => $sessionId, - 'context_summary' => $newSession->context_summary, - ]); - }, fn () => $this->error('Agentic service temporarily unavailable.', 'service_unavailable')); - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionResume.php b/src/php/Mcp/Tools/Agent/Session/SessionResume.php deleted file mode 100644 index e85083b..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionResume.php +++ /dev/null @@ -1,74 +0,0 @@ - 'object', - 'properties' => [ - 'session_id' => [ - 'type' => 'string', - 'description' => 'Session ID to resume', - ], - ], - 'required' => ['session_id'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $sessionId = $this->require($args, 'session_id'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $sessionService = app(AgentSessionService::class); - $session = $sessionService->resume($sessionId); - - if (! $session) { - return $this->error("Session not found: {$sessionId}"); - } - - // Get handoff context if available - $handoffContext = $session->getHandoffContext(); - - return $this->success([ - 'session' => [ - 'session_id' => $session->session_id, - 'agent_type' => $session->agent_type, - 'status' => $session->status, - 'plan' => $session->plan?->slug, - 'duration' => $session->getDurationFormatted(), - ], - 'handoff_context' => $handoffContext['handoff_notes'] ?? null, - 'recent_actions' => $handoffContext['recent_actions'] ?? [], - 'artifacts' => $handoffContext['artifacts'] ?? [], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Session/SessionStart.php b/src/php/Mcp/Tools/Agent/Session/SessionStart.php deleted file mode 100644 index f2605c4..0000000 --- a/src/php/Mcp/Tools/Agent/Session/SessionStart.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ - public function dependencies(): array - { - // Soft dependency - workspace can come from plan - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required (or provide plan_slug)') - ->asOptional(), - ]; - } - - public function name(): string - { - return 'session_start'; - } - - public function description(): string - { - return 'Start a new agent session for a plan'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'agent_type' => [ - 'type' => 'string', - 'description' => 'Type of agent (e.g., opus, sonnet, haiku)', - ], - 'context' => [ - 'type' => 'object', - 'description' => 'Initial session context', - ], - ], - 'required' => ['agent_type'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session, or provide a valid plan_slug to infer workspace context. See: https://host.uk.com/ai'); - } - - try { - $session = StartSession::run( - $args['agent_type'] ?? '', - $args['plan_slug'] ?? null, - (int) $workspaceId, - $args['context'] ?? [], - ); - - return $this->success([ - 'session' => [ - 'session_id' => $session->session_id, - 'agent_type' => $session->agent_type, - 'plan' => $session->plan?->slug, - 'status' => $session->status, - ], - ]); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/State/StateGet.php b/src/php/Mcp/Tools/Agent/State/StateGet.php deleted file mode 100644 index 590043f..0000000 --- a/src/php/Mcp/Tools/Agent/State/StateGet.php +++ /dev/null @@ -1,99 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required for state operations'), - ]; - } - - public function name(): string - { - return 'state_get'; - } - - public function description(): string - { - return 'Get a workspace state value'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'key' => [ - 'type' => 'string', - 'description' => 'State key', - ], - ], - 'required' => ['plan_slug', 'key'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $planSlug = $this->require($args, 'plan_slug'); - $key = $this->require($args, 'key'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - // Validate workspace context for tenant isolation - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session. See: https://host.uk.com/ai'); - } - - // Query plan with workspace scope to prevent cross-tenant access - $plan = AgentPlan::forWorkspace($workspaceId) - ->where('slug', $planSlug) - ->first(); - - if (! $plan) { - return $this->error("Plan not found: {$planSlug}"); - } - - $state = $plan->states()->where('key', $key)->first(); - - if (! $state) { - return $this->error("State not found: {$key}"); - } - - return $this->success([ - 'key' => $state->key, - 'value' => $state->value, - 'category' => $state->category, - 'updated_at' => $state->updated_at->toIso8601String(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/State/StateList.php b/src/php/Mcp/Tools/Agent/State/StateList.php deleted file mode 100644 index 694ab61..0000000 --- a/src/php/Mcp/Tools/Agent/State/StateList.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required for state operations'), - ]; - } - - public function name(): string - { - return 'state_list'; - } - - public function description(): string - { - return 'List all state values for a plan'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'category' => [ - 'type' => 'string', - 'description' => 'Filter by category', - ], - ], - 'required' => ['plan_slug'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $planSlug = $this->require($args, 'plan_slug'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - // Validate workspace context for tenant isolation - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session. See: https://host.uk.com/ai'); - } - - // Query plan with workspace scope to prevent cross-tenant access - $plan = AgentPlan::forWorkspace($workspaceId) - ->where('slug', $planSlug) - ->first(); - - if (! $plan) { - return $this->error("Plan not found: {$planSlug}"); - } - - $query = $plan->states(); - - $category = $this->optional($args, 'category'); - if (! empty($category)) { - $query->where('category', $category); - } - - $states = $query->get(); - - return $this->success([ - 'states' => $states->map(fn ($state) => [ - 'key' => $state->key, - 'value' => $state->value, - 'category' => $state->category, - ])->all(), - 'total' => $states->count(), - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/State/StateSet.php b/src/php/Mcp/Tools/Agent/State/StateSet.php deleted file mode 100644 index f7c6b1d..0000000 --- a/src/php/Mcp/Tools/Agent/State/StateSet.php +++ /dev/null @@ -1,115 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::contextExists('workspace_id', 'Workspace context required for state operations'), - ]; - } - - public function name(): string - { - return 'state_set'; - } - - public function description(): string - { - return 'Set a workspace state value'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'key' => [ - 'type' => 'string', - 'description' => 'State key', - ], - 'value' => [ - 'type' => ['string', 'number', 'boolean', 'object', 'array'], - 'description' => 'State value', - ], - 'category' => [ - 'type' => 'string', - 'description' => 'State category for organisation', - ], - ], - 'required' => ['plan_slug', 'key', 'value'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $planSlug = $this->require($args, 'plan_slug'); - $key = $this->require($args, 'key'); - $value = $this->require($args, 'value'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - // Validate workspace context for tenant isolation - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key and started a session. See: https://host.uk.com/ai'); - } - - // Query plan with workspace scope to prevent cross-tenant access - $plan = AgentPlan::forWorkspace($workspaceId) - ->where('slug', $planSlug) - ->first(); - - if (! $plan) { - return $this->error("Plan not found: {$planSlug}"); - } - - $state = WorkspaceState::updateOrCreate( - [ - 'agent_plan_id' => $plan->id, - 'key' => $key, - ], - [ - 'value' => $value, - 'category' => $this->optional($args, 'category', 'general'), - ] - ); - - return $this->success([ - 'state' => [ - 'key' => $state->key, - 'value' => $state->value, - 'category' => $state->category, - ], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Task/TaskToggle.php b/src/php/Mcp/Tools/Agent/Task/TaskToggle.php deleted file mode 100644 index 266ec76..0000000 --- a/src/php/Mcp/Tools/Agent/Task/TaskToggle.php +++ /dev/null @@ -1,84 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::entityExists('plan', 'Plan must exist', ['arg_key' => 'plan_slug']), - ]; - } - - public function name(): string - { - return 'task_toggle'; - } - - public function description(): string - { - return 'Toggle a task completion status'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'phase' => [ - 'type' => 'string', - 'description' => 'Phase identifier (number or name)', - ], - 'task_index' => [ - 'type' => 'integer', - 'description' => 'Task index (0-based)', - ], - ], - 'required' => ['plan_slug', 'phase', 'task_index'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $result = ToggleTask::run( - $args['plan_slug'] ?? '', - $args['phase'] ?? '', - (int) ($args['task_index'] ?? 0), - (int) $workspaceId, - ); - - return $this->success($result); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Task/TaskUpdate.php b/src/php/Mcp/Tools/Agent/Task/TaskUpdate.php deleted file mode 100644 index 09d2c96..0000000 --- a/src/php/Mcp/Tools/Agent/Task/TaskUpdate.php +++ /dev/null @@ -1,95 +0,0 @@ - - */ - public function dependencies(): array - { - return [ - ToolDependency::entityExists('plan', 'Plan must exist', ['arg_key' => 'plan_slug']), - ]; - } - - public function name(): string - { - return 'task_update'; - } - - public function description(): string - { - return 'Update task details (status, notes)'; - } - - public function inputSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'plan_slug' => [ - 'type' => 'string', - 'description' => 'Plan slug identifier', - ], - 'phase' => [ - 'type' => 'string', - 'description' => 'Phase identifier (number or name)', - ], - 'task_index' => [ - 'type' => 'integer', - 'description' => 'Task index (0-based)', - ], - 'status' => [ - 'type' => 'string', - 'description' => 'New status', - 'enum' => ['pending', 'in_progress', 'completed', 'blocked', 'skipped'], - ], - 'notes' => [ - 'type' => 'string', - 'description' => 'Task notes', - ], - ], - 'required' => ['plan_slug', 'phase', 'task_index'], - ]; - } - - public function handle(array $args, array $context = []): array - { - $workspaceId = $context['workspace_id'] ?? null; - if ($workspaceId === null) { - return $this->error('workspace_id is required'); - } - - try { - $result = UpdateTask::run( - $args['plan_slug'] ?? '', - $args['phase'] ?? '', - (int) ($args['task_index'] ?? 0), - (int) $workspaceId, - $args['status'] ?? null, - $args['notes'] ?? null, - ); - - return $this->success($result); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - } -} diff --git a/src/php/Mcp/Tools/Agent/Template/TemplateCreatePlan.php b/src/php/Mcp/Tools/Agent/Template/TemplateCreatePlan.php deleted file mode 100644 index 0b4439b..0000000 --- a/src/php/Mcp/Tools/Agent/Template/TemplateCreatePlan.php +++ /dev/null @@ -1,99 +0,0 @@ - 'object', - 'properties' => [ - 'template' => [ - 'type' => 'string', - 'description' => 'Template name/slug', - ], - 'variables' => [ - 'type' => 'object', - 'description' => 'Variable values for the template', - ], - 'slug' => [ - 'type' => 'string', - 'description' => 'Custom slug for the plan', - ], - ], - 'required' => ['template', 'variables'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $templateSlug = $this->require($args, 'template'); - $variables = $this->require($args, 'variables'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $templateService = app(PlanTemplateService::class); - - $options = []; - $customSlug = $this->optional($args, 'slug'); - if (! empty($customSlug)) { - $options['slug'] = $customSlug; - } - - if (isset($context['workspace_id'])) { - $options['workspace_id'] = $context['workspace_id']; - } - - try { - $plan = $templateService->createPlan($templateSlug, $variables, $options); - } catch (\Throwable $e) { - return $this->error('Failed to create plan from template: '.$e->getMessage()); - } - - if (! $plan) { - return $this->error('Failed to create plan from template'); - } - - $phases = $plan->agentPhases; - $progress = $plan->getProgress(); - - return $this->success([ - 'plan' => [ - 'slug' => $plan->slug, - 'title' => $plan->title, - 'status' => $plan->status, - 'phases' => $phases?->count() ?? 0, - 'total_tasks' => $progress['total'] ?? 0, - ], - 'commands' => [ - 'view' => "php artisan plan:show {$plan->slug}", - 'activate' => "php artisan plan:status {$plan->slug} --set=active", - ], - ]); - } -} diff --git a/src/php/Mcp/Tools/Agent/Template/TemplateList.php b/src/php/Mcp/Tools/Agent/Template/TemplateList.php deleted file mode 100644 index dbd0cef..0000000 --- a/src/php/Mcp/Tools/Agent/Template/TemplateList.php +++ /dev/null @@ -1,57 +0,0 @@ - 'object', - 'properties' => [ - 'category' => [ - 'type' => 'string', - 'description' => 'Filter by category', - ], - ], - ]; - } - - public function handle(array $args, array $context = []): array - { - $templateService = app(PlanTemplateService::class); - $templates = $templateService->listTemplates(); - - $category = $this->optional($args, 'category'); - if (! empty($category)) { - $templates = array_filter($templates, fn ($t) => ($t['category'] ?? '') === $category); - } - - return [ - 'templates' => array_values($templates), - 'total' => count($templates), - ]; - } -} diff --git a/src/php/Mcp/Tools/Agent/Template/TemplatePreview.php b/src/php/Mcp/Tools/Agent/Template/TemplatePreview.php deleted file mode 100644 index da6f9d8..0000000 --- a/src/php/Mcp/Tools/Agent/Template/TemplatePreview.php +++ /dev/null @@ -1,69 +0,0 @@ - 'object', - 'properties' => [ - 'template' => [ - 'type' => 'string', - 'description' => 'Template name/slug', - ], - 'variables' => [ - 'type' => 'object', - 'description' => 'Variable values for the template', - ], - ], - 'required' => ['template'], - ]; - } - - public function handle(array $args, array $context = []): array - { - try { - $templateSlug = $this->require($args, 'template'); - } catch (\InvalidArgumentException $e) { - return $this->error($e->getMessage()); - } - - $templateService = app(PlanTemplateService::class); - $variables = $this->optional($args, 'variables', []); - - $preview = $templateService->previewTemplate($templateSlug, $variables); - - if (! $preview) { - return $this->error("Template not found: {$templateSlug}"); - } - - return [ - 'template' => $templateSlug, - 'preview' => $preview, - ]; - } -} diff --git a/src/php/tests/views/mcp/admin/api-key-manager.blade.php b/src/php/tests/views/mcp/admin/api-key-manager.blade.php deleted file mode 100644 index 7a3abb3..0000000 --- a/src/php/tests/views/mcp/admin/api-key-manager.blade.php +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/php/tests/views/mcp/admin/playground.blade.php b/src/php/tests/views/mcp/admin/playground.blade.php deleted file mode 100644 index f261550..0000000 --- a/src/php/tests/views/mcp/admin/playground.blade.php +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/php/tests/views/mcp/admin/request-log.blade.php b/src/php/tests/views/mcp/admin/request-log.blade.php deleted file mode 100644 index 0999e49..0000000 --- a/src/php/tests/views/mcp/admin/request-log.blade.php +++ /dev/null @@ -1 +0,0 @@ -