@aion0/forge 0.5.26 → 0.5.27
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/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +11 -28
- package/app/api/terminal-bell/route.ts +6 -2
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge MCP Server — agent communication bus via Model Context Protocol.
|
|
3
|
+
*
|
|
4
|
+
* Each Claude Code session connects with context baked into the SSE URL:
|
|
5
|
+
* http://localhost:8406/sse?workspaceId=xxx&agentId=yyy
|
|
6
|
+
*
|
|
7
|
+
* The agent doesn't need to know IDs. It just calls:
|
|
8
|
+
* send_message(to: "Reviewer", content: "fixed the bug")
|
|
9
|
+
* get_inbox()
|
|
10
|
+
* get_status()
|
|
11
|
+
*
|
|
12
|
+
* Forge resolves everything from the connection context.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
19
|
+
|
|
20
|
+
// Lazy imports to avoid circular deps (workspace modules)
|
|
21
|
+
let _getOrchestrator: ((workspaceId: string) => any) | null = null;
|
|
22
|
+
|
|
23
|
+
export function setOrchestratorResolver(fn: (id: string) => any): void {
|
|
24
|
+
_getOrchestrator = fn;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getOrch(workspaceId: string): any {
|
|
28
|
+
if (!_getOrchestrator) throw new Error('Orchestrator resolver not set');
|
|
29
|
+
return _getOrchestrator(workspaceId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Per-session context (resolved from SSE URL + orchestrator topo)
|
|
33
|
+
interface SessionContext {
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
agentId: string; // resolved dynamically, may be empty for non-agent terminals
|
|
36
|
+
}
|
|
37
|
+
const sessionContexts = new Map<string, SessionContext>();
|
|
38
|
+
|
|
39
|
+
/** Resolve agentId from orchestrator's agent-tmux mapping */
|
|
40
|
+
function resolveAgentFromOrch(workspaceId: string): string {
|
|
41
|
+
// For now, default to primary agent. Future: resolve from tmux session → agent map
|
|
42
|
+
try {
|
|
43
|
+
const orch = getOrch(workspaceId);
|
|
44
|
+
const primary = orch.getPrimaryAgent();
|
|
45
|
+
return primary?.config?.id || '';
|
|
46
|
+
} catch { return ''; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── MCP Server Definition ──────────────────────────────
|
|
50
|
+
|
|
51
|
+
function createForgeMcpServer(sessionId: string): McpServer {
|
|
52
|
+
const server = new McpServer({
|
|
53
|
+
name: 'forge',
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Helper: get context for this session
|
|
58
|
+
const ctx = () => sessionContexts.get(sessionId) || { workspaceId: '', agentId: '' };
|
|
59
|
+
|
|
60
|
+
// ── send_message ──────────────────────────
|
|
61
|
+
server.tool(
|
|
62
|
+
'send_message',
|
|
63
|
+
'Send a message to another agent in the workspace. Set noReply=true for notifications that do not need a response.',
|
|
64
|
+
{
|
|
65
|
+
to: z.string().describe('Target agent — name like "Reviewer", or description like "the one who does testing"'),
|
|
66
|
+
content: z.string().describe('Message content'),
|
|
67
|
+
action: z.string().optional().describe('Message type: fix_request, update_notify, question, review, info_request'),
|
|
68
|
+
noReply: z.boolean().optional().describe('If true, recipient should not reply to this message'),
|
|
69
|
+
},
|
|
70
|
+
async (params) => {
|
|
71
|
+
const { to, content, action = 'update_notify', noReply } = params;
|
|
72
|
+
const { workspaceId, agentId } = ctx();
|
|
73
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const orch = getOrch(workspaceId);
|
|
77
|
+
const snapshot = orch.getSnapshot();
|
|
78
|
+
const candidates = snapshot.agents.filter((a: any) => a.type !== 'input' && a.id !== agentId);
|
|
79
|
+
|
|
80
|
+
// Match: exact label > label contains > role contains
|
|
81
|
+
const toLower = to.toLowerCase();
|
|
82
|
+
let target = candidates.find((a: any) => a.label.toLowerCase() === toLower);
|
|
83
|
+
if (!target) target = candidates.find((a: any) => a.label.toLowerCase().includes(toLower));
|
|
84
|
+
if (!target) target = candidates.find((a: any) => (a.role || '').toLowerCase().includes(toLower));
|
|
85
|
+
|
|
86
|
+
if (!target) {
|
|
87
|
+
const available = candidates.map((a: any) => `${a.label} (${(a.role || '').slice(0, 50)})`).join(', ');
|
|
88
|
+
return { content: [{ type: 'text', text: `No agent matches "${to}". Available: ${available}` }] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Block reply to agents who have a running/pending message to us.
|
|
92
|
+
// The system auto-delivers completion status — use /forge-send only for NEW messages.
|
|
93
|
+
// Uses findLast to check the most recent message state (not oldest/stale ones).
|
|
94
|
+
const incomingFromTarget = orch.getBus().getLog().findLast((m: any) =>
|
|
95
|
+
m.to === agentId && m.from === target.id &&
|
|
96
|
+
(m.status === 'running' || m.status === 'pending')
|
|
97
|
+
);
|
|
98
|
+
if (incomingFromTarget && !incomingFromTarget.payload?.noReply) {
|
|
99
|
+
return { content: [{ type: 'text', text: `Skipped: you are processing a message from ${target.label}. Your completion is delivered automatically — no need to reply via send_message.` }] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const payload: any = { action, content };
|
|
103
|
+
if (noReply) payload.noReply = true;
|
|
104
|
+
orch.getBus().send(agentId, target.id, 'notify', payload);
|
|
105
|
+
return { content: [{ type: 'text', text: `Message sent to ${target.label}${noReply ? ' (no-reply)' : ''}` }] };
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ── get_inbox ─────────────────────────────
|
|
113
|
+
server.tool(
|
|
114
|
+
'get_inbox',
|
|
115
|
+
'Check inbox messages from other agents',
|
|
116
|
+
{},
|
|
117
|
+
async () => {
|
|
118
|
+
const { workspaceId, agentId } = ctx();
|
|
119
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const orch = getOrch(workspaceId);
|
|
123
|
+
const messages = orch.getBus().getLog()
|
|
124
|
+
.filter((m: any) => m.to === agentId && m.type !== 'ack')
|
|
125
|
+
.slice(-20);
|
|
126
|
+
|
|
127
|
+
if (messages.length === 0) {
|
|
128
|
+
return { content: [{ type: 'text', text: 'No messages in inbox.' }] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const snapshot = orch.getSnapshot();
|
|
132
|
+
const getLabel = (id: string) => snapshot.agents.find((a: any) => a.id === id)?.label || id;
|
|
133
|
+
|
|
134
|
+
const formatted = messages.map((m: any) => {
|
|
135
|
+
const noReplyTag = m.payload?.noReply ? ' [no-reply]' : '';
|
|
136
|
+
const refInfo = m.payload?.ref ? ` [ref: ${m.payload.ref}]` : '';
|
|
137
|
+
return `[${m.status}] From ${getLabel(m.from)}${noReplyTag}: ${m.payload?.content || m.payload?.action || '(no content)'}${refInfo} (${m.id.slice(0, 8)})`;
|
|
138
|
+
}).join('\n');
|
|
139
|
+
|
|
140
|
+
return { content: [{ type: 'text', text: formatted }] };
|
|
141
|
+
} catch (err: any) {
|
|
142
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// ── mark_message_done ─────────────────────
|
|
148
|
+
server.tool(
|
|
149
|
+
'mark_message_done',
|
|
150
|
+
'Mark an inbox message as done after handling it',
|
|
151
|
+
{
|
|
152
|
+
message_id: z.string().describe('Message ID (first 8 chars or full UUID)'),
|
|
153
|
+
},
|
|
154
|
+
async (params) => {
|
|
155
|
+
const { workspaceId, agentId } = ctx();
|
|
156
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const orch = getOrch(workspaceId);
|
|
160
|
+
const msg = orch.getBus().getLog().find((m: any) =>
|
|
161
|
+
(m.id === params.message_id || m.id.startsWith(params.message_id)) && m.to === agentId
|
|
162
|
+
);
|
|
163
|
+
if (!msg) return { content: [{ type: 'text', text: 'Message not found' }] };
|
|
164
|
+
|
|
165
|
+
msg.status = 'done';
|
|
166
|
+
return { content: [{ type: 'text', text: `Message ${params.message_id} marked as done` }] };
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── get_status ────────────────────────────
|
|
174
|
+
server.tool(
|
|
175
|
+
'get_status',
|
|
176
|
+
'Get live status of all agents in the workspace (from topology cache)',
|
|
177
|
+
{},
|
|
178
|
+
async () => {
|
|
179
|
+
const { workspaceId } = ctx();
|
|
180
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const orch = getOrch(workspaceId);
|
|
184
|
+
const topo = orch.getWorkspaceTopo();
|
|
185
|
+
|
|
186
|
+
const lines = topo.agents.map((a: any) => {
|
|
187
|
+
const icon = a.smithStatus === 'active'
|
|
188
|
+
? (a.taskStatus === 'running' ? '🔵' : a.taskStatus === 'done' ? '✅' : a.taskStatus === 'failed' ? '🔴' : '🟢')
|
|
189
|
+
: '⬚';
|
|
190
|
+
return `${icon} ${a.label}: smith=${a.smithStatus} task=${a.taskStatus}`;
|
|
191
|
+
});
|
|
192
|
+
lines.unshift(`Flow: ${topo.flow}\n`);
|
|
193
|
+
|
|
194
|
+
return { content: [{ type: 'text', text: lines.join('\n') || 'No agents configured.' }] };
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// ── get_agents ────────────────────────────
|
|
202
|
+
server.tool(
|
|
203
|
+
'get_agents',
|
|
204
|
+
'Get workspace topology — all agents, their roles, relationships, current status, and execution flow. Cached and auto-refreshed on any agent change. Call this to understand the full team composition before planning work.',
|
|
205
|
+
{},
|
|
206
|
+
async () => {
|
|
207
|
+
const { workspaceId, agentId } = ctx();
|
|
208
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const orch = getOrch(workspaceId);
|
|
212
|
+
const topo = orch.getWorkspaceTopo();
|
|
213
|
+
|
|
214
|
+
const lines: string[] = [];
|
|
215
|
+
lines.push(`## Workspace Topology (${topo.agents.length} agents)`);
|
|
216
|
+
lines.push(`Flow: ${topo.flow}\n`);
|
|
217
|
+
|
|
218
|
+
// Identify present and missing standard roles
|
|
219
|
+
const labels = new Set(topo.agents.map((a: any) => a.label.toLowerCase()));
|
|
220
|
+
const standardRoles = ['architect', 'engineer', 'qa', 'reviewer', 'pm', 'lead'];
|
|
221
|
+
const present = standardRoles.filter(r => labels.has(r));
|
|
222
|
+
const missing = standardRoles.filter(r => !labels.has(r));
|
|
223
|
+
if (missing.length > 0) {
|
|
224
|
+
lines.push(`Present roles: ${present.join(', ') || 'none'}`);
|
|
225
|
+
lines.push(`Missing roles: ${missing.join(', ')} — these responsibilities must be covered by existing agents\n`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const a of topo.agents as any[]) {
|
|
229
|
+
const isMe = a.id === agentId;
|
|
230
|
+
lines.push(`### ${a.icon} ${a.label}${isMe ? ' ← YOU' : ''}${a.primary ? ' [PRIMARY]' : ''}`);
|
|
231
|
+
lines.push(`Status: smith=${a.smithStatus} task=${a.taskStatus}`);
|
|
232
|
+
lines.push(`Role: ${a.roleSummary}`);
|
|
233
|
+
if (a.dependsOn.length > 0) lines.push(`Depends on: ${a.dependsOn.join(', ')}`);
|
|
234
|
+
if (a.workDir !== './') lines.push(`Work dir: ${a.workDir}`);
|
|
235
|
+
if (a.outputs.length > 0) lines.push(`Outputs: ${a.outputs.join(', ')}`);
|
|
236
|
+
if (a.steps.length > 0) lines.push(`Steps: ${a.steps.join(' → ')}`);
|
|
237
|
+
lines.push('');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
241
|
+
} catch (err: any) {
|
|
242
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ── sync_progress ─────────────────────────
|
|
248
|
+
server.tool(
|
|
249
|
+
'sync_progress',
|
|
250
|
+
'Report your work progress to the workspace (what you did, files changed)',
|
|
251
|
+
{
|
|
252
|
+
summary: z.string().describe('Brief summary of what you accomplished'),
|
|
253
|
+
files: z.array(z.string()).optional().describe('List of files changed'),
|
|
254
|
+
},
|
|
255
|
+
async (params) => {
|
|
256
|
+
const { workspaceId, agentId } = ctx();
|
|
257
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const orch = getOrch(workspaceId);
|
|
261
|
+
const entry = orch.getSnapshot().agents.find((a: any) => a.id === agentId);
|
|
262
|
+
if (!entry) return { content: [{ type: 'text', text: 'Agent not found in workspace' }] };
|
|
263
|
+
|
|
264
|
+
orch.completeManualAgent(agentId, params.files || []);
|
|
265
|
+
|
|
266
|
+
return { content: [{ type: 'text', text: `Progress synced: ${params.summary}` }] };
|
|
267
|
+
} catch (err: any) {
|
|
268
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// ── check_outbox ──────────────────────────
|
|
274
|
+
server.tool(
|
|
275
|
+
'check_outbox',
|
|
276
|
+
'Check status of messages you sent to other agents. See if they replied or completed.',
|
|
277
|
+
{},
|
|
278
|
+
async () => {
|
|
279
|
+
const { workspaceId, agentId } = ctx();
|
|
280
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context' }] };
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const orch = getOrch(workspaceId);
|
|
284
|
+
const snapshot = orch.getSnapshot();
|
|
285
|
+
const getLabel = (id: string) => snapshot.agents.find((a: any) => a.id === id)?.label || id;
|
|
286
|
+
|
|
287
|
+
// Messages sent BY this agent
|
|
288
|
+
const sent = orch.getBus().getLog()
|
|
289
|
+
.filter((m: any) => m.from === agentId && m.type !== 'ack')
|
|
290
|
+
.slice(-20);
|
|
291
|
+
|
|
292
|
+
if (sent.length === 0) {
|
|
293
|
+
return { content: [{ type: 'text', text: 'No messages sent.' }] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check for replies
|
|
297
|
+
const formatted = sent.map((m: any) => {
|
|
298
|
+
const targetLabel = getLabel(m.to);
|
|
299
|
+
const replies = orch.getBus().getLog().filter((r: any) =>
|
|
300
|
+
r.from === m.to && r.to === agentId && r.timestamp > m.timestamp && r.type !== 'ack'
|
|
301
|
+
);
|
|
302
|
+
const replyInfo = replies.length > 0
|
|
303
|
+
? `replied: ${replies[replies.length - 1].payload?.content?.slice(0, 100) || '(no content)'}`
|
|
304
|
+
: 'no reply yet';
|
|
305
|
+
return `→ ${targetLabel}: [${m.status}] ${(m.payload?.content || '').slice(0, 60)} | ${replyInfo}`;
|
|
306
|
+
}).join('\n');
|
|
307
|
+
|
|
308
|
+
return { content: [{ type: 'text', text: formatted }] };
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// ── trigger_pipeline ──────────────────────────
|
|
316
|
+
server.tool(
|
|
317
|
+
'trigger_pipeline',
|
|
318
|
+
'Trigger a pipeline workflow. Lists available workflows if called without arguments.',
|
|
319
|
+
{
|
|
320
|
+
workflow: z.string().optional().describe('Workflow name to trigger. Omit to list available workflows.'),
|
|
321
|
+
input: z.record(z.string(), z.string()).optional().describe('Input variables for the pipeline (e.g., { project: "my-app" })'),
|
|
322
|
+
},
|
|
323
|
+
async (params) => {
|
|
324
|
+
try {
|
|
325
|
+
if (!params.workflow) {
|
|
326
|
+
// List available workflows
|
|
327
|
+
const { listWorkflows } = await import('./pipeline');
|
|
328
|
+
const workflows = listWorkflows();
|
|
329
|
+
if (workflows.length === 0) {
|
|
330
|
+
return { content: [{ type: 'text', text: 'No workflows found. Create .yaml files in ~/.forge/flows/' }] };
|
|
331
|
+
}
|
|
332
|
+
const list = workflows.map((w: any) => `• ${w.name}${w.description ? ' — ' + w.description : ''} (${Object.keys(w.nodes || {}).length} nodes)`).join('\n');
|
|
333
|
+
return { content: [{ type: 'text', text: `Available workflows:\n${list}` }] };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const { startPipeline } = await import('./pipeline');
|
|
337
|
+
const pipeline = startPipeline(params.workflow, (params.input || {}) as Record<string, string>);
|
|
338
|
+
return { content: [{ type: 'text', text: `Pipeline started: ${pipeline.id} (workflow: ${params.workflow}, status: ${pipeline.status})` }] };
|
|
339
|
+
} catch (err: any) {
|
|
340
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// ── run_plugin ──────────────────────────────────
|
|
346
|
+
server.tool(
|
|
347
|
+
'run_plugin',
|
|
348
|
+
'Run an installed plugin action directly. Lists installed plugins if called without arguments.',
|
|
349
|
+
{
|
|
350
|
+
plugin: z.string().optional().describe('Plugin ID (e.g., "jenkins", "shell-command", "docker"). Omit to list installed plugins.'),
|
|
351
|
+
action: z.string().optional().describe('Action name (e.g., "trigger", "run", "build"). Uses default action if omitted.'),
|
|
352
|
+
params: z.record(z.string(), z.string()).optional().describe('Parameters for the action. Keys matching plugin config fields will override config values.'),
|
|
353
|
+
wait: z.boolean().optional().describe('Auto-run "wait" action after main action (for async operations like Jenkins builds)'),
|
|
354
|
+
},
|
|
355
|
+
async (params) => {
|
|
356
|
+
try {
|
|
357
|
+
const { listInstalledPlugins, getInstalledPlugin } = await import('./plugins/registry');
|
|
358
|
+
|
|
359
|
+
if (!params.plugin) {
|
|
360
|
+
const installed = listInstalledPlugins();
|
|
361
|
+
if (installed.length === 0) {
|
|
362
|
+
return { content: [{ type: 'text', text: 'No plugins installed. Install from the Plugins page.' }] };
|
|
363
|
+
}
|
|
364
|
+
const list = installed.map((p: any) => {
|
|
365
|
+
const actions = Object.keys(p.definition.actions).join(', ');
|
|
366
|
+
return `• ${p.definition.icon} ${p.id} — ${p.definition.description || p.definition.name}\n actions: ${actions}`;
|
|
367
|
+
}).join('\n');
|
|
368
|
+
return { content: [{ type: 'text', text: `Installed plugins:\n${list}` }] };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const inst = getInstalledPlugin(params.plugin);
|
|
372
|
+
if (!inst) return { content: [{ type: 'text', text: `Plugin "${params.plugin}" not installed.` }] };
|
|
373
|
+
if (!inst.enabled) return { content: [{ type: 'text', text: `Plugin "${params.plugin}" is disabled.` }] };
|
|
374
|
+
|
|
375
|
+
const { executePluginWithWait } = await import('./plugins/executor');
|
|
376
|
+
const actionName = params.action || inst.definition.defaultAction || Object.keys(inst.definition.actions)[0];
|
|
377
|
+
|
|
378
|
+
if (!inst.definition.actions[actionName]) {
|
|
379
|
+
const available = Object.keys(inst.definition.actions).join(', ');
|
|
380
|
+
return { content: [{ type: 'text', text: `Action "${actionName}" not found. Available: ${available}` }] };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const result = await executePluginWithWait(inst, actionName, params.params || {}, params.wait || false);
|
|
384
|
+
|
|
385
|
+
const output = JSON.stringify(result.output, null, 2);
|
|
386
|
+
const status = result.ok ? 'OK' : 'FAILED';
|
|
387
|
+
const duration = result.duration ? ` (${result.duration}ms)` : '';
|
|
388
|
+
const error = result.error ? `\nError: ${result.error}` : '';
|
|
389
|
+
|
|
390
|
+
return { content: [{ type: 'text', text: `${status}${duration}${error}\n${output}` }] };
|
|
391
|
+
} catch (err: any) {
|
|
392
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// ── get_pipeline_status ────────────────────────
|
|
398
|
+
server.tool(
|
|
399
|
+
'get_pipeline_status',
|
|
400
|
+
'Check the status and results of a running or completed pipeline.',
|
|
401
|
+
{
|
|
402
|
+
pipeline_id: z.string().describe('Pipeline ID to check'),
|
|
403
|
+
},
|
|
404
|
+
async (params) => {
|
|
405
|
+
try {
|
|
406
|
+
const { getPipeline } = await import('./pipeline');
|
|
407
|
+
const pipeline = getPipeline(params.pipeline_id);
|
|
408
|
+
if (!pipeline) return { content: [{ type: 'text', text: `Pipeline "${params.pipeline_id}" not found.` }] };
|
|
409
|
+
|
|
410
|
+
const nodes = Object.entries(pipeline.nodes).map(([id, n]: [string, any]) => {
|
|
411
|
+
let line = ` ${id}: ${n.status}`;
|
|
412
|
+
if (n.error) line += ` — ${n.error}`;
|
|
413
|
+
if (n.outputs && Object.keys(n.outputs).length > 0) {
|
|
414
|
+
for (const [k, v] of Object.entries(n.outputs)) {
|
|
415
|
+
line += `\n ${k}: ${String(v).slice(0, 200)}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return line;
|
|
419
|
+
}).join('\n');
|
|
420
|
+
|
|
421
|
+
return { content: [{ type: 'text', text: `Pipeline ${pipeline.id} [${pipeline.status}] (${pipeline.workflowName})\n${nodes}` }] };
|
|
422
|
+
} catch (err: any) {
|
|
423
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// ─── Request/Response Document Tools ─────────────────────
|
|
429
|
+
|
|
430
|
+
server.tool(
|
|
431
|
+
'create_request',
|
|
432
|
+
'Create a new request document for implementation. Auto-notifies downstream agents via DAG.',
|
|
433
|
+
{
|
|
434
|
+
title: z.string().describe('Short title for the request'),
|
|
435
|
+
description: z.string().describe('Detailed description of what to implement'),
|
|
436
|
+
type: z.enum(['feature', 'bugfix', 'refactor', 'task']).optional().describe('Request type (default: feature)'),
|
|
437
|
+
modules: z.array(z.object({
|
|
438
|
+
name: z.string(),
|
|
439
|
+
description: z.string(),
|
|
440
|
+
acceptance_criteria: z.array(z.string()),
|
|
441
|
+
})).describe('Feature modules with acceptance criteria'),
|
|
442
|
+
batch: z.string().optional().describe('Batch name to group related requests (default: auto-generated from date)'),
|
|
443
|
+
priority: z.enum(['high', 'medium', 'low']).optional().describe('Priority level (default: medium)'),
|
|
444
|
+
},
|
|
445
|
+
async (params) => {
|
|
446
|
+
const { workspaceId, agentId } = ctx();
|
|
447
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
448
|
+
try {
|
|
449
|
+
const orch = getOrch(workspaceId);
|
|
450
|
+
const { createRequest } = await import('./workspace/requests') as any;
|
|
451
|
+
const batch = params.batch || `delivery-${new Date().toISOString().slice(0, 10)}`;
|
|
452
|
+
const agentLabel = orch.getSnapshot().agents.find((a: any) => a.id === agentId)?.label || agentId;
|
|
453
|
+
|
|
454
|
+
const ref = createRequest(orch.projectPath, {
|
|
455
|
+
title: params.title,
|
|
456
|
+
description: params.description,
|
|
457
|
+
type: params.type || 'feature',
|
|
458
|
+
modules: params.modules,
|
|
459
|
+
batch,
|
|
460
|
+
priority: params.priority || 'medium',
|
|
461
|
+
status: 'open',
|
|
462
|
+
assigned_to: '',
|
|
463
|
+
created_by: agentLabel,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Auto-notify downstream agents via DAG
|
|
467
|
+
const snapshot = orch.getSnapshot();
|
|
468
|
+
const notified: string[] = [];
|
|
469
|
+
for (const agent of snapshot.agents) {
|
|
470
|
+
if (agent.type === 'input') continue;
|
|
471
|
+
if (!agent.dependsOn?.includes(agentId)) continue;
|
|
472
|
+
orch.getBus().send(agentId, agent.id, 'notify', {
|
|
473
|
+
action: 'new_request',
|
|
474
|
+
content: `New request: ${params.title} [${params.priority || 'medium'}] — ${params.modules.length} module(s). Use list_requests and claim_request to pick it up.`,
|
|
475
|
+
ref,
|
|
476
|
+
});
|
|
477
|
+
notified.push(agent.label);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { content: [{ type: 'text', text: `Created request: ${ref}\nBatch: ${batch}\nModules: ${params.modules.length}\nNotified: ${notified.length > 0 ? notified.join(', ') : '(no downstream agents)'}` }] };
|
|
481
|
+
} catch (err: any) {
|
|
482
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
server.tool(
|
|
488
|
+
'claim_request',
|
|
489
|
+
'Claim an open request for implementation. Prevents other agents from working on the same request.',
|
|
490
|
+
{
|
|
491
|
+
request_id: z.string().describe('Request ID to claim (e.g., REQ-20260403-001)'),
|
|
492
|
+
},
|
|
493
|
+
async (params) => {
|
|
494
|
+
const { workspaceId, agentId } = ctx();
|
|
495
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
496
|
+
try {
|
|
497
|
+
const orch = getOrch(workspaceId);
|
|
498
|
+
const { claimRequest } = await import('./workspace/requests') as any;
|
|
499
|
+
const agentLabel = orch.getSnapshot().agents.find((a: any) => a.id === agentId)?.label || agentId;
|
|
500
|
+
|
|
501
|
+
const result = claimRequest(orch.projectPath, params.request_id, agentLabel);
|
|
502
|
+
if (!result.ok) {
|
|
503
|
+
return { content: [{ type: 'text', text: `Cannot claim ${params.request_id}: already claimed by ${result.claimedBy}. Use list_requests(status: "open") to find available requests.` }] };
|
|
504
|
+
}
|
|
505
|
+
return { content: [{ type: 'text', text: `Claimed ${params.request_id}. Status: in_progress. You can now implement it and use update_response when done.` }] };
|
|
506
|
+
} catch (err: any) {
|
|
507
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
server.tool(
|
|
513
|
+
'update_response',
|
|
514
|
+
'Update a response document with your work results. Auto-advances status and notifies downstream agents via DAG.',
|
|
515
|
+
{
|
|
516
|
+
request_id: z.string().describe('Request ID (e.g., REQ-20260403-001)'),
|
|
517
|
+
section: z.enum(['engineer', 'review', 'qa']).describe('Which section to update'),
|
|
518
|
+
data: z.object({
|
|
519
|
+
files_changed: z.array(z.string()).optional().describe('Files modified (engineer)'),
|
|
520
|
+
notes: z.string().optional().describe('Implementation notes (engineer)'),
|
|
521
|
+
result: z.string().optional().describe('Result: approved/changes_requested/rejected (review) or passed/failed (qa)'),
|
|
522
|
+
findings: z.array(z.object({
|
|
523
|
+
severity: z.string(),
|
|
524
|
+
description: z.string(),
|
|
525
|
+
})).optional().describe('Issues found (review/qa)'),
|
|
526
|
+
test_files: z.array(z.string()).optional().describe('Test files run (qa)'),
|
|
527
|
+
}).describe('Response data for your section'),
|
|
528
|
+
},
|
|
529
|
+
async (params) => {
|
|
530
|
+
const { workspaceId, agentId } = ctx();
|
|
531
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
532
|
+
try {
|
|
533
|
+
const orch = getOrch(workspaceId);
|
|
534
|
+
const { updateResponse, getRequest } = await import('./workspace/requests') as any;
|
|
535
|
+
|
|
536
|
+
const ref = updateResponse(orch.projectPath, params.request_id, params.section, params.data);
|
|
537
|
+
const updated = getRequest(orch.projectPath, params.request_id);
|
|
538
|
+
const newStatus = updated?.request?.status || 'unknown';
|
|
539
|
+
|
|
540
|
+
// Auto-notify downstream agents via DAG
|
|
541
|
+
const snapshot = orch.getSnapshot();
|
|
542
|
+
const notified: string[] = [];
|
|
543
|
+
for (const agent of snapshot.agents) {
|
|
544
|
+
if (agent.type === 'input') continue;
|
|
545
|
+
if (!agent.dependsOn?.includes(agentId)) continue;
|
|
546
|
+
orch.getBus().send(agentId, agent.id, 'notify', {
|
|
547
|
+
action: 'response_updated',
|
|
548
|
+
content: `${params.section} completed for ${params.request_id} → status: ${newStatus}. Use get_request to review details.`,
|
|
549
|
+
ref,
|
|
550
|
+
});
|
|
551
|
+
notified.push(agent.label);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return { content: [{ type: 'text', text: `Updated ${params.request_id} [${params.section}] → status: ${newStatus}\nNotified: ${notified.length > 0 ? notified.join(', ') : '(no downstream agents)'}` }] };
|
|
555
|
+
} catch (err: any) {
|
|
556
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
server.tool(
|
|
562
|
+
'list_requests',
|
|
563
|
+
'List all request documents in the project, optionally filtered by batch or status.',
|
|
564
|
+
{
|
|
565
|
+
batch: z.string().optional().describe('Filter by batch/delivery name'),
|
|
566
|
+
status: z.enum(['open', 'in_progress', 'review', 'qa', 'done', 'rejected']).optional().describe('Filter by status'),
|
|
567
|
+
},
|
|
568
|
+
async (params) => {
|
|
569
|
+
const { workspaceId } = ctx();
|
|
570
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
571
|
+
try {
|
|
572
|
+
const orch = getOrch(workspaceId);
|
|
573
|
+
const { listRequests, getBatchStatus } = await import('./workspace/requests') as any;
|
|
574
|
+
|
|
575
|
+
const requests = listRequests(orch.projectPath, { batch: params.batch, status: params.status as any });
|
|
576
|
+
if (requests.length === 0) {
|
|
577
|
+
return { content: [{ type: 'text', text: params.batch || params.status ? 'No requests match the filter.' : 'No requests found. Use create_request to create one.' }] };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const lines = requests.map((r: any) =>
|
|
581
|
+
`[${r.status}] ${r.id}: ${r.title} (${r.priority}) — ${r.modules?.length || 0} module(s)${r.assigned_to ? ` → ${r.assigned_to}` : ''}`
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// Add batch summary if filtering by batch
|
|
585
|
+
if (params.batch) {
|
|
586
|
+
const bs = getBatchStatus(orch.projectPath, params.batch);
|
|
587
|
+
lines.push(`\nBatch "${params.batch}": ${bs.done}/${bs.total} done${bs.allDone ? ' ✓ ALL COMPLETE' : ''}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
591
|
+
} catch (err: any) {
|
|
592
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
server.tool(
|
|
598
|
+
'get_request',
|
|
599
|
+
'Get full details of a request document and its response.',
|
|
600
|
+
{
|
|
601
|
+
request_id: z.string().describe('Request ID (e.g., REQ-20260403-001)'),
|
|
602
|
+
},
|
|
603
|
+
async (params) => {
|
|
604
|
+
const { workspaceId } = ctx();
|
|
605
|
+
if (!workspaceId) return { content: [{ type: 'text', text: 'Error: No workspace context.' }] };
|
|
606
|
+
try {
|
|
607
|
+
const orch = getOrch(workspaceId);
|
|
608
|
+
const { getRequest } = await import('./workspace/requests') as any;
|
|
609
|
+
|
|
610
|
+
const result = getRequest(orch.projectPath, params.request_id);
|
|
611
|
+
if (!result) return { content: [{ type: 'text', text: `Request "${params.request_id}" not found.` }] };
|
|
612
|
+
|
|
613
|
+
const YAML = (await import('yaml')).default;
|
|
614
|
+
let text = `# Request: ${result.request.title}\n\n`;
|
|
615
|
+
text += YAML.stringify(result.request);
|
|
616
|
+
if (result.response) {
|
|
617
|
+
text += `\n---\n# Response\n\n`;
|
|
618
|
+
text += YAML.stringify(result.response);
|
|
619
|
+
} else {
|
|
620
|
+
text += `\n---\nNo response yet.`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return { content: [{ type: 'text', text }] };
|
|
624
|
+
} catch (err: any) {
|
|
625
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Update get_inbox to show ref field
|
|
631
|
+
// (Already handled — ref is part of payload, shown via content)
|
|
632
|
+
|
|
633
|
+
return server;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ─── HTTP Server with SSE Transport ─────────────────────
|
|
637
|
+
|
|
638
|
+
let mcpHttpServer: ReturnType<typeof createServer> | null = null;
|
|
639
|
+
const transports = new Map<string, SSEServerTransport>();
|
|
640
|
+
|
|
641
|
+
export async function startMcpServer(port: number): Promise<void> {
|
|
642
|
+
mcpHttpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
643
|
+
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
644
|
+
|
|
645
|
+
// CORS
|
|
646
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
647
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
648
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
649
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
650
|
+
|
|
651
|
+
// SSE endpoint — each connection gets its own MCP server instance
|
|
652
|
+
if (url.pathname === '/sse' && req.method === 'GET') {
|
|
653
|
+
const transport = new SSEServerTransport('/message', res);
|
|
654
|
+
const sessionId = transport.sessionId;
|
|
655
|
+
transports.set(sessionId, transport);
|
|
656
|
+
|
|
657
|
+
// Extract workspace context from URL params
|
|
658
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
659
|
+
const agentId = url.searchParams.get('agentId') || (workspaceId ? resolveAgentFromOrch(workspaceId) : '');
|
|
660
|
+
sessionContexts.set(sessionId, { workspaceId, agentId });
|
|
661
|
+
|
|
662
|
+
transport.onclose = () => {
|
|
663
|
+
transports.delete(sessionId);
|
|
664
|
+
sessionContexts.delete(sessionId);
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// Each session gets its own MCP server with context
|
|
668
|
+
const server = createForgeMcpServer(sessionId);
|
|
669
|
+
await server.connect(transport);
|
|
670
|
+
let agentLabel = 'unknown';
|
|
671
|
+
try { agentLabel = workspaceId ? (getOrch(workspaceId)?.getSnapshot()?.agents?.find((a: any) => a.id === agentId)?.label || agentId) : 'unknown'; } catch {}
|
|
672
|
+
console.log(`[forge-mcp] Client connected: ${agentLabel} (ws=${workspaceId?.slice(0, 8) || '?'}, session=${sessionId})`);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Message endpoint — route by sessionId query param
|
|
677
|
+
if (url.pathname === '/message' && req.method === 'POST') {
|
|
678
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
679
|
+
if (!sessionId) {
|
|
680
|
+
res.writeHead(400);
|
|
681
|
+
res.end('Missing sessionId parameter');
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const transport = transports.get(sessionId);
|
|
686
|
+
if (!transport) {
|
|
687
|
+
res.writeHead(404);
|
|
688
|
+
res.end('Session not found');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Read body and pass to transport
|
|
693
|
+
const body: Buffer[] = [];
|
|
694
|
+
req.on('data', (chunk: Buffer) => body.push(chunk));
|
|
695
|
+
req.on('end', async () => {
|
|
696
|
+
try {
|
|
697
|
+
const parsed = JSON.parse(Buffer.concat(body).toString());
|
|
698
|
+
await transport.handlePostMessage(req, res, parsed);
|
|
699
|
+
} catch (err: any) {
|
|
700
|
+
if (!res.headersSent) { res.writeHead(400); res.end('Invalid JSON'); }
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Health check
|
|
707
|
+
if (url.pathname === '/health') {
|
|
708
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
709
|
+
res.end(JSON.stringify({ ok: true, sessions: transports.size }));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
res.writeHead(404);
|
|
714
|
+
res.end('Not found');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
mcpHttpServer.listen(port, () => {
|
|
718
|
+
console.log(`[forge-mcp] MCP Server running on http://localhost:${port}`);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function stopMcpServer(): void {
|
|
723
|
+
if (mcpHttpServer) {
|
|
724
|
+
mcpHttpServer.close();
|
|
725
|
+
mcpHttpServer = null;
|
|
726
|
+
transports.clear();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export function getMcpPort(): number {
|
|
731
|
+
return Number(process.env.MCP_PORT) || 8406;
|
|
732
|
+
}
|