@aion0/forge 0.5.21 → 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.forge/agent-context.json +1 -1
- package/.forge/mcp.json +1 -1
- package/RELEASE_NOTES.md +6 -10
- package/app/api/plugins/route.ts +75 -0
- package/components/Dashboard.tsx +1 -0
- package/components/PipelineEditor.tsx +135 -9
- package/components/PluginsPanel.tsx +472 -0
- package/components/ProjectDetail.tsx +36 -98
- package/components/SessionView.tsx +4 -4
- package/components/SettingsModal.tsx +166 -67
- package/components/SkillsPanel.tsx +14 -5
- package/components/TerminalLauncher.tsx +398 -0
- package/components/WebTerminal.tsx +84 -84
- package/components/WorkspaceView.tsx +256 -76
- package/lib/agents/index.ts +7 -4
- package/lib/builtin-plugins/docker.yaml +70 -0
- package/lib/builtin-plugins/http.yaml +66 -0
- package/lib/builtin-plugins/jenkins.yaml +92 -0
- package/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/lib/builtin-plugins/playwright.yaml +111 -0
- package/lib/builtin-plugins/shell-command.yaml +60 -0
- package/lib/builtin-plugins/slack.yaml +48 -0
- package/lib/builtin-plugins/webhook.yaml +56 -0
- package/lib/forge-mcp-server.ts +116 -2
- package/lib/pipeline.ts +62 -5
- package/lib/plugins/executor.ts +347 -0
- package/lib/plugins/registry.ts +228 -0
- package/lib/plugins/types.ts +103 -0
- package/lib/project-sessions.ts +7 -2
- package/lib/session-utils.ts +7 -3
- package/lib/terminal-standalone.ts +6 -34
- package/lib/workspace/agent-worker.ts +1 -1
- package/lib/workspace/orchestrator.ts +443 -136
- package/lib/workspace/presets.ts +5 -3
- package/lib/workspace/session-monitor.ts +14 -10
- package/lib/workspace/types.ts +3 -1
- package/lib/workspace-standalone.ts +38 -21
- package/package.json +1 -1
- package/qa/.forge/agent-context.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
id: http
|
|
2
|
+
name: HTTP Request
|
|
3
|
+
icon: "🌐"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Make HTTP requests to any API endpoint.
|
|
8
|
+
Create instances for common API calls:
|
|
9
|
+
- "Health Check" → default_url: https://api.example.com/health, default_method: GET
|
|
10
|
+
- "Deploy Hook" → default_url: https://deploy.example.com/trigger, default_method: POST
|
|
11
|
+
- "Notify Slack" → default_url: https://hooks.slack.com/xxx, default_method: POST
|
|
12
|
+
|
|
13
|
+
config:
|
|
14
|
+
default_url:
|
|
15
|
+
type: string
|
|
16
|
+
label: Default URL
|
|
17
|
+
description: "Pre-configured URL"
|
|
18
|
+
default_method:
|
|
19
|
+
type: select
|
|
20
|
+
label: Default Method
|
|
21
|
+
options: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
22
|
+
default: "GET"
|
|
23
|
+
default_headers:
|
|
24
|
+
type: json
|
|
25
|
+
label: Default Headers
|
|
26
|
+
description: 'e.g., {"Authorization": "Bearer xxx"}'
|
|
27
|
+
default_body:
|
|
28
|
+
type: json
|
|
29
|
+
label: Default Body
|
|
30
|
+
description: "Default request body (for POST/PUT)"
|
|
31
|
+
expected_status:
|
|
32
|
+
type: string
|
|
33
|
+
label: Expected Status Code
|
|
34
|
+
description: "e.g., 200, 201 — used to validate response"
|
|
35
|
+
description:
|
|
36
|
+
type: string
|
|
37
|
+
label: Description
|
|
38
|
+
description: "What this request does (shown to agents)"
|
|
39
|
+
|
|
40
|
+
params:
|
|
41
|
+
url:
|
|
42
|
+
type: string
|
|
43
|
+
label: URL
|
|
44
|
+
description: "Overrides default_url if set"
|
|
45
|
+
method:
|
|
46
|
+
type: select
|
|
47
|
+
label: Method
|
|
48
|
+
options: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
49
|
+
headers:
|
|
50
|
+
type: json
|
|
51
|
+
label: Headers
|
|
52
|
+
body:
|
|
53
|
+
type: json
|
|
54
|
+
label: Body
|
|
55
|
+
|
|
56
|
+
defaultAction: request
|
|
57
|
+
|
|
58
|
+
actions:
|
|
59
|
+
request:
|
|
60
|
+
run: http
|
|
61
|
+
method: "{{params.method}}"
|
|
62
|
+
url: "{{params.url}}"
|
|
63
|
+
headers: "{{params.headers}}"
|
|
64
|
+
body: "{{params.body}}"
|
|
65
|
+
output:
|
|
66
|
+
result: "$body"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
id: jenkins
|
|
2
|
+
name: Jenkins CI
|
|
3
|
+
icon: "🔧"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Trigger and monitor Jenkins CI/CD jobs.
|
|
8
|
+
Create instances per job or per Jenkins server:
|
|
9
|
+
- "Build Backend" → default_job: backend-build
|
|
10
|
+
- "E2E Tests" → default_job: e2e-test-suite
|
|
11
|
+
- "Security Scan" → default_job: sonar-scan
|
|
12
|
+
|
|
13
|
+
config:
|
|
14
|
+
url:
|
|
15
|
+
type: string
|
|
16
|
+
label: Jenkins URL
|
|
17
|
+
required: true
|
|
18
|
+
description: "e.g., https://jenkins.example.com"
|
|
19
|
+
user:
|
|
20
|
+
type: string
|
|
21
|
+
label: Username
|
|
22
|
+
required: true
|
|
23
|
+
token:
|
|
24
|
+
type: secret
|
|
25
|
+
label: API Token
|
|
26
|
+
required: true
|
|
27
|
+
default_job:
|
|
28
|
+
type: string
|
|
29
|
+
label: Default Job Name
|
|
30
|
+
description: "Pre-configured job — no need to pass job param each time"
|
|
31
|
+
|
|
32
|
+
params:
|
|
33
|
+
job:
|
|
34
|
+
type: string
|
|
35
|
+
label: Job Name
|
|
36
|
+
description: "Overrides default_job if set"
|
|
37
|
+
parameters:
|
|
38
|
+
type: json
|
|
39
|
+
label: Build Parameters
|
|
40
|
+
description: "JSON object of build parameters"
|
|
41
|
+
build_number:
|
|
42
|
+
type: string
|
|
43
|
+
label: Build Number
|
|
44
|
+
description: "Specific build number (for log/status). Defaults to lastBuild"
|
|
45
|
+
|
|
46
|
+
defaultAction: trigger
|
|
47
|
+
|
|
48
|
+
actions:
|
|
49
|
+
trigger:
|
|
50
|
+
run: http
|
|
51
|
+
method: POST
|
|
52
|
+
url: "{{config.url}}/job/{{params.job}}/buildWithParameters"
|
|
53
|
+
headers:
|
|
54
|
+
Authorization: "Basic {{config.user}}:{{config.token}}"
|
|
55
|
+
body: "{{params.parameters | json}}"
|
|
56
|
+
output:
|
|
57
|
+
queue_url: "$.headers.location"
|
|
58
|
+
|
|
59
|
+
wait:
|
|
60
|
+
run: poll
|
|
61
|
+
url: "{{config.url}}/job/{{params.job}}/lastBuild/api/json"
|
|
62
|
+
headers:
|
|
63
|
+
Authorization: "Basic {{config.user}}:{{config.token}}"
|
|
64
|
+
interval: 30
|
|
65
|
+
until: "$.result != null"
|
|
66
|
+
timeout: 3600
|
|
67
|
+
output:
|
|
68
|
+
result: "$.result"
|
|
69
|
+
duration: "$.duration"
|
|
70
|
+
url: "$.url"
|
|
71
|
+
build_number: "$.number"
|
|
72
|
+
|
|
73
|
+
get_log:
|
|
74
|
+
run: http
|
|
75
|
+
method: GET
|
|
76
|
+
url: "{{config.url}}/job/{{params.job}}/{{params.build_number}}/consoleText"
|
|
77
|
+
headers:
|
|
78
|
+
Authorization: "Basic {{config.user}}:{{config.token}}"
|
|
79
|
+
output:
|
|
80
|
+
log: "$body"
|
|
81
|
+
|
|
82
|
+
status:
|
|
83
|
+
run: http
|
|
84
|
+
method: GET
|
|
85
|
+
url: "{{config.url}}/job/{{params.job}}/lastBuild/api/json"
|
|
86
|
+
headers:
|
|
87
|
+
Authorization: "Basic {{config.user}}:{{config.token}}"
|
|
88
|
+
output:
|
|
89
|
+
result: "$.result"
|
|
90
|
+
building: "$.building"
|
|
91
|
+
number: "$.number"
|
|
92
|
+
timestamp: "$.timestamp"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
id: llm-vision
|
|
2
|
+
name: LLM Vision
|
|
3
|
+
icon: "👁"
|
|
4
|
+
version: "0.1.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Send images to vision-capable LLMs for evaluation.
|
|
8
|
+
Supports OpenAI (GPT-4o), Anthropic (Claude), Google (Gemini).
|
|
9
|
+
Create instances per model for multi-model evaluation.
|
|
10
|
+
|
|
11
|
+
config:
|
|
12
|
+
provider:
|
|
13
|
+
type: select
|
|
14
|
+
label: Provider
|
|
15
|
+
options: ["openai", "anthropic", "google"]
|
|
16
|
+
required: true
|
|
17
|
+
api_key:
|
|
18
|
+
type: secret
|
|
19
|
+
label: API Key
|
|
20
|
+
required: true
|
|
21
|
+
model:
|
|
22
|
+
type: string
|
|
23
|
+
label: Model
|
|
24
|
+
description: "e.g., gpt-4o, claude-sonnet-4-6, gemini-2.0-flash"
|
|
25
|
+
default: "gpt-4o"
|
|
26
|
+
default_prompt:
|
|
27
|
+
type: string
|
|
28
|
+
label: Default Evaluation Prompt
|
|
29
|
+
description: "Default prompt if none provided per-call"
|
|
30
|
+
|
|
31
|
+
params:
|
|
32
|
+
image:
|
|
33
|
+
type: string
|
|
34
|
+
label: Image file path
|
|
35
|
+
required: true
|
|
36
|
+
description: "Local path to image file"
|
|
37
|
+
prompt:
|
|
38
|
+
type: string
|
|
39
|
+
label: Evaluation prompt
|
|
40
|
+
description: "What to evaluate — overrides default_prompt"
|
|
41
|
+
max_tokens:
|
|
42
|
+
type: number
|
|
43
|
+
label: Max tokens
|
|
44
|
+
default: 1024
|
|
45
|
+
|
|
46
|
+
defaultAction: evaluate
|
|
47
|
+
|
|
48
|
+
actions:
|
|
49
|
+
evaluate:
|
|
50
|
+
run: shell
|
|
51
|
+
command: |
|
|
52
|
+
PROVIDER="{{config.provider}}"
|
|
53
|
+
IMAGE="{{params.image}}"
|
|
54
|
+
PROMPT="{{params.prompt}}"
|
|
55
|
+
MODEL="{{config.model}}"
|
|
56
|
+
MAX_TOKENS="{{params.max_tokens}}"
|
|
57
|
+
[ -z "$MAX_TOKENS" ] && MAX_TOKENS=1024
|
|
58
|
+
IMG_BASE64=$(base64 < "$IMAGE" | tr -d '\n')
|
|
59
|
+
MIME="image/png"
|
|
60
|
+
case "$IMAGE" in *.jpg|*.jpeg) MIME="image/jpeg";; *.webp) MIME="image/webp";; esac
|
|
61
|
+
|
|
62
|
+
if [ "$PROVIDER" = "openai" ]; then
|
|
63
|
+
curl -s https://api.openai.com/v1/chat/completions \
|
|
64
|
+
-H "Authorization: Bearer {{config.api_key}}" \
|
|
65
|
+
-H "Content-Type: application/json" \
|
|
66
|
+
-d "{\"model\":\"$MODEL\",\"max_tokens\":$MAX_TOKENS,\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"$PROMPT\"},{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:$MIME;base64,$IMG_BASE64\"}}]}]}" \
|
|
67
|
+
| python3 -c "import sys,json; print(json.load(sys.stdin)['choices'][0]['message']['content'])" 2>&1
|
|
68
|
+
|
|
69
|
+
elif [ "$PROVIDER" = "anthropic" ]; then
|
|
70
|
+
curl -s https://api.anthropic.com/v1/messages \
|
|
71
|
+
-H "x-api-key: {{config.api_key}}" \
|
|
72
|
+
-H "anthropic-version: 2023-06-01" \
|
|
73
|
+
-H "Content-Type: application/json" \
|
|
74
|
+
-d "{\"model\":\"$MODEL\",\"max_tokens\":$MAX_TOKENS,\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"$PROMPT\"},{\"type\":\"image\",\"source\":{\"type\":\"base64\",\"media_type\":\"$MIME\",\"data\":\"$IMG_BASE64\"}}]}]}" \
|
|
75
|
+
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['content'][0]['text'])" 2>&1
|
|
76
|
+
|
|
77
|
+
elif [ "$PROVIDER" = "google" ]; then
|
|
78
|
+
curl -s "https://generativelanguage.googleapis.com/v1beta/models/$MODEL:generateContent?key={{config.api_key}}" \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d "{\"contents\":[{\"parts\":[{\"text\":\"$PROMPT\"},{\"inline_data\":{\"mime_type\":\"$MIME\",\"data\":\"$IMG_BASE64\"}}]}]}" \
|
|
81
|
+
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['candidates'][0]['content']['parts'][0]['text'])" 2>&1
|
|
82
|
+
fi
|
|
83
|
+
timeout: 120
|
|
84
|
+
output:
|
|
85
|
+
result: "$stdout"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
id: playwright
|
|
2
|
+
name: Playwright
|
|
3
|
+
icon: "🎭"
|
|
4
|
+
version: "0.3.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Browser testing and screenshots via Playwright.
|
|
8
|
+
Create one instance per environment (local, docker, or remote).
|
|
9
|
+
Forge does not install Playwright. Set up your own environment:
|
|
10
|
+
- Local: https://playwright.dev/docs/intro
|
|
11
|
+
- Docker: docker pull mcr.microsoft.com/playwright:v1.52.0-noble
|
|
12
|
+
- Remote: https://www.browserless.io/docs
|
|
13
|
+
|
|
14
|
+
config:
|
|
15
|
+
mode:
|
|
16
|
+
type: select
|
|
17
|
+
label: Execution Mode
|
|
18
|
+
options: ["local", "docker", "remote"]
|
|
19
|
+
default: "local"
|
|
20
|
+
required: true
|
|
21
|
+
base_url:
|
|
22
|
+
type: string
|
|
23
|
+
label: Application URL
|
|
24
|
+
description: "URL of the app being tested"
|
|
25
|
+
default: "http://localhost:3000"
|
|
26
|
+
required: true
|
|
27
|
+
server_url:
|
|
28
|
+
type: string
|
|
29
|
+
label: Remote WebSocket URL
|
|
30
|
+
description: "For remote mode (e.g., ws://playwright:3000)"
|
|
31
|
+
docker_image:
|
|
32
|
+
type: string
|
|
33
|
+
label: Docker Image
|
|
34
|
+
default: "mcr.microsoft.com/playwright:v1.52.0-noble"
|
|
35
|
+
project_dir:
|
|
36
|
+
type: string
|
|
37
|
+
label: Project Directory
|
|
38
|
+
description: "For docker mode — local path to mount as /app"
|
|
39
|
+
headless:
|
|
40
|
+
type: select
|
|
41
|
+
label: Headless Mode
|
|
42
|
+
options: ["true", "false"]
|
|
43
|
+
default: "true"
|
|
44
|
+
description: "false = show browser window (local mode only)"
|
|
45
|
+
|
|
46
|
+
params:
|
|
47
|
+
test_file:
|
|
48
|
+
type: string
|
|
49
|
+
label: Test file or directory
|
|
50
|
+
url:
|
|
51
|
+
type: string
|
|
52
|
+
label: URL to screenshot
|
|
53
|
+
args:
|
|
54
|
+
type: string
|
|
55
|
+
label: Extra CLI arguments
|
|
56
|
+
|
|
57
|
+
defaultAction: test
|
|
58
|
+
|
|
59
|
+
actions:
|
|
60
|
+
# Local mode
|
|
61
|
+
local_test:
|
|
62
|
+
run: shell
|
|
63
|
+
command: "BASE_URL={{config.base_url}} npx playwright test {{params.test_file}} {{params.args}} --reporter=line $([ '{{config.headless}}' = 'false' ] && echo '--headed') 2>&1"
|
|
64
|
+
timeout: 600
|
|
65
|
+
output:
|
|
66
|
+
result: "$stdout"
|
|
67
|
+
|
|
68
|
+
local_screenshot:
|
|
69
|
+
run: shell
|
|
70
|
+
command: "npx playwright screenshot --wait-for-timeout=3000 {{config.base_url}} /tmp/forge-screenshot-$(date +%s).png 2>&1"
|
|
71
|
+
timeout: 60
|
|
72
|
+
output:
|
|
73
|
+
result: "$stdout"
|
|
74
|
+
|
|
75
|
+
# Docker mode
|
|
76
|
+
docker_test:
|
|
77
|
+
run: shell
|
|
78
|
+
command: "docker run --rm -e BASE_URL={{config.base_url}} -v {{config.project_dir}}:/app -w /app {{config.docker_image}} npx playwright test {{params.test_file}} {{params.args}} --reporter=line 2>&1"
|
|
79
|
+
timeout: 600
|
|
80
|
+
output:
|
|
81
|
+
result: "$stdout"
|
|
82
|
+
|
|
83
|
+
docker_screenshot:
|
|
84
|
+
run: shell
|
|
85
|
+
command: "docker run --rm {{config.docker_image}} npx playwright screenshot --wait-for-timeout=3000 {{config.base_url}} /tmp/screenshot.png 2>&1"
|
|
86
|
+
timeout: 60
|
|
87
|
+
output:
|
|
88
|
+
result: "$stdout"
|
|
89
|
+
|
|
90
|
+
# Remote mode
|
|
91
|
+
remote_test:
|
|
92
|
+
run: shell
|
|
93
|
+
command: "PW_TEST_CONNECT_WS_ENDPOINT={{config.server_url}} BASE_URL={{config.base_url}} npx playwright test {{params.test_file}} {{params.args}} --reporter=line 2>&1"
|
|
94
|
+
timeout: 600
|
|
95
|
+
output:
|
|
96
|
+
result: "$stdout"
|
|
97
|
+
|
|
98
|
+
remote_screenshot:
|
|
99
|
+
run: shell
|
|
100
|
+
command: "PW_TEST_CONNECT_WS_ENDPOINT={{config.server_url}} npx playwright screenshot --wait-for-timeout=3000 {{config.base_url}} /tmp/forge-screenshot-$(date +%s).png 2>&1"
|
|
101
|
+
timeout: 60
|
|
102
|
+
output:
|
|
103
|
+
result: "$stdout"
|
|
104
|
+
|
|
105
|
+
# Common (all modes)
|
|
106
|
+
check_url:
|
|
107
|
+
run: shell
|
|
108
|
+
command: "curl -sf -o /dev/null -w '%{http_code}' {{config.base_url}}"
|
|
109
|
+
timeout: 10
|
|
110
|
+
output:
|
|
111
|
+
status_code: "$stdout"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
id: shell-command
|
|
2
|
+
name: Shell Command
|
|
3
|
+
icon: "💻"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Run shell commands with structured output.
|
|
8
|
+
Create instances for common operations:
|
|
9
|
+
- "Run Tests" → default_command: npm test
|
|
10
|
+
- "Build" → default_command: npm run build
|
|
11
|
+
- "Deploy Staging" → default_command: ssh staging 'cd /app && git pull && pm2 restart'
|
|
12
|
+
|
|
13
|
+
config:
|
|
14
|
+
default_command:
|
|
15
|
+
type: string
|
|
16
|
+
label: Default Command
|
|
17
|
+
description: "Pre-configured command to run if none provided"
|
|
18
|
+
default_cwd:
|
|
19
|
+
type: string
|
|
20
|
+
label: Working Directory
|
|
21
|
+
description: "Default working directory"
|
|
22
|
+
default_timeout:
|
|
23
|
+
type: number
|
|
24
|
+
label: Timeout (seconds)
|
|
25
|
+
default: 300
|
|
26
|
+
expected_output:
|
|
27
|
+
type: string
|
|
28
|
+
label: Expected Output Pattern
|
|
29
|
+
description: "Regex pattern to validate output (e.g., 'SUCCESS|PASSED')"
|
|
30
|
+
description:
|
|
31
|
+
type: string
|
|
32
|
+
label: Description
|
|
33
|
+
description: "What this command does (shown to agents)"
|
|
34
|
+
|
|
35
|
+
params:
|
|
36
|
+
command:
|
|
37
|
+
type: string
|
|
38
|
+
label: Command
|
|
39
|
+
description: "Overrides default_command if set"
|
|
40
|
+
cwd:
|
|
41
|
+
type: string
|
|
42
|
+
label: Working Directory
|
|
43
|
+
timeout:
|
|
44
|
+
type: number
|
|
45
|
+
label: Timeout (seconds)
|
|
46
|
+
args:
|
|
47
|
+
type: string
|
|
48
|
+
label: Extra Arguments
|
|
49
|
+
description: "Appended to the command"
|
|
50
|
+
|
|
51
|
+
defaultAction: run
|
|
52
|
+
|
|
53
|
+
actions:
|
|
54
|
+
run:
|
|
55
|
+
run: shell
|
|
56
|
+
command: "{{params.command}} {{params.args}}"
|
|
57
|
+
cwd: "{{params.cwd}}"
|
|
58
|
+
timeout: 300
|
|
59
|
+
output:
|
|
60
|
+
stdout: "$stdout"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
id: slack
|
|
2
|
+
name: Slack
|
|
3
|
+
icon: "📢"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Send messages to Slack channels via Incoming Webhook.
|
|
8
|
+
Create instances per channel or workspace:
|
|
9
|
+
- "Dev Alerts" → webhook for #dev-alerts
|
|
10
|
+
- "Deploy Notify" → webhook for #deployments
|
|
11
|
+
|
|
12
|
+
config:
|
|
13
|
+
webhook_url:
|
|
14
|
+
type: secret
|
|
15
|
+
label: Webhook URL
|
|
16
|
+
required: true
|
|
17
|
+
description: "Slack Incoming Webhook URL"
|
|
18
|
+
default_channel:
|
|
19
|
+
type: string
|
|
20
|
+
label: Default Channel
|
|
21
|
+
description: "Override channel (e.g., #dev-alerts)"
|
|
22
|
+
description:
|
|
23
|
+
type: string
|
|
24
|
+
label: Description
|
|
25
|
+
description: "What this webhook is for (shown to agents)"
|
|
26
|
+
|
|
27
|
+
params:
|
|
28
|
+
text:
|
|
29
|
+
type: string
|
|
30
|
+
label: Message
|
|
31
|
+
required: true
|
|
32
|
+
channel:
|
|
33
|
+
type: string
|
|
34
|
+
label: Channel
|
|
35
|
+
description: "Overrides default_channel"
|
|
36
|
+
|
|
37
|
+
defaultAction: send
|
|
38
|
+
|
|
39
|
+
actions:
|
|
40
|
+
send:
|
|
41
|
+
run: http
|
|
42
|
+
method: POST
|
|
43
|
+
url: "{{config.webhook_url}}"
|
|
44
|
+
headers:
|
|
45
|
+
Content-Type: application/json
|
|
46
|
+
body: '{"text":"{{params.text}}","channel":"{{params.channel}}"}'
|
|
47
|
+
output:
|
|
48
|
+
status: "$body"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
id: webhook
|
|
2
|
+
name: Webhook
|
|
3
|
+
icon: "🪝"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
author: forge
|
|
6
|
+
description: |
|
|
7
|
+
Send webhook notifications to any URL.
|
|
8
|
+
Create instances per endpoint:
|
|
9
|
+
- "Deploy Hook" → url: https://deploy.example.com/hook
|
|
10
|
+
- "CI Trigger" → url: https://ci.example.com/trigger
|
|
11
|
+
|
|
12
|
+
config:
|
|
13
|
+
url:
|
|
14
|
+
type: string
|
|
15
|
+
label: Webhook URL
|
|
16
|
+
required: true
|
|
17
|
+
description: "Target URL to send webhooks to"
|
|
18
|
+
secret:
|
|
19
|
+
type: secret
|
|
20
|
+
label: Secret
|
|
21
|
+
description: "Shared secret for signature verification (optional)"
|
|
22
|
+
default_event:
|
|
23
|
+
type: string
|
|
24
|
+
label: Default Event Type
|
|
25
|
+
default: "forge.notification"
|
|
26
|
+
description: "Sent as X-Forge-Event header"
|
|
27
|
+
default_method:
|
|
28
|
+
type: select
|
|
29
|
+
label: HTTP Method
|
|
30
|
+
options: ["POST", "PUT", "PATCH"]
|
|
31
|
+
default: "POST"
|
|
32
|
+
|
|
33
|
+
params:
|
|
34
|
+
payload:
|
|
35
|
+
type: json
|
|
36
|
+
label: Payload
|
|
37
|
+
description: "JSON body to send"
|
|
38
|
+
event:
|
|
39
|
+
type: string
|
|
40
|
+
label: Event Type
|
|
41
|
+
description: "Overrides default_event"
|
|
42
|
+
|
|
43
|
+
defaultAction: send
|
|
44
|
+
|
|
45
|
+
actions:
|
|
46
|
+
send:
|
|
47
|
+
run: http
|
|
48
|
+
method: "{{params.method}}"
|
|
49
|
+
url: "{{config.url}}"
|
|
50
|
+
headers:
|
|
51
|
+
Content-Type: application/json
|
|
52
|
+
X-Forge-Event: "{{params.event}}"
|
|
53
|
+
X-Forge-Secret: "{{config.secret}}"
|
|
54
|
+
body: "{{params | json}}"
|
|
55
|
+
output:
|
|
56
|
+
status: "$body"
|
package/lib/forge-mcp-server.ts
CHANGED
|
@@ -288,6 +288,119 @@ function createForgeMcpServer(sessionId: string): McpServer {
|
|
|
288
288
|
}
|
|
289
289
|
);
|
|
290
290
|
|
|
291
|
+
// ── trigger_pipeline ──────────────────────────
|
|
292
|
+
server.tool(
|
|
293
|
+
'trigger_pipeline',
|
|
294
|
+
'Trigger a pipeline workflow. Lists available workflows if called without arguments.',
|
|
295
|
+
{
|
|
296
|
+
workflow: z.string().optional().describe('Workflow name to trigger. Omit to list available workflows.'),
|
|
297
|
+
input: z.record(z.string(), z.string()).optional().describe('Input variables for the pipeline (e.g., { project: "my-app" })'),
|
|
298
|
+
},
|
|
299
|
+
async (params) => {
|
|
300
|
+
try {
|
|
301
|
+
if (!params.workflow) {
|
|
302
|
+
// List available workflows
|
|
303
|
+
const { listWorkflows } = await import('./pipeline');
|
|
304
|
+
const workflows = listWorkflows();
|
|
305
|
+
if (workflows.length === 0) {
|
|
306
|
+
return { content: [{ type: 'text', text: 'No workflows found. Create .yaml files in ~/.forge/flows/' }] };
|
|
307
|
+
}
|
|
308
|
+
const list = workflows.map((w: any) => `• ${w.name}${w.description ? ' — ' + w.description : ''} (${Object.keys(w.nodes || {}).length} nodes)`).join('\n');
|
|
309
|
+
return { content: [{ type: 'text', text: `Available workflows:\n${list}` }] };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const { startPipeline } = await import('./pipeline');
|
|
313
|
+
const pipeline = startPipeline(params.workflow, (params.input || {}) as Record<string, string>);
|
|
314
|
+
return { content: [{ type: 'text', text: `Pipeline started: ${pipeline.id} (workflow: ${params.workflow}, status: ${pipeline.status})` }] };
|
|
315
|
+
} catch (err: any) {
|
|
316
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// ── run_plugin ──────────────────────────────────
|
|
322
|
+
server.tool(
|
|
323
|
+
'run_plugin',
|
|
324
|
+
'Run an installed plugin action directly. Lists installed plugins if called without arguments.',
|
|
325
|
+
{
|
|
326
|
+
plugin: z.string().optional().describe('Plugin ID (e.g., "jenkins", "shell-command", "docker"). Omit to list installed plugins.'),
|
|
327
|
+
action: z.string().optional().describe('Action name (e.g., "trigger", "run", "build"). Uses default action if omitted.'),
|
|
328
|
+
params: z.record(z.string(), z.string()).optional().describe('Parameters for the action. Keys matching plugin config fields will override config values.'),
|
|
329
|
+
wait: z.boolean().optional().describe('Auto-run "wait" action after main action (for async operations like Jenkins builds)'),
|
|
330
|
+
},
|
|
331
|
+
async (params) => {
|
|
332
|
+
try {
|
|
333
|
+
const { listInstalledPlugins, getInstalledPlugin } = await import('./plugins/registry');
|
|
334
|
+
|
|
335
|
+
if (!params.plugin) {
|
|
336
|
+
const installed = listInstalledPlugins();
|
|
337
|
+
if (installed.length === 0) {
|
|
338
|
+
return { content: [{ type: 'text', text: 'No plugins installed. Install from the Plugins page.' }] };
|
|
339
|
+
}
|
|
340
|
+
const list = installed.map((p: any) => {
|
|
341
|
+
const actions = Object.keys(p.definition.actions).join(', ');
|
|
342
|
+
return `• ${p.definition.icon} ${p.id} — ${p.definition.description || p.definition.name}\n actions: ${actions}`;
|
|
343
|
+
}).join('\n');
|
|
344
|
+
return { content: [{ type: 'text', text: `Installed plugins:\n${list}` }] };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const inst = getInstalledPlugin(params.plugin);
|
|
348
|
+
if (!inst) return { content: [{ type: 'text', text: `Plugin "${params.plugin}" not installed.` }] };
|
|
349
|
+
if (!inst.enabled) return { content: [{ type: 'text', text: `Plugin "${params.plugin}" is disabled.` }] };
|
|
350
|
+
|
|
351
|
+
const { executePluginWithWait } = await import('./plugins/executor');
|
|
352
|
+
const actionName = params.action || inst.definition.defaultAction || Object.keys(inst.definition.actions)[0];
|
|
353
|
+
|
|
354
|
+
if (!inst.definition.actions[actionName]) {
|
|
355
|
+
const available = Object.keys(inst.definition.actions).join(', ');
|
|
356
|
+
return { content: [{ type: 'text', text: `Action "${actionName}" not found. Available: ${available}` }] };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const result = await executePluginWithWait(inst, actionName, params.params || {}, params.wait || false);
|
|
360
|
+
|
|
361
|
+
const output = JSON.stringify(result.output, null, 2);
|
|
362
|
+
const status = result.ok ? 'OK' : 'FAILED';
|
|
363
|
+
const duration = result.duration ? ` (${result.duration}ms)` : '';
|
|
364
|
+
const error = result.error ? `\nError: ${result.error}` : '';
|
|
365
|
+
|
|
366
|
+
return { content: [{ type: 'text', text: `${status}${duration}${error}\n${output}` }] };
|
|
367
|
+
} catch (err: any) {
|
|
368
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// ── get_pipeline_status ────────────────────────
|
|
374
|
+
server.tool(
|
|
375
|
+
'get_pipeline_status',
|
|
376
|
+
'Check the status and results of a running or completed pipeline.',
|
|
377
|
+
{
|
|
378
|
+
pipeline_id: z.string().describe('Pipeline ID to check'),
|
|
379
|
+
},
|
|
380
|
+
async (params) => {
|
|
381
|
+
try {
|
|
382
|
+
const { getPipeline } = await import('./pipeline');
|
|
383
|
+
const pipeline = getPipeline(params.pipeline_id);
|
|
384
|
+
if (!pipeline) return { content: [{ type: 'text', text: `Pipeline "${params.pipeline_id}" not found.` }] };
|
|
385
|
+
|
|
386
|
+
const nodes = Object.entries(pipeline.nodes).map(([id, n]: [string, any]) => {
|
|
387
|
+
let line = ` ${id}: ${n.status}`;
|
|
388
|
+
if (n.error) line += ` — ${n.error}`;
|
|
389
|
+
if (n.outputs && Object.keys(n.outputs).length > 0) {
|
|
390
|
+
for (const [k, v] of Object.entries(n.outputs)) {
|
|
391
|
+
line += `\n ${k}: ${String(v).slice(0, 200)}`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return line;
|
|
395
|
+
}).join('\n');
|
|
396
|
+
|
|
397
|
+
return { content: [{ type: 'text', text: `Pipeline ${pipeline.id} [${pipeline.status}] (${pipeline.workflowName})\n${nodes}` }] };
|
|
398
|
+
} catch (err: any) {
|
|
399
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
|
|
291
404
|
return server;
|
|
292
405
|
}
|
|
293
406
|
|
|
@@ -325,8 +438,9 @@ export async function startMcpServer(port: number): Promise<void> {
|
|
|
325
438
|
// Each session gets its own MCP server with context
|
|
326
439
|
const server = createForgeMcpServer(sessionId);
|
|
327
440
|
await server.connect(transport);
|
|
328
|
-
|
|
329
|
-
|
|
441
|
+
let agentLabel = 'unknown';
|
|
442
|
+
try { agentLabel = workspaceId ? (getOrch(workspaceId)?.getSnapshot()?.agents?.find((a: any) => a.id === agentId)?.label || agentId) : 'unknown'; } catch {}
|
|
443
|
+
console.log(`[forge-mcp] Client connected: ${agentLabel} (ws=${workspaceId?.slice(0, 8) || '?'}, session=${sessionId})`);
|
|
330
444
|
return;
|
|
331
445
|
}
|
|
332
446
|
|