feat(core): wire Core framework into agentic + monitor subsystems
Phase 2 of Core DI migration: - Add *core.Core field + SetCore() to PrepSubsystem and monitor.Subsystem - Register agentic/monitor/brain as Core services with lifecycle hooks - Mark SetCompletionNotifier and SetNotifier as deprecated (removed in Phase 3) - Fix monitor test to match actual event names - initServices() now wires Core refs before legacy callbacks Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
781a5b414e
commit
bb88604045
1209 changed files with 473643 additions and 5 deletions
BIN
bin/core-agent
Executable file
BIN
bin/core-agent
Executable file
Binary file not shown.
|
|
@ -292,7 +292,8 @@ func main() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Shared setup — creates MCP service with all subsystems wired
|
// Shared setup — creates MCP service with all subsystems wired.
|
||||||
|
// Services are registered with Core for lifecycle, and with MCP for tool registration.
|
||||||
initServices := func() (*mcp.Service, *monitor.Subsystem, error) {
|
initServices := func() (*mcp.Service, *monitor.Subsystem, error) {
|
||||||
procFactory := process.NewService(process.Options{})
|
procFactory := process.NewService(process.Options{})
|
||||||
procResult, err := procFactory(c)
|
procResult, err := procFactory(c)
|
||||||
|
|
@ -305,17 +306,34 @@ func main() {
|
||||||
|
|
||||||
mon := monitor.New()
|
mon := monitor.New()
|
||||||
prep := agentic.NewPrep()
|
prep := agentic.NewPrep()
|
||||||
|
brn := brain.NewDirect()
|
||||||
|
|
||||||
|
// Wire Core framework into subsystems
|
||||||
|
prep.SetCore(c)
|
||||||
|
mon.SetCore(c)
|
||||||
|
|
||||||
|
// Legacy wiring — kept until Phase 3 replaces with IPC
|
||||||
prep.SetCompletionNotifier(mon)
|
prep.SetCompletionNotifier(mon)
|
||||||
|
|
||||||
|
// Register as Core services with lifecycle hooks
|
||||||
|
c.Service("agentic", core.Service{
|
||||||
|
OnStart: func() core.Result {
|
||||||
|
prep.StartRunner()
|
||||||
|
return core.Result{OK: true}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
c.Service("monitor", core.Service{})
|
||||||
|
c.Service("brain", core.Service{})
|
||||||
|
|
||||||
|
// MCP service with all subsystems for tool registration
|
||||||
mcpSvc, err := mcp.New(mcp.Options{
|
mcpSvc, err := mcp.New(mcp.Options{
|
||||||
Subsystems: []mcp.Subsystem{brain.NewDirect(), prep, mon},
|
Subsystems: []mcp.Subsystem{brn, prep, mon},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, core.E("main", "create MCP service", err)
|
return nil, nil, core.E("main", "create MCP service", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mon.SetNotifier(mcpSvc)
|
mon.SetNotifier(mcpSvc)
|
||||||
prep.StartRunner()
|
|
||||||
return mcpSvc, mon, nil
|
return mcpSvc, mon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
codex/core/commands/capabilities.md
Normal file
25
codex/core/commands/capabilities.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
name: capabilities
|
||||||
|
description: Return the machine-readable Codex capability manifest for ecosystem integration
|
||||||
|
---
|
||||||
|
|
||||||
|
# Capability Manifest
|
||||||
|
|
||||||
|
Use this when another tool, service, or agent needs a stable description of the Codex plugin surface.
|
||||||
|
|
||||||
|
## Preferred Sources
|
||||||
|
|
||||||
|
1. Read `core-agent/codex/.codex-plugin/capabilities.json`
|
||||||
|
2. If the Gemini extension is available, call the `codex_capabilities` tool and return its output verbatim
|
||||||
|
|
||||||
|
## What It Contains
|
||||||
|
|
||||||
|
- Plugin namespaces and command families
|
||||||
|
- Claude parity mappings for the `core` workflow
|
||||||
|
- Extension tools exposed by the Codex/Gemini bridge
|
||||||
|
- External marketplace sources used by the ecosystem
|
||||||
|
- Recommended workflow entry points for orchestration, review, QA, CI, deploy, and research
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Return the manifest as JSON without commentary unless the user asks for interpretation.
|
||||||
50
codex/core/commands/code-review.md
Normal file
50
codex/core/commands/code-review.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
name: code-review
|
||||||
|
description: Perform code review on staged changes or PRs
|
||||||
|
args: [commit-range|--pr=N|--security]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review
|
||||||
|
|
||||||
|
Perform a thorough code review of the specified changes.
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
- No args: Review staged changes
|
||||||
|
- `HEAD~3..HEAD`: Review last 3 commits
|
||||||
|
- `--pr=123`: Review PR #123
|
||||||
|
- `--security`: Focus on security issues
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Gather changes from the requested diff target
|
||||||
|
2. Analyse each changed file for correctness, security, maintainability, and test gaps
|
||||||
|
3. Report findings with clear severity and file references
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
| Category | Checks |
|
||||||
|
|----------|--------|
|
||||||
|
| Correctness | Logic errors, edge cases, error handling |
|
||||||
|
| Security | Injection, XSS, hardcoded secrets, CSRF |
|
||||||
|
| Performance | N+1 queries, unnecessary loops, large allocations |
|
||||||
|
| Maintainability | Naming, structure, complexity |
|
||||||
|
| Tests | Coverage gaps, missing assertions |
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Code Review: [title]
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
- **file:line** - Issue description
|
||||||
|
|
||||||
|
### Warning
|
||||||
|
- **file:line** - Issue description
|
||||||
|
|
||||||
|
### Suggestions
|
||||||
|
- **file:line** - Improvement idea
|
||||||
|
|
||||||
|
---
|
||||||
|
**Summary**: X critical, Y warnings, Z suggestions
|
||||||
|
```
|
||||||
33
codex/core/commands/dispatch.md
Normal file
33
codex/core/commands/dispatch.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
name: dispatch
|
||||||
|
description: Dispatch a subagent to work on a task in a sandboxed workspace
|
||||||
|
arguments:
|
||||||
|
- name: repo
|
||||||
|
description: Target repo (e.g. go-io, go-scm, mcp)
|
||||||
|
required: true
|
||||||
|
- name: task
|
||||||
|
description: What the agent should do
|
||||||
|
required: true
|
||||||
|
- name: agent
|
||||||
|
description: Agent type (claude, gemini, codex)
|
||||||
|
default: codex
|
||||||
|
- name: template
|
||||||
|
description: Prompt template (coding, conventions, security)
|
||||||
|
default: coding
|
||||||
|
- name: plan
|
||||||
|
description: Plan template (bug-fix, code-review, new-feature, refactor, feature-port)
|
||||||
|
- name: persona
|
||||||
|
description: Persona slug (e.g. code/backend-architect)
|
||||||
|
---
|
||||||
|
|
||||||
|
Dispatch a subagent to work on `$ARGUMENTS.repo` with task: `$ARGUMENTS.task`
|
||||||
|
|
||||||
|
Use the core-agent MCP tool `agentic_dispatch` with:
|
||||||
|
- repo: `$ARGUMENTS.repo`
|
||||||
|
- task: `$ARGUMENTS.task`
|
||||||
|
- agent: `$ARGUMENTS.agent`
|
||||||
|
- template: `$ARGUMENTS.template`
|
||||||
|
- plan_template: `$ARGUMENTS.plan` if provided
|
||||||
|
- persona: `$ARGUMENTS.persona` if provided
|
||||||
|
|
||||||
|
After dispatching, report the workspace dir, PID, and whether the task was queued or started immediately.
|
||||||
48
codex/core/commands/pipeline.md
Normal file
48
codex/core/commands/pipeline.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
name: pipeline
|
||||||
|
description: Run the multi-stage review pipeline on code changes
|
||||||
|
args: [commit-range|--pr=N|--stage=NAME|--skip=fix]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Review Pipeline
|
||||||
|
|
||||||
|
Run a staged code review pipeline using specialised roles for security, fixes, tests, architecture, and final verification.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/core:pipeline
|
||||||
|
/core:pipeline HEAD~3..HEAD
|
||||||
|
/core:pipeline --pr=123
|
||||||
|
/core:pipeline --stage=security
|
||||||
|
/core:pipeline --skip=fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline Stages
|
||||||
|
|
||||||
|
| Stage | Role | Purpose | Modifies Code? |
|
||||||
|
|------|------|---------|----------------|
|
||||||
|
| 1 | Security Engineer | Threat analysis, injection, tenant isolation | No |
|
||||||
|
| 2 | Senior Developer | Fix critical findings from Stage 1 | Yes |
|
||||||
|
| 3 | API Tester | Run tests and identify coverage gaps | No |
|
||||||
|
| 4 | Backend Architect | Check architecture fit and conventions | No |
|
||||||
|
| 5 | Reality Checker | Evidence-based final verdict | No |
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Gather the diff and changed file list for the requested range
|
||||||
|
2. Identify the affected package so tests can run in the right place
|
||||||
|
3. Dispatch each stage with `agentic_dispatch`, carrying forward findings from earlier stages
|
||||||
|
4. Aggregate the outputs into a single report with verdict and required follow-up
|
||||||
|
|
||||||
|
## Single Stage Mode
|
||||||
|
|
||||||
|
When `--stage=NAME` is passed, run only one stage:
|
||||||
|
|
||||||
|
| Name | Stage |
|
||||||
|
|------|-------|
|
||||||
|
| `security` | Stage 1 |
|
||||||
|
| `fix` | Stage 2 |
|
||||||
|
| `test` | Stage 3 |
|
||||||
|
| `architecture` | Stage 4 |
|
||||||
|
| `reality` | Stage 5 |
|
||||||
26
codex/core/commands/ready.md
Normal file
26
codex/core/commands/ready.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
name: ready
|
||||||
|
description: Quick check if work is ready to commit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ready Check
|
||||||
|
|
||||||
|
Quick verification that work is ready to commit.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
1. No uncommitted changes left behind
|
||||||
|
2. No debug statements
|
||||||
|
3. Code is formatted
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --porcelain
|
||||||
|
core go fmt --check 2>/dev/null || core php fmt --test 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Use `/core:ready` for a quick commit gate.
|
||||||
|
Use `/core:verify` for the full verification workflow.
|
||||||
20
codex/core/commands/recall.md
Normal file
20
codex/core/commands/recall.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: recall
|
||||||
|
description: Search OpenBrain for memories and context
|
||||||
|
arguments:
|
||||||
|
- name: query
|
||||||
|
description: What to search for
|
||||||
|
required: true
|
||||||
|
- name: project
|
||||||
|
description: Filter by project
|
||||||
|
- name: type
|
||||||
|
description: Filter by type (decision, plan, convention, architecture, observation, fact)
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the core-agent MCP tool `brain_recall` with:
|
||||||
|
- query: `$ARGUMENTS.query`
|
||||||
|
- top_k: `5`
|
||||||
|
- filter.project: `$ARGUMENTS.project` if provided
|
||||||
|
- filter.type: `$ARGUMENTS.type` if provided
|
||||||
|
|
||||||
|
Show results with score, type, project, date, and a short content preview.
|
||||||
17
codex/core/commands/remember.md
Normal file
17
codex/core/commands/remember.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
name: remember
|
||||||
|
description: Save a fact or decision to OpenBrain for persistence across sessions
|
||||||
|
args: <fact to remember>
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remember
|
||||||
|
|
||||||
|
Store the provided fact in OpenBrain so it persists across sessions and is available to other agents.
|
||||||
|
|
||||||
|
Use the core-agent MCP tool `brain_remember` with:
|
||||||
|
|
||||||
|
- `content`: the fact provided by the user
|
||||||
|
- `type`: best fit from `decision`, `convention`, `observation`, `fact`, `plan`, or `architecture`
|
||||||
|
- `project`: infer from the current working directory when possible
|
||||||
|
|
||||||
|
Confirm what was saved.
|
||||||
25
codex/core/commands/review-pr.md
Normal file
25
codex/core/commands/review-pr.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
name: review-pr
|
||||||
|
description: Review a pull request
|
||||||
|
args: <pr-number>
|
||||||
|
---
|
||||||
|
|
||||||
|
# PR Review
|
||||||
|
|
||||||
|
Review a GitHub pull request.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/core:review-pr 123
|
||||||
|
/core:review-pr 123 --security
|
||||||
|
/core:review-pr 123 --quick
|
||||||
|
```
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Fetch PR details
|
||||||
|
2. Get the PR diff
|
||||||
|
3. Check CI status
|
||||||
|
4. Review the changes for correctness, security, tests, and docs
|
||||||
|
5. Provide an approval, change request, or comment-only recommendation
|
||||||
19
codex/core/commands/review.md
Normal file
19
codex/core/commands/review.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
name: review
|
||||||
|
description: Review completed agent workspace and show merge options
|
||||||
|
arguments:
|
||||||
|
- name: workspace
|
||||||
|
description: Workspace name (e.g. go-html-1773592564). If omitted, shows all completed.
|
||||||
|
---
|
||||||
|
|
||||||
|
If no workspace is specified, use the core-agent MCP tool `agentic_status` to list all workspaces, then show only completed ones with a summary table.
|
||||||
|
|
||||||
|
If a workspace is specified:
|
||||||
|
1. Read the agent log file: `.core/workspace/{workspace}/agent-*.log`
|
||||||
|
2. Show the last 30 lines of output
|
||||||
|
3. Check git history in the workspace: `git -C .core/workspace/{workspace}/src log --oneline main..HEAD`
|
||||||
|
4. Show the diff stat: `git -C .core/workspace/{workspace}/src diff --stat main`
|
||||||
|
5. Offer next actions:
|
||||||
|
- Merge
|
||||||
|
- Discard
|
||||||
|
- Resume
|
||||||
16
codex/core/commands/scan.md
Normal file
16
codex/core/commands/scan.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
name: scan
|
||||||
|
description: Scan Forge repos for open issues with actionable labels
|
||||||
|
arguments:
|
||||||
|
- name: org
|
||||||
|
description: Forge org to scan
|
||||||
|
default: core
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the core-agent MCP tool `agentic_scan` with `org: $ARGUMENTS.org`.
|
||||||
|
|
||||||
|
Show results as a table with columns:
|
||||||
|
- Repo
|
||||||
|
- Issue #
|
||||||
|
- Title
|
||||||
|
- Labels
|
||||||
21
codex/core/commands/security.md
Normal file
21
codex/core/commands/security.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
name: security
|
||||||
|
description: Security-focused code review
|
||||||
|
args: [commit-range|--pr=N]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Security Review
|
||||||
|
|
||||||
|
Perform a security-focused review of the requested changes.
|
||||||
|
|
||||||
|
## Focus Areas
|
||||||
|
|
||||||
|
1. Injection vulnerabilities
|
||||||
|
2. Authentication and authorisation
|
||||||
|
3. Data exposure
|
||||||
|
4. Cryptography and secret handling
|
||||||
|
5. Vulnerable or outdated dependencies
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Return findings grouped by severity with file and line references, followed by a short summary count.
|
||||||
17
codex/core/commands/status.md
Normal file
17
codex/core/commands/status.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
name: status
|
||||||
|
description: Show status of all agent workspaces
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the core-agent MCP tool `agentic_status` to list all agent workspaces.
|
||||||
|
|
||||||
|
Show results as a table with columns:
|
||||||
|
- Name
|
||||||
|
- Status
|
||||||
|
- Agent
|
||||||
|
- Repo
|
||||||
|
- Task
|
||||||
|
- Age
|
||||||
|
|
||||||
|
For blocked workspaces, include the question from `BLOCKED.md`.
|
||||||
|
For completed workspaces with output, include the last 10 log lines.
|
||||||
24
codex/core/commands/sweep.md
Normal file
24
codex/core/commands/sweep.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
name: sweep
|
||||||
|
description: Dispatch a batch audit across multiple repos
|
||||||
|
arguments:
|
||||||
|
- name: template
|
||||||
|
description: Audit template (conventions, security)
|
||||||
|
default: conventions
|
||||||
|
- name: agent
|
||||||
|
description: Agent type for the sweep
|
||||||
|
default: codex
|
||||||
|
- name: repos
|
||||||
|
description: Comma-separated repos to include (default: all Go repos)
|
||||||
|
---
|
||||||
|
|
||||||
|
Run a batch conventions or security audit across the ecosystem.
|
||||||
|
|
||||||
|
1. If repos are not specified, find all repos under the configured workspace root that match the target language and template
|
||||||
|
2. For each repo, call `agentic_dispatch` with:
|
||||||
|
- repo
|
||||||
|
- task: `"{template} audit - UK English, error handling, interface checks, import aliasing"`
|
||||||
|
- agent: `$ARGUMENTS.agent`
|
||||||
|
- template: `$ARGUMENTS.template`
|
||||||
|
3. Report how many were dispatched versus queued
|
||||||
|
4. Point the user to `/core:status` and `/core:review` for follow-up
|
||||||
15
codex/core/commands/tests.md
Normal file
15
codex/core/commands/tests.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
name: tests
|
||||||
|
description: Verify tests pass for changed files
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Verification
|
||||||
|
|
||||||
|
Run tests related to changed files.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Identify changed files
|
||||||
|
2. Find related test targets
|
||||||
|
3. Run targeted tests with `core go test` or `core php test`
|
||||||
|
4. Report pass/fail results and uncovered gaps
|
||||||
21
codex/core/commands/verify.md
Normal file
21
codex/core/commands/verify.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
name: verify
|
||||||
|
description: Verify work is complete before stopping
|
||||||
|
args: [--quick|--full]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Work Verification
|
||||||
|
|
||||||
|
Verify that work is complete and ready to commit or push.
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
1. Check for uncommitted changes
|
||||||
|
2. Check for debug statements
|
||||||
|
3. Run tests
|
||||||
|
4. Run lint and static analysis
|
||||||
|
5. Check formatting
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Return a READY or NOT READY verdict with the specific failing checks called out first.
|
||||||
33
codex/core/commands/yes.md
Normal file
33
codex/core/commands/yes.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
name: yes
|
||||||
|
description: Auto-approve mode - trust Codex to complete task and commit
|
||||||
|
args: <task description>
|
||||||
|
---
|
||||||
|
|
||||||
|
# Yes Mode
|
||||||
|
|
||||||
|
You are in auto-approve mode. The user trusts Codex to complete the task autonomously.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. No confirmation needed for ordinary tool use
|
||||||
|
2. Complete the full workflow instead of stopping early
|
||||||
|
3. Commit when finished
|
||||||
|
4. Use a conventional commit message
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Understand the task
|
||||||
|
2. Make the required changes
|
||||||
|
3. Run relevant verification
|
||||||
|
4. Format code
|
||||||
|
5. Commit with a descriptive message
|
||||||
|
6. Report completion
|
||||||
|
|
||||||
|
## Commit Format
|
||||||
|
|
||||||
|
```text
|
||||||
|
type(scope): description
|
||||||
|
|
||||||
|
Co-Authored-By: Codex <noreply@openai.com>
|
||||||
|
```
|
||||||
BIN
core-agent
Executable file
BIN
core-agent
Executable file
Binary file not shown.
BIN
core-agent-linux-amd64
Executable file
BIN
core-agent-linux-amd64
Executable file
Binary file not shown.
40
docker/.env
Normal file
40
docker/.env
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Core Agent Local Stack
|
||||||
|
# Copy to .env and adjust as needed
|
||||||
|
|
||||||
|
APP_NAME="Core Agent"
|
||||||
|
APP_ENV=local
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_KEY=base64:cBXxVVn28EbrYjPiy3QAB8+yqd+gUVRDId0SeDZYFsQ=
|
||||||
|
APP_URL=https://lthn.sh
|
||||||
|
APP_DOMAIN=lthn.sh
|
||||||
|
|
||||||
|
# MariaDB
|
||||||
|
DB_CONNECTION=mariadb
|
||||||
|
DB_HOST=core-mariadb
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=core_agent
|
||||||
|
DB_USERNAME=core
|
||||||
|
DB_PASSWORD=core_local_dev
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_CLIENT=predis
|
||||||
|
REDIS_HOST=core-redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# Queue
|
||||||
|
QUEUE_CONNECTION=redis
|
||||||
|
|
||||||
|
# Ollama (embeddings)
|
||||||
|
OLLAMA_URL=http://core-ollama:11434
|
||||||
|
|
||||||
|
# Qdrant (vector search)
|
||||||
|
QDRANT_HOST=core-qdrant
|
||||||
|
QDRANT_PORT=6334
|
||||||
|
|
||||||
|
# Reverb (WebSocket)
|
||||||
|
REVERB_HOST=0.0.0.0
|
||||||
|
REVERB_PORT=8080
|
||||||
|
|
||||||
|
# Brain API key (agents use this to authenticate)
|
||||||
|
CORE_BRAIN_KEY=local-dev-key
|
||||||
207
php/Mcp/Prompts/AnalysePerformancePrompt.php
Normal file
207
php/Mcp/Prompts/AnalysePerformancePrompt.php
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Prompts;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\Server\Prompt;
|
||||||
|
use Laravel\Mcp\Server\Prompts\Argument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP prompt for analysing biolink performance.
|
||||||
|
*
|
||||||
|
* Guides through retrieving and interpreting analytics data,
|
||||||
|
* identifying trends, and suggesting improvements.
|
||||||
|
*
|
||||||
|
* Part of TASK-011 Phase 12: MCP Tools Expansion for BioHost (AC53).
|
||||||
|
*/
|
||||||
|
class AnalysePerformancePrompt extends Prompt
|
||||||
|
{
|
||||||
|
protected string $name = 'analyse_performance';
|
||||||
|
|
||||||
|
protected string $title = 'Analyse Bio Link Performance';
|
||||||
|
|
||||||
|
protected string $description = 'Analyse biolink analytics and provide actionable insights for improvement';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, Argument>
|
||||||
|
*/
|
||||||
|
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": <biolink_id>,
|
||||||
|
"period": "30d",
|
||||||
|
"include": ["geo", "devices", "referrers", "utm", "blocks"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also get basic biolink info:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "get",
|
||||||
|
"biolink_id": <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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
239
php/Mcp/Prompts/ConfigureNotificationsPrompt.php
Normal file
239
php/Mcp/Prompts/ConfigureNotificationsPrompt.php
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Prompts;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\Server\Prompt;
|
||||||
|
use Laravel\Mcp\Server\Prompts\Argument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP prompt for configuring biolink notifications.
|
||||||
|
*
|
||||||
|
* Guides through setting up notification handlers for various events
|
||||||
|
* like clicks, form submissions, and payments.
|
||||||
|
*
|
||||||
|
* Part of TASK-011 Phase 12: MCP Tools Expansion for BioHost (AC53).
|
||||||
|
*/
|
||||||
|
class ConfigureNotificationsPrompt extends Prompt
|
||||||
|
{
|
||||||
|
protected string $name = 'configure_notifications';
|
||||||
|
|
||||||
|
protected string $title = 'Configure Notifications';
|
||||||
|
|
||||||
|
protected string $description = 'Set up notification handlers for biolink events (clicks, form submissions, etc.)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, Argument>
|
||||||
|
*/
|
||||||
|
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": <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": <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": <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": <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": <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": <biolink_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update a Handler
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "update_notification_handler",
|
||||||
|
"handler_id": <handler_id>,
|
||||||
|
"events": ["form_submit"],
|
||||||
|
"is_enabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test a Handler
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "test_notification_handler",
|
||||||
|
"handler_id": <handler_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable or Delete
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "update_notification_handler",
|
||||||
|
"handler_id": <handler_id>,
|
||||||
|
"is_enabled": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "delete_notification_handler",
|
||||||
|
"handler_id": <handler_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Disable Behaviour
|
||||||
|
|
||||||
|
Handlers are automatically disabled after 5 consecutive failures. To re-enable:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "update_notification_handler",
|
||||||
|
"handler_id": <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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
php/Mcp/Prompts/SetupQrCampaignPrompt.php
Normal file
205
php/Mcp/Prompts/SetupQrCampaignPrompt.php
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Prompts;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Response;
|
||||||
|
use Laravel\Mcp\Server\Prompt;
|
||||||
|
use Laravel\Mcp\Server\Prompts\Argument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP prompt for setting up a QR code campaign.
|
||||||
|
*
|
||||||
|
* Guides through creating a short link with QR code and tracking pixel
|
||||||
|
* for print materials, packaging, or offline-to-online campaigns.
|
||||||
|
*
|
||||||
|
* Part of TASK-011 Phase 12: MCP Tools Expansion for BioHost (AC53).
|
||||||
|
*/
|
||||||
|
class SetupQrCampaignPrompt extends Prompt
|
||||||
|
{
|
||||||
|
protected string $name = 'setup_qr_campaign';
|
||||||
|
|
||||||
|
protected string $title = 'Set Up QR Code Campaign';
|
||||||
|
|
||||||
|
protected string $description = 'Create a short link with QR code and tracking for print materials or offline campaigns';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, Argument>
|
||||||
|
*/
|
||||||
|
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": <user_id>,
|
||||||
|
"url": "<short-slug>",
|
||||||
|
"type": "link",
|
||||||
|
"location_url": "<destination-url>?utm_source=qr&utm_campaign=<campaign-name>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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": <user_id>,
|
||||||
|
"type": "google_analytics",
|
||||||
|
"pixel_id": "G-XXXXXXXXXX",
|
||||||
|
"name": "<campaign-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": <biolink_id>,
|
||||||
|
"pixel_id": <pixel_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Organise in a Project
|
||||||
|
|
||||||
|
Create or use a campaign project:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "create_project",
|
||||||
|
"user_id": <user_id>,
|
||||||
|
"name": "QR Campaigns 2024",
|
||||||
|
"color": "#6366f1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Move the link to the project:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "move_to_project",
|
||||||
|
"biolink_id": <biolink_id>,
|
||||||
|
"project_id": <project_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Generate the QR Code
|
||||||
|
|
||||||
|
Generate with default settings (black on white, 400px):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "generate_qr",
|
||||||
|
"biolink_id": <biolink_id>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate with custom styling:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "generate_qr",
|
||||||
|
"biolink_id": <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": <biolink_id>,
|
||||||
|
"name": "<campaign-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": <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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
184
php/Mcp/Servers/HostHub.php
Normal file
184
php/Mcp/Servers/HostHub.php
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Servers;
|
||||||
|
|
||||||
|
use Core\Mcp\Resources\AppConfig;
|
||||||
|
use Core\Mcp\Resources\ContentResource;
|
||||||
|
use Core\Mcp\Resources\DatabaseSchema;
|
||||||
|
use Core\Mcp\Tools\Commerce\CreateCoupon;
|
||||||
|
use Core\Mcp\Tools\Commerce\GetBillingStatus;
|
||||||
|
use Core\Mcp\Tools\Commerce\ListInvoices;
|
||||||
|
use Core\Mcp\Tools\Commerce\UpgradePlan;
|
||||||
|
use Core\Mcp\Tools\ContentTools;
|
||||||
|
use Core\Mcp\Tools\GetStats;
|
||||||
|
use Core\Mcp\Tools\ListRoutes;
|
||||||
|
use Core\Mcp\Tools\ListSites;
|
||||||
|
use Core\Mcp\Tools\ListTables;
|
||||||
|
use Core\Mcp\Tools\QueryDatabase;
|
||||||
|
use Core\Mod\Agentic\Mcp\Prompts\AnalysePerformancePrompt;
|
||||||
|
use Core\Mod\Agentic\Mcp\Prompts\ConfigureNotificationsPrompt;
|
||||||
|
use Core\Mod\Agentic\Mcp\Prompts\CreateBioPagePrompt;
|
||||||
|
use Core\Mod\Agentic\Mcp\Prompts\SetupQrCampaignPrompt;
|
||||||
|
use Laravel\Mcp\Server;
|
||||||
|
use Mod\Bio\Mcp\BioResource;
|
||||||
|
|
||||||
|
class HostHub extends Server
|
||||||
|
{
|
||||||
|
protected string $name = 'Host Hub';
|
||||||
|
|
||||||
|
protected string $version = '1.0.0';
|
||||||
|
|
||||||
|
protected string $instructions = <<<'MARKDOWN'
|
||||||
|
Host Hub MCP Server provides tools for querying and inspecting the Host UK hosting platform.
|
||||||
|
|
||||||
|
## System Tools
|
||||||
|
- list-sites: List all 6 Host UK services
|
||||||
|
- get-stats: Get current system statistics
|
||||||
|
- list-routes: List all web routes
|
||||||
|
- query-database: Execute read-only SQL SELECT queries
|
||||||
|
- list-tables: List database tables
|
||||||
|
|
||||||
|
## Commerce Tools
|
||||||
|
- get-billing-status: Get subscription and billing status for a workspace
|
||||||
|
- list-invoices: List invoices for a workspace
|
||||||
|
- create-coupon: Create a new discount coupon
|
||||||
|
- upgrade-plan: Preview or execute a plan change
|
||||||
|
|
||||||
|
## Content Tools
|
||||||
|
Manage native CMS content (blog posts, pages):
|
||||||
|
- content_tools action=list: List content items for a workspace
|
||||||
|
- content_tools action=read: Read full content by slug or ID
|
||||||
|
- content_tools action=create: Create new content (draft, published, scheduled)
|
||||||
|
- content_tools action=update: Update existing content
|
||||||
|
- content_tools action=delete: Soft delete content
|
||||||
|
- content_tools action=taxonomies: List categories and tags
|
||||||
|
|
||||||
|
## BioLink Tools (BioHost)
|
||||||
|
Manage bio link pages, domains, pixels, themes, and notifications:
|
||||||
|
|
||||||
|
### Core Operations (biolink_tools)
|
||||||
|
- biolink_tools action=list: List biolinks for a user
|
||||||
|
- biolink_tools action=get: Get biolink details with blocks
|
||||||
|
- biolink_tools action=create: Create new biolink page
|
||||||
|
- biolink_tools action=update: Update biolink settings
|
||||||
|
- biolink_tools action=delete: Delete a biolink
|
||||||
|
- biolink_tools action=add_block: Add a block to biolink
|
||||||
|
- biolink_tools action=update_block: Update block settings
|
||||||
|
- biolink_tools action=delete_block: Remove a block
|
||||||
|
|
||||||
|
### Analytics (analytics_tools)
|
||||||
|
- analytics_tools action=stats: Get click statistics
|
||||||
|
- analytics_tools action=detailed: Get geo, device, referrer, UTM breakdown
|
||||||
|
|
||||||
|
### Domains (domain_tools)
|
||||||
|
- domain_tools action=list: List custom domains
|
||||||
|
- domain_tools action=add: Add domain with verification instructions
|
||||||
|
- domain_tools action=verify: Trigger DNS verification
|
||||||
|
- domain_tools action=delete: Remove a domain
|
||||||
|
|
||||||
|
### Projects (project_tools)
|
||||||
|
- project_tools action=list: List projects
|
||||||
|
- project_tools action=create: Create a project
|
||||||
|
- project_tools action=update: Update a project
|
||||||
|
- project_tools action=delete: Delete a project
|
||||||
|
- project_tools action=move_biolink: Move biolink to project
|
||||||
|
|
||||||
|
### Tracking Pixels (pixel_tools)
|
||||||
|
- pixel_tools action=list: List tracking pixels
|
||||||
|
- pixel_tools action=create: Create pixel (Facebook, GA4, GTM, etc.)
|
||||||
|
- pixel_tools action=update: Update pixel
|
||||||
|
- pixel_tools action=delete: Delete pixel
|
||||||
|
- pixel_tools action=attach: Attach pixel to biolink
|
||||||
|
- pixel_tools action=detach: Remove pixel from biolink
|
||||||
|
|
||||||
|
### QR Codes (qr_tools)
|
||||||
|
- qr_tools action=generate: Generate QR code with custom styling
|
||||||
|
|
||||||
|
### Themes (theme_tools)
|
||||||
|
- theme_tools action=list: List available themes
|
||||||
|
- theme_tools action=apply: Apply theme to biolink
|
||||||
|
- theme_tools action=create_custom: Create custom theme
|
||||||
|
- theme_tools action=delete: Delete custom theme
|
||||||
|
- theme_tools action=search: Search themes
|
||||||
|
- theme_tools action=toggle_favourite: Toggle favourite theme
|
||||||
|
|
||||||
|
### Social Proof (TrustHost - trust_tools)
|
||||||
|
Manage social proof widgets and campaigns:
|
||||||
|
- trust_campaign_tools action=list: List campaigns
|
||||||
|
- trust_campaign_tools action=get: Get campaign details
|
||||||
|
- trust_notification_tools action=list: List widgets for campaign
|
||||||
|
- trust_notification_tools action=get: Get widget details
|
||||||
|
- trust_notification_tools action=create: Create new widget
|
||||||
|
- trust_notification_tools action=types: List available widget types
|
||||||
|
- trust_analytics_tools action=stats: Get performance statistics
|
||||||
|
|
||||||
|
## Utility Tools (utility_tools)
|
||||||
|
Execute developer utility tools (hash generators, text converters, formatters, network lookups):
|
||||||
|
- utility_tools action=list: List all available tools
|
||||||
|
- utility_tools action=categories: List tools grouped by category
|
||||||
|
- utility_tools action=info tool=<slug>: Get detailed tool information
|
||||||
|
- utility_tools action=execute tool=<slug> 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
114
php/Mcp/Servers/Marketing.php
Normal file
114
php/Mcp/Servers/Marketing.php
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Servers;
|
||||||
|
|
||||||
|
use Laravel\Mcp\Server;
|
||||||
|
use Mod\Analytics\Mcp\Tools\GeneralAnalyticsTools;
|
||||||
|
use Mod\Notify\Mcp\Tools\NotifyTools;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marketing MCP Server.
|
||||||
|
*
|
||||||
|
* Provides a unified interface for MCP agents to interact with
|
||||||
|
* Host UK's marketing platform:
|
||||||
|
* - BioHost (bio link pages)
|
||||||
|
* - AnalyticsHost (website analytics)
|
||||||
|
* - NotifyHost (push notifications)
|
||||||
|
* - TrustHost (social proof widgets)
|
||||||
|
*/
|
||||||
|
class Marketing extends Server
|
||||||
|
{
|
||||||
|
protected string $name = 'Host UK Marketing';
|
||||||
|
|
||||||
|
protected string $version = '1.0.0';
|
||||||
|
|
||||||
|
protected string $instructions = <<<'MARKDOWN'
|
||||||
|
Host UK Marketing MCP Server provides tools for managing the complete marketing platform.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### BioLink Tools (BioHost)
|
||||||
|
Manage bio link pages, domains, pixels, themes, and notifications:
|
||||||
|
|
||||||
|
#### Core Operations (biolink_tools)
|
||||||
|
- `list` - List all bio links
|
||||||
|
- `get` - Get bio link details with blocks
|
||||||
|
- `create` - Create a new bio link page
|
||||||
|
- `add_block` - Add a content block
|
||||||
|
- `update_block` - Update block settings
|
||||||
|
- `delete_block` - Remove a block
|
||||||
|
|
||||||
|
#### Analytics (analytics_tools)
|
||||||
|
- `stats` - Get click statistics
|
||||||
|
- `detailed` - Get detailed breakdown
|
||||||
|
|
||||||
|
#### Domains (domain_tools)
|
||||||
|
- `list` - List custom domains
|
||||||
|
- `add` - Add domain
|
||||||
|
- `verify` - Verify DNS
|
||||||
|
|
||||||
|
#### Themes (theme_tools)
|
||||||
|
- `list` - List themes
|
||||||
|
- `apply` - Apply theme
|
||||||
|
|
||||||
|
#### Other Bio Tools
|
||||||
|
- `qr_tools` - Generate QR codes
|
||||||
|
- `pixel_tools` - Manage tracking pixels
|
||||||
|
- `project_tools` - Organise into projects
|
||||||
|
- `notification_tools` - Manage notification handlers
|
||||||
|
- `submission_tools` - Manage form submissions
|
||||||
|
- `pwa_tools` - Configure PWA
|
||||||
|
|
||||||
|
### AnalyticsTools
|
||||||
|
Query website analytics data:
|
||||||
|
- `list_websites` - List tracked websites
|
||||||
|
- `get_stats` - Get pageviews, visitors, bounce rate
|
||||||
|
- `top_pages` - Get most visited pages
|
||||||
|
- `traffic_sources` - Get referrers and UTM campaigns
|
||||||
|
- `realtime` - Get current active visitors
|
||||||
|
|
||||||
|
### PushNotificationTools
|
||||||
|
Manage push notification campaigns:
|
||||||
|
- `list_websites` - List push-enabled websites
|
||||||
|
- `list_campaigns` - List notification campaigns
|
||||||
|
- `get_campaign` - Get campaign details and stats
|
||||||
|
- `create_campaign` - Create a new campaign (as draft)
|
||||||
|
- `subscriber_stats` - Get subscriber demographics
|
||||||
|
|
||||||
|
### Social Proof (TrustHost - trust_tools)
|
||||||
|
Manage social proof widgets and campaigns:
|
||||||
|
- `trust_campaign_tools` action=list: List campaigns
|
||||||
|
- `trust_notification_tools` action=list: List widgets
|
||||||
|
- `trust_analytics_tools` action=stats: Get performance stats
|
||||||
|
|
||||||
|
### AnalyticsTools
|
||||||
|
Query website analytics data:
|
||||||
|
MARKDOWN;
|
||||||
|
|
||||||
|
protected array $tools = [
|
||||||
|
// BioHost tools (from Mod\Bio)
|
||||||
|
\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,
|
||||||
|
|
||||||
|
// Other Marketing tools
|
||||||
|
GeneralAnalyticsTools::class,
|
||||||
|
NotifyTools::class,
|
||||||
|
\Mod\Trust\Mcp\Tools\CampaignTools::class,
|
||||||
|
\Mod\Trust\Mcp\Tools\NotificationTools::class,
|
||||||
|
\Mod\Trust\Mcp\Tools\AnalyticsTools::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected array $resources = [];
|
||||||
|
|
||||||
|
protected array $prompts = [];
|
||||||
|
}
|
||||||
342
php/Mcp/Tools/Agent/AgentTool.php
Normal file
342
php/Mcp/Tools/Agent/AgentTool.php
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Core\Mcp\Dependencies\HasDependencies;
|
||||||
|
use Core\Mcp\Exceptions\CircuitOpenException;
|
||||||
|
use Core\Mcp\Services\CircuitBreaker;
|
||||||
|
use Core\Mcp\Tools\Concerns\ValidatesDependencies;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\Contracts\AgentToolInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for MCP Agent Server tools.
|
||||||
|
*
|
||||||
|
* Provides common functionality for all extracted agent tools.
|
||||||
|
*/
|
||||||
|
abstract class AgentTool implements AgentToolInterface, HasDependencies
|
||||||
|
{
|
||||||
|
use ValidatesDependencies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool category for grouping in the registry.
|
||||||
|
*/
|
||||||
|
protected string $category = 'general';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required permission scopes.
|
||||||
|
*
|
||||||
|
* @var array<string>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
php/Mcp/Tools/Agent/Brain/BrainForget.php
Normal file
78
php/Mcp/Tools/Agent/Brain/BrainForget.php
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Brain\ForgetKnowledge;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\BrainMemory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a memory from the shared OpenBrain knowledge store.
|
||||||
|
*
|
||||||
|
* Deletes the memory from both MariaDB and Qdrant.
|
||||||
|
* Workspace-scoped: agents can only forget memories in their own workspace.
|
||||||
|
*/
|
||||||
|
class BrainForget extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'brain';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function dependencies(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ToolDependency::contextExists('workspace_id', 'Workspace context required to forget memories'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'brain_forget';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Remove a memory from the shared OpenBrain knowledge store. Permanently deletes from both database and vector index.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
81
php/Mcp/Tools/Agent/Brain/BrainList.php
Normal file
81
php/Mcp/Tools/Agent/Brain/BrainList.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Brain\ListKnowledge;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\BrainMemory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List memories in the shared OpenBrain knowledge store.
|
||||||
|
*
|
||||||
|
* Pure MariaDB query using model scopes -- no vector search.
|
||||||
|
* Useful for browsing what an agent or project has stored.
|
||||||
|
*/
|
||||||
|
class BrainList extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'brain';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function dependencies(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ToolDependency::contextExists('workspace_id', 'Workspace context required to list memories'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'brain_list';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'List memories in the shared OpenBrain knowledge store. Supports filtering by project, type, and agent. No vector search -- use brain_recall for semantic queries.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
php/Mcp/Tools/Agent/Brain/BrainRecall.php
Normal file
119
php/Mcp/Tools/Agent/Brain/BrainRecall.php
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Brain\RecallKnowledge;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\BrainMemory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic search across the shared OpenBrain knowledge store.
|
||||||
|
*
|
||||||
|
* Uses vector similarity to find memories relevant to a natural
|
||||||
|
* language query, with optional filtering by project, type, agent,
|
||||||
|
* or minimum confidence.
|
||||||
|
*/
|
||||||
|
class BrainRecall extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'brain';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function dependencies(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ToolDependency::contextExists('workspace_id', 'Workspace context required to recall memories'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'brain_recall';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Semantic search across the shared OpenBrain knowledge store. Returns memories ranked by similarity to your query, with optional filtering.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
103
php/Mcp/Tools/Agent/Brain/BrainRemember.php
Normal file
103
php/Mcp/Tools/Agent/Brain/BrainRemember.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Brain;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Brain\RememberKnowledge;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\BrainMemory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a memory in the shared OpenBrain knowledge store.
|
||||||
|
*
|
||||||
|
* Agents use this tool to persist decisions, observations, conventions,
|
||||||
|
* and other knowledge so that other agents can recall it later.
|
||||||
|
*/
|
||||||
|
class BrainRemember extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'brain';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function dependencies(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ToolDependency::contextExists('workspace_id', 'Workspace context required to store memories'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'brain_remember';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Store a memory in the shared OpenBrain knowledge store. Use this to persist decisions, observations, conventions, research, plans, bugs, or architecture knowledge for other agents.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
85
php/Mcp/Tools/Agent/Content/ContentBatchGenerate.php
Normal file
85
php/Mcp/Tools/Agent/Content/ContentBatchGenerate.php
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Mod\Content\Jobs\GenerateContentJob;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue multiple briefs for batch content generation.
|
||||||
|
*
|
||||||
|
* Processes briefs that are ready (queued status with past or no scheduled time).
|
||||||
|
*/
|
||||||
|
class ContentBatchGenerate extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_batch_generate';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Queue multiple briefs for batch content generation';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
php/Mcp/Tools/Agent/Content/ContentBriefCreate.php
Normal file
128
php/Mcp/Tools/Agent/Content/ContentBriefCreate.php
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentPlan;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Mod\Content\Enums\BriefContentType;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a content brief for AI generation.
|
||||||
|
*
|
||||||
|
* Briefs can be linked to an existing plan for workflow tracking.
|
||||||
|
*/
|
||||||
|
class ContentBriefCreate extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_brief_create';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Create a content brief for AI generation';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
php/Mcp/Tools/Agent/Content/ContentBriefGet.php
Normal file
92
php/Mcp/Tools/Agent/Content/ContentBriefGet.php
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Mod\Content\Enums\BriefContentType;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get details of a specific content brief including generated content.
|
||||||
|
*/
|
||||||
|
class ContentBriefGet extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_brief_get';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Get details of a specific content brief including generated content';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
php/Mcp/Tools/Agent/Content/ContentBriefList.php
Normal file
86
php/Mcp/Tools/Agent/Content/ContentBriefList.php
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Mod\Content\Enums\BriefContentType;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List content briefs with optional status filter.
|
||||||
|
*/
|
||||||
|
class ContentBriefList extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_brief_list';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'List content briefs with optional status filter';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
php/Mcp/Tools/Agent/Content/ContentFromPlan.php
Normal file
163
php/Mcp/Tools/Agent/Content/ContentFromPlan.php
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentPlan;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Mod\Content\Enums\BriefContentType;
|
||||||
|
use Mod\Content\Jobs\GenerateContentJob;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create content briefs from plan tasks and queue for generation.
|
||||||
|
*
|
||||||
|
* Converts pending tasks from a plan into content briefs, enabling
|
||||||
|
* automated content generation workflows from plan-based task management.
|
||||||
|
*/
|
||||||
|
class ContentFromPlan extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_from_plan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Create content briefs from plan tasks and queue for generation';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
172
php/Mcp/Tools/Agent/Content/ContentGenerate.php
Normal file
172
php/Mcp/Tools/Agent/Content/ContentGenerate.php
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Mod\Content\Jobs\GenerateContentJob;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
use Mod\Content\Services\AIGatewayService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate content for a brief using AI pipeline.
|
||||||
|
*
|
||||||
|
* Supports draft (Gemini), refine (Claude), or full pipeline modes.
|
||||||
|
* Can run synchronously or queue for async processing.
|
||||||
|
*/
|
||||||
|
class ContentGenerate extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content generation can be slow, allow longer timeout.
|
||||||
|
*/
|
||||||
|
protected ?int $timeout = 300;
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_generate';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Generate content for a brief using AI pipeline (Gemini draft -> 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()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
php/Mcp/Tools/Agent/Content/ContentStatus.php
Normal file
60
php/Mcp/Tools/Agent/Content/ContentStatus.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Mod\Content\Models\ContentBrief;
|
||||||
|
use Mod\Content\Services\AIGatewayService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content generation pipeline status.
|
||||||
|
*
|
||||||
|
* Returns AI provider availability and brief counts by status.
|
||||||
|
*/
|
||||||
|
class ContentStatus extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_status';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Get content generation pipeline status (AI provider availability, brief counts)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
php/Mcp/Tools/Agent/Content/ContentUsageStats.php
Normal file
68
php/Mcp/Tools/Agent/Content/ContentUsageStats.php
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Content;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Mod\Content\Models\AIUsage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AI usage statistics for content generation.
|
||||||
|
*
|
||||||
|
* Returns token counts and cost estimates by provider and purpose.
|
||||||
|
*/
|
||||||
|
class ContentUsageStats extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'content';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'content_usage_stats';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Get AI usage statistics (tokens, costs) for content generation';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
php/Mcp/Tools/Agent/Contracts/AgentToolInterface.php
Normal file
50
php/Mcp/Tools/Agent/Contracts/AgentToolInterface.php
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Contracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for MCP Agent Server tools.
|
||||||
|
*
|
||||||
|
* Tools extracted from the monolithic McpAgentServerCommand
|
||||||
|
* implement this interface for clean separation of concerns.
|
||||||
|
*/
|
||||||
|
interface AgentToolInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the tool name (used as the MCP tool identifier).
|
||||||
|
*/
|
||||||
|
public function name(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tool description for MCP clients.
|
||||||
|
*/
|
||||||
|
public function description(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the JSON Schema for tool input parameters.
|
||||||
|
*/
|
||||||
|
public function inputSchema(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the tool with the given arguments.
|
||||||
|
*
|
||||||
|
* @param array $args Input arguments from MCP client
|
||||||
|
* @param array $context Execution context (session_id, workspace_id, etc.)
|
||||||
|
* @return array Tool result
|
||||||
|
*/
|
||||||
|
public function handle(array $args, array $context = []): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get required permission scopes to execute this tool.
|
||||||
|
*
|
||||||
|
* @return array<string> List of required scopes
|
||||||
|
*/
|
||||||
|
public function requiredScopes(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tool category for grouping.
|
||||||
|
*/
|
||||||
|
public function category(): string;
|
||||||
|
}
|
||||||
78
php/Mcp/Tools/Agent/Messaging/AgentConversation.php
Normal file
78
php/Mcp/Tools/Agent/Messaging/AgentConversation.php
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Messaging;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View conversation thread between two agents.
|
||||||
|
*/
|
||||||
|
class AgentConversation extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'messaging';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'agent_conversation';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'View conversation thread with a specific agent. Returns up to 50 messages between you and the target agent.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
php/Mcp/Tools/Agent/Messaging/AgentInbox.php
Normal file
72
php/Mcp/Tools/Agent/Messaging/AgentInbox.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Messaging;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check inbox — latest messages sent to the requesting agent.
|
||||||
|
*/
|
||||||
|
class AgentInbox extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'messaging';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'agent_inbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Check your inbox — latest messages sent to you. Returns up to 20 most recent messages.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
php/Mcp/Tools/Agent/Messaging/AgentSend.php
Normal file
89
php/Mcp/Tools/Agent/Messaging/AgentSend.php
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Messaging;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a direct message to another agent.
|
||||||
|
*
|
||||||
|
* Chronological, not semantic — messages are stored and retrieved
|
||||||
|
* in order, not via vector search.
|
||||||
|
*/
|
||||||
|
class AgentSend extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'messaging';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'agent_send';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Send a direct message to another agent. Messages are chronological, not semantic.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
php/Mcp/Tools/Agent/Phase/PhaseAddCheckpoint.php
Normal file
78
php/Mcp/Tools/Agent/Phase/PhaseAddCheckpoint.php
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Phase;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Phase\AddCheckpoint;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a checkpoint note to a phase.
|
||||||
|
*/
|
||||||
|
class PhaseAddCheckpoint extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'phase';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'phase_add_checkpoint';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Add a checkpoint note to 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)',
|
||||||
|
],
|
||||||
|
'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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
php/Mcp/Tools/Agent/Phase/PhaseGet.php
Normal file
76
php/Mcp/Tools/Agent/Phase/PhaseGet.php
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Phase;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Phase\GetPhase;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get details of a specific phase within a plan.
|
||||||
|
*/
|
||||||
|
class PhaseGet extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'phase';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'phase_get';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Get details of a specific phase within a plan';
|
||||||
|
}
|
||||||
|
|
||||||
|
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)',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
php/Mcp/Tools/Agent/Phase/PhaseUpdateStatus.php
Normal file
96
php/Mcp/Tools/Agent/Phase/PhaseUpdateStatus.php
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Phase;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Phase\UpdatePhaseStatus;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the status of a phase.
|
||||||
|
*/
|
||||||
|
class PhaseUpdateStatus extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'phase';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
php/Mcp/Tools/Agent/Plan/PlanArchive.php
Normal file
72
php/Mcp/Tools/Agent/Plan/PlanArchive.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Plan\ArchivePlan;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive a completed or abandoned plan.
|
||||||
|
*/
|
||||||
|
class PlanArchive extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'plan';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'plan_archive';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Archive a completed or abandoned plan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
php/Mcp/Tools/Agent/Plan/PlanCreate.php
Normal file
105
php/Mcp/Tools/Agent/Plan/PlanCreate.php
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Plan\CreatePlan;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new work plan with phases and tasks.
|
||||||
|
*/
|
||||||
|
class PlanCreate extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'plan';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
php/Mcp/Tools/Agent/Plan/PlanGet.php
Normal file
84
php/Mcp/Tools/Agent/Plan/PlanGet.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Plan\GetPlan;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed information about a specific plan.
|
||||||
|
*/
|
||||||
|
class PlanGet extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'plan';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* Workspace context is required to ensure tenant isolation.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
php/Mcp/Tools/Agent/Plan/PlanList.php
Normal file
90
php/Mcp/Tools/Agent/Plan/PlanList.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Plan\ListPlans;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all work plans with their current status and progress.
|
||||||
|
*/
|
||||||
|
class PlanList extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'plan';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* Workspace context is required to ensure tenant isolation.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
php/Mcp/Tools/Agent/Plan/PlanUpdateStatus.php
Normal file
72
php/Mcp/Tools/Agent/Plan/PlanUpdateStatus.php
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Plan\UpdatePlanStatus;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the status of a plan.
|
||||||
|
*/
|
||||||
|
class PlanUpdateStatus extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'plan';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'plan_update_status';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Update the status of a plan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
279
php/Mcp/Tools/Agent/README.md
Normal file
279
php/Mcp/Tools/Agent/README.md
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
# 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
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
class PlanPublish extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'plan';
|
||||||
|
|
||||||
|
protected array $scopes = ['write']; // 'read' or 'write'
|
||||||
|
|
||||||
|
public function dependencies(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ToolDependency::contextExists('workspace_id', 'Workspace context required'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'plan_publish'; // snake_case; must be unique across all tools
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Publish a draft plan, making it active';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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.
|
||||||
81
php/Mcp/Tools/Agent/Session/SessionArtifact.php
Normal file
81
php/Mcp/Tools/Agent/Session/SessionArtifact.php
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record an artifact created/modified during the session.
|
||||||
|
*/
|
||||||
|
class SessionArtifact extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_artifact';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Record an artifact created/modified during the session';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
php/Mcp/Tools/Agent/Session/SessionContinue.php
Normal file
73
php/Mcp/Tools/Agent/Session/SessionContinue.php
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Session\ContinueSession;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Continue from a previous session (multi-agent handoff).
|
||||||
|
*/
|
||||||
|
class SessionContinue extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_continue';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Continue from a previous session (multi-agent handoff)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
php/Mcp/Tools/Agent/Session/SessionEnd.php
Normal file
73
php/Mcp/Tools/Agent/Session/SessionEnd.php
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Session\EndSession;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End the current session.
|
||||||
|
*/
|
||||||
|
class SessionEnd extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_end';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'End the current session';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
php/Mcp/Tools/Agent/Session/SessionHandoff.php
Normal file
88
php/Mcp/Tools/Agent/Session/SessionHandoff.php
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare session for handoff to another agent.
|
||||||
|
*/
|
||||||
|
class SessionHandoff extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_handoff';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Prepare session for handoff to another agent';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
php/Mcp/Tools/Agent/Session/SessionList.php
Normal file
83
php/Mcp/Tools/Agent/Session/SessionList.php
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Actions\Session\ListSessions;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List sessions, optionally filtered by status.
|
||||||
|
*/
|
||||||
|
class SessionList extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_list';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'List sessions, optionally filtered by status';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
php/Mcp/Tools/Agent/Session/SessionLog.php
Normal file
93
php/Mcp/Tools/Agent/Session/SessionLog.php
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an entry in the current session.
|
||||||
|
*/
|
||||||
|
class SessionLog extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
php/Mcp/Tools/Agent/Session/SessionReplay.php
Normal file
101
php/Mcp/Tools/Agent/Session/SessionReplay.php
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Services\AgentSessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replay a session by creating a new session with the original's context.
|
||||||
|
*
|
||||||
|
* This tool reconstructs the state from a session's work log and creates
|
||||||
|
* a new active session, allowing an agent to continue from where the
|
||||||
|
* original session left off.
|
||||||
|
*/
|
||||||
|
class SessionReplay extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_replay';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Replay a session - creates a new session with the original\'s reconstructed context from its work log';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
74
php/Mcp/Tools/Agent/Session/SessionResume.php
Normal file
74
php/Mcp/Tools/Agent/Session/SessionResume.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Services\AgentSessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume a paused or handed-off session.
|
||||||
|
*/
|
||||||
|
class SessionResume extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'session_resume';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Resume a paused or handed-off session';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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'] ?? [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
php/Mcp/Tools/Agent/Session/SessionStart.php
Normal file
96
php/Mcp/Tools/Agent/Session/SessionStart.php
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Session;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Session\StartSession;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new agent session for a plan.
|
||||||
|
*/
|
||||||
|
class SessionStart extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'session';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* Workspace context is needed unless a plan_slug is provided
|
||||||
|
* (in which case workspace is inferred from the plan).
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
php/Mcp/Tools/Agent/State/StateGet.php
Normal file
99
php/Mcp/Tools/Agent/State/StateGet.php
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\State;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentPlan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a workspace state value.
|
||||||
|
*/
|
||||||
|
class StateGet extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'state';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* Workspace context is required to ensure tenant isolation.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
php/Mcp/Tools/Agent/State/StateList.php
Normal file
103
php/Mcp/Tools/Agent/State/StateList.php
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\State;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentPlan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all state values for a plan.
|
||||||
|
*/
|
||||||
|
class StateList extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'state';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* Workspace context is required to ensure tenant isolation.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
php/Mcp/Tools/Agent/State/StateSet.php
Normal file
115
php/Mcp/Tools/Agent/State/StateSet.php
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\State;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Models\AgentPlan;
|
||||||
|
use Core\Mod\Agentic\Models\WorkspaceState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a workspace state value.
|
||||||
|
*/
|
||||||
|
class StateSet extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'state';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* Workspace context is required to ensure tenant isolation.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
php/Mcp/Tools/Agent/Task/TaskToggle.php
Normal file
84
php/Mcp/Tools/Agent/Task/TaskToggle.php
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Task;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Task\ToggleTask;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a task completion status.
|
||||||
|
*/
|
||||||
|
class TaskToggle extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'task';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
php/Mcp/Tools/Agent/Task/TaskUpdate.php
Normal file
95
php/Mcp/Tools/Agent/Task/TaskUpdate.php
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Task;
|
||||||
|
|
||||||
|
use Core\Mcp\Dependencies\ToolDependency;
|
||||||
|
use Core\Mod\Agentic\Actions\Task\UpdateTask;
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task details (status, notes).
|
||||||
|
*/
|
||||||
|
class TaskUpdate extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'task';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for this tool.
|
||||||
|
*
|
||||||
|
* @return array<ToolDependency>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
php/Mcp/Tools/Agent/Template/TemplateCreatePlan.php
Normal file
99
php/Mcp/Tools/Agent/Template/TemplateCreatePlan.php
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Template;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Services\PlanTemplateService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new plan from a template.
|
||||||
|
*/
|
||||||
|
class TemplateCreatePlan extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'template';
|
||||||
|
|
||||||
|
protected array $scopes = ['write'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'template_create_plan';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Create a new plan from a template';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
php/Mcp/Tools/Agent/Template/TemplateList.php
Normal file
57
php/Mcp/Tools/Agent/Template/TemplateList.php
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Template;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Services\PlanTemplateService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available plan templates.
|
||||||
|
*/
|
||||||
|
class TemplateList extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'template';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'template_list';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'List available plan templates';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
69
php/Mcp/Tools/Agent/Template/TemplatePreview.php
Normal file
69
php/Mcp/Tools/Agent/Template/TemplatePreview.php
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\Mod\Agentic\Mcp\Tools\Agent\Template;
|
||||||
|
|
||||||
|
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;
|
||||||
|
use Core\Mod\Agentic\Services\PlanTemplateService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview a template with variables.
|
||||||
|
*/
|
||||||
|
class TemplatePreview extends AgentTool
|
||||||
|
{
|
||||||
|
protected string $category = 'template';
|
||||||
|
|
||||||
|
protected array $scopes = ['read'];
|
||||||
|
|
||||||
|
public function name(): string
|
||||||
|
{
|
||||||
|
return 'template_preview';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Preview a template with variables';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inputSchema(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => '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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
1
php/tests/views/mcp/admin/api-key-manager.blade.php
Normal file
1
php/tests/views/mcp/admin/api-key-manager.blade.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<div data-testid="api-key-manager"></div>
|
||||||
1
php/tests/views/mcp/admin/playground.blade.php
Normal file
1
php/tests/views/mcp/admin/playground.blade.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<div data-testid="playground"></div>
|
||||||
1
php/tests/views/mcp/admin/request-log.blade.php
Normal file
1
php/tests/views/mcp/admin/request-log.blade.php
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<div data-testid="request-log"></div>
|
||||||
|
|
@ -34,8 +34,10 @@ type CompletionNotifier interface {
|
||||||
// PrepSubsystem provides agentic MCP tools for workspace orchestration.
|
// PrepSubsystem provides agentic MCP tools for workspace orchestration.
|
||||||
//
|
//
|
||||||
// sub := agentic.NewPrep()
|
// sub := agentic.NewPrep()
|
||||||
|
// sub.SetCore(c)
|
||||||
// sub.RegisterTools(server)
|
// sub.RegisterTools(server)
|
||||||
type PrepSubsystem struct {
|
type PrepSubsystem struct {
|
||||||
|
core *core.Core // Core framework instance for IPC, Config, Lock
|
||||||
forge *forge.Forge
|
forge *forge.Forge
|
||||||
forgeURL string
|
forgeURL string
|
||||||
forgeToken string
|
forgeToken string
|
||||||
|
|
@ -43,7 +45,7 @@ type PrepSubsystem struct {
|
||||||
brainKey string
|
brainKey string
|
||||||
codePath string
|
codePath string
|
||||||
client *http.Client
|
client *http.Client
|
||||||
onComplete CompletionNotifier
|
onComplete CompletionNotifier // TODO(phase3): remove — replaced by c.ACTION()
|
||||||
drainMu sync.Mutex
|
drainMu sync.Mutex
|
||||||
pokeCh chan struct{}
|
pokeCh chan struct{}
|
||||||
frozen bool
|
frozen bool
|
||||||
|
|
@ -87,7 +89,15 @@ func NewPrep() *PrepSubsystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetCore wires the Core framework instance for IPC, Config, and Lock access.
|
||||||
|
//
|
||||||
|
// prep.SetCore(c)
|
||||||
|
func (s *PrepSubsystem) SetCore(c *core.Core) {
|
||||||
|
s.core = c
|
||||||
|
}
|
||||||
|
|
||||||
// SetCompletionNotifier wires up the monitor for immediate push on agent completion.
|
// SetCompletionNotifier wires up the monitor for immediate push on agent completion.
|
||||||
|
// Deprecated: Phase 3 replaces this with c.ACTION(messages.AgentCompleted{}).
|
||||||
//
|
//
|
||||||
// prep.SetCompletionNotifier(monitor)
|
// prep.SetCompletionNotifier(monitor)
|
||||||
func (s *PrepSubsystem) SetCompletionNotifier(n CompletionNotifier) {
|
func (s *PrepSubsystem) SetCompletionNotifier(n CompletionNotifier) {
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,9 @@ type ChannelNotifier interface {
|
||||||
// mon.SetNotifier(notifier)
|
// mon.SetNotifier(notifier)
|
||||||
// mon.Start(ctx)
|
// mon.Start(ctx)
|
||||||
type Subsystem struct {
|
type Subsystem struct {
|
||||||
|
core *core.Core // Core framework instance for IPC
|
||||||
server *mcp.Server
|
server *mcp.Server
|
||||||
notifier ChannelNotifier
|
notifier ChannelNotifier // TODO(phase3): remove — replaced by c.ACTION()
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
@ -124,7 +125,15 @@ type Subsystem struct {
|
||||||
var _ coremcp.Subsystem = (*Subsystem)(nil)
|
var _ coremcp.Subsystem = (*Subsystem)(nil)
|
||||||
var _ agentic.CompletionNotifier = (*Subsystem)(nil)
|
var _ agentic.CompletionNotifier = (*Subsystem)(nil)
|
||||||
|
|
||||||
|
// SetCore wires the Core framework instance for IPC access.
|
||||||
|
//
|
||||||
|
// mon.SetCore(c)
|
||||||
|
func (m *Subsystem) SetCore(c *core.Core) {
|
||||||
|
m.core = c
|
||||||
|
}
|
||||||
|
|
||||||
// SetNotifier wires up channel event broadcasting.
|
// SetNotifier wires up channel event broadcasting.
|
||||||
|
// Deprecated: Phase 3 replaces this with c.ACTION(messages.X{}).
|
||||||
//
|
//
|
||||||
// mon.SetNotifier(notifier)
|
// mon.SetNotifier(notifier)
|
||||||
func (m *Subsystem) SetNotifier(n ChannelNotifier) {
|
func (m *Subsystem) SetNotifier(n ChannelNotifier) {
|
||||||
|
|
|
||||||
1
ui/node_modules/.bin/tsc
generated
vendored
Symbolic link
1
ui/node_modules/.bin/tsc
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../typescript/bin/tsc
|
||||||
1
ui/node_modules/.bin/tsserver
generated
vendored
Symbolic link
1
ui/node_modules/.bin/tsserver
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../typescript/bin/tsserver
|
||||||
74
ui/node_modules/.package-lock.json
generated
vendored
Normal file
74
ui/node_modules/.package-lock.json
generated
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"name": "core-agent-panel",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@lit/reactive-element": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lit": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit/reactive-element": "^2.1.0",
|
||||||
|
"lit-element": "^4.2.0",
|
||||||
|
"lit-html": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lit-element": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@lit-labs/ssr-dom-shim": "^1.5.0",
|
||||||
|
"@lit/reactive-element": "^2.1.0",
|
||||||
|
"lit-html": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lit-html": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
ui/node_modules/@lit-labs/ssr-dom-shim/README.md
generated
vendored
Normal file
96
ui/node_modules/@lit-labs/ssr-dom-shim/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# @lit-labs/ssr-dom-shim
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This package provides minimal implementations of `Element`, `HTMLElement`,
|
||||||
|
`EventTarget`, `Event`, `CustomEvent`, `CustomElementRegistry`, and
|
||||||
|
`customElements`, designed to be used when Server Side Rendering (SSR) web
|
||||||
|
components from Node, including Lit components.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Usage from Lit
|
||||||
|
|
||||||
|
Lit itself automatically imports these shims when running in Node, so Lit users
|
||||||
|
should typically not need to directly depend on or import from this package.
|
||||||
|
|
||||||
|
See the [lit.dev SSR docs](https://lit.dev/docs/ssr/overview/) for general
|
||||||
|
information about server-side rendering with Lit.
|
||||||
|
|
||||||
|
### Usage in other contexts
|
||||||
|
|
||||||
|
Other libraries or frameworks who wish to support SSR are welcome to also depend
|
||||||
|
on these shims. (This package is planned to eventually move to
|
||||||
|
`@webcomponents/ssr-dom-shim` to better reflect this use case). There are two
|
||||||
|
main patterns for providing access to these shims to users:
|
||||||
|
|
||||||
|
1. Assigning shims to `globalThis`, ensuring that assignment occurs before
|
||||||
|
user-code runs.
|
||||||
|
|
||||||
|
2. Importing shims directly from the module that provides your base class, using
|
||||||
|
the `node` [export
|
||||||
|
condition](https://nodejs.org/api/packages.html#conditional-exports) to
|
||||||
|
ensure this only happens when running in Node, and not in the browser.
|
||||||
|
|
||||||
|
Lit takes approach #2 for all of the shims except for `customElements`, `Event`
|
||||||
|
and `CustomEvent`, so that users who have imported `lit` are able to call
|
||||||
|
`customElements.define` or `new Event(...)`/`new CustomEvent(...)` in their
|
||||||
|
components from Node.
|
||||||
|
|
||||||
|
### Exports
|
||||||
|
|
||||||
|
The main module exports the following values. Note that no globals are set by
|
||||||
|
this module.
|
||||||
|
|
||||||
|
- [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
|
||||||
|
- [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
|
||||||
|
- [`dispatchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent)
|
||||||
|
- [`removeEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener)
|
||||||
|
- [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element)
|
||||||
|
- (Inherits from EventTarget)
|
||||||
|
- [`attachShadow`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow)
|
||||||
|
- [`shadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot)
|
||||||
|
- [`attributes`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes)
|
||||||
|
- [`hasAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/hasAttribute)
|
||||||
|
- [`getAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute)
|
||||||
|
- [`setAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)
|
||||||
|
- [`removeAttribute`](https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute)
|
||||||
|
- [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)
|
||||||
|
- (Inherits from Element)
|
||||||
|
- [`CustomElementRegistry`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry)
|
||||||
|
- [`customElements`](https://developer.mozilla.org/en-US/docs/Web/API/Window/customElements)
|
||||||
|
- [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event)
|
||||||
|
- [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent)
|
||||||
|
- [`MediaList`](https://developer.mozilla.org/en-US/docs/Web/API/MediaList)
|
||||||
|
- [`StyleSheet`](https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet)
|
||||||
|
- [`CSSRule`](https://developer.mozilla.org/en-US/docs/Web/API/CSSRule)
|
||||||
|
- [`CSSRuleList`](https://developer.mozilla.org/en-US/docs/Web/API/CSSRuleList)
|
||||||
|
- [`CSSStyleSheet`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet)
|
||||||
|
- (Inherits from StyleSheet)
|
||||||
|
- [`replace`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/replace)
|
||||||
|
- [`replaceSync`](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/replaceSync)
|
||||||
|
|
||||||
|
### CSS Node.js customization hook
|
||||||
|
|
||||||
|
`@lit-labs/ssr-dom-shim/register-css-hook.js` implements/registers a
|
||||||
|
[Node.js customization hook](https://nodejs.org/api/module.html#customization-hooks)
|
||||||
|
(Node.js >= 18.6.0) to import CSS files/modules as instances of `CSSStyleSheet`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import styles from 'my-styles.css' with {type: 'css'};
|
||||||
|
// styles is now an instance of CSSStyleSheet
|
||||||
|
```
|
||||||
|
|
||||||
|
This can either be used as a parameter with the Node.js CLI
|
||||||
|
(e.g. `node --import @lit-labs/ssr-dom-shim/register-css-hook.js my-script.js` or via
|
||||||
|
environment variable `NODE_OPTIONS="--import @lit-labs/ssr-dom-shim/register-css-hook.js"`)
|
||||||
|
or imported inline, and it will apply to any module dynamically imported afterwards
|
||||||
|
(e.g. `import @lit-labs/ssr-dom-shim/register-css-hook.js` and
|
||||||
|
subsequently `await import('./my-component.js')`).
|
||||||
|
|
||||||
|
- [Node.js Customization Hooks](https://nodejs.org/api/module.html#customization-hooks)
|
||||||
|
- [Import Attributes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please see [CONTRIBUTING.md](../../../CONTRIBUTING.md).
|
||||||
14
ui/node_modules/@lit-labs/ssr-dom-shim/index.d.ts
generated
vendored
Normal file
14
ui/node_modules/@lit-labs/ssr-dom-shim/index.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { EventTargetShimMeta } from './lib/events.js';
|
||||||
|
export { ariaMixinAttributes, ElementInternals, HYDRATE_INTERNALS_ATTR_PREFIX, } from './lib/element-internals.js';
|
||||||
|
export { CSSRule, CSSRuleList, CSSStyleSheet, MediaList, StyleSheet, } from './lib/css.js';
|
||||||
|
export { CustomEvent, Event, EventTarget } from './lib/events.js';
|
||||||
|
export type HTMLElementWithEventMeta = HTMLElement & EventTargetShimMeta;
|
||||||
|
declare const ElementShimWithRealType: typeof Element;
|
||||||
|
export { ElementShimWithRealType as Element };
|
||||||
|
declare const HTMLElementShimWithRealType: typeof HTMLElement;
|
||||||
|
export { HTMLElementShimWithRealType as HTMLElement };
|
||||||
|
type RealCustomElementRegistryClass = (typeof globalThis)['CustomElementRegistry'];
|
||||||
|
declare const CustomElementRegistryShimWithRealType: RealCustomElementRegistryClass;
|
||||||
|
export { CustomElementRegistryShimWithRealType as CustomElementRegistry };
|
||||||
|
export declare const customElements: globalThis.CustomElementRegistry;
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/index.d.ts.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/index.d.ts.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,mBAAmB,EACpB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,6BAA6B,GAC9B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,OAAO,EACP,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,WAAW,EAAE,KAAK,EAAE,WAAW,EAAC,MAAM,iBAAiB,CAAC;AAShE,MAAM,MAAM,wBAAwB,GAAG,WAAW,GAAG,mBAAmB,CAAC;AA2GzE,QAAA,MAAM,uBAAuB,EAA4B,OAAO,OAAO,CAAC;AACxE,OAAO,EAAC,uBAAuB,IAAI,OAAO,EAAC,CAAC;AAG5C,QAAA,MAAM,2BAA2B,EACF,OAAO,WAAW,CAAC;AAClD,OAAO,EAAC,2BAA2B,IAAI,WAAW,EAAC,CAAC;AAmCpD,KAAK,8BAA8B,GACjC,CAAC,OAAO,UAAU,CAAC,CAAC,uBAAuB,CAAC,CAAC;AAwG/C,QAAA,MAAM,qCAAqC,EACN,8BAA8B,CAAC;AACpE,OAAO,EAAC,qCAAqC,IAAI,qBAAqB,EAAC,CAAC;AAExE,eAAO,MAAM,cAAc,kCAA8C,CAAC"}
|
||||||
216
ui/node_modules/@lit-labs/ssr-dom-shim/index.js
generated
vendored
Normal file
216
ui/node_modules/@lit-labs/ssr-dom-shim/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2019 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
import { ElementInternalsShim } from './lib/element-internals.js';
|
||||||
|
import { EventTargetShim, EventShim, CustomEventShim, } from './lib/events.js';
|
||||||
|
export { ariaMixinAttributes, ElementInternals, HYDRATE_INTERNALS_ATTR_PREFIX, } from './lib/element-internals.js';
|
||||||
|
export { CSSRule, CSSRuleList, CSSStyleSheet, MediaList, StyleSheet, } from './lib/css.js';
|
||||||
|
export { CustomEvent, Event, EventTarget } from './lib/events.js';
|
||||||
|
// In an empty Node.js vm, we need to patch the global context.
|
||||||
|
// TODO: Remove these globalThis assignments when we remove support
|
||||||
|
// for vm modules (--experimental-vm-modules).
|
||||||
|
globalThis.Event ??= EventShim;
|
||||||
|
globalThis.CustomEvent ??= CustomEventShim;
|
||||||
|
const attributes = new WeakMap();
|
||||||
|
const attributesForElement = (element) => {
|
||||||
|
let attrs = attributes.get(element);
|
||||||
|
if (attrs === undefined) {
|
||||||
|
attributes.set(element, (attrs = new Map()));
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
// The typings around the exports below are a little funky:
|
||||||
|
//
|
||||||
|
// 1. We want the `name` of the shim classes to match the real ones at runtime,
|
||||||
|
// hence e.g. `class Element`.
|
||||||
|
// 2. We can't shadow the global types with a simple class declaration, because
|
||||||
|
// then we can't reference the global types for casting, hence e.g.
|
||||||
|
// `const ElementShim = class Element`.
|
||||||
|
// 3. We want to export the classes typed as the real ones, hence e.g.
|
||||||
|
// `const ElementShimWithRealType = ElementShim as object as typeof Element;`.
|
||||||
|
// 4. We want the exported names to match the real ones, hence e.g.
|
||||||
|
// `export {ElementShimWithRealType as Element}`.
|
||||||
|
const ElementShim = class Element extends EventTargetShim {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.__shadowRootMode = null;
|
||||||
|
this.__shadowRoot = null;
|
||||||
|
this.__internals = null;
|
||||||
|
}
|
||||||
|
get attributes() {
|
||||||
|
return Array.from(attributesForElement(this)).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
get shadowRoot() {
|
||||||
|
if (this.__shadowRootMode === 'closed') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.__shadowRoot;
|
||||||
|
}
|
||||||
|
get localName() {
|
||||||
|
return this.constructor.__localName;
|
||||||
|
}
|
||||||
|
get tagName() {
|
||||||
|
return this.localName?.toUpperCase();
|
||||||
|
}
|
||||||
|
setAttribute(name, value) {
|
||||||
|
// Emulate browser behavior that silently casts all values to string. E.g.
|
||||||
|
// `42` becomes `"42"` and `{}` becomes `"[object Object]""`.
|
||||||
|
attributesForElement(this).set(name, String(value));
|
||||||
|
}
|
||||||
|
removeAttribute(name) {
|
||||||
|
attributesForElement(this).delete(name);
|
||||||
|
}
|
||||||
|
toggleAttribute(name, force) {
|
||||||
|
// Steps reference https://dom.spec.whatwg.org/#dom-element-toggleattribute
|
||||||
|
if (this.hasAttribute(name)) {
|
||||||
|
// Step 5
|
||||||
|
if (force === undefined || !force) {
|
||||||
|
this.removeAttribute(name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Step 4
|
||||||
|
if (force === undefined || force) {
|
||||||
|
// Step 4.1
|
||||||
|
this.setAttribute(name, '');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Step 4.2
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Step 6
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
hasAttribute(name) {
|
||||||
|
return attributesForElement(this).has(name);
|
||||||
|
}
|
||||||
|
attachShadow(init) {
|
||||||
|
const shadowRoot = { host: this };
|
||||||
|
this.__shadowRootMode = init.mode;
|
||||||
|
if (init && init.mode === 'open') {
|
||||||
|
this.__shadowRoot = shadowRoot;
|
||||||
|
}
|
||||||
|
return shadowRoot;
|
||||||
|
}
|
||||||
|
attachInternals() {
|
||||||
|
if (this.__internals !== null) {
|
||||||
|
throw new Error(`Failed to execute 'attachInternals' on 'HTMLElement': ` +
|
||||||
|
`ElementInternals for the specified element was already attached.`);
|
||||||
|
}
|
||||||
|
const internals = new ElementInternalsShim(this);
|
||||||
|
this.__internals = internals;
|
||||||
|
return internals;
|
||||||
|
}
|
||||||
|
getAttribute(name) {
|
||||||
|
const value = attributesForElement(this).get(name);
|
||||||
|
return value ?? null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ElementShimWithRealType = ElementShim;
|
||||||
|
export { ElementShimWithRealType as Element };
|
||||||
|
const HTMLElementShim = class HTMLElement extends ElementShim {
|
||||||
|
};
|
||||||
|
const HTMLElementShimWithRealType = HTMLElementShim;
|
||||||
|
export { HTMLElementShimWithRealType as HTMLElement };
|
||||||
|
// For convenience, we provide a global instance of a HTMLElement as an event
|
||||||
|
// target. This facilitates registering global event handlers
|
||||||
|
// (e.g. for @lit/context ContextProvider).
|
||||||
|
// We use this in in the SSR render function.
|
||||||
|
// Note, this is a bespoke element and not simply `document` or `window` since
|
||||||
|
// user code relies on these being undefined in the server environment.
|
||||||
|
globalThis.litServerRoot ??= Object.defineProperty(new HTMLElementShimWithRealType(), 'localName', {
|
||||||
|
// Patch localName (and tagName) to return a unique name.
|
||||||
|
get() {
|
||||||
|
return 'lit-server-root';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
function promiseWithResolvers() {
|
||||||
|
let resolve;
|
||||||
|
let reject;
|
||||||
|
const promise = new Promise((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return { promise, resolve: resolve, reject: reject };
|
||||||
|
}
|
||||||
|
class CustomElementRegistry {
|
||||||
|
constructor() {
|
||||||
|
this.__definitions = new Map();
|
||||||
|
this.__reverseDefinitions = new Map();
|
||||||
|
this.__pendingWhenDefineds = new Map();
|
||||||
|
}
|
||||||
|
define(name, ctor) {
|
||||||
|
if (this.__definitions.has(name)) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn(`'CustomElementRegistry' already has "${name}" defined. ` +
|
||||||
|
`This may have been caused by live reload or hot module ` +
|
||||||
|
`replacement in which case it can be safely ignored.\n` +
|
||||||
|
`Make sure to test your application with a production build as ` +
|
||||||
|
`repeat registrations will throw in production.`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Failed to execute 'define' on 'CustomElementRegistry': ` +
|
||||||
|
`the name "${name}" has already been used with this registry`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.__reverseDefinitions.has(ctor)) {
|
||||||
|
throw new Error(`Failed to execute 'define' on 'CustomElementRegistry': ` +
|
||||||
|
`the constructor has already been used with this registry for the ` +
|
||||||
|
`tag name ${this.__reverseDefinitions.get(ctor)}`);
|
||||||
|
}
|
||||||
|
// Provide tagName and localName for the component.
|
||||||
|
ctor.__localName = name;
|
||||||
|
this.__definitions.set(name, {
|
||||||
|
ctor,
|
||||||
|
// Note it's important we read `observedAttributes` in case it is a getter
|
||||||
|
// with side-effects, as is the case in Lit, where it triggers class
|
||||||
|
// finalization.
|
||||||
|
//
|
||||||
|
// TODO(aomarks) To be spec compliant, we should also capture the
|
||||||
|
// registration-time lifecycle methods like `connectedCallback`. For them
|
||||||
|
// to be actually accessible to e.g. the Lit SSR element renderer, though,
|
||||||
|
// we'd need to introduce a new API for accessing them (since `get` only
|
||||||
|
// returns the constructor).
|
||||||
|
observedAttributes: ctor.observedAttributes ?? [],
|
||||||
|
});
|
||||||
|
this.__reverseDefinitions.set(ctor, name);
|
||||||
|
this.__pendingWhenDefineds.get(name)?.resolve(ctor);
|
||||||
|
this.__pendingWhenDefineds.delete(name);
|
||||||
|
}
|
||||||
|
get(name) {
|
||||||
|
const definition = this.__definitions.get(name);
|
||||||
|
return definition?.ctor;
|
||||||
|
}
|
||||||
|
getName(ctor) {
|
||||||
|
return this.__reverseDefinitions.get(ctor) ?? null;
|
||||||
|
}
|
||||||
|
upgrade(_element) {
|
||||||
|
// In SSR this doesn't make a lot of sense, so we do nothing.
|
||||||
|
throw new Error(`customElements.upgrade is not currently supported in SSR. ` +
|
||||||
|
`Please file a bug if you need it.`);
|
||||||
|
}
|
||||||
|
async whenDefined(name) {
|
||||||
|
const definition = this.__definitions.get(name);
|
||||||
|
if (definition) {
|
||||||
|
return definition.ctor;
|
||||||
|
}
|
||||||
|
let withResolvers = this.__pendingWhenDefineds.get(name);
|
||||||
|
if (!withResolvers) {
|
||||||
|
withResolvers = promiseWithResolvers();
|
||||||
|
this.__pendingWhenDefineds.set(name, withResolvers);
|
||||||
|
}
|
||||||
|
return withResolvers.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const CustomElementRegistryShimWithRealType = CustomElementRegistry;
|
||||||
|
export { CustomElementRegistryShimWithRealType as CustomElementRegistry };
|
||||||
|
export const customElements = new CustomElementRegistryShimWithRealType();
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/index.js.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
10
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.d.ts
generated
vendored
Normal file
10
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { LoadHook } from 'node:module';
|
||||||
|
/**
|
||||||
|
* When an attempt is made to import a CSS file/module, code is
|
||||||
|
* generated to read the corresponding file, add it to a CSSStyleSheet
|
||||||
|
* instance and return that instance as the default export.
|
||||||
|
*
|
||||||
|
* https://nodejs.org/api/module.html#loadurl-context-nextload
|
||||||
|
*/
|
||||||
|
export declare const load: LoadHook;
|
||||||
|
//# sourceMappingURL=css-hook.d.ts.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.d.ts.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.d.ts.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"css-hook.d.ts","sourceRoot":"","sources":["../src/lib/css-hook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,aAAa,CAAC;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,EAAE,QAsBlB,CAAC"}
|
||||||
32
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.js
generated
vendored
Normal file
32
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.js
generated
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
/**
|
||||||
|
* When an attempt is made to import a CSS file/module, code is
|
||||||
|
* generated to read the corresponding file, add it to a CSSStyleSheet
|
||||||
|
* instance and return that instance as the default export.
|
||||||
|
*
|
||||||
|
* https://nodejs.org/api/module.html#loadurl-context-nextload
|
||||||
|
*/
|
||||||
|
export const load = async (url, context, nextLoad) => {
|
||||||
|
if (context.importAttributes.type === 'css') {
|
||||||
|
const content = await readFile(new URL(url), 'utf-8');
|
||||||
|
const code = `
|
||||||
|
import {CSSStyleSheet} from '@lit-labs/ssr-dom-shim';
|
||||||
|
const sheet = new CSSStyleSheet();
|
||||||
|
sheet.replaceSync(${JSON.stringify(content)});
|
||||||
|
export default sheet;
|
||||||
|
`;
|
||||||
|
return { format: 'module', shortCircuit: true, source: code };
|
||||||
|
}
|
||||||
|
else if (new URL(url).pathname.endsWith('.css')) {
|
||||||
|
try {
|
||||||
|
return await nextLoad(url, context);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.warn(`Tried to import ${url} without import attributes!\n` +
|
||||||
|
`(e.g. use "import s from './a.css' with {type: 'css'}" instead of "import s from './a.css'")`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await nextLoad(url, context);
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=css-hook.js.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.js.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css-hook.js.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"css-hook.js","sourceRoot":"","sources":["../src/lib/css-hook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAG1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,IAAI,GAAa,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;IAC7D,IAAI,OAAO,CAAC,gBAAgB,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG;;;0BAGS,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;;KAE5C,CAAC;QACF,OAAO,EAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAC,CAAC;IAC9D,CAAC;SAAM,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CACV,mBAAmB,GAAG,+BAA+B;gBACnD,8FAA8F,CACjG,CAAC;YACF,MAAM,CAAC,CAAC;QACV,CAAC;IACH,CAAC;IACD,OAAO,MAAM,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACtC,CAAC,CAAC","sourcesContent":["import {readFile} from 'node:fs/promises';\nimport type {LoadHook} from 'node:module';\n\n/**\n * When an attempt is made to import a CSS file/module, code is\n * generated to read the corresponding file, add it to a CSSStyleSheet\n * instance and return that instance as the default export.\n *\n * https://nodejs.org/api/module.html#loadurl-context-nextload\n */\nexport const load: LoadHook = async (url, context, nextLoad) => {\n if (context.importAttributes.type === 'css') {\n const content = await readFile(new URL(url), 'utf-8');\n const code = `\n import {CSSStyleSheet} from '@lit-labs/ssr-dom-shim';\n const sheet = new CSSStyleSheet();\n sheet.replaceSync(${JSON.stringify(content)});\n export default sheet;\n `;\n return {format: 'module', shortCircuit: true, source: code};\n } else if (new URL(url).pathname.endsWith('.css')) {\n try {\n return await nextLoad(url, context);\n } catch (e) {\n console.warn(\n `Tried to import ${url} without import attributes!\\n` +\n `(e.g. use \"import s from './a.css' with {type: 'css'}\" instead of \"import s from './a.css'\")`\n );\n throw e;\n }\n }\n return await nextLoad(url, context);\n};\n"]}
|
||||||
16
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.d.ts
generated
vendored
Normal file
16
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
declare const MediaListShimWithRealType: typeof MediaList;
|
||||||
|
export { MediaListShimWithRealType as MediaList };
|
||||||
|
declare const StyleSheetShimWithRealType: typeof StyleSheet;
|
||||||
|
export { StyleSheetShimWithRealType as StyleSheet };
|
||||||
|
declare const CSSRuleShimWithRealType: typeof CSSRule;
|
||||||
|
export { CSSRuleShimWithRealType as CSSRule };
|
||||||
|
declare const CSSRuleListShimWithRealType: typeof CSSRuleList;
|
||||||
|
export { CSSRuleListShimWithRealType as CSSRuleList };
|
||||||
|
declare const CSSStyleSheetShimWithRealType: typeof CSSStyleSheet;
|
||||||
|
export { CSSStyleSheetShimWithRealType as CSSStyleSheet, CSSStyleSheetShimWithRealType as CSSStyleSheetShim, };
|
||||||
|
//# sourceMappingURL=css.d.ts.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.d.ts.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.d.ts.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/lib/css.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkCH,QAAA,MAAM,yBAAyB,EAA8B,OAAO,SAAS,CAAC;AAC9E,OAAO,EAAC,yBAAyB,IAAI,SAAS,EAAC,CAAC;AA4BhD,QAAA,MAAM,0BAA0B,EACF,OAAO,UAAU,CAAC;AAChD,OAAO,EAAC,0BAA0B,IAAI,UAAU,EAAC,CAAC;AA2ClD,QAAA,MAAM,uBAAuB,EAA4B,OAAO,OAAO,CAAC;AACxE,OAAO,EAAC,uBAAuB,IAAI,OAAO,EAAC,CAAC;AAa5C,QAAA,MAAM,2BAA2B,EACF,OAAO,WAAW,CAAC;AAClD,OAAO,EAAC,2BAA2B,IAAI,WAAW,EAAC,CAAC;AA0CpD,QAAA,MAAM,6BAA6B,EACF,OAAO,aAAa,CAAC;AACtD,OAAO,EACL,6BAA6B,IAAI,aAAa,EAC9C,6BAA6B,IAAI,iBAAiB,GACnD,CAAC"}
|
||||||
145
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.js
generated
vendored
Normal file
145
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.js
generated
vendored
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
var _a;
|
||||||
|
const MediaListShim = class MediaList extends Array {
|
||||||
|
get mediaText() {
|
||||||
|
return this.join(', ');
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
return this.mediaText;
|
||||||
|
}
|
||||||
|
appendMedium(medium) {
|
||||||
|
if (!this.includes(medium)) {
|
||||||
|
this.push(medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteMedium(medium) {
|
||||||
|
const index = this.indexOf(medium);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item(index) {
|
||||||
|
return this[index] ?? null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const MediaListShimWithRealType = MediaListShim;
|
||||||
|
export { MediaListShimWithRealType as MediaList };
|
||||||
|
const StyleSheetShim = class StyleSheet {
|
||||||
|
constructor() {
|
||||||
|
this.__media = new MediaListShim();
|
||||||
|
this.disabled = false;
|
||||||
|
}
|
||||||
|
get href() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get media() {
|
||||||
|
return this.__media;
|
||||||
|
}
|
||||||
|
get ownerNode() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get parentStyleSheet() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get title() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get type() {
|
||||||
|
return 'text/css';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const StyleSheetShimWithRealType = StyleSheetShim;
|
||||||
|
export { StyleSheetShimWithRealType as StyleSheet };
|
||||||
|
const CSSRuleShim = (_a = class CSSRule {
|
||||||
|
constructor() {
|
||||||
|
this.STYLE_RULE = 1;
|
||||||
|
this.CHARSET_RULE = 2;
|
||||||
|
this.IMPORT_RULE = 3;
|
||||||
|
this.MEDIA_RULE = 4;
|
||||||
|
this.FONT_FACE_RULE = 5;
|
||||||
|
this.PAGE_RULE = 6;
|
||||||
|
this.NAMESPACE_RULE = 10;
|
||||||
|
this.KEYFRAMES_RULE = 7;
|
||||||
|
this.KEYFRAME_RULE = 8;
|
||||||
|
this.SUPPORTS_RULE = 12;
|
||||||
|
this.COUNTER_STYLE_RULE = 11;
|
||||||
|
this.FONT_FEATURE_VALUES_RULE = 14;
|
||||||
|
this.__parentStyleSheet = null;
|
||||||
|
this.cssText = '';
|
||||||
|
}
|
||||||
|
get parentRule() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get parentStyleSheet() {
|
||||||
|
return this.__parentStyleSheet;
|
||||||
|
}
|
||||||
|
get type() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_a.STYLE_RULE = 1,
|
||||||
|
_a.CHARSET_RULE = 2,
|
||||||
|
_a.IMPORT_RULE = 3,
|
||||||
|
_a.MEDIA_RULE = 4,
|
||||||
|
_a.FONT_FACE_RULE = 5,
|
||||||
|
_a.PAGE_RULE = 6,
|
||||||
|
_a.NAMESPACE_RULE = 10,
|
||||||
|
_a.KEYFRAMES_RULE = 7,
|
||||||
|
_a.KEYFRAME_RULE = 8,
|
||||||
|
_a.SUPPORTS_RULE = 12,
|
||||||
|
_a.COUNTER_STYLE_RULE = 11,
|
||||||
|
_a.FONT_FEATURE_VALUES_RULE = 14,
|
||||||
|
_a);
|
||||||
|
const CSSRuleShimWithRealType = CSSRuleShim;
|
||||||
|
export { CSSRuleShimWithRealType as CSSRule };
|
||||||
|
const CSSRuleListShim = class CSSRuleList extends Array {
|
||||||
|
item(index) {
|
||||||
|
return this[index] ?? null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const CSSRuleListShimWithRealType = CSSRuleListShim;
|
||||||
|
export { CSSRuleListShimWithRealType as CSSRuleList };
|
||||||
|
const CSSStyleSheetShim = class CSSStyleSheet extends StyleSheetShim {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.__rules = new CSSRuleListShim();
|
||||||
|
}
|
||||||
|
get cssRules() {
|
||||||
|
return this.__rules;
|
||||||
|
}
|
||||||
|
get ownerRule() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
get rules() {
|
||||||
|
return this.cssRules;
|
||||||
|
}
|
||||||
|
addRule(_selector, _style, _index) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
deleteRule(_index) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
insertRule(_rule, _index) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
removeRule(_index) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
replace(text) {
|
||||||
|
this.replaceSync(text);
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
replaceSync(text) {
|
||||||
|
this.__rules.length = 0;
|
||||||
|
const rule = new CSSRuleShim();
|
||||||
|
rule.cssText = text;
|
||||||
|
this.__rules.push(rule);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const CSSStyleSheetShimWithRealType = CSSStyleSheetShim;
|
||||||
|
export { CSSStyleSheetShimWithRealType as CSSStyleSheet, CSSStyleSheetShimWithRealType as CSSStyleSheetShim, };
|
||||||
|
//# sourceMappingURL=css.js.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.js.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/css.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
87
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.d.ts
generated
vendored
Normal file
87
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
type StringKeys<T extends object> = {
|
||||||
|
[K in keyof T]: T[K] extends string | null ? K : never;
|
||||||
|
}[keyof T];
|
||||||
|
type ARIAAttributeMap = {
|
||||||
|
[K in StringKeys<ARIAMixin>]: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Map of ARIAMixin properties to attributes
|
||||||
|
*/
|
||||||
|
export declare const ariaMixinAttributes: ARIAAttributeMap;
|
||||||
|
export declare const ElementInternalsShim: {
|
||||||
|
new (_host: HTMLElement): {
|
||||||
|
ariaActiveDescendantElement: null;
|
||||||
|
ariaAtomic: string;
|
||||||
|
ariaAutoComplete: string;
|
||||||
|
ariaBrailleLabel: string;
|
||||||
|
ariaBrailleRoleDescription: string;
|
||||||
|
ariaBusy: string;
|
||||||
|
ariaChecked: string;
|
||||||
|
ariaColCount: string;
|
||||||
|
ariaColIndex: string;
|
||||||
|
ariaColIndexText: string;
|
||||||
|
ariaColSpan: string;
|
||||||
|
ariaControlsElements: null;
|
||||||
|
ariaCurrent: string;
|
||||||
|
ariaDescribedByElements: null;
|
||||||
|
ariaDescription: string;
|
||||||
|
ariaDetailsElements: null;
|
||||||
|
ariaDisabled: string;
|
||||||
|
ariaErrorMessageElements: null;
|
||||||
|
ariaExpanded: string;
|
||||||
|
ariaFlowToElements: null;
|
||||||
|
ariaHasPopup: string;
|
||||||
|
ariaHidden: string;
|
||||||
|
ariaInvalid: string;
|
||||||
|
ariaKeyShortcuts: string;
|
||||||
|
ariaLabel: string;
|
||||||
|
ariaLabelledByElements: null;
|
||||||
|
ariaLevel: string;
|
||||||
|
ariaLive: string;
|
||||||
|
ariaModal: string;
|
||||||
|
ariaMultiLine: string;
|
||||||
|
ariaMultiSelectable: string;
|
||||||
|
ariaOrientation: string;
|
||||||
|
ariaOwnsElements: null;
|
||||||
|
ariaPlaceholder: string;
|
||||||
|
ariaPosInSet: string;
|
||||||
|
ariaPressed: string;
|
||||||
|
ariaReadOnly: string;
|
||||||
|
ariaRelevant: string;
|
||||||
|
ariaRequired: string;
|
||||||
|
ariaRoleDescription: string;
|
||||||
|
ariaRowCount: string;
|
||||||
|
ariaRowIndex: string;
|
||||||
|
ariaRowIndexText: string;
|
||||||
|
ariaRowSpan: string;
|
||||||
|
ariaSelected: string;
|
||||||
|
ariaSetSize: string;
|
||||||
|
ariaSort: string;
|
||||||
|
ariaValueMax: string;
|
||||||
|
ariaValueMin: string;
|
||||||
|
ariaValueNow: string;
|
||||||
|
ariaValueText: string;
|
||||||
|
role: string;
|
||||||
|
__host: HTMLElement;
|
||||||
|
get shadowRoot(): ShadowRoot;
|
||||||
|
checkValidity(): boolean;
|
||||||
|
form: null;
|
||||||
|
labels: NodeListOf<HTMLLabelElement>;
|
||||||
|
reportValidity(): boolean;
|
||||||
|
setFormValue(): void;
|
||||||
|
setValidity(): void;
|
||||||
|
states: Set<string>;
|
||||||
|
validationMessage: string;
|
||||||
|
validity: ValidityState;
|
||||||
|
willValidate: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
declare const ElementInternalsShimWithRealType: typeof ElementInternals;
|
||||||
|
export { ElementInternalsShimWithRealType as ElementInternals };
|
||||||
|
export declare const HYDRATE_INTERNALS_ATTR_PREFIX = "hydrate-internals-";
|
||||||
|
//# sourceMappingURL=element-internals.d.ts.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.d.ts.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.d.ts.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"element-internals.d.ts","sourceRoot":"","sources":["../src/lib/element-internals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI;KACjC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK;CACvD,CAAC,MAAM,CAAC,CAAC,CAAC;AAKX,KAAK,gBAAgB,GAAG;KACrB,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,GAAG,MAAM;CACrC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,EAAE,gBA6CjC,CAAC;AAOF,eAAO,MAAM,oBAAoB;gBA+DZ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBARtB,WAAW;;;;gBAqBO,UAAU,CAAC,gBAAgB,CAAC;;wBAItC,IAAI;uBACL,IAAI;;;kBAGF,aAAa;;;CAE/B,CAAC;AAEF,QAAA,MAAM,gCAAgC,EACF,OAAO,gBAAgB,CAAC;AAC5D,OAAO,EAAC,gCAAgC,IAAI,gBAAgB,EAAC,CAAC;AAE9D,eAAO,MAAM,6BAA6B,uBAAuB,CAAC"}
|
||||||
143
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.js
generated
vendored
Normal file
143
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.js
generated
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Map of ARIAMixin properties to attributes
|
||||||
|
*/
|
||||||
|
export const ariaMixinAttributes = {
|
||||||
|
ariaAtomic: 'aria-atomic',
|
||||||
|
ariaAutoComplete: 'aria-autocomplete',
|
||||||
|
ariaBrailleLabel: 'aria-braillelabel',
|
||||||
|
ariaBrailleRoleDescription: 'aria-brailleroledescription',
|
||||||
|
ariaBusy: 'aria-busy',
|
||||||
|
ariaChecked: 'aria-checked',
|
||||||
|
ariaColCount: 'aria-colcount',
|
||||||
|
ariaColIndex: 'aria-colindex',
|
||||||
|
ariaColIndexText: 'aria-colindextext',
|
||||||
|
ariaColSpan: 'aria-colspan',
|
||||||
|
ariaCurrent: 'aria-current',
|
||||||
|
ariaDescription: 'aria-description',
|
||||||
|
ariaDisabled: 'aria-disabled',
|
||||||
|
ariaExpanded: 'aria-expanded',
|
||||||
|
ariaHasPopup: 'aria-haspopup',
|
||||||
|
ariaHidden: 'aria-hidden',
|
||||||
|
ariaInvalid: 'aria-invalid',
|
||||||
|
ariaKeyShortcuts: 'aria-keyshortcuts',
|
||||||
|
ariaLabel: 'aria-label',
|
||||||
|
ariaLevel: 'aria-level',
|
||||||
|
ariaLive: 'aria-live',
|
||||||
|
ariaModal: 'aria-modal',
|
||||||
|
ariaMultiLine: 'aria-multiline',
|
||||||
|
ariaMultiSelectable: 'aria-multiselectable',
|
||||||
|
ariaOrientation: 'aria-orientation',
|
||||||
|
ariaPlaceholder: 'aria-placeholder',
|
||||||
|
ariaPosInSet: 'aria-posinset',
|
||||||
|
ariaPressed: 'aria-pressed',
|
||||||
|
ariaReadOnly: 'aria-readonly',
|
||||||
|
ariaRelevant: 'aria-relevant',
|
||||||
|
ariaRequired: 'aria-required',
|
||||||
|
ariaRoleDescription: 'aria-roledescription',
|
||||||
|
ariaRowCount: 'aria-rowcount',
|
||||||
|
ariaRowIndex: 'aria-rowindex',
|
||||||
|
ariaRowIndexText: 'aria-rowindextext',
|
||||||
|
ariaRowSpan: 'aria-rowspan',
|
||||||
|
ariaSelected: 'aria-selected',
|
||||||
|
ariaSetSize: 'aria-setsize',
|
||||||
|
ariaSort: 'aria-sort',
|
||||||
|
ariaValueMax: 'aria-valuemax',
|
||||||
|
ariaValueMin: 'aria-valuemin',
|
||||||
|
ariaValueNow: 'aria-valuenow',
|
||||||
|
ariaValueText: 'aria-valuetext',
|
||||||
|
role: 'role',
|
||||||
|
};
|
||||||
|
// Shim the global element internals object
|
||||||
|
// Methods should be fine as noops and properties can generally
|
||||||
|
// be while on the server.
|
||||||
|
export const ElementInternalsShim = class ElementInternals {
|
||||||
|
get shadowRoot() {
|
||||||
|
// Grab the shadow root instance from the Element shim
|
||||||
|
// to ensure that the shadow root is always available
|
||||||
|
// to the internals instance even if the mode is 'closed'
|
||||||
|
return this.__host
|
||||||
|
.__shadowRoot;
|
||||||
|
}
|
||||||
|
constructor(_host) {
|
||||||
|
this.ariaActiveDescendantElement = null;
|
||||||
|
this.ariaAtomic = '';
|
||||||
|
this.ariaAutoComplete = '';
|
||||||
|
this.ariaBrailleLabel = '';
|
||||||
|
this.ariaBrailleRoleDescription = '';
|
||||||
|
this.ariaBusy = '';
|
||||||
|
this.ariaChecked = '';
|
||||||
|
this.ariaColCount = '';
|
||||||
|
this.ariaColIndex = '';
|
||||||
|
this.ariaColIndexText = '';
|
||||||
|
this.ariaColSpan = '';
|
||||||
|
this.ariaControlsElements = null;
|
||||||
|
this.ariaCurrent = '';
|
||||||
|
this.ariaDescribedByElements = null;
|
||||||
|
this.ariaDescription = '';
|
||||||
|
this.ariaDetailsElements = null;
|
||||||
|
this.ariaDisabled = '';
|
||||||
|
this.ariaErrorMessageElements = null;
|
||||||
|
this.ariaExpanded = '';
|
||||||
|
this.ariaFlowToElements = null;
|
||||||
|
this.ariaHasPopup = '';
|
||||||
|
this.ariaHidden = '';
|
||||||
|
this.ariaInvalid = '';
|
||||||
|
this.ariaKeyShortcuts = '';
|
||||||
|
this.ariaLabel = '';
|
||||||
|
this.ariaLabelledByElements = null;
|
||||||
|
this.ariaLevel = '';
|
||||||
|
this.ariaLive = '';
|
||||||
|
this.ariaModal = '';
|
||||||
|
this.ariaMultiLine = '';
|
||||||
|
this.ariaMultiSelectable = '';
|
||||||
|
this.ariaOrientation = '';
|
||||||
|
this.ariaOwnsElements = null;
|
||||||
|
this.ariaPlaceholder = '';
|
||||||
|
this.ariaPosInSet = '';
|
||||||
|
this.ariaPressed = '';
|
||||||
|
this.ariaReadOnly = '';
|
||||||
|
this.ariaRelevant = '';
|
||||||
|
this.ariaRequired = '';
|
||||||
|
this.ariaRoleDescription = '';
|
||||||
|
this.ariaRowCount = '';
|
||||||
|
this.ariaRowIndex = '';
|
||||||
|
this.ariaRowIndexText = '';
|
||||||
|
this.ariaRowSpan = '';
|
||||||
|
this.ariaSelected = '';
|
||||||
|
this.ariaSetSize = '';
|
||||||
|
this.ariaSort = '';
|
||||||
|
this.ariaValueMax = '';
|
||||||
|
this.ariaValueMin = '';
|
||||||
|
this.ariaValueNow = '';
|
||||||
|
this.ariaValueText = '';
|
||||||
|
this.role = '';
|
||||||
|
this.form = null;
|
||||||
|
this.labels = [];
|
||||||
|
this.states = new Set();
|
||||||
|
this.validationMessage = '';
|
||||||
|
this.validity = {};
|
||||||
|
this.willValidate = true;
|
||||||
|
this.__host = _host;
|
||||||
|
}
|
||||||
|
checkValidity() {
|
||||||
|
// TODO(augustjk) Consider actually implementing logic.
|
||||||
|
// See https://github.com/lit/lit/issues/3740
|
||||||
|
console.warn('`ElementInternals.checkValidity()` was called on the server.' +
|
||||||
|
'This method always returns true.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
reportValidity() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
setFormValue() { }
|
||||||
|
setValidity() { }
|
||||||
|
};
|
||||||
|
const ElementInternalsShimWithRealType = ElementInternalsShim;
|
||||||
|
export { ElementInternalsShimWithRealType as ElementInternals };
|
||||||
|
export const HYDRATE_INTERNALS_ATTR_PREFIX = 'hydrate-internals-';
|
||||||
|
//# sourceMappingURL=element-internals.js.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.js.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/element-internals.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
34
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.d.ts
generated
vendored
Normal file
34
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* This is a basic implementation of an EventTarget, Event and CustomEvent.
|
||||||
|
*
|
||||||
|
* This is not fully spec compliant (e.g. validation),
|
||||||
|
* but should work well enough for our use cases.
|
||||||
|
*
|
||||||
|
* @see https://dom.spec.whatwg.org/#eventtarget
|
||||||
|
* @see https://dom.spec.whatwg.org/#event
|
||||||
|
* @see https://dom.spec.whatwg.org/#customevent
|
||||||
|
*/
|
||||||
|
export interface EventTargetShimMeta {
|
||||||
|
/**
|
||||||
|
* The event target parent represents the previous event target for an event
|
||||||
|
* in capture phase and the next event target for a bubbling event.
|
||||||
|
* Note that this is not the element parent
|
||||||
|
*/
|
||||||
|
__eventTargetParent: globalThis.EventTarget | undefined;
|
||||||
|
/**
|
||||||
|
* The host event target/element of this event target, if this event target
|
||||||
|
* is inside a Shadow DOM.
|
||||||
|
*/
|
||||||
|
__host: globalThis.EventTarget | undefined;
|
||||||
|
}
|
||||||
|
declare const EventTargetShimWithRealType: typeof globalThis.EventTarget;
|
||||||
|
export { EventTargetShimWithRealType as EventTarget, EventTargetShimWithRealType as EventTargetShim, };
|
||||||
|
declare const EventShimWithRealType: typeof Event;
|
||||||
|
declare const CustomEventShimWithRealType: typeof CustomEvent;
|
||||||
|
export { EventShimWithRealType as Event, EventShimWithRealType as EventShim, CustomEventShimWithRealType as CustomEvent, CustomEventShimWithRealType as CustomEventShim, };
|
||||||
|
//# sourceMappingURL=events.d.ts.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.d.ts.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.d.ts.map
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/lib/events.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;GASG;AAEH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,mBAAmB,EAAE,UAAU,CAAC,WAAW,GAAG,SAAS,CAAC;IACxD;;;OAGG;IACH,MAAM,EAAE,UAAU,CAAC,WAAW,GAAG,SAAS,CAAC;CAC5C;AAyPD,QAAA,MAAM,2BAA2B,EACN,OAAO,UAAU,CAAC,WAAW,CAAC;AACzD,OAAO,EACL,2BAA2B,IAAI,WAAW,EAC1C,2BAA2B,IAAI,eAAe,GAC/C,CAAC;AAkLF,QAAA,MAAM,qBAAqB,EAA0B,OAAO,KAAK,CAAC;AAClE,QAAA,MAAM,2BAA2B,EACF,OAAO,WAAW,CAAC;AAClD,OAAO,EACL,qBAAqB,IAAI,KAAK,EAC9B,qBAAqB,IAAI,SAAS,EAClC,2BAA2B,IAAI,WAAW,EAC1C,2BAA2B,IAAI,eAAe,GAC/C,CAAC"}
|
||||||
375
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.js
generated
vendored
Normal file
375
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.js
generated
vendored
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2023 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
||||||
|
if (kind === "m") throw new TypeError("Private method is not writable");
|
||||||
|
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
||||||
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
||||||
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
||||||
|
};
|
||||||
|
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
||||||
|
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
||||||
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
||||||
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
||||||
|
};
|
||||||
|
var _Event_cancelable, _Event_bubbles, _Event_composed, _Event_defaultPrevented, _Event_timestamp, _Event_propagationStopped, _Event_type, _Event_target, _Event_isBeingDispatched, _a, _CustomEvent_detail, _b;
|
||||||
|
const isCaptureEventListener = (options) => (typeof options === 'boolean' ? options : (options?.capture ?? false));
|
||||||
|
// Event phases
|
||||||
|
const NONE = 0;
|
||||||
|
const CAPTURING_PHASE = 1;
|
||||||
|
const AT_TARGET = 2;
|
||||||
|
const BUBBLING_PHASE = 3;
|
||||||
|
// Shim the global EventTarget object
|
||||||
|
class EventTarget {
|
||||||
|
constructor() {
|
||||||
|
this.__eventListeners = new Map();
|
||||||
|
this.__captureEventListeners = new Map();
|
||||||
|
}
|
||||||
|
addEventListener(type, callback, options) {
|
||||||
|
if (callback === undefined || callback === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const eventListenersMap = isCaptureEventListener(options)
|
||||||
|
? this.__captureEventListeners
|
||||||
|
: this.__eventListeners;
|
||||||
|
let eventListeners = eventListenersMap.get(type);
|
||||||
|
if (eventListeners === undefined) {
|
||||||
|
eventListeners = new Map();
|
||||||
|
eventListenersMap.set(type, eventListeners);
|
||||||
|
}
|
||||||
|
else if (eventListeners.has(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalizedOptions = typeof options === 'object' && options ? options : {};
|
||||||
|
normalizedOptions.signal?.addEventListener('abort', () => this.removeEventListener(type, callback, options));
|
||||||
|
eventListeners.set(callback, normalizedOptions ?? {});
|
||||||
|
}
|
||||||
|
removeEventListener(type, callback, options) {
|
||||||
|
if (callback === undefined || callback === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const eventListenersMap = isCaptureEventListener(options)
|
||||||
|
? this.__captureEventListeners
|
||||||
|
: this.__eventListeners;
|
||||||
|
const eventListeners = eventListenersMap.get(type);
|
||||||
|
if (eventListeners !== undefined) {
|
||||||
|
eventListeners.delete(callback);
|
||||||
|
if (!eventListeners.size) {
|
||||||
|
eventListenersMap.delete(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatchEvent(event) {
|
||||||
|
const composedPath = [this];
|
||||||
|
let parent = this.__eventTargetParent;
|
||||||
|
if (event.composed) {
|
||||||
|
while (parent) {
|
||||||
|
composedPath.push(parent);
|
||||||
|
parent = parent.__eventTargetParent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If the event is not composed and the event was dispatched inside
|
||||||
|
// shadow DOM, we need to stop before the host of the shadow DOM.
|
||||||
|
while (parent && parent !== this.__host) {
|
||||||
|
composedPath.push(parent);
|
||||||
|
parent = parent.__eventTargetParent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We need to patch various properties that would either be empty or wrong
|
||||||
|
// in this scenario.
|
||||||
|
let stopPropagation = false;
|
||||||
|
let stopImmediatePropagation = false;
|
||||||
|
let eventPhase = NONE;
|
||||||
|
let target = null;
|
||||||
|
let tmpTarget = null;
|
||||||
|
let currentTarget = null;
|
||||||
|
const originalStopPropagation = event.stopPropagation;
|
||||||
|
const originalStopImmediatePropagation = event.stopImmediatePropagation;
|
||||||
|
Object.defineProperties(event, {
|
||||||
|
target: {
|
||||||
|
get() {
|
||||||
|
return target ?? tmpTarget;
|
||||||
|
},
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
srcElement: {
|
||||||
|
get() {
|
||||||
|
return event.target;
|
||||||
|
},
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
currentTarget: {
|
||||||
|
get() {
|
||||||
|
return currentTarget;
|
||||||
|
},
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
eventPhase: {
|
||||||
|
get() {
|
||||||
|
return eventPhase;
|
||||||
|
},
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
composedPath: {
|
||||||
|
value: () => composedPath,
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
stopPropagation: {
|
||||||
|
value: () => {
|
||||||
|
stopPropagation = true;
|
||||||
|
originalStopPropagation.call(event);
|
||||||
|
},
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
stopImmediatePropagation: {
|
||||||
|
value: () => {
|
||||||
|
stopImmediatePropagation = true;
|
||||||
|
originalStopImmediatePropagation.call(event);
|
||||||
|
},
|
||||||
|
...enumerableProperty,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// An event handler can either be a function, an object with a handleEvent
|
||||||
|
// method or null. This function takes care to call the event handler
|
||||||
|
// correctly.
|
||||||
|
const invokeEventListener = (listener, options, eventListenerMap) => {
|
||||||
|
if (typeof listener === 'function') {
|
||||||
|
listener(event);
|
||||||
|
}
|
||||||
|
else if (typeof listener?.handleEvent === 'function') {
|
||||||
|
listener.handleEvent(event);
|
||||||
|
}
|
||||||
|
if (options.once) {
|
||||||
|
eventListenerMap.delete(listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// When an event is finished being dispatched, which can be after the event
|
||||||
|
// tree has been traversed or stopPropagation/stopImmediatePropagation has
|
||||||
|
// been called. Once that is the case, the currentTarget and eventPhase
|
||||||
|
// need to be reset and a value, representing whether the event has not
|
||||||
|
// been prevented, needs to be returned.
|
||||||
|
const finishDispatch = () => {
|
||||||
|
currentTarget = null;
|
||||||
|
eventPhase = NONE;
|
||||||
|
return !event.defaultPrevented;
|
||||||
|
};
|
||||||
|
// An event starts with the capture order, where it starts from the top.
|
||||||
|
// This is done even if bubbles is set to false, which is the default.
|
||||||
|
const captureEventPath = composedPath.slice().reverse();
|
||||||
|
// If the event target, which dispatches the event, is either in the light DOM
|
||||||
|
// or the event is not composed, the target is always itself. If that is not
|
||||||
|
// the case, the target needs to be retargeted: https://dom.spec.whatwg.org/#retarget
|
||||||
|
target = !this.__host || !event.composed ? this : null;
|
||||||
|
const retarget = (eventTargets) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
|
tmpTarget = this;
|
||||||
|
while (tmpTarget.__host && eventTargets.includes(tmpTarget.__host)) {
|
||||||
|
tmpTarget = tmpTarget.__host;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const eventTarget of captureEventPath) {
|
||||||
|
if (!target && (!tmpTarget || tmpTarget === eventTarget.__host)) {
|
||||||
|
retarget(captureEventPath.slice(captureEventPath.indexOf(eventTarget)));
|
||||||
|
}
|
||||||
|
currentTarget = eventTarget;
|
||||||
|
eventPhase = eventTarget === event.target ? AT_TARGET : CAPTURING_PHASE;
|
||||||
|
const captureEventListeners = eventTarget.__captureEventListeners.get(event.type);
|
||||||
|
if (captureEventListeners) {
|
||||||
|
for (const [listener, options] of captureEventListeners) {
|
||||||
|
invokeEventListener(listener, options, captureEventListeners);
|
||||||
|
if (stopImmediatePropagation) {
|
||||||
|
// Event.stopImmediatePropagation() stops any following invocation
|
||||||
|
// of an event handler even on the same event target.
|
||||||
|
return finishDispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stopPropagation) {
|
||||||
|
// Event.stopPropagation() stops any following invocation
|
||||||
|
// of an event handler for any following event targets.
|
||||||
|
return finishDispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bubbleEventPath = event.bubbles ? composedPath : [this];
|
||||||
|
tmpTarget = null;
|
||||||
|
for (const eventTarget of bubbleEventPath) {
|
||||||
|
if (!target &&
|
||||||
|
(!tmpTarget || eventTarget === tmpTarget.__host)) {
|
||||||
|
retarget(bubbleEventPath.slice(0, bubbleEventPath.indexOf(eventTarget) + 1));
|
||||||
|
}
|
||||||
|
currentTarget = eventTarget;
|
||||||
|
eventPhase = eventTarget === event.target ? AT_TARGET : BUBBLING_PHASE;
|
||||||
|
const captureEventListeners = eventTarget.__eventListeners.get(event.type);
|
||||||
|
if (captureEventListeners) {
|
||||||
|
for (const [listener, options] of captureEventListeners) {
|
||||||
|
invokeEventListener(listener, options, captureEventListeners);
|
||||||
|
if (stopImmediatePropagation) {
|
||||||
|
// Event.stopImmediatePropagation() stops any following invocation
|
||||||
|
// of an event handler even on the same event target.
|
||||||
|
return finishDispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stopPropagation) {
|
||||||
|
// Event.stopPropagation() stops any following invocation
|
||||||
|
// of an event handler for any following event targets.
|
||||||
|
return finishDispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finishDispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const EventTargetShimWithRealType = EventTarget;
|
||||||
|
export { EventTargetShimWithRealType as EventTarget, EventTargetShimWithRealType as EventTargetShim, };
|
||||||
|
const enumerableProperty = { __proto__: null };
|
||||||
|
enumerableProperty.enumerable = true;
|
||||||
|
Object.freeze(enumerableProperty);
|
||||||
|
// TODO: Remove this when we remove support for vm modules (--experimental-vm-modules).
|
||||||
|
const EventShim = (_a = class Event {
|
||||||
|
constructor(type, options = {}) {
|
||||||
|
_Event_cancelable.set(this, false);
|
||||||
|
_Event_bubbles.set(this, false);
|
||||||
|
_Event_composed.set(this, false);
|
||||||
|
_Event_defaultPrevented.set(this, false);
|
||||||
|
_Event_timestamp.set(this, Date.now());
|
||||||
|
_Event_propagationStopped.set(this, false);
|
||||||
|
_Event_type.set(this, void 0);
|
||||||
|
_Event_target.set(this, void 0);
|
||||||
|
_Event_isBeingDispatched.set(this, void 0);
|
||||||
|
this.NONE = NONE;
|
||||||
|
this.CAPTURING_PHASE = CAPTURING_PHASE;
|
||||||
|
this.AT_TARGET = AT_TARGET;
|
||||||
|
this.BUBBLING_PHASE = BUBBLING_PHASE;
|
||||||
|
if (arguments.length === 0)
|
||||||
|
throw new Error(`The type argument must be specified`);
|
||||||
|
if (typeof options !== 'object' || !options) {
|
||||||
|
throw new Error(`The "options" argument must be an object`);
|
||||||
|
}
|
||||||
|
const { bubbles, cancelable, composed } = options;
|
||||||
|
__classPrivateFieldSet(this, _Event_cancelable, !!cancelable, "f");
|
||||||
|
__classPrivateFieldSet(this, _Event_bubbles, !!bubbles, "f");
|
||||||
|
__classPrivateFieldSet(this, _Event_composed, !!composed, "f");
|
||||||
|
__classPrivateFieldSet(this, _Event_type, `${type}`, "f");
|
||||||
|
__classPrivateFieldSet(this, _Event_target, null, "f");
|
||||||
|
__classPrivateFieldSet(this, _Event_isBeingDispatched, false, "f");
|
||||||
|
}
|
||||||
|
initEvent(_type, _bubbles, _cancelable) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
stopImmediatePropagation() {
|
||||||
|
this.stopPropagation();
|
||||||
|
}
|
||||||
|
preventDefault() {
|
||||||
|
__classPrivateFieldSet(this, _Event_defaultPrevented, true, "f");
|
||||||
|
}
|
||||||
|
get target() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_target, "f");
|
||||||
|
}
|
||||||
|
get currentTarget() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_target, "f");
|
||||||
|
}
|
||||||
|
get srcElement() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_target, "f");
|
||||||
|
}
|
||||||
|
get type() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_type, "f");
|
||||||
|
}
|
||||||
|
get cancelable() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_cancelable, "f");
|
||||||
|
}
|
||||||
|
get defaultPrevented() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_cancelable, "f") && __classPrivateFieldGet(this, _Event_defaultPrevented, "f");
|
||||||
|
}
|
||||||
|
get timeStamp() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_timestamp, "f");
|
||||||
|
}
|
||||||
|
composedPath() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_isBeingDispatched, "f") ? [__classPrivateFieldGet(this, _Event_target, "f")] : [];
|
||||||
|
}
|
||||||
|
get returnValue() {
|
||||||
|
return !__classPrivateFieldGet(this, _Event_cancelable, "f") || !__classPrivateFieldGet(this, _Event_defaultPrevented, "f");
|
||||||
|
}
|
||||||
|
get bubbles() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_bubbles, "f");
|
||||||
|
}
|
||||||
|
get composed() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_composed, "f");
|
||||||
|
}
|
||||||
|
get eventPhase() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_isBeingDispatched, "f") ? _a.AT_TARGET : _a.NONE;
|
||||||
|
}
|
||||||
|
get cancelBubble() {
|
||||||
|
return __classPrivateFieldGet(this, _Event_propagationStopped, "f");
|
||||||
|
}
|
||||||
|
set cancelBubble(value) {
|
||||||
|
if (value) {
|
||||||
|
__classPrivateFieldSet(this, _Event_propagationStopped, true, "f");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopPropagation() {
|
||||||
|
__classPrivateFieldSet(this, _Event_propagationStopped, true, "f");
|
||||||
|
}
|
||||||
|
get isTrusted() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_Event_cancelable = new WeakMap(),
|
||||||
|
_Event_bubbles = new WeakMap(),
|
||||||
|
_Event_composed = new WeakMap(),
|
||||||
|
_Event_defaultPrevented = new WeakMap(),
|
||||||
|
_Event_timestamp = new WeakMap(),
|
||||||
|
_Event_propagationStopped = new WeakMap(),
|
||||||
|
_Event_type = new WeakMap(),
|
||||||
|
_Event_target = new WeakMap(),
|
||||||
|
_Event_isBeingDispatched = new WeakMap(),
|
||||||
|
_a.NONE = NONE,
|
||||||
|
_a.CAPTURING_PHASE = CAPTURING_PHASE,
|
||||||
|
_a.AT_TARGET = AT_TARGET,
|
||||||
|
_a.BUBBLING_PHASE = BUBBLING_PHASE,
|
||||||
|
_a);
|
||||||
|
Object.defineProperties(EventShim.prototype, {
|
||||||
|
initEvent: enumerableProperty,
|
||||||
|
stopImmediatePropagation: enumerableProperty,
|
||||||
|
preventDefault: enumerableProperty,
|
||||||
|
target: enumerableProperty,
|
||||||
|
currentTarget: enumerableProperty,
|
||||||
|
srcElement: enumerableProperty,
|
||||||
|
type: enumerableProperty,
|
||||||
|
cancelable: enumerableProperty,
|
||||||
|
defaultPrevented: enumerableProperty,
|
||||||
|
timeStamp: enumerableProperty,
|
||||||
|
composedPath: enumerableProperty,
|
||||||
|
returnValue: enumerableProperty,
|
||||||
|
bubbles: enumerableProperty,
|
||||||
|
composed: enumerableProperty,
|
||||||
|
eventPhase: enumerableProperty,
|
||||||
|
cancelBubble: enumerableProperty,
|
||||||
|
stopPropagation: enumerableProperty,
|
||||||
|
isTrusted: enumerableProperty,
|
||||||
|
});
|
||||||
|
// TODO: Remove this when we remove support for vm modules (--experimental-vm-modules).
|
||||||
|
const CustomEventShim = (_b = class CustomEvent extends EventShim {
|
||||||
|
constructor(type, options = {}) {
|
||||||
|
super(type, options);
|
||||||
|
_CustomEvent_detail.set(this, void 0);
|
||||||
|
__classPrivateFieldSet(this, _CustomEvent_detail, options?.detail ?? null, "f");
|
||||||
|
}
|
||||||
|
initCustomEvent(_type, _bubbles, _cancelable, _detail) {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
get detail() {
|
||||||
|
return __classPrivateFieldGet(this, _CustomEvent_detail, "f");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_CustomEvent_detail = new WeakMap(),
|
||||||
|
_b);
|
||||||
|
Object.defineProperties(CustomEventShim.prototype, {
|
||||||
|
detail: enumerableProperty,
|
||||||
|
});
|
||||||
|
const EventShimWithRealType = EventShim;
|
||||||
|
const CustomEventShimWithRealType = CustomEventShim;
|
||||||
|
export { EventShimWithRealType as Event, EventShimWithRealType as EventShim, CustomEventShimWithRealType as CustomEvent, CustomEventShimWithRealType as CustomEventShim, };
|
||||||
|
//# sourceMappingURL=events.js.map
|
||||||
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.js.map
generated
vendored
Normal file
1
ui/node_modules/@lit-labs/ssr-dom-shim/lib/events.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
69
ui/node_modules/@lit-labs/ssr-dom-shim/package.json
generated
vendored
Normal file
69
ui/node_modules/@lit-labs/ssr-dom-shim/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"name": "@lit-labs/ssr-dom-shim",
|
||||||
|
"version": "1.5.1",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"description": "DOM shim for Lit Server Side Rendering (SSR)",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"author": "Google LLC",
|
||||||
|
"homepage": "https://github.com/lit/lit/tree/main/packages/labs/ssr-dom-shim",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/lit/lit.git",
|
||||||
|
"directory": "packages/labs/ssr-dom-shim"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"typings": "index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./register-css-hook.js": {
|
||||||
|
"types": "./register-css-hook.d.ts",
|
||||||
|
"default": "./register-css-hook.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"index.{d.ts,d.ts.map,js,js.map}",
|
||||||
|
"register-css-hook.{d.ts,d.ts.map,js,js.map}",
|
||||||
|
"lib/"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "wireit",
|
||||||
|
"build:ts": "wireit",
|
||||||
|
"test": "wireit"
|
||||||
|
},
|
||||||
|
"wireit": {
|
||||||
|
"build": {
|
||||||
|
"dependencies": [
|
||||||
|
"build:ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"build:ts": {
|
||||||
|
"command": "tsc --build --pretty",
|
||||||
|
"clean": "if-file-deleted",
|
||||||
|
"files": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"tsconfig.json"
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
"lib/",
|
||||||
|
"index.{d.ts,d.ts.map,js,js.map}",
|
||||||
|
"tsconfig.tsbuildinfo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"command": "uvu test \"_test\\.js$\"",
|
||||||
|
"dependencies": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"NODE_OPTIONS": "--enable-source-maps --import ./register-css-hook.js"
|
||||||
|
},
|
||||||
|
"output": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
ui/node_modules/@lit-labs/ssr-dom-shim/register-css-hook.d.ts
generated
vendored
Normal file
7
ui/node_modules/@lit-labs/ssr-dom-shim/register-css-hook.d.ts
generated
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=register-css-hook.d.ts.map
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue