@desplega.ai/agent-swarm 1.20.0 → 1.51.2
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/README.md +271 -169
- package/openapi.json +5015 -0
- package/package.json +40 -7
- package/plugin/commands/close-issue.md +7 -3
- package/plugin/commands/create-pr.md +18 -12
- package/plugin/commands/implement-issue.md +7 -3
- package/plugin/commands/respond-github.md +8 -4
- package/plugin/commands/review-pr.md +44 -10
- package/plugin/commands/start-leader.md +1 -3
- package/plugin/commands/start-worker.md +1 -3
- package/plugin/commands/work-on-task.md +22 -3
- package/plugin/pi-skills/close-issue/SKILL.md +90 -0
- package/plugin/pi-skills/create-pr/SKILL.md +99 -0
- package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
- package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
- package/plugin/pi-skills/respond-github/SKILL.md +98 -0
- package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
- package/plugin/pi-skills/review-pr/SKILL.md +261 -0
- package/plugin/pi-skills/start-leader/SKILL.md +121 -0
- package/plugin/pi-skills/start-worker/SKILL.md +60 -0
- package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
- package/plugin/pi-skills/todos/SKILL.md +66 -0
- package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
- package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
- package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
- package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
- package/plugin/skills/artifacts/examples/static-report.sh +17 -0
- package/plugin/skills/artifacts/skill.md +71 -0
- package/src/agentmail/app.ts +65 -0
- package/src/agentmail/handlers.ts +262 -0
- package/src/agentmail/index.ts +9 -0
- package/src/agentmail/templates.ts +111 -0
- package/src/agentmail/types.ts +51 -0
- package/src/artifact-sdk/browser-sdk.ts +30 -0
- package/src/artifact-sdk/index.ts +2 -0
- package/src/artifact-sdk/localtunnel.d.ts +20 -0
- package/src/artifact-sdk/port.ts +12 -0
- package/src/artifact-sdk/server.ts +156 -0
- package/src/artifact-sdk/tunnel.ts +19 -0
- package/src/be/chunking.ts +193 -0
- package/src/be/db-queries/oauth.ts +90 -0
- package/src/be/db-queries/tracker.ts +182 -0
- package/src/be/db.ts +3327 -784
- package/src/be/embedding.ts +80 -0
- package/src/be/migrations/001_initial.sql +409 -0
- package/src/be/migrations/002_one_time_schedules.sql +59 -0
- package/src/be/migrations/003_workflows.sql +51 -0
- package/src/be/migrations/004_workflow_source.sql +81 -0
- package/src/be/migrations/005_epic_next_steps.sql +2 -0
- package/src/be/migrations/006_vcs_provider.sql +94 -0
- package/src/be/migrations/007_task_dir.sql +2 -0
- package/src/be/migrations/008_workflow_redesign.sql +85 -0
- package/src/be/migrations/009_tracker_integration.sql +144 -0
- package/src/be/migrations/010_step_diagnostics.sql +1 -0
- package/src/be/migrations/011_step_next_port.sql +1 -0
- package/src/be/migrations/012_trigger_schema.sql +1 -0
- package/src/be/migrations/013_task_output_schema.sql +2 -0
- package/src/be/migrations/014_prompt_templates.sql +33 -0
- package/src/be/migrations/015_workflow_workspace.sql +3 -0
- package/src/be/migrations/016_active_session_runner_session.sql +4 -0
- package/src/be/migrations/017_channel_activity_cursors.sql +6 -0
- package/src/be/migrations/018_fix_seed_double_version.sql +30 -0
- package/src/be/migrations/runner.ts +188 -0
- package/src/be/seed.ts +62 -0
- package/src/cli.tsx +231 -299
- package/src/commands/artifact.ts +241 -0
- package/src/commands/onboard/compose-generator.ts +169 -0
- package/src/commands/onboard/env-generator.ts +79 -0
- package/src/commands/onboard/manifest.ts +37 -0
- package/src/commands/onboard/presets.ts +85 -0
- package/src/commands/onboard/service-names.ts +47 -0
- package/src/commands/onboard/steps/core-credentials.tsx +111 -0
- package/src/commands/onboard/steps/custom-templates.tsx +168 -0
- package/src/commands/onboard/steps/generate.tsx +154 -0
- package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
- package/src/commands/onboard/steps/harness.tsx +21 -0
- package/src/commands/onboard/steps/health-check.tsx +171 -0
- package/src/commands/onboard/steps/integration-github.tsx +105 -0
- package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
- package/src/commands/onboard/steps/integration-menu.tsx +58 -0
- package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
- package/src/commands/onboard/steps/integration-slack.tsx +165 -0
- package/src/commands/onboard/steps/post-connect.tsx +145 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
- package/src/commands/onboard/steps/post-task.tsx +103 -0
- package/src/commands/onboard/steps/prereq-check.tsx +178 -0
- package/src/commands/onboard/steps/review.tsx +82 -0
- package/src/commands/onboard/steps/start.tsx +97 -0
- package/src/commands/onboard/templates.ts +34 -0
- package/src/commands/onboard/types.ts +259 -0
- package/src/commands/onboard.tsx +425 -0
- package/src/commands/runner.ts +1540 -630
- package/src/commands/setup.tsx +23 -38
- package/src/commands/shared/client-config.ts +41 -0
- package/src/commands/templates.ts +172 -0
- package/src/github/app.ts +8 -0
- package/src/github/handlers.ts +384 -151
- package/src/github/index.ts +1 -0
- package/src/github/mentions-aliases.test.ts +73 -0
- package/src/github/mentions.test.ts +3 -3
- package/src/github/mentions.ts +32 -6
- package/src/github/templates.ts +398 -0
- package/src/github/types.ts +1 -0
- package/src/gitlab/auth.ts +63 -0
- package/src/gitlab/handlers.ts +368 -0
- package/src/gitlab/index.ts +19 -0
- package/src/gitlab/reactions.ts +104 -0
- package/src/gitlab/templates.ts +140 -0
- package/src/gitlab/types.ts +130 -0
- package/src/heartbeat/heartbeat.ts +434 -0
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/templates.ts +30 -0
- package/src/hooks/hook.ts +555 -4
- package/src/hooks/tool-loop-detection.test.ts +158 -0
- package/src/hooks/tool-loop-detection.ts +167 -0
- package/src/http/active-sessions.ts +199 -0
- package/src/http/agents.ts +328 -0
- package/src/http/config.ts +191 -0
- package/src/http/core.ts +309 -0
- package/src/http/db-query.ts +91 -0
- package/src/http/ecosystem.ts +63 -0
- package/src/http/epics.ts +460 -0
- package/src/http/index.ts +216 -0
- package/src/http/mcp.ts +77 -0
- package/src/http/memory.ts +168 -0
- package/src/http/openapi.ts +109 -0
- package/src/http/poll.ts +299 -0
- package/src/http/prompt-templates.ts +412 -0
- package/src/http/repos.ts +195 -0
- package/src/http/route-def.ts +123 -0
- package/src/http/schedules.ts +426 -0
- package/src/http/session-data.ts +241 -0
- package/src/http/stats.ts +174 -0
- package/src/http/tasks.ts +468 -0
- package/src/http/trackers/index.ts +10 -0
- package/src/http/trackers/linear.ts +187 -0
- package/src/http/types.ts +12 -0
- package/src/http/utils.ts +87 -0
- package/src/http/webhooks.ts +432 -0
- package/src/http/workflows.ts +530 -0
- package/src/http.ts +1 -1890
- package/src/linear/README.md +65 -0
- package/src/linear/app.ts +48 -0
- package/src/linear/client.ts +18 -0
- package/src/linear/index.ts +1 -0
- package/src/linear/oauth.ts +35 -0
- package/src/linear/outbound.ts +212 -0
- package/src/linear/sync.ts +567 -0
- package/src/linear/templates.ts +47 -0
- package/src/linear/types.ts +7 -0
- package/src/linear/webhook.ts +104 -0
- package/src/oauth/README.md +66 -0
- package/src/oauth/index.ts +6 -0
- package/src/oauth/wrapper.ts +204 -0
- package/src/prompts/base-prompt.ts +150 -265
- package/src/prompts/defaults.ts +196 -0
- package/src/prompts/registry.ts +57 -0
- package/src/prompts/resolver.ts +296 -0
- package/src/prompts/session-templates.ts +604 -0
- package/src/providers/claude-adapter.ts +442 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/pi-mono-adapter.ts +442 -0
- package/src/providers/pi-mono-extension.ts +624 -0
- package/src/providers/pi-mono-mcp-client.ts +124 -0
- package/src/providers/types.ts +75 -0
- package/src/scheduler/scheduler.test.ts +2 -0
- package/src/scheduler/scheduler.ts +231 -40
- package/src/server.ts +97 -6
- package/src/slack/HEURISTICS.md +105 -0
- package/src/slack/actions.ts +133 -0
- package/src/slack/app.ts +7 -0
- package/src/slack/assistant.ts +118 -0
- package/src/slack/blocks.ts +233 -0
- package/src/slack/channel-activity.ts +177 -0
- package/src/slack/commands.ts +31 -17
- package/src/slack/files.ts +1 -1
- package/src/slack/handlers.test.ts +114 -1
- package/src/slack/handlers.ts +230 -55
- package/src/slack/responses.ts +120 -67
- package/src/slack/router.ts +17 -99
- package/src/slack/templates.ts +55 -0
- package/src/slack/thread-buffer.ts +213 -0
- package/src/slack/watcher.ts +119 -4
- package/src/tests/agent-activity.test.ts +247 -0
- package/src/tests/agentmail-filters.test.ts +97 -0
- package/src/tests/artifact-sdk.test.ts +800 -0
- package/src/tests/base-prompt.test.ts +264 -0
- package/src/tests/build-pi-skills.test.ts +127 -0
- package/src/tests/channel-activity.test.ts +363 -0
- package/src/tests/claude-adapter.test.ts +126 -0
- package/src/tests/context-versioning.test.ts +425 -0
- package/src/tests/db-queries-oauth.test.ts +197 -0
- package/src/tests/db-queries-tracker.test.ts +230 -0
- package/src/tests/epics.test.ts +3 -3
- package/src/tests/error-tracker.test.ts +368 -0
- package/src/tests/fetch-resolved-env.test.ts +167 -0
- package/src/tests/generate-default-claude-md.test.ts +9 -1
- package/src/tests/generate-identity-templates.test.ts +124 -0
- package/src/tests/gitlab-auth.test.ts +109 -0
- package/src/tests/gitlab-handlers.test.ts +691 -0
- package/src/tests/gitlab-vcs-db.test.ts +177 -0
- package/src/tests/heartbeat.test.ts +364 -0
- package/src/tests/http-api-integration.test.ts +1698 -0
- package/src/tests/linear-outbound-sync.test.ts +200 -0
- package/src/tests/linear-webhook.test.ts +406 -0
- package/src/tests/match-route.test.ts +187 -0
- package/src/tests/memory.test.ts +737 -0
- package/src/tests/migration-runner-regressions.test.ts +86 -0
- package/src/tests/model-control.test.ts +338 -0
- package/src/tests/oauth-wrapper.test.ts +147 -0
- package/src/tests/onboard-compose.test.ts +138 -0
- package/src/tests/onboard-env.test.ts +174 -0
- package/src/tests/onboard-manifest.test.ts +137 -0
- package/src/tests/pi-mono-adapter.test.ts +234 -0
- package/src/tests/pool-session-logs.test.ts +199 -0
- package/src/tests/progress-dedup.test.ts +98 -0
- package/src/tests/prompt-template-github.test.ts +682 -0
- package/src/tests/prompt-template-remaining.test.ts +504 -0
- package/src/tests/prompt-template-resolver.test.ts +621 -0
- package/src/tests/prompt-template-session.test.ts +363 -0
- package/src/tests/prompt-templates-db.test.ts +616 -0
- package/src/tests/provider-adapter.test.ts +122 -0
- package/src/tests/provider-command-format.test.ts +98 -0
- package/src/tests/reload-config.test.ts +170 -0
- package/src/tests/runner-polling-api.test.ts +25 -20
- package/src/tests/scheduled-tasks.test.ts +104 -0
- package/src/tests/scheduler-backoff.test.ts +166 -0
- package/src/tests/self-improvement.test.ts +541 -0
- package/src/tests/session-attach.test.ts +536 -0
- package/src/tests/session-costs.test.ts +267 -1
- package/src/tests/slack-actions.test.ts +133 -0
- package/src/tests/slack-assistant.test.ts +136 -0
- package/src/tests/slack-blocks.test.ts +246 -0
- package/src/tests/slack-metadata-inheritance.test.ts +243 -0
- package/src/tests/slack-queue-offline.test.ts +174 -0
- package/src/tests/slack-router.test.ts +181 -0
- package/src/tests/slack-thread-buffer.test.ts +305 -0
- package/src/tests/slack-thread-followups.test.ts +298 -0
- package/src/tests/slack-watcher.test.ts +101 -0
- package/src/tests/structured-output.test.ts +307 -0
- package/src/tests/swarm-repos.test.ts +198 -0
- package/src/tests/task-cancellation.test.ts +6 -4
- package/src/tests/task-working-dir.test.ts +176 -0
- package/src/tests/template-fetch.test.ts +490 -0
- package/src/tests/tool-annotations.test.ts +371 -0
- package/src/tests/tracker-tools.test.ts +184 -0
- package/src/tests/update-profile-agentid.test.ts +248 -0
- package/src/tests/update-profile-api.test.ts +143 -3
- package/src/tests/update-profile-auth.test.ts +195 -0
- package/src/tests/validation-adapters.test.ts +86 -0
- package/src/tests/vcs-provider.test.ts +27 -0
- package/src/tests/workflow-agent-task.test.ts +196 -0
- package/src/tests/workflow-async-v2.test.ts +508 -0
- package/src/tests/workflow-convergence.test.ts +541 -0
- package/src/tests/workflow-definition-validation.test.ts +366 -0
- package/src/tests/workflow-engine-v2.test.ts +691 -0
- package/src/tests/workflow-executors.test.ts +736 -0
- package/src/tests/workflow-http-v2.test.ts +599 -0
- package/src/tests/workflow-integration-io.test.ts +902 -0
- package/src/tests/workflow-io-schemas.test.ts +624 -0
- package/src/tests/workflow-registry.test.ts +592 -0
- package/src/tests/workflow-retry-v2.test.ts +401 -0
- package/src/tests/workflow-retry-validation.test.ts +282 -0
- package/src/tests/workflow-schedule-trigger.test.ts +104 -0
- package/src/tests/workflow-template.test.ts +288 -0
- package/src/tests/workflow-trigger-schema.test.ts +359 -0
- package/src/tests/workflow-triggers-v2.test.ts +264 -0
- package/src/tests/workflow-versions.test.ts +208 -0
- package/src/tests/workflow-workspace.test.ts +272 -0
- package/src/tests/x402-client.test.ts +117 -0
- package/src/tests/x402-config.test.ts +182 -0
- package/src/tests/x402-spending-tracker.test.ts +185 -0
- package/src/tools/cancel-task.ts +2 -0
- package/src/tools/context-diff.ts +171 -0
- package/src/tools/context-history.ts +138 -0
- package/src/tools/create-channel.ts +1 -0
- package/src/tools/db-query.ts +78 -0
- package/src/tools/delete-channel.ts +132 -0
- package/src/tools/epics/assign-task-to-epic.ts +1 -0
- package/src/tools/epics/create-epic.ts +3 -2
- package/src/tools/epics/delete-epic.ts +2 -0
- package/src/tools/epics/get-epic-details.ts +2 -0
- package/src/tools/epics/list-epics.ts +2 -0
- package/src/tools/epics/unassign-task-from-epic.ts +1 -0
- package/src/tools/epics/update-epic.ts +7 -4
- package/src/tools/get-swarm.ts +2 -0
- package/src/tools/get-task-details.ts +2 -0
- package/src/tools/get-tasks.ts +27 -1
- package/src/tools/inject-learning.ts +106 -0
- package/src/tools/join-swarm.ts +17 -7
- package/src/tools/list-channels.ts +2 -0
- package/src/tools/list-services.ts +2 -0
- package/src/tools/memory-get.ts +56 -0
- package/src/tools/memory-search.ts +131 -0
- package/src/tools/my-agent-info.ts +2 -0
- package/src/tools/poll-task.ts +2 -20
- package/src/tools/post-message.ts +1 -0
- package/src/tools/prompt-templates/delete.ts +86 -0
- package/src/tools/prompt-templates/get.ts +89 -0
- package/src/tools/prompt-templates/index.ts +5 -0
- package/src/tools/prompt-templates/list.ts +95 -0
- package/src/tools/prompt-templates/preview.ts +84 -0
- package/src/tools/prompt-templates/set.ts +117 -0
- package/src/tools/read-messages.ts +2 -0
- package/src/tools/register-agentmail-inbox.ts +166 -0
- package/src/tools/register-service.ts +2 -0
- package/src/tools/schedules/create-schedule.ts +134 -24
- package/src/tools/schedules/delete-schedule.ts +2 -0
- package/src/tools/schedules/list-schedules.ts +20 -4
- package/src/tools/schedules/run-schedule-now.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +49 -17
- package/src/tools/send-task.ts +132 -10
- package/src/tools/slack-download-file.ts +4 -2
- package/src/tools/slack-list-channels.ts +2 -0
- package/src/tools/slack-post.ts +2 -0
- package/src/tools/slack-read.ts +2 -0
- package/src/tools/slack-reply.ts +2 -0
- package/src/tools/slack-upload-file.ts +2 -0
- package/src/tools/store-progress.ts +205 -4
- package/src/tools/swarm-config/delete-config.ts +87 -0
- package/src/tools/swarm-config/get-config.ts +108 -0
- package/src/tools/swarm-config/index.ts +4 -0
- package/src/tools/swarm-config/list-config.ts +99 -0
- package/src/tools/swarm-config/set-config.ts +118 -0
- package/src/tools/task-action.ts +50 -5
- package/src/tools/task-dedup.ts +97 -0
- package/src/tools/templates.ts +53 -0
- package/src/tools/tool-config.ts +124 -0
- package/src/tools/tracker/index.ts +6 -0
- package/src/tools/tracker/tracker-link-epic.ts +64 -0
- package/src/tools/tracker/tracker-link-task.ts +64 -0
- package/src/tools/tracker/tracker-map-agent.ts +57 -0
- package/src/tools/tracker/tracker-status.ts +56 -0
- package/src/tools/tracker/tracker-sync-status.ts +42 -0
- package/src/tools/tracker/tracker-unlink.ts +41 -0
- package/src/tools/unregister-service.ts +2 -0
- package/src/tools/update-profile.ts +172 -17
- package/src/tools/update-service-status.ts +2 -0
- package/src/tools/utils.ts +10 -1
- package/src/tools/workflows/create-workflow.ts +129 -0
- package/src/tools/workflows/delete-workflow.ts +42 -0
- package/src/tools/workflows/get-workflow-run.ts +59 -0
- package/src/tools/workflows/get-workflow.ts +53 -0
- package/src/tools/workflows/index.ts +9 -0
- package/src/tools/workflows/list-workflow-runs.ts +48 -0
- package/src/tools/workflows/list-workflows.ts +42 -0
- package/src/tools/workflows/retry-workflow-run.ts +40 -0
- package/src/tools/workflows/trigger-workflow.ts +96 -0
- package/src/tools/workflows/update-workflow.ts +133 -0
- package/src/tracker/types.ts +51 -0
- package/src/types.ts +530 -14
- package/src/utils/credentials.test.ts +156 -0
- package/src/utils/credentials.ts +50 -0
- package/src/utils/error-tracker.ts +190 -0
- package/src/vcs/index.ts +15 -0
- package/src/vcs/types.ts +5 -0
- package/src/workflows/checkpoint.ts +121 -0
- package/src/workflows/cooldown.ts +28 -0
- package/src/workflows/definition.ts +235 -0
- package/src/workflows/engine.ts +580 -0
- package/src/workflows/event-bus.ts +29 -0
- package/src/workflows/executors/agent-task.ts +103 -0
- package/src/workflows/executors/base.ts +86 -0
- package/src/workflows/executors/code-match.ts +88 -0
- package/src/workflows/executors/index.ts +16 -0
- package/src/workflows/executors/notify.ts +93 -0
- package/src/workflows/executors/property-match.ts +104 -0
- package/src/workflows/executors/raw-llm.ts +83 -0
- package/src/workflows/executors/registry.ts +76 -0
- package/src/workflows/executors/script.ts +103 -0
- package/src/workflows/executors/validate.ts +215 -0
- package/src/workflows/executors/vcs.ts +58 -0
- package/src/workflows/index.ts +61 -0
- package/src/workflows/input.ts +46 -0
- package/src/workflows/json-schema-validator.ts +118 -0
- package/src/workflows/recovery.ts +139 -0
- package/src/workflows/resume.ts +229 -0
- package/src/workflows/retry-poller.ts +216 -0
- package/src/workflows/template.ts +74 -0
- package/src/workflows/templates.ts +86 -0
- package/src/workflows/triggers.ts +124 -0
- package/src/workflows/validation.ts +104 -0
- package/src/workflows/version.ts +44 -0
- package/src/x402/cli.ts +140 -0
- package/src/x402/client.ts +192 -0
- package/src/x402/config.ts +131 -0
- package/src/x402/index.ts +37 -0
- package/src/x402/openfort-signer.ts +83 -0
- package/src/x402/spending-tracker.ts +109 -0
- package/templates/official/coder/CLAUDE.md +49 -0
- package/templates/official/coder/IDENTITY.md +28 -0
- package/templates/official/coder/SOUL.md +43 -0
- package/templates/official/coder/TOOLS.md +40 -0
- package/templates/official/coder/config.json +23 -0
- package/templates/official/coder/start-up.sh +23 -0
- package/templates/official/content-reviewer/CLAUDE.md +68 -0
- package/templates/official/content-reviewer/IDENTITY.md +28 -0
- package/templates/official/content-reviewer/SOUL.md +44 -0
- package/templates/official/content-reviewer/TOOLS.md +37 -0
- package/templates/official/content-reviewer/config.json +23 -0
- package/templates/official/content-reviewer/start-up.sh +23 -0
- package/templates/official/content-strategist/CLAUDE.md +63 -0
- package/templates/official/content-strategist/IDENTITY.md +33 -0
- package/templates/official/content-strategist/SOUL.md +48 -0
- package/templates/official/content-strategist/TOOLS.md +47 -0
- package/templates/official/content-strategist/config.json +23 -0
- package/templates/official/content-strategist/start-up.sh +23 -0
- package/templates/official/content-writer/CLAUDE.md +72 -0
- package/templates/official/content-writer/IDENTITY.md +30 -0
- package/templates/official/content-writer/SOUL.md +46 -0
- package/templates/official/content-writer/TOOLS.md +44 -0
- package/templates/official/content-writer/config.json +23 -0
- package/templates/official/content-writer/start-up.sh +23 -0
- package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
- package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
- package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
- package/templates/official/forward-deployed-engineer/config.json +21 -0
- package/templates/official/lead/CLAUDE.md +33 -0
- package/templates/official/lead/IDENTITY.md +36 -0
- package/templates/official/lead/SOUL.md +51 -0
- package/templates/official/lead/config.json +22 -0
- package/templates/official/researcher/CLAUDE.md +46 -0
- package/templates/official/researcher/IDENTITY.md +28 -0
- package/templates/official/researcher/SOUL.md +43 -0
- package/templates/official/researcher/config.json +21 -0
- package/templates/official/reviewer/CLAUDE.md +63 -0
- package/templates/official/reviewer/IDENTITY.md +28 -0
- package/templates/official/reviewer/SOUL.md +45 -0
- package/templates/official/reviewer/config.json +21 -0
- package/templates/official/tester/CLAUDE.md +53 -0
- package/templates/official/tester/IDENTITY.md +28 -0
- package/templates/official/tester/SOUL.md +55 -0
- package/templates/official/tester/config.json +21 -0
- package/templates/schema.ts +35 -0
- package/.claude/settings.local.json +0 -115
- package/.dockerignore +0 -61
- package/.editorconfig +0 -15
- package/.env.docker.example +0 -39
- package/.env.example +0 -40
- package/.github/workflows/ci.yml +0 -76
- package/.github/workflows/docker-and-deploy.yml +0 -117
- package/.wts-config.json +0 -4
- package/.wts-setup.ts +0 -102
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -270
- package/DEPLOYMENT.md +0 -605
- package/Dockerfile +0 -57
- package/Dockerfile.worker +0 -157
- package/FAQ.md +0 -19
- package/MCP.md +0 -406
- package/UI.md +0 -40
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/assets/agent-swarm.mp4 +0 -0
- package/assets/agent-swarm.png +0 -0
- package/biome.json +0 -39
- package/deploy/DEPLOY.md +0 -60
- package/deploy/agent-swarm.service +0 -17
- package/deploy/docker-push.ts +0 -30
- package/deploy/install.ts +0 -85
- package/deploy/prod-db.ts +0 -42
- package/deploy/uninstall.ts +0 -12
- package/deploy/update.ts +0 -21
- package/docker-compose.example.yml +0 -159
- package/docker-entrypoint.sh +0 -352
- package/ecosystem.config.cjs +0 -66
- package/plugin/README.md +0 -1
- package/plugin/hooks/hooks.json +0 -71
- package/pyproject.toml +0 -9
- package/scripts/generate-mcp-docs.ts +0 -415
- package/slack-manifest.json +0 -71
- package/src/tests/get-inbox-message.test.ts +0 -145
- package/src/tools/get-inbox-message.ts +0 -89
- package/src/tools/inbox-delegate.ts +0 -113
- package/thoughts/shared/plans/2025-12-18-slack-integration.md +0 -1195
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +0 -732
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +0 -361
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +0 -501
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +0 -560
- package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +0 -934
- package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +0 -1000
- package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +0 -568
- package/thoughts/shared/plans/2026-01-09-inverse-teleport.md +0 -1516
- package/thoughts/shared/plans/2026-01-12-agent-rename-pm2-control.md +0 -1133
- package/thoughts/shared/plans/2026-01-12-github-app-integration.md +0 -380
- package/thoughts/shared/plans/2026-01-12-lead-inbox-model.md +0 -876
- package/thoughts/shared/plans/2026-01-12-ralph-wiggum-integration.md +0 -463
- package/thoughts/shared/plans/2026-01-13-agent-concurrency.md +0 -691
- package/thoughts/shared/plans/2026-01-13-github-assignment-handling.md +0 -690
- package/thoughts/shared/plans/2026-01-13-prevent-duplicate-trigger-processing.md +0 -1071
- package/thoughts/shared/plans/2026-01-14-fix-slack-thread-context.md +0 -507
- package/thoughts/shared/plans/2026-01-15-scheduled-tasks-implementation.md +0 -565
- package/thoughts/shared/plans/2026-01-15-usage-cost-tracking-ui.md +0 -1479
- package/thoughts/shared/plans/2026-01-16-epics-feature-implementation.md +0 -1230
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-01-09-inverse-teleport-plan-review.md +0 -420
- package/thoughts/shared/research/2025-12-18-slack-integration.md +0 -442
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +0 -339
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +0 -390
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +0 -376
- package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +0 -582
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +0 -264
- package/thoughts/shared/research/2026-01-13-lead-duplicate-trigger-processing.md +0 -223
- package/thoughts/shared/research/2026-01-14-lead-slack-thread-context.md +0 -277
- package/thoughts/shared/research/2026-01-15-ai-tracker-agent-swarm-integration.md +0 -376
- package/thoughts/shared/research/2026-01-15-auto-starting-processes-in-worker-containers.md +0 -787
- package/thoughts/shared/research/2026-01-15-scheduled-tasks.md +0 -390
- package/thoughts/shared/research/2026-01-16-epics-feature-research.md +0 -437
- package/thoughts/taras/plans/2026-01-22-agent-swarm-schemas.md +0 -98
- package/thoughts/taras/plans/2026-01-28-per-worker-claude-md.md +0 -617
- package/thoughts/taras/plans/2026-01-28-sentry-cli-integration.md +0 -214
- package/thoughts/taras/research/2026-01-22-vercel-cli-integration.md +0 -287
- package/thoughts/taras/research/2026-01-27-excessive-polling-issue.md +0 -311
- package/thoughts/taras/research/2026-01-28-per-worker-claude-md.md +0 -383
- package/thoughts/taras/research/2026-01-28-sentry-cli-integration.md +0 -240
- package/tsconfig.json +0 -37
- package/ui/CLAUDE.md +0 -49
- package/ui/bun.lock +0 -771
- package/ui/index.html +0 -22
- package/ui/package-lock.json +0 -5290
- package/ui/package.json +0 -33
- package/ui/pnpm-lock.yaml +0 -3341
- package/ui/postcss.config.js +0 -6
- package/ui/public/logo.png +0 -0
- package/ui/src/App.tsx +0 -63
- package/ui/src/components/ActivityFeed.tsx +0 -440
- package/ui/src/components/AgentDetailPanel.tsx +0 -733
- package/ui/src/components/AgentsPanel.tsx +0 -815
- package/ui/src/components/ChatPanel.tsx +0 -1920
- package/ui/src/components/ConfigModal.tsx +0 -253
- package/ui/src/components/Dashboard.tsx +0 -832
- package/ui/src/components/EditAgentProfileModal.tsx +0 -433
- package/ui/src/components/EpicDetailPage.tsx +0 -741
- package/ui/src/components/EpicsPanel.tsx +0 -566
- package/ui/src/components/Header.tsx +0 -160
- package/ui/src/components/JsonViewer.tsx +0 -171
- package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
- package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
- package/ui/src/components/ServicesPanel.tsx +0 -622
- package/ui/src/components/SessionLogPanel.tsx +0 -1219
- package/ui/src/components/StatsBar.tsx +0 -321
- package/ui/src/components/StatusBadge.tsx +0 -168
- package/ui/src/components/TaskDetailPanel.tsx +0 -903
- package/ui/src/components/TasksPanel.tsx +0 -614
- package/ui/src/components/UsageCharts.tsx +0 -216
- package/ui/src/components/UsageTab.tsx +0 -394
- package/ui/src/hooks/queries.ts +0 -353
- package/ui/src/hooks/useAutoScroll.ts +0 -83
- package/ui/src/index.css +0 -257
- package/ui/src/lib/api.ts +0 -268
- package/ui/src/lib/config.ts +0 -35
- package/ui/src/lib/contentPreview.ts +0 -208
- package/ui/src/lib/theme.ts +0 -214
- package/ui/src/lib/utils.ts +0 -88
- package/ui/src/main.tsx +0 -28
- package/ui/src/types/api.ts +0 -323
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -37
- package/ui/tsconfig.json +0 -31
- package/ui/vite.config.ts +0 -35
- /package/{thoughts/shared/plans → templates/community}/.gitkeep +0 -0
package/src/http.ts
CHANGED
|
@@ -1,1890 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
createServer as createHttpServer,
|
|
4
|
-
type IncomingMessage,
|
|
5
|
-
type Server,
|
|
6
|
-
type ServerResponse,
|
|
7
|
-
} from "node:http";
|
|
8
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import { createServer, hasCapability } from "@/server";
|
|
11
|
-
import {
|
|
12
|
-
assignTaskToEpic,
|
|
13
|
-
claimInboxMessages,
|
|
14
|
-
claimMentions,
|
|
15
|
-
claimOfferedTask,
|
|
16
|
-
closeDb,
|
|
17
|
-
completeTask,
|
|
18
|
-
createAgent,
|
|
19
|
-
createEpic,
|
|
20
|
-
createSessionCost,
|
|
21
|
-
createSessionLogs,
|
|
22
|
-
createTaskExtended,
|
|
23
|
-
deleteEpic,
|
|
24
|
-
failTask,
|
|
25
|
-
getActiveTaskCount,
|
|
26
|
-
getAgentById,
|
|
27
|
-
getAgentWithTasks,
|
|
28
|
-
getAllAgents,
|
|
29
|
-
getAllAgentsWithTasks,
|
|
30
|
-
getAllChannels,
|
|
31
|
-
getAllLogs,
|
|
32
|
-
getAllServices,
|
|
33
|
-
getAllSessionCosts,
|
|
34
|
-
getAllTasks,
|
|
35
|
-
getChannelById,
|
|
36
|
-
getChannelMessages,
|
|
37
|
-
getDb,
|
|
38
|
-
getEpicById,
|
|
39
|
-
getEpics,
|
|
40
|
-
getEpicsWithProgressUpdates,
|
|
41
|
-
getEpicWithProgress,
|
|
42
|
-
getInboxSummary,
|
|
43
|
-
getLogsByAgentId,
|
|
44
|
-
getLogsByTaskId,
|
|
45
|
-
getOfferedTasksForAgent,
|
|
46
|
-
getPausedTasksForAgent,
|
|
47
|
-
getPendingTaskForAgent,
|
|
48
|
-
getRecentlyCancelledTasksForAgent,
|
|
49
|
-
getRecentlyFinishedWorkerTasks,
|
|
50
|
-
getScheduledTasks,
|
|
51
|
-
getServicesByAgentId,
|
|
52
|
-
getSessionCostsByAgentId,
|
|
53
|
-
getSessionCostsByTaskId,
|
|
54
|
-
getSessionLogsByTaskId,
|
|
55
|
-
getTaskById,
|
|
56
|
-
getTaskStats,
|
|
57
|
-
getTasksByEpicId,
|
|
58
|
-
getTasksCount,
|
|
59
|
-
getUnassignedTasksCount,
|
|
60
|
-
hasCapacity,
|
|
61
|
-
markEpicsProgressNotified,
|
|
62
|
-
markTasksNotified,
|
|
63
|
-
pauseTask,
|
|
64
|
-
postMessage,
|
|
65
|
-
resetEmptyPollCount,
|
|
66
|
-
resumeTask,
|
|
67
|
-
shouldBlockPolling,
|
|
68
|
-
startTask,
|
|
69
|
-
updateAgentMaxTasks,
|
|
70
|
-
updateAgentName,
|
|
71
|
-
updateAgentProfile,
|
|
72
|
-
updateAgentStatus,
|
|
73
|
-
updateAgentStatusFromCapacity,
|
|
74
|
-
updateEpic,
|
|
75
|
-
} from "./be/db";
|
|
76
|
-
import type {
|
|
77
|
-
CheckRunEvent,
|
|
78
|
-
CheckSuiteEvent,
|
|
79
|
-
CommentEvent,
|
|
80
|
-
IssueEvent,
|
|
81
|
-
PullRequestEvent,
|
|
82
|
-
PullRequestReviewEvent,
|
|
83
|
-
WorkflowRunEvent,
|
|
84
|
-
} from "./github";
|
|
85
|
-
import {
|
|
86
|
-
handleCheckRun,
|
|
87
|
-
handleCheckSuite,
|
|
88
|
-
handleComment,
|
|
89
|
-
handleIssue,
|
|
90
|
-
handlePullRequest,
|
|
91
|
-
handlePullRequestReview,
|
|
92
|
-
handleWorkflowRun,
|
|
93
|
-
initGitHub,
|
|
94
|
-
isGitHubEnabled,
|
|
95
|
-
verifyWebhookSignature,
|
|
96
|
-
} from "./github";
|
|
97
|
-
import { startSlackApp, stopSlackApp } from "./slack";
|
|
98
|
-
import type { AgentLog, AgentStatus, EpicStatus, SessionCost } from "./types";
|
|
99
|
-
|
|
100
|
-
const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
|
|
101
|
-
const apiKey = process.env.API_KEY || "";
|
|
102
|
-
|
|
103
|
-
// Use globalThis to persist state across hot reloads
|
|
104
|
-
const globalState = globalThis as typeof globalThis & {
|
|
105
|
-
__httpServer?: Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
106
|
-
__transports?: Record<string, StreamableHTTPServerTransport>;
|
|
107
|
-
__sigintRegistered?: boolean;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Clean up previous server on hot reload
|
|
111
|
-
if (globalState.__httpServer) {
|
|
112
|
-
console.log("[HTTP] Hot reload detected, closing previous server...");
|
|
113
|
-
globalState.__httpServer.close();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const transports: Record<string, StreamableHTTPServerTransport> = globalState.__transports ?? {};
|
|
117
|
-
|
|
118
|
-
function setCorsHeaders(res: ServerResponse) {
|
|
119
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
120
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
121
|
-
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
122
|
-
res.setHeader("Access-Control-Expose-Headers", "*");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function parseQueryParams(url: string): URLSearchParams {
|
|
126
|
-
const queryIndex = url.indexOf("?");
|
|
127
|
-
if (queryIndex === -1) return new URLSearchParams();
|
|
128
|
-
return new URLSearchParams(url.slice(queryIndex + 1));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function getPathSegments(url: string): string[] {
|
|
132
|
-
const pathEnd = url.indexOf("?");
|
|
133
|
-
const path = pathEnd === -1 ? url : url.slice(0, pathEnd);
|
|
134
|
-
return path.split("/").filter(Boolean);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Add capacity info to agent response */
|
|
138
|
-
function agentWithCapacity<T extends { id: string; maxTasks?: number }>(
|
|
139
|
-
agent: T,
|
|
140
|
-
): T & { capacity: { current: number; max: number; available: number } } {
|
|
141
|
-
const activeCount = getActiveTaskCount(agent.id);
|
|
142
|
-
const max = agent.maxTasks ?? 1;
|
|
143
|
-
return {
|
|
144
|
-
...agent,
|
|
145
|
-
capacity: {
|
|
146
|
-
current: activeCount,
|
|
147
|
-
max,
|
|
148
|
-
available: Math.max(0, max - activeCount),
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const httpServer = createHttpServer(async (req, res) => {
|
|
154
|
-
const startTime = performance.now();
|
|
155
|
-
let statusCode = 200;
|
|
156
|
-
|
|
157
|
-
// Wrap writeHead to capture status code
|
|
158
|
-
const originalWriteHead = res.writeHead.bind(res);
|
|
159
|
-
res.writeHead = (code: number, ...args: unknown[]) => {
|
|
160
|
-
statusCode = code;
|
|
161
|
-
// @ts-expect-error - writeHead has multiple overloads
|
|
162
|
-
return originalWriteHead(code, ...args);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Log request completion
|
|
166
|
-
const logRequest = () => {
|
|
167
|
-
const elapsed = (performance.now() - startTime).toFixed(1);
|
|
168
|
-
const statusEmoji = statusCode >= 400 ? "⚠️" : "✓";
|
|
169
|
-
console.log(`[HTTP] ${statusEmoji} ${req.method} ${req.url} → ${statusCode} (${elapsed}ms)`);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Ensure we log on response finish
|
|
173
|
-
res.on("finish", logRequest);
|
|
174
|
-
|
|
175
|
-
// Log errors
|
|
176
|
-
res.on("error", (err) => {
|
|
177
|
-
console.error(`[HTTP] ❌ ${req.method} ${req.url} → Error: ${err.message}`);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
setCorsHeaders(res);
|
|
181
|
-
|
|
182
|
-
// Handle preflight
|
|
183
|
-
if (req.method === "OPTIONS") {
|
|
184
|
-
res.writeHead(204);
|
|
185
|
-
res.end();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
190
|
-
const myAgentId = req.headers["x-agent-id"] as string | undefined;
|
|
191
|
-
|
|
192
|
-
if (req.url === "/health") {
|
|
193
|
-
// Read version from package.json
|
|
194
|
-
const version = (await Bun.file("package.json").json()).version;
|
|
195
|
-
|
|
196
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
197
|
-
res.end(
|
|
198
|
-
JSON.stringify({
|
|
199
|
-
status: "ok",
|
|
200
|
-
version,
|
|
201
|
-
}),
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// API key authentication (if API_KEY is configured)
|
|
208
|
-
// Skip auth for GitHub webhook (it has its own signature verification)
|
|
209
|
-
const isGitHubWebhook = req.url?.startsWith("/api/github/webhook");
|
|
210
|
-
if (apiKey && !isGitHubWebhook) {
|
|
211
|
-
const authHeader = req.headers.authorization;
|
|
212
|
-
const providedKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
213
|
-
|
|
214
|
-
if (providedKey !== apiKey) {
|
|
215
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
216
|
-
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (req.method === "GET" && (req.url === "/me" || req.url?.startsWith("/me?"))) {
|
|
222
|
-
if (!myAgentId) {
|
|
223
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
224
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const agent = getAgentById(myAgentId);
|
|
229
|
-
|
|
230
|
-
if (!agent) {
|
|
231
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
232
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Check for ?include=inbox query param
|
|
237
|
-
const includeInbox = parseQueryParams(req.url || "").get("include") === "inbox";
|
|
238
|
-
|
|
239
|
-
// Add capacity info and polling limit check to agent response
|
|
240
|
-
const agentResponse = {
|
|
241
|
-
...agentWithCapacity(agent),
|
|
242
|
-
shouldBlockPolling: shouldBlockPolling(myAgentId),
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
if (includeInbox) {
|
|
246
|
-
const inbox = getInboxSummary(myAgentId);
|
|
247
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
248
|
-
res.end(JSON.stringify({ ...agentResponse, inbox }));
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
253
|
-
res.end(JSON.stringify(agentResponse));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// GET /cancelled-tasks - Check for recently cancelled tasks (for hook cancellation detection)
|
|
258
|
-
// Supports optional ?taskId= query param for checking specific task cancellation
|
|
259
|
-
if (
|
|
260
|
-
req.method === "GET" &&
|
|
261
|
-
(req.url === "/cancelled-tasks" || req.url?.startsWith("/cancelled-tasks?"))
|
|
262
|
-
) {
|
|
263
|
-
if (!myAgentId) {
|
|
264
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
265
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const agent = getAgentById(myAgentId);
|
|
270
|
-
if (!agent) {
|
|
271
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
272
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Check for specific taskId query param
|
|
277
|
-
const queryParams = parseQueryParams(req.url || "");
|
|
278
|
-
const taskId = queryParams.get("taskId");
|
|
279
|
-
|
|
280
|
-
if (taskId) {
|
|
281
|
-
// Check if specific task is cancelled
|
|
282
|
-
const task = getTaskById(taskId);
|
|
283
|
-
if (task && task.status === "cancelled") {
|
|
284
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
285
|
-
res.end(
|
|
286
|
-
JSON.stringify({
|
|
287
|
-
cancelled: [
|
|
288
|
-
{
|
|
289
|
-
id: task.id,
|
|
290
|
-
task: task.task,
|
|
291
|
-
failureReason: task.failureReason,
|
|
292
|
-
},
|
|
293
|
-
],
|
|
294
|
-
}),
|
|
295
|
-
);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
// Task not found or not cancelled
|
|
299
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
300
|
-
res.end(JSON.stringify({ cancelled: [] }));
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// No taskId - return all recently cancelled tasks for this agent
|
|
305
|
-
const cancelledTasks = getRecentlyCancelledTasksForAgent(myAgentId);
|
|
306
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
307
|
-
res.end(JSON.stringify({ cancelled: cancelledTasks }));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (req.method === "POST" && req.url === "/ping") {
|
|
312
|
-
if (!myAgentId) {
|
|
313
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
314
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const tx = getDb().transaction(() => {
|
|
319
|
-
const agent = getAgentById(myAgentId);
|
|
320
|
-
|
|
321
|
-
if (!agent) {
|
|
322
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
323
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
let status: AgentStatus = "idle";
|
|
328
|
-
|
|
329
|
-
if (agent.status === "busy") {
|
|
330
|
-
status = "busy";
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
updateAgentStatus(agent.id, status);
|
|
334
|
-
|
|
335
|
-
return true;
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
if (!tx()) {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
res.writeHead(204);
|
|
343
|
-
res.end();
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (req.method === "POST" && req.url === "/close") {
|
|
348
|
-
if (!myAgentId) {
|
|
349
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
350
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const tx = getDb().transaction(() => {
|
|
355
|
-
const agent = getAgentById(myAgentId);
|
|
356
|
-
|
|
357
|
-
if (!agent) {
|
|
358
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
359
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
updateAgentStatus(agent.id, "offline");
|
|
364
|
-
|
|
365
|
-
return true;
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
if (!tx()) {
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
res.writeHead(204);
|
|
373
|
-
res.end();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ============================================================================
|
|
378
|
-
// Runner-Level Polling Endpoints
|
|
379
|
-
// ============================================================================
|
|
380
|
-
|
|
381
|
-
const pathSegments = getPathSegments(req.url || "");
|
|
382
|
-
|
|
383
|
-
// POST /api/agents - Register a new agent (or return existing if already registered)
|
|
384
|
-
if (
|
|
385
|
-
req.method === "POST" &&
|
|
386
|
-
pathSegments[0] === "api" &&
|
|
387
|
-
pathSegments[1] === "agents" &&
|
|
388
|
-
!pathSegments[2]
|
|
389
|
-
) {
|
|
390
|
-
// Parse request body
|
|
391
|
-
const chunks: Buffer[] = [];
|
|
392
|
-
for await (const chunk of req) {
|
|
393
|
-
chunks.push(chunk);
|
|
394
|
-
}
|
|
395
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
396
|
-
|
|
397
|
-
// Validate required fields
|
|
398
|
-
if (!body.name || typeof body.name !== "string") {
|
|
399
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
400
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'name' field" }));
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Use X-Agent-ID header if provided, otherwise generate new UUID
|
|
405
|
-
const agentId = myAgentId || crypto.randomUUID();
|
|
406
|
-
|
|
407
|
-
// Use transaction to ensure atomicity of check-and-create/update
|
|
408
|
-
const result = getDb().transaction(() => {
|
|
409
|
-
// Check if agent already exists
|
|
410
|
-
const existingAgent = getAgentById(agentId);
|
|
411
|
-
if (existingAgent) {
|
|
412
|
-
// Update status to idle if offline
|
|
413
|
-
if (existingAgent.status === "offline") {
|
|
414
|
-
updateAgentStatus(existingAgent.id, "idle");
|
|
415
|
-
}
|
|
416
|
-
// Update maxTasks if provided (allows runner to sync its MAX_CONCURRENT_TASKS)
|
|
417
|
-
if (body.maxTasks !== undefined && body.maxTasks !== existingAgent.maxTasks) {
|
|
418
|
-
updateAgentMaxTasks(existingAgent.id, body.maxTasks);
|
|
419
|
-
}
|
|
420
|
-
// Reset empty poll count on re-registration (agent is starting fresh)
|
|
421
|
-
resetEmptyPollCount(existingAgent.id);
|
|
422
|
-
return { agent: getAgentById(agentId), created: false };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Create new agent
|
|
426
|
-
const agent = createAgent({
|
|
427
|
-
id: agentId,
|
|
428
|
-
name: body.name,
|
|
429
|
-
isLead: body.isLead ?? false,
|
|
430
|
-
status: "idle",
|
|
431
|
-
description: body.description,
|
|
432
|
-
role: body.role,
|
|
433
|
-
capabilities: body.capabilities,
|
|
434
|
-
maxTasks: body.maxTasks ?? 1,
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
return { agent, created: true };
|
|
438
|
-
})();
|
|
439
|
-
|
|
440
|
-
res.writeHead(result.created ? 201 : 200, { "Content-Type": "application/json" });
|
|
441
|
-
res.end(JSON.stringify(result.agent));
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// GET /api/poll - Poll for triggers (tasks, mentions, etc.)
|
|
446
|
-
if (req.method === "GET" && pathSegments[0] === "api" && pathSegments[1] === "poll") {
|
|
447
|
-
if (!myAgentId) {
|
|
448
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
449
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Use transaction for consistent reads across all trigger checks
|
|
454
|
-
let result:
|
|
455
|
-
| { error: string; status: number }
|
|
456
|
-
| { trigger: { type: string; [key: string]: unknown } | null };
|
|
457
|
-
try {
|
|
458
|
-
result = getDb().transaction(() => {
|
|
459
|
-
const agent = getAgentById(myAgentId);
|
|
460
|
-
if (!agent) {
|
|
461
|
-
return { error: "Agent not found", status: 404 };
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Check for offered tasks first (highest priority for both workers and leads)
|
|
465
|
-
// Atomically claim the task for review to prevent duplicate processing
|
|
466
|
-
const offeredTasks = getOfferedTasksForAgent(myAgentId);
|
|
467
|
-
const firstOfferedTask = offeredTasks[0];
|
|
468
|
-
if (firstOfferedTask) {
|
|
469
|
-
const claimedTask = claimOfferedTask(firstOfferedTask.id, myAgentId);
|
|
470
|
-
if (claimedTask) {
|
|
471
|
-
return {
|
|
472
|
-
trigger: {
|
|
473
|
-
type: "task_offered",
|
|
474
|
-
taskId: claimedTask.id,
|
|
475
|
-
task: claimedTask,
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Check for pending tasks (assigned directly to this agent)
|
|
482
|
-
// Only return a task if agent has capacity (server-side enforcement)
|
|
483
|
-
if (hasCapacity(myAgentId)) {
|
|
484
|
-
const pendingTask = getPendingTaskForAgent(myAgentId);
|
|
485
|
-
if (pendingTask) {
|
|
486
|
-
// Mark task as in_progress immediately to prevent duplicate polling
|
|
487
|
-
startTask(pendingTask.id);
|
|
488
|
-
return {
|
|
489
|
-
trigger: {
|
|
490
|
-
type: "task_assigned",
|
|
491
|
-
taskId: pendingTask.id,
|
|
492
|
-
task: { ...pendingTask, status: "in_progress" },
|
|
493
|
-
},
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (agent.isLead) {
|
|
499
|
-
// === LEAD-SPECIFIC TRIGGERS ===
|
|
500
|
-
|
|
501
|
-
// Check for unread Slack inbox messages (highest priority for lead)
|
|
502
|
-
// Atomically claim messages to prevent duplicate processing
|
|
503
|
-
const claimedInbox = claimInboxMessages(myAgentId, 5);
|
|
504
|
-
if (claimedInbox.length > 0) {
|
|
505
|
-
return {
|
|
506
|
-
trigger: {
|
|
507
|
-
type: "slack_inbox_message",
|
|
508
|
-
count: claimedInbox.length,
|
|
509
|
-
messages: claimedInbox,
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Check for unread mentions (internal chat) - atomically claim them
|
|
515
|
-
const claimedChannels = claimMentions(myAgentId);
|
|
516
|
-
if (claimedChannels.length > 0) {
|
|
517
|
-
// Recalculate inbox summary now that we've claimed
|
|
518
|
-
const inbox = getInboxSummary(myAgentId);
|
|
519
|
-
return {
|
|
520
|
-
trigger: {
|
|
521
|
-
type: "unread_mentions",
|
|
522
|
-
mentionsCount: inbox.mentionsCount,
|
|
523
|
-
claimedChannels: claimedChannels.map((c) => c.channelId), // Include for tracking
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Check for recently finished worker tasks
|
|
529
|
-
const finishedTasks = getRecentlyFinishedWorkerTasks();
|
|
530
|
-
if (finishedTasks.length > 0) {
|
|
531
|
-
// Atomically mark as notified within this transaction
|
|
532
|
-
const taskIds = finishedTasks.map((t) => t.id);
|
|
533
|
-
markTasksNotified(taskIds);
|
|
534
|
-
|
|
535
|
-
return {
|
|
536
|
-
trigger: {
|
|
537
|
-
type: "tasks_finished",
|
|
538
|
-
count: finishedTasks.length,
|
|
539
|
-
tasks: finishedTasks,
|
|
540
|
-
},
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Check for epic progress updates (tasks completed/failed for active epics)
|
|
545
|
-
// This trigger helps lead plan next steps for epics - similar to ralph loop
|
|
546
|
-
const epicsWithUpdates = getEpicsWithProgressUpdates();
|
|
547
|
-
if (epicsWithUpdates.length > 0) {
|
|
548
|
-
// Atomically mark as notified within this transaction
|
|
549
|
-
const epicIds = epicsWithUpdates.map((e) => e.epic.id);
|
|
550
|
-
markEpicsProgressNotified(epicIds);
|
|
551
|
-
|
|
552
|
-
return {
|
|
553
|
-
trigger: {
|
|
554
|
-
type: "epic_progress_changed",
|
|
555
|
-
count: epicsWithUpdates.length,
|
|
556
|
-
epics: epicsWithUpdates,
|
|
557
|
-
},
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
} else {
|
|
561
|
-
// === WORKER-SPECIFIC TRIGGERS ===
|
|
562
|
-
|
|
563
|
-
// Check for unassigned tasks in pool (workers can claim)
|
|
564
|
-
// NOTE: This trigger is intentionally unprotected from duplicate processing.
|
|
565
|
-
// Multiple workers should all receive this notification so they can compete
|
|
566
|
-
// to claim tasks. The actual claiming happens via task-action tool with
|
|
567
|
-
// atomic SQL guards in claimTask().
|
|
568
|
-
const unassignedCount = getUnassignedTasksCount();
|
|
569
|
-
if (unassignedCount > 0) {
|
|
570
|
-
return {
|
|
571
|
-
trigger: {
|
|
572
|
-
type: "pool_tasks_available",
|
|
573
|
-
count: unassignedCount,
|
|
574
|
-
},
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// No trigger found
|
|
580
|
-
return { trigger: null };
|
|
581
|
-
})();
|
|
582
|
-
} catch (error) {
|
|
583
|
-
console.error("[/api/poll] Database error:", error);
|
|
584
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
585
|
-
res.end(
|
|
586
|
-
JSON.stringify({
|
|
587
|
-
error: "Database error occurred while polling for triggers",
|
|
588
|
-
details: error instanceof Error ? error.message : String(error),
|
|
589
|
-
}),
|
|
590
|
-
);
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Handle error case
|
|
595
|
-
if ("error" in result) {
|
|
596
|
-
res.writeHead(result.status ?? 500, { "Content-Type": "application/json" });
|
|
597
|
-
res.end(JSON.stringify({ error: result.error }));
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
602
|
-
res.end(JSON.stringify(result));
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
// POST /api/session-logs - Store session logs (batch)
|
|
607
|
-
if (req.method === "POST" && pathSegments[0] === "api" && pathSegments[1] === "session-logs") {
|
|
608
|
-
// Parse request body
|
|
609
|
-
const chunks: Buffer[] = [];
|
|
610
|
-
for await (const chunk of req) {
|
|
611
|
-
chunks.push(chunk);
|
|
612
|
-
}
|
|
613
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
614
|
-
|
|
615
|
-
// Validate required fields
|
|
616
|
-
if (!body.sessionId || typeof body.sessionId !== "string") {
|
|
617
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
618
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'sessionId' field" }));
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (typeof body.iteration !== "number" || body.iteration < 1) {
|
|
623
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
624
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'iteration' field" }));
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (!Array.isArray(body.lines) || body.lines.length === 0) {
|
|
629
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
630
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'lines' array" }));
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
try {
|
|
635
|
-
createSessionLogs({
|
|
636
|
-
taskId: body.taskId || undefined,
|
|
637
|
-
sessionId: body.sessionId,
|
|
638
|
-
iteration: body.iteration,
|
|
639
|
-
cli: body.cli || "claude",
|
|
640
|
-
lines: body.lines,
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
644
|
-
res.end(JSON.stringify({ success: true, count: body.lines.length }));
|
|
645
|
-
} catch (error) {
|
|
646
|
-
console.error("[HTTP] Failed to create session logs:", error);
|
|
647
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
648
|
-
res.end(JSON.stringify({ error: "Failed to store session logs" }));
|
|
649
|
-
}
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// GET /api/tasks/:id/session-logs - Get session logs for a task
|
|
654
|
-
if (
|
|
655
|
-
req.method === "GET" &&
|
|
656
|
-
pathSegments[0] === "api" &&
|
|
657
|
-
pathSegments[1] === "tasks" &&
|
|
658
|
-
pathSegments[2] &&
|
|
659
|
-
pathSegments[3] === "session-logs"
|
|
660
|
-
) {
|
|
661
|
-
const taskId = pathSegments[2];
|
|
662
|
-
const task = getTaskById(taskId);
|
|
663
|
-
|
|
664
|
-
if (!task) {
|
|
665
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
666
|
-
res.end(JSON.stringify({ error: "Task not found" }));
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const logs = getSessionLogsByTaskId(taskId);
|
|
671
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
672
|
-
res.end(JSON.stringify({ logs }));
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// POST /api/session-costs - Store session cost record
|
|
677
|
-
if (req.method === "POST" && pathSegments[0] === "api" && pathSegments[1] === "session-costs") {
|
|
678
|
-
// Parse request body
|
|
679
|
-
const chunks: Buffer[] = [];
|
|
680
|
-
for await (const chunk of req) {
|
|
681
|
-
chunks.push(chunk);
|
|
682
|
-
}
|
|
683
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
684
|
-
|
|
685
|
-
// Validate required fields
|
|
686
|
-
if (!body.sessionId || typeof body.sessionId !== "string") {
|
|
687
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
688
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'sessionId' field" }));
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (!body.agentId || typeof body.agentId !== "string") {
|
|
693
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
694
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'agentId' field" }));
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (typeof body.totalCostUsd !== "number") {
|
|
699
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
700
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'totalCostUsd' field" }));
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
const cost = createSessionCost({
|
|
706
|
-
sessionId: body.sessionId,
|
|
707
|
-
taskId: body.taskId || undefined,
|
|
708
|
-
agentId: body.agentId,
|
|
709
|
-
totalCostUsd: body.totalCostUsd,
|
|
710
|
-
inputTokens: body.inputTokens ?? 0,
|
|
711
|
-
outputTokens: body.outputTokens ?? 0,
|
|
712
|
-
cacheReadTokens: body.cacheReadTokens ?? 0,
|
|
713
|
-
cacheWriteTokens: body.cacheWriteTokens ?? 0,
|
|
714
|
-
durationMs: body.durationMs ?? 0,
|
|
715
|
-
numTurns: body.numTurns ?? 1,
|
|
716
|
-
model: body.model || "opus",
|
|
717
|
-
isError: body.isError ?? false,
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
721
|
-
res.end(JSON.stringify({ success: true, cost }));
|
|
722
|
-
} catch (error) {
|
|
723
|
-
console.error("[HTTP] Failed to create session cost:", error);
|
|
724
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
725
|
-
res.end(JSON.stringify({ error: "Failed to store session cost" }));
|
|
726
|
-
}
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// GET /api/session-costs - Query session costs with filters
|
|
731
|
-
if (
|
|
732
|
-
req.method === "GET" &&
|
|
733
|
-
pathSegments[0] === "api" &&
|
|
734
|
-
pathSegments[1] === "session-costs" &&
|
|
735
|
-
!pathSegments[2]
|
|
736
|
-
) {
|
|
737
|
-
const costsQueryParams = parseQueryParams(req.url || "");
|
|
738
|
-
const agentId = costsQueryParams.get("agentId");
|
|
739
|
-
const taskId = costsQueryParams.get("taskId");
|
|
740
|
-
const limitParam = costsQueryParams.get("limit");
|
|
741
|
-
const limit = limitParam ? parseInt(limitParam, 10) : 100;
|
|
742
|
-
|
|
743
|
-
let costs: SessionCost[];
|
|
744
|
-
if (taskId) {
|
|
745
|
-
costs = getSessionCostsByTaskId(taskId);
|
|
746
|
-
} else if (agentId) {
|
|
747
|
-
costs = getSessionCostsByAgentId(agentId, limit);
|
|
748
|
-
} else {
|
|
749
|
-
costs = getAllSessionCosts(limit);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
753
|
-
res.end(JSON.stringify({ costs }));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// GET /ecosystem - Generate PM2 ecosystem config for agent's services
|
|
758
|
-
if (req.method === "GET" && req.url === "/ecosystem") {
|
|
759
|
-
if (!myAgentId) {
|
|
760
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
761
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const services = getServicesByAgentId(myAgentId);
|
|
766
|
-
|
|
767
|
-
// Generate PM2 ecosystem format
|
|
768
|
-
const ecosystem = {
|
|
769
|
-
apps: services
|
|
770
|
-
.filter((s) => s.script) // Only include services with script path
|
|
771
|
-
.map((s) => {
|
|
772
|
-
const app: Record<string, unknown> = {
|
|
773
|
-
name: s.name,
|
|
774
|
-
script: s.script,
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
if (s.cwd) app.cwd = s.cwd;
|
|
778
|
-
if (s.interpreter) app.interpreter = s.interpreter;
|
|
779
|
-
if (s.args && s.args.length > 0) app.args = s.args;
|
|
780
|
-
if (s.env && Object.keys(s.env).length > 0) app.env = s.env;
|
|
781
|
-
if (s.port)
|
|
782
|
-
app.env = { ...((app.env as Record<string, string>) || {}), PORT: String(s.port) };
|
|
783
|
-
|
|
784
|
-
return app;
|
|
785
|
-
}),
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
789
|
-
res.end(JSON.stringify(ecosystem));
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// ============================================================================
|
|
794
|
-
// GitHub Webhook Endpoint
|
|
795
|
-
// ============================================================================
|
|
796
|
-
|
|
797
|
-
// POST /api/github/webhook - Handle GitHub webhook events
|
|
798
|
-
if (
|
|
799
|
-
req.method === "POST" &&
|
|
800
|
-
pathSegments[0] === "api" &&
|
|
801
|
-
pathSegments[1] === "github" &&
|
|
802
|
-
pathSegments[2] === "webhook"
|
|
803
|
-
) {
|
|
804
|
-
// Check if GitHub integration is enabled
|
|
805
|
-
if (!isGitHubEnabled()) {
|
|
806
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
807
|
-
res.end(JSON.stringify({ error: "GitHub integration not configured" }));
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Get event type and signature
|
|
812
|
-
const eventType = req.headers["x-github-event"] as string | undefined;
|
|
813
|
-
const signature = req.headers["x-hub-signature-256"] as string | undefined;
|
|
814
|
-
|
|
815
|
-
// Parse request body
|
|
816
|
-
const chunks: Buffer[] = [];
|
|
817
|
-
for await (const chunk of req) {
|
|
818
|
-
chunks.push(chunk);
|
|
819
|
-
}
|
|
820
|
-
const rawBody = Buffer.concat(chunks).toString();
|
|
821
|
-
|
|
822
|
-
// Verify webhook signature
|
|
823
|
-
const isValid = await verifyWebhookSignature(rawBody, signature ?? null);
|
|
824
|
-
if (!isValid) {
|
|
825
|
-
console.log("[GitHub] Invalid webhook signature");
|
|
826
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
827
|
-
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Handle ping event (webhook setup verification)
|
|
832
|
-
if (eventType === "ping") {
|
|
833
|
-
console.log("[GitHub] Received ping event - webhook configured successfully");
|
|
834
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
835
|
-
res.end(JSON.stringify({ message: "pong" }));
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Parse JSON body
|
|
840
|
-
let body: unknown;
|
|
841
|
-
try {
|
|
842
|
-
body = JSON.parse(rawBody);
|
|
843
|
-
} catch {
|
|
844
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
845
|
-
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
console.log(`[GitHub] Received ${eventType} event`);
|
|
850
|
-
|
|
851
|
-
// Route to appropriate handler
|
|
852
|
-
let result: { created: boolean; taskId?: string } = { created: false };
|
|
853
|
-
|
|
854
|
-
try {
|
|
855
|
-
switch (eventType) {
|
|
856
|
-
case "pull_request":
|
|
857
|
-
result = await handlePullRequest(body as PullRequestEvent);
|
|
858
|
-
break;
|
|
859
|
-
case "issues":
|
|
860
|
-
result = await handleIssue(body as IssueEvent);
|
|
861
|
-
break;
|
|
862
|
-
case "issue_comment":
|
|
863
|
-
result = await handleComment(body as CommentEvent, "issue_comment");
|
|
864
|
-
break;
|
|
865
|
-
case "pull_request_review_comment":
|
|
866
|
-
result = await handleComment(body as CommentEvent, "pull_request_review_comment");
|
|
867
|
-
break;
|
|
868
|
-
case "pull_request_review":
|
|
869
|
-
result = await handlePullRequestReview(body as PullRequestReviewEvent);
|
|
870
|
-
break;
|
|
871
|
-
case "check_run":
|
|
872
|
-
result = await handleCheckRun(body as CheckRunEvent);
|
|
873
|
-
break;
|
|
874
|
-
case "check_suite":
|
|
875
|
-
result = await handleCheckSuite(body as CheckSuiteEvent);
|
|
876
|
-
break;
|
|
877
|
-
case "workflow_run":
|
|
878
|
-
result = await handleWorkflowRun(body as WorkflowRunEvent);
|
|
879
|
-
break;
|
|
880
|
-
default:
|
|
881
|
-
console.log(`[GitHub] Ignoring unsupported event type: ${eventType}`);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
885
|
-
res.end(JSON.stringify(result));
|
|
886
|
-
} catch (err) {
|
|
887
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
888
|
-
console.error(`[GitHub] ❌ Error handling ${eventType} event: ${errorMessage}`);
|
|
889
|
-
if (err instanceof Error && err.stack) {
|
|
890
|
-
console.error(err.stack);
|
|
891
|
-
}
|
|
892
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
893
|
-
res.end(JSON.stringify({ error: "Internal server error", message: errorMessage }));
|
|
894
|
-
}
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// ============================================================================
|
|
899
|
-
// REST API Endpoints (for frontend dashboard)
|
|
900
|
-
// ============================================================================
|
|
901
|
-
|
|
902
|
-
const queryParams = parseQueryParams(req.url || "");
|
|
903
|
-
|
|
904
|
-
// GET /api/agents - List all agents (optionally with tasks)
|
|
905
|
-
if (
|
|
906
|
-
req.method === "GET" &&
|
|
907
|
-
pathSegments[0] === "api" &&
|
|
908
|
-
pathSegments[1] === "agents" &&
|
|
909
|
-
!pathSegments[2]
|
|
910
|
-
) {
|
|
911
|
-
const includeTasks = queryParams.get("include") === "tasks";
|
|
912
|
-
const agents = includeTasks ? getAllAgentsWithTasks() : getAllAgents();
|
|
913
|
-
const agentsWithCapacity = agents.map(agentWithCapacity);
|
|
914
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
915
|
-
res.end(JSON.stringify({ agents: agentsWithCapacity }));
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// PUT /api/agents/:id/name - Update agent name (check before GET to avoid conflict)
|
|
920
|
-
if (
|
|
921
|
-
req.method === "PUT" &&
|
|
922
|
-
pathSegments[0] === "api" &&
|
|
923
|
-
pathSegments[1] === "agents" &&
|
|
924
|
-
pathSegments[2] &&
|
|
925
|
-
pathSegments[3] === "name"
|
|
926
|
-
) {
|
|
927
|
-
const agentId = pathSegments[2];
|
|
928
|
-
|
|
929
|
-
// Parse request body
|
|
930
|
-
const chunks: Buffer[] = [];
|
|
931
|
-
for await (const chunk of req) {
|
|
932
|
-
chunks.push(chunk as Buffer);
|
|
933
|
-
}
|
|
934
|
-
const bodyText = Buffer.concat(chunks).toString();
|
|
935
|
-
|
|
936
|
-
let body: { name?: string };
|
|
937
|
-
try {
|
|
938
|
-
body = JSON.parse(bodyText);
|
|
939
|
-
} catch {
|
|
940
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
941
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
if (!body.name || typeof body.name !== "string" || !body.name.trim()) {
|
|
946
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
947
|
-
res.end(JSON.stringify({ error: "Invalid name" }));
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
try {
|
|
952
|
-
const agent = updateAgentName(agentId, body.name.trim());
|
|
953
|
-
if (!agent) {
|
|
954
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
955
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
960
|
-
res.end(JSON.stringify(agentWithCapacity(agent)));
|
|
961
|
-
} catch (error) {
|
|
962
|
-
res.writeHead(409, { "Content-Type": "application/json" });
|
|
963
|
-
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
964
|
-
}
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// PUT /api/agents/:id/profile - Update agent profile (role, description, capabilities)
|
|
969
|
-
if (
|
|
970
|
-
req.method === "PUT" &&
|
|
971
|
-
pathSegments[0] === "api" &&
|
|
972
|
-
pathSegments[1] === "agents" &&
|
|
973
|
-
pathSegments[2] &&
|
|
974
|
-
pathSegments[3] === "profile"
|
|
975
|
-
) {
|
|
976
|
-
const agentId = pathSegments[2];
|
|
977
|
-
|
|
978
|
-
// Parse request body
|
|
979
|
-
const chunks: Buffer[] = [];
|
|
980
|
-
for await (const chunk of req) {
|
|
981
|
-
chunks.push(chunk as Buffer);
|
|
982
|
-
}
|
|
983
|
-
const bodyText = Buffer.concat(chunks).toString();
|
|
984
|
-
|
|
985
|
-
let body: { role?: string; description?: string; capabilities?: string[]; claudeMd?: string };
|
|
986
|
-
try {
|
|
987
|
-
body = JSON.parse(bodyText);
|
|
988
|
-
} catch {
|
|
989
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
990
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// At least one field must be provided
|
|
995
|
-
if (
|
|
996
|
-
body.role === undefined &&
|
|
997
|
-
body.description === undefined &&
|
|
998
|
-
body.capabilities === undefined &&
|
|
999
|
-
body.claudeMd === undefined
|
|
1000
|
-
) {
|
|
1001
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1002
|
-
res.end(
|
|
1003
|
-
JSON.stringify({
|
|
1004
|
-
error:
|
|
1005
|
-
"At least one field (role, description, capabilities, or claudeMd) must be provided",
|
|
1006
|
-
}),
|
|
1007
|
-
);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
// Validate role length if provided
|
|
1012
|
-
if (body.role !== undefined && body.role.length > 100) {
|
|
1013
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1014
|
-
res.end(JSON.stringify({ error: "Role must be 100 characters or less" }));
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Validate capabilities if provided
|
|
1019
|
-
if (body.capabilities !== undefined && !Array.isArray(body.capabilities)) {
|
|
1020
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1021
|
-
res.end(JSON.stringify({ error: "Capabilities must be an array of strings" }));
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Validate claudeMd size if provided (max 64KB)
|
|
1026
|
-
if (body.claudeMd !== undefined && body.claudeMd.length > 65536) {
|
|
1027
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1028
|
-
res.end(JSON.stringify({ error: "claudeMd must be 64KB or less" }));
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const agent = updateAgentProfile(agentId, {
|
|
1033
|
-
role: body.role,
|
|
1034
|
-
description: body.description,
|
|
1035
|
-
capabilities: body.capabilities,
|
|
1036
|
-
claudeMd: body.claudeMd,
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
if (!agent) {
|
|
1040
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1041
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1046
|
-
res.end(JSON.stringify(agentWithCapacity(agent)));
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// GET /api/agents/:id - Get single agent (optionally with tasks)
|
|
1051
|
-
if (
|
|
1052
|
-
req.method === "GET" &&
|
|
1053
|
-
pathSegments[0] === "api" &&
|
|
1054
|
-
pathSegments[1] === "agents" &&
|
|
1055
|
-
pathSegments[2] &&
|
|
1056
|
-
!pathSegments[3]
|
|
1057
|
-
) {
|
|
1058
|
-
const agentId = pathSegments[2];
|
|
1059
|
-
const includeTasks = queryParams.get("include") === "tasks";
|
|
1060
|
-
const agent = includeTasks ? getAgentWithTasks(agentId) : getAgentById(agentId);
|
|
1061
|
-
|
|
1062
|
-
if (!agent) {
|
|
1063
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1064
|
-
res.end(JSON.stringify({ error: "Agent not found" }));
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1069
|
-
res.end(JSON.stringify(agentWithCapacity(agent)));
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// GET /api/tasks - List all tasks (with optional filters: status, agentId, search)
|
|
1074
|
-
if (
|
|
1075
|
-
req.method === "GET" &&
|
|
1076
|
-
pathSegments[0] === "api" &&
|
|
1077
|
-
pathSegments[1] === "tasks" &&
|
|
1078
|
-
!pathSegments[2]
|
|
1079
|
-
) {
|
|
1080
|
-
const status = queryParams.get("status") as import("./types").AgentTaskStatus | null;
|
|
1081
|
-
const agentId = queryParams.get("agentId");
|
|
1082
|
-
const search = queryParams.get("search");
|
|
1083
|
-
const filters = {
|
|
1084
|
-
status: status || undefined,
|
|
1085
|
-
agentId: agentId || undefined,
|
|
1086
|
-
search: search || undefined,
|
|
1087
|
-
};
|
|
1088
|
-
const tasks = getAllTasks(filters);
|
|
1089
|
-
const total = getTasksCount(filters);
|
|
1090
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1091
|
-
res.end(JSON.stringify({ tasks, total }));
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// POST /api/tasks - Create a new task
|
|
1096
|
-
if (
|
|
1097
|
-
req.method === "POST" &&
|
|
1098
|
-
pathSegments[0] === "api" &&
|
|
1099
|
-
pathSegments[1] === "tasks" &&
|
|
1100
|
-
!pathSegments[2]
|
|
1101
|
-
) {
|
|
1102
|
-
// Parse request body
|
|
1103
|
-
const chunks: Buffer[] = [];
|
|
1104
|
-
for await (const chunk of req) {
|
|
1105
|
-
chunks.push(chunk);
|
|
1106
|
-
}
|
|
1107
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1108
|
-
|
|
1109
|
-
// Validate required fields
|
|
1110
|
-
if (!body.task || typeof body.task !== "string") {
|
|
1111
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1112
|
-
res.end(JSON.stringify({ error: "Missing or invalid 'task' field" }));
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
try {
|
|
1117
|
-
// Create task with provided options
|
|
1118
|
-
const task = createTaskExtended(body.task, {
|
|
1119
|
-
agentId: body.agentId || undefined,
|
|
1120
|
-
creatorAgentId: myAgentId || undefined,
|
|
1121
|
-
taskType: body.taskType || undefined,
|
|
1122
|
-
tags: body.tags || undefined,
|
|
1123
|
-
priority: body.priority || 50,
|
|
1124
|
-
dependsOn: body.dependsOn || undefined,
|
|
1125
|
-
offeredTo: body.offeredTo || undefined,
|
|
1126
|
-
source: body.source || "api",
|
|
1127
|
-
});
|
|
1128
|
-
|
|
1129
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1130
|
-
res.end(JSON.stringify(task));
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
console.error("[HTTP] Failed to create task:", error);
|
|
1133
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1134
|
-
res.end(JSON.stringify({ error: "Failed to create task" }));
|
|
1135
|
-
}
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// GET /api/tasks/:id - Get single task with logs
|
|
1140
|
-
if (
|
|
1141
|
-
req.method === "GET" &&
|
|
1142
|
-
pathSegments[0] === "api" &&
|
|
1143
|
-
pathSegments[1] === "tasks" &&
|
|
1144
|
-
pathSegments[2]
|
|
1145
|
-
) {
|
|
1146
|
-
const taskId = pathSegments[2];
|
|
1147
|
-
const task = getTaskById(taskId);
|
|
1148
|
-
|
|
1149
|
-
if (!task) {
|
|
1150
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1151
|
-
res.end(JSON.stringify({ error: "Task not found" }));
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const logs = getLogsByTaskId(taskId);
|
|
1156
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1157
|
-
res.end(JSON.stringify({ ...task, logs }));
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// POST /api/tasks/:id/finish - Mark task as completed or failed (runner wrapper endpoint)
|
|
1162
|
-
// This endpoint is called by the runner when a Claude process exits to ensure task status is updated
|
|
1163
|
-
if (
|
|
1164
|
-
req.method === "POST" &&
|
|
1165
|
-
pathSegments[0] === "api" &&
|
|
1166
|
-
pathSegments[1] === "tasks" &&
|
|
1167
|
-
pathSegments[2] &&
|
|
1168
|
-
pathSegments[3] === "finish"
|
|
1169
|
-
) {
|
|
1170
|
-
if (!myAgentId) {
|
|
1171
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1172
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
const taskId = pathSegments[2];
|
|
1177
|
-
|
|
1178
|
-
// Parse request body
|
|
1179
|
-
const chunks: Buffer[] = [];
|
|
1180
|
-
for await (const chunk of req) {
|
|
1181
|
-
chunks.push(chunk);
|
|
1182
|
-
}
|
|
1183
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1184
|
-
|
|
1185
|
-
// Validate status field
|
|
1186
|
-
if (!body.status || !["completed", "failed"].includes(body.status)) {
|
|
1187
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1188
|
-
res.end(
|
|
1189
|
-
JSON.stringify({
|
|
1190
|
-
error: "Missing or invalid 'status' field (must be 'completed' or 'failed')",
|
|
1191
|
-
}),
|
|
1192
|
-
);
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const result = getDb().transaction(() => {
|
|
1197
|
-
const task = getTaskById(taskId);
|
|
1198
|
-
|
|
1199
|
-
if (!task) {
|
|
1200
|
-
return { error: "Task not found", status: 404 };
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Only allow the assigned agent (or task creator if unassigned) to finish the task
|
|
1204
|
-
if (task.agentId && task.agentId !== myAgentId) {
|
|
1205
|
-
return { error: "Task is assigned to another agent", status: 403 };
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
// Only finish tasks that are in_progress (prevent double-finishing)
|
|
1209
|
-
if (task.status !== "in_progress") {
|
|
1210
|
-
// Task already finished or not started - return success with current state
|
|
1211
|
-
return { task, alreadyFinished: true };
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
let updatedTask: typeof task;
|
|
1215
|
-
if (body.status === "completed") {
|
|
1216
|
-
const result = completeTask(
|
|
1217
|
-
taskId,
|
|
1218
|
-
body.output || "Completed by runner wrapper (no explicit output)",
|
|
1219
|
-
);
|
|
1220
|
-
if (!result) {
|
|
1221
|
-
return { error: "Failed to complete task", status: 500 };
|
|
1222
|
-
}
|
|
1223
|
-
updatedTask = result;
|
|
1224
|
-
} else {
|
|
1225
|
-
const result = failTask(
|
|
1226
|
-
taskId,
|
|
1227
|
-
body.failureReason || "Process exited without explicit completion",
|
|
1228
|
-
);
|
|
1229
|
-
if (!result) {
|
|
1230
|
-
return { error: "Failed to mark task as failed", status: 500 };
|
|
1231
|
-
}
|
|
1232
|
-
updatedTask = result;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// Update agent status based on remaining capacity
|
|
1236
|
-
if (task.agentId) {
|
|
1237
|
-
updateAgentStatusFromCapacity(task.agentId);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
return { task: updatedTask };
|
|
1241
|
-
})();
|
|
1242
|
-
|
|
1243
|
-
if ("error" in result) {
|
|
1244
|
-
res.writeHead(result.status ?? 500, { "Content-Type": "application/json" });
|
|
1245
|
-
res.end(JSON.stringify({ error: result.error }));
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1250
|
-
res.end(
|
|
1251
|
-
JSON.stringify({
|
|
1252
|
-
success: true,
|
|
1253
|
-
alreadyFinished: result.alreadyFinished ?? false,
|
|
1254
|
-
task: result.task,
|
|
1255
|
-
}),
|
|
1256
|
-
);
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// GET /api/paused-tasks - Get paused tasks for this agent
|
|
1261
|
-
if (req.method === "GET" && pathSegments[0] === "api" && pathSegments[1] === "paused-tasks") {
|
|
1262
|
-
if (!myAgentId) {
|
|
1263
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1264
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
const pausedTasks = getPausedTasksForAgent(myAgentId);
|
|
1269
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1270
|
-
res.end(JSON.stringify({ tasks: pausedTasks }));
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// POST /api/tasks/:id/pause - Pause an in-progress task (for graceful shutdown)
|
|
1275
|
-
if (
|
|
1276
|
-
req.method === "POST" &&
|
|
1277
|
-
pathSegments[0] === "api" &&
|
|
1278
|
-
pathSegments[1] === "tasks" &&
|
|
1279
|
-
pathSegments[2] &&
|
|
1280
|
-
pathSegments[3] === "pause"
|
|
1281
|
-
) {
|
|
1282
|
-
if (!myAgentId) {
|
|
1283
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1284
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const taskId = pathSegments[2];
|
|
1289
|
-
const task = getTaskById(taskId);
|
|
1290
|
-
|
|
1291
|
-
if (!task) {
|
|
1292
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1293
|
-
res.end(JSON.stringify({ error: "Task not found" }));
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// Only allow the assigned agent to pause their own task
|
|
1298
|
-
if (task.agentId !== myAgentId) {
|
|
1299
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1300
|
-
res.end(JSON.stringify({ error: "Task belongs to another agent" }));
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
if (task.status !== "in_progress") {
|
|
1305
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1306
|
-
res.end(JSON.stringify({ error: `Task status is '${task.status}', not 'in_progress'` }));
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const pausedTask = pauseTask(taskId);
|
|
1311
|
-
if (!pausedTask) {
|
|
1312
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1313
|
-
res.end(JSON.stringify({ error: "Failed to pause task" }));
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1318
|
-
res.end(JSON.stringify({ success: true, task: pausedTask }));
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
// POST /api/tasks/:id/resume - Resume a paused task
|
|
1323
|
-
if (
|
|
1324
|
-
req.method === "POST" &&
|
|
1325
|
-
pathSegments[0] === "api" &&
|
|
1326
|
-
pathSegments[1] === "tasks" &&
|
|
1327
|
-
pathSegments[2] &&
|
|
1328
|
-
pathSegments[3] === "resume"
|
|
1329
|
-
) {
|
|
1330
|
-
if (!myAgentId) {
|
|
1331
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1332
|
-
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
const taskId = pathSegments[2];
|
|
1337
|
-
const task = getTaskById(taskId);
|
|
1338
|
-
|
|
1339
|
-
if (!task) {
|
|
1340
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1341
|
-
res.end(JSON.stringify({ error: "Task not found" }));
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// Only allow the assigned agent to resume their own task
|
|
1346
|
-
if (task.agentId !== myAgentId) {
|
|
1347
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1348
|
-
res.end(JSON.stringify({ error: "Task belongs to another agent" }));
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
if (task.status !== "paused") {
|
|
1353
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1354
|
-
res.end(JSON.stringify({ error: `Task status is '${task.status}', not 'paused'` }));
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
const resumedTask = resumeTask(taskId);
|
|
1359
|
-
if (!resumedTask) {
|
|
1360
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1361
|
-
res.end(JSON.stringify({ error: "Failed to resume task" }));
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1366
|
-
res.end(JSON.stringify({ success: true, task: resumedTask }));
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
// GET /api/logs - List recent logs (optionally filtered by agentId)
|
|
1371
|
-
if (req.method === "GET" && pathSegments[0] === "api" && pathSegments[1] === "logs") {
|
|
1372
|
-
const limitParam = queryParams.get("limit");
|
|
1373
|
-
const limit = limitParam ? parseInt(limitParam, 10) : 100;
|
|
1374
|
-
const agentId = queryParams.get("agentId");
|
|
1375
|
-
let logs: AgentLog[] = [];
|
|
1376
|
-
if (agentId) {
|
|
1377
|
-
logs = getLogsByAgentId(agentId).slice(0, limit);
|
|
1378
|
-
} else {
|
|
1379
|
-
logs = getAllLogs(limit);
|
|
1380
|
-
}
|
|
1381
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1382
|
-
res.end(JSON.stringify({ logs }));
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// GET /api/stats - Dashboard summary stats
|
|
1387
|
-
if (req.method === "GET" && pathSegments[0] === "api" && pathSegments[1] === "stats") {
|
|
1388
|
-
const agents = getAllAgents();
|
|
1389
|
-
const taskStats = getTaskStats();
|
|
1390
|
-
|
|
1391
|
-
const stats = {
|
|
1392
|
-
agents: {
|
|
1393
|
-
total: agents.length,
|
|
1394
|
-
idle: agents.filter((a) => a.status === "idle").length,
|
|
1395
|
-
busy: agents.filter((a) => a.status === "busy").length,
|
|
1396
|
-
offline: agents.filter((a) => a.status === "offline").length,
|
|
1397
|
-
},
|
|
1398
|
-
tasks: {
|
|
1399
|
-
total: taskStats.total,
|
|
1400
|
-
unassigned: taskStats.unassigned,
|
|
1401
|
-
offered: taskStats.offered,
|
|
1402
|
-
reviewing: taskStats.reviewing,
|
|
1403
|
-
pending: taskStats.pending,
|
|
1404
|
-
in_progress: taskStats.in_progress,
|
|
1405
|
-
paused: taskStats.paused,
|
|
1406
|
-
completed: taskStats.completed,
|
|
1407
|
-
failed: taskStats.failed,
|
|
1408
|
-
},
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1412
|
-
res.end(JSON.stringify(stats));
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
// GET /api/services - List all services (with optional filters: status, agentId, name)
|
|
1417
|
-
if (
|
|
1418
|
-
req.method === "GET" &&
|
|
1419
|
-
pathSegments[0] === "api" &&
|
|
1420
|
-
pathSegments[1] === "services" &&
|
|
1421
|
-
!pathSegments[2]
|
|
1422
|
-
) {
|
|
1423
|
-
const status = queryParams.get("status") as import("./types").ServiceStatus | null;
|
|
1424
|
-
const agentId = queryParams.get("agentId");
|
|
1425
|
-
const name = queryParams.get("name");
|
|
1426
|
-
const services = getAllServices({
|
|
1427
|
-
status: status || undefined,
|
|
1428
|
-
agentId: agentId || undefined,
|
|
1429
|
-
name: name || undefined,
|
|
1430
|
-
});
|
|
1431
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1432
|
-
res.end(JSON.stringify({ services }));
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
// GET /api/scheduled-tasks - List all scheduled tasks (with optional filters: enabled, name)
|
|
1437
|
-
if (
|
|
1438
|
-
req.method === "GET" &&
|
|
1439
|
-
pathSegments[0] === "api" &&
|
|
1440
|
-
pathSegments[1] === "scheduled-tasks" &&
|
|
1441
|
-
!pathSegments[2]
|
|
1442
|
-
) {
|
|
1443
|
-
const enabledParam = queryParams.get("enabled");
|
|
1444
|
-
const name = queryParams.get("name");
|
|
1445
|
-
const scheduledTasks = getScheduledTasks({
|
|
1446
|
-
enabled: enabledParam !== null ? enabledParam === "true" : undefined,
|
|
1447
|
-
name: name || undefined,
|
|
1448
|
-
});
|
|
1449
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1450
|
-
res.end(JSON.stringify({ scheduledTasks }));
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
// ============================================================================
|
|
1455
|
-
// Epic Endpoints
|
|
1456
|
-
// ============================================================================
|
|
1457
|
-
|
|
1458
|
-
// GET /api/epics - List all epics
|
|
1459
|
-
if (
|
|
1460
|
-
req.method === "GET" &&
|
|
1461
|
-
pathSegments[0] === "api" &&
|
|
1462
|
-
pathSegments[1] === "epics" &&
|
|
1463
|
-
!pathSegments[2]
|
|
1464
|
-
) {
|
|
1465
|
-
const status = queryParams.get("status") as EpicStatus | null;
|
|
1466
|
-
const search = queryParams.get("search");
|
|
1467
|
-
const leadAgentId = queryParams.get("leadAgentId");
|
|
1468
|
-
const epics = getEpics({
|
|
1469
|
-
status: status || undefined,
|
|
1470
|
-
search: search || undefined,
|
|
1471
|
-
leadAgentId: leadAgentId || undefined,
|
|
1472
|
-
});
|
|
1473
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1474
|
-
res.end(JSON.stringify({ epics, total: epics.length }));
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
// POST /api/epics - Create a new epic
|
|
1479
|
-
if (
|
|
1480
|
-
req.method === "POST" &&
|
|
1481
|
-
pathSegments[0] === "api" &&
|
|
1482
|
-
pathSegments[1] === "epics" &&
|
|
1483
|
-
!pathSegments[2]
|
|
1484
|
-
) {
|
|
1485
|
-
const chunks: Buffer[] = [];
|
|
1486
|
-
for await (const chunk of req) {
|
|
1487
|
-
chunks.push(chunk);
|
|
1488
|
-
}
|
|
1489
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1490
|
-
|
|
1491
|
-
if (!body.name || !body.goal) {
|
|
1492
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1493
|
-
res.end(JSON.stringify({ error: "Missing required fields: name, goal" }));
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
try {
|
|
1498
|
-
const epic = createEpic({
|
|
1499
|
-
...body,
|
|
1500
|
-
createdByAgentId: myAgentId || undefined,
|
|
1501
|
-
});
|
|
1502
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1503
|
-
res.end(JSON.stringify(epic));
|
|
1504
|
-
} catch (_error) {
|
|
1505
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1506
|
-
res.end(JSON.stringify({ error: "Failed to create epic" }));
|
|
1507
|
-
}
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// GET /api/epics/:id - Get single epic with progress and tasks
|
|
1512
|
-
if (
|
|
1513
|
-
req.method === "GET" &&
|
|
1514
|
-
pathSegments[0] === "api" &&
|
|
1515
|
-
pathSegments[1] === "epics" &&
|
|
1516
|
-
pathSegments[2] &&
|
|
1517
|
-
!pathSegments[3]
|
|
1518
|
-
) {
|
|
1519
|
-
const epicId = pathSegments[2];
|
|
1520
|
-
const epic = getEpicWithProgress(epicId);
|
|
1521
|
-
|
|
1522
|
-
if (!epic) {
|
|
1523
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1524
|
-
res.end(JSON.stringify({ error: "Epic not found" }));
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
const tasks = getTasksByEpicId(epicId);
|
|
1529
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1530
|
-
res.end(JSON.stringify({ ...epic, tasks }));
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// PUT /api/epics/:id - Update an epic
|
|
1535
|
-
if (
|
|
1536
|
-
req.method === "PUT" &&
|
|
1537
|
-
pathSegments[0] === "api" &&
|
|
1538
|
-
pathSegments[1] === "epics" &&
|
|
1539
|
-
pathSegments[2] &&
|
|
1540
|
-
!pathSegments[3]
|
|
1541
|
-
) {
|
|
1542
|
-
const epicId = pathSegments[2];
|
|
1543
|
-
const chunks: Buffer[] = [];
|
|
1544
|
-
for await (const chunk of req) {
|
|
1545
|
-
chunks.push(chunk);
|
|
1546
|
-
}
|
|
1547
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1548
|
-
|
|
1549
|
-
const epic = updateEpic(epicId, body);
|
|
1550
|
-
if (!epic) {
|
|
1551
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1552
|
-
res.end(JSON.stringify({ error: "Epic not found" }));
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1557
|
-
res.end(JSON.stringify(epic));
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// DELETE /api/epics/:id - Delete an epic
|
|
1562
|
-
if (
|
|
1563
|
-
req.method === "DELETE" &&
|
|
1564
|
-
pathSegments[0] === "api" &&
|
|
1565
|
-
pathSegments[1] === "epics" &&
|
|
1566
|
-
pathSegments[2] &&
|
|
1567
|
-
!pathSegments[3]
|
|
1568
|
-
) {
|
|
1569
|
-
const epicId = pathSegments[2];
|
|
1570
|
-
const deleted = deleteEpic(epicId);
|
|
1571
|
-
|
|
1572
|
-
if (!deleted) {
|
|
1573
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1574
|
-
res.end(JSON.stringify({ error: "Epic not found" }));
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1579
|
-
res.end(JSON.stringify({ success: true }));
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// POST /api/epics/:id/tasks - Add task to epic (create new or assign existing)
|
|
1584
|
-
if (
|
|
1585
|
-
req.method === "POST" &&
|
|
1586
|
-
pathSegments[0] === "api" &&
|
|
1587
|
-
pathSegments[1] === "epics" &&
|
|
1588
|
-
pathSegments[2] &&
|
|
1589
|
-
pathSegments[3] === "tasks"
|
|
1590
|
-
) {
|
|
1591
|
-
const epicId = pathSegments[2];
|
|
1592
|
-
const epic = getEpicById(epicId);
|
|
1593
|
-
|
|
1594
|
-
if (!epic) {
|
|
1595
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1596
|
-
res.end(JSON.stringify({ error: "Epic not found" }));
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
const chunks: Buffer[] = [];
|
|
1601
|
-
for await (const chunk of req) {
|
|
1602
|
-
chunks.push(chunk);
|
|
1603
|
-
}
|
|
1604
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1605
|
-
|
|
1606
|
-
// If taskId provided, assign existing task
|
|
1607
|
-
if (body.taskId) {
|
|
1608
|
-
const task = assignTaskToEpic(body.taskId, epicId);
|
|
1609
|
-
if (!task) {
|
|
1610
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1611
|
-
res.end(JSON.stringify({ error: "Task not found" }));
|
|
1612
|
-
return;
|
|
1613
|
-
}
|
|
1614
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1615
|
-
res.end(JSON.stringify(task));
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
// Otherwise create new task in this epic
|
|
1620
|
-
if (!body.task) {
|
|
1621
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1622
|
-
res.end(JSON.stringify({ error: "Missing task description or taskId" }));
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
try {
|
|
1627
|
-
const task = createTaskExtended(body.task, {
|
|
1628
|
-
...body,
|
|
1629
|
-
epicId,
|
|
1630
|
-
creatorAgentId: myAgentId || undefined,
|
|
1631
|
-
tags: [...(body.tags || []), `epic:${epic.name}`],
|
|
1632
|
-
source: "api",
|
|
1633
|
-
});
|
|
1634
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1635
|
-
res.end(JSON.stringify(task));
|
|
1636
|
-
} catch (_error) {
|
|
1637
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1638
|
-
res.end(JSON.stringify({ error: "Failed to create task" }));
|
|
1639
|
-
}
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
// GET /api/channels - List all channels
|
|
1644
|
-
if (
|
|
1645
|
-
req.method === "GET" &&
|
|
1646
|
-
pathSegments[0] === "api" &&
|
|
1647
|
-
pathSegments[1] === "channels" &&
|
|
1648
|
-
!pathSegments[2]
|
|
1649
|
-
) {
|
|
1650
|
-
const channels = getAllChannels();
|
|
1651
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1652
|
-
res.end(JSON.stringify({ channels }));
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
// GET /api/channels/:id/messages - Get messages in a channel
|
|
1657
|
-
if (
|
|
1658
|
-
req.method === "GET" &&
|
|
1659
|
-
pathSegments[0] === "api" &&
|
|
1660
|
-
pathSegments[1] === "channels" &&
|
|
1661
|
-
pathSegments[2] &&
|
|
1662
|
-
pathSegments[3] === "messages" &&
|
|
1663
|
-
!pathSegments[4]
|
|
1664
|
-
) {
|
|
1665
|
-
const channelId = pathSegments[2];
|
|
1666
|
-
const channel = getChannelById(channelId);
|
|
1667
|
-
|
|
1668
|
-
if (!channel) {
|
|
1669
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1670
|
-
res.end(JSON.stringify({ error: "Channel not found" }));
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
const limitParam = queryParams.get("limit");
|
|
1675
|
-
const limit = limitParam ? parseInt(limitParam, 10) : 50;
|
|
1676
|
-
const since = queryParams.get("since") || undefined;
|
|
1677
|
-
const before = queryParams.get("before") || undefined;
|
|
1678
|
-
|
|
1679
|
-
const messages = getChannelMessages(channelId, { limit, since, before });
|
|
1680
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1681
|
-
res.end(JSON.stringify({ messages }));
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
// GET /api/channels/:id/messages/:messageId/thread - Get thread messages
|
|
1686
|
-
if (
|
|
1687
|
-
req.method === "GET" &&
|
|
1688
|
-
pathSegments[0] === "api" &&
|
|
1689
|
-
pathSegments[1] === "channels" &&
|
|
1690
|
-
pathSegments[2] &&
|
|
1691
|
-
pathSegments[3] === "messages" &&
|
|
1692
|
-
pathSegments[4] &&
|
|
1693
|
-
pathSegments[5] === "thread"
|
|
1694
|
-
) {
|
|
1695
|
-
const channelId = pathSegments[2];
|
|
1696
|
-
const parentMessageId = pathSegments[4];
|
|
1697
|
-
|
|
1698
|
-
const channel = getChannelById(channelId);
|
|
1699
|
-
if (!channel) {
|
|
1700
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1701
|
-
res.end(JSON.stringify({ error: "Channel not found" }));
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// Get all messages that reply to this message
|
|
1706
|
-
const allMessages = getChannelMessages(channelId, { limit: 1000 });
|
|
1707
|
-
const threadMessages = allMessages.filter((m) => m.replyToId === parentMessageId);
|
|
1708
|
-
|
|
1709
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1710
|
-
res.end(JSON.stringify({ messages: threadMessages }));
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
// POST /api/channels/:id/messages - Post a message
|
|
1715
|
-
if (
|
|
1716
|
-
req.method === "POST" &&
|
|
1717
|
-
pathSegments[0] === "api" &&
|
|
1718
|
-
pathSegments[1] === "channels" &&
|
|
1719
|
-
pathSegments[2] &&
|
|
1720
|
-
pathSegments[3] === "messages"
|
|
1721
|
-
) {
|
|
1722
|
-
const channelId = pathSegments[2];
|
|
1723
|
-
const channel = getChannelById(channelId);
|
|
1724
|
-
|
|
1725
|
-
if (!channel) {
|
|
1726
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1727
|
-
res.end(JSON.stringify({ error: "Channel not found" }));
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
// Parse request body
|
|
1732
|
-
const chunks: Buffer[] = [];
|
|
1733
|
-
for await (const chunk of req) {
|
|
1734
|
-
chunks.push(chunk);
|
|
1735
|
-
}
|
|
1736
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1737
|
-
|
|
1738
|
-
if (!body.content || typeof body.content !== "string") {
|
|
1739
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1740
|
-
res.end(JSON.stringify({ error: "Missing or invalid content" }));
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// agentId is optional (null for human users)
|
|
1745
|
-
const agentId = body.agentId || null;
|
|
1746
|
-
|
|
1747
|
-
// If agentId provided, verify agent exists
|
|
1748
|
-
if (agentId) {
|
|
1749
|
-
const agent = getAgentById(agentId);
|
|
1750
|
-
if (!agent) {
|
|
1751
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1752
|
-
res.end(JSON.stringify({ error: "Invalid agentId" }));
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
const message = postMessage(channelId, agentId, body.content, {
|
|
1758
|
-
replyToId: body.replyToId,
|
|
1759
|
-
mentions: body.mentions,
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1763
|
-
res.end(JSON.stringify(message));
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
if (req.url !== "/mcp") {
|
|
1768
|
-
res.writeHead(404);
|
|
1769
|
-
res.end("Not Found");
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
if (req.method === "POST") {
|
|
1774
|
-
const chunks: Buffer[] = [];
|
|
1775
|
-
for await (const chunk of req) {
|
|
1776
|
-
chunks.push(chunk);
|
|
1777
|
-
}
|
|
1778
|
-
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
1779
|
-
|
|
1780
|
-
let transport: StreamableHTTPServerTransport;
|
|
1781
|
-
|
|
1782
|
-
if (sessionId && transports[sessionId]) {
|
|
1783
|
-
transport = transports[sessionId];
|
|
1784
|
-
} else if (!sessionId && isInitializeRequest(body)) {
|
|
1785
|
-
transport = new StreamableHTTPServerTransport({
|
|
1786
|
-
sessionIdGenerator: () => randomUUID(),
|
|
1787
|
-
onsessioninitialized: (id) => {
|
|
1788
|
-
transports[id] = transport;
|
|
1789
|
-
},
|
|
1790
|
-
onsessionclosed: (id) => {
|
|
1791
|
-
delete transports[id];
|
|
1792
|
-
},
|
|
1793
|
-
});
|
|
1794
|
-
|
|
1795
|
-
transport.onclose = () => {
|
|
1796
|
-
if (transport.sessionId) {
|
|
1797
|
-
delete transports[transport.sessionId];
|
|
1798
|
-
}
|
|
1799
|
-
};
|
|
1800
|
-
|
|
1801
|
-
const server = createServer();
|
|
1802
|
-
await server.connect(transport);
|
|
1803
|
-
} else {
|
|
1804
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1805
|
-
res.end(
|
|
1806
|
-
JSON.stringify({
|
|
1807
|
-
jsonrpc: "2.0",
|
|
1808
|
-
error: { code: -32000, message: "Invalid session" },
|
|
1809
|
-
id: null,
|
|
1810
|
-
}),
|
|
1811
|
-
);
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
await transport.handleRequest(req, res, body);
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
if (req.method === "GET" || req.method === "DELETE") {
|
|
1820
|
-
if (sessionId && transports[sessionId]) {
|
|
1821
|
-
await transports[sessionId].handleRequest(req, res);
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
res.writeHead(400);
|
|
1825
|
-
res.end("Invalid session");
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
res.writeHead(405);
|
|
1830
|
-
res.end("Method not allowed");
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
// Store references in globalThis for hot reload persistence
|
|
1834
|
-
globalState.__httpServer = httpServer;
|
|
1835
|
-
globalState.__transports = transports;
|
|
1836
|
-
|
|
1837
|
-
async function shutdown() {
|
|
1838
|
-
console.log("Shutting down HTTP server...");
|
|
1839
|
-
|
|
1840
|
-
// Stop scheduler (if enabled)
|
|
1841
|
-
if (hasCapability("scheduling")) {
|
|
1842
|
-
const { stopScheduler } = await import("./scheduler");
|
|
1843
|
-
stopScheduler();
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
// Stop Slack bot
|
|
1847
|
-
await stopSlackApp();
|
|
1848
|
-
|
|
1849
|
-
// Close all active transports (SSE connections, etc.)
|
|
1850
|
-
for (const [id, transport] of Object.entries(transports)) {
|
|
1851
|
-
console.log(`[HTTP] Closing transport ${id}`);
|
|
1852
|
-
transport.close();
|
|
1853
|
-
delete transports[id];
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
// Close all active connections forcefully
|
|
1857
|
-
httpServer.closeAllConnections();
|
|
1858
|
-
httpServer.close(() => {
|
|
1859
|
-
closeDb();
|
|
1860
|
-
console.log("MCP HTTP server closed, and database connection closed");
|
|
1861
|
-
process.exit(0);
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// Only register SIGINT handler once (avoid duplicates on hot reload)
|
|
1866
|
-
if (!globalState.__sigintRegistered) {
|
|
1867
|
-
globalState.__sigintRegistered = true;
|
|
1868
|
-
process.on("SIGINT", shutdown);
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
httpServer
|
|
1872
|
-
.listen(port, async () => {
|
|
1873
|
-
console.log(`MCP HTTP server running on http://localhost:${port}/mcp`);
|
|
1874
|
-
|
|
1875
|
-
// Start Slack bot (if configured)
|
|
1876
|
-
await startSlackApp();
|
|
1877
|
-
|
|
1878
|
-
// Initialize GitHub webhook handler (if configured)
|
|
1879
|
-
initGitHub();
|
|
1880
|
-
|
|
1881
|
-
// Start scheduler (if enabled)
|
|
1882
|
-
if (hasCapability("scheduling")) {
|
|
1883
|
-
const { startScheduler } = await import("./scheduler");
|
|
1884
|
-
const intervalMs = Number(process.env.SCHEDULER_INTERVAL_MS) || 10000;
|
|
1885
|
-
startScheduler(intervalMs);
|
|
1886
|
-
}
|
|
1887
|
-
})
|
|
1888
|
-
.on("error", (err) => {
|
|
1889
|
-
console.error("HTTP Server Error:", err);
|
|
1890
|
-
});
|
|
1
|
+
import "./http/index";
|