@decibelsystems/tools 2.0.0
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/LICENSE +21 -0
- package/README.md +342 -0
- package/dist/agentic/compiler.d.ts +21 -0
- package/dist/agentic/compiler.d.ts.map +1 -0
- package/dist/agentic/compiler.js +267 -0
- package/dist/agentic/compiler.js.map +1 -0
- package/dist/agentic/golden.d.ts +25 -0
- package/dist/agentic/golden.d.ts.map +1 -0
- package/dist/agentic/golden.js +255 -0
- package/dist/agentic/golden.js.map +1 -0
- package/dist/agentic/index.d.ts +17 -0
- package/dist/agentic/index.d.ts.map +1 -0
- package/dist/agentic/index.js +153 -0
- package/dist/agentic/index.js.map +1 -0
- package/dist/agentic/linter.d.ts +20 -0
- package/dist/agentic/linter.d.ts.map +1 -0
- package/dist/agentic/linter.js +340 -0
- package/dist/agentic/linter.js.map +1 -0
- package/dist/agentic/renderer.d.ts +17 -0
- package/dist/agentic/renderer.d.ts.map +1 -0
- package/dist/agentic/renderer.js +277 -0
- package/dist/agentic/renderer.js.map +1 -0
- package/dist/agentic/types.d.ts +199 -0
- package/dist/agentic/types.d.ts.map +1 -0
- package/dist/agentic/types.js +8 -0
- package/dist/agentic/types.js.map +1 -0
- package/dist/architectAdrs.d.ts +32 -0
- package/dist/architectAdrs.d.ts.map +1 -0
- package/dist/architectAdrs.js +162 -0
- package/dist/architectAdrs.js.map +1 -0
- package/dist/client/facade-client.d.ts +41 -0
- package/dist/client/facade-client.d.ts.map +1 -0
- package/dist/client/facade-client.js +243 -0
- package/dist/client/facade-client.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/transports.d.ts +78 -0
- package/dist/client/transports.d.ts.map +1 -0
- package/dist/client/transports.js +258 -0
- package/dist/client/transports.js.map +1 -0
- package/dist/client/types.d.ts +49 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +8 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon.d.ts +77 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +374 -0
- package/dist/daemon.js.map +1 -0
- package/dist/daemonConfig.d.ts +43 -0
- package/dist/daemonConfig.d.ts.map +1 -0
- package/dist/daemonConfig.js +113 -0
- package/dist/daemonConfig.js.map +1 -0
- package/dist/dataRoot.d.ts +5 -0
- package/dist/dataRoot.d.ts.map +1 -0
- package/dist/dataRoot.js +23 -0
- package/dist/dataRoot.js.map +1 -0
- package/dist/decibelPaths.d.ts +42 -0
- package/dist/decibelPaths.d.ts.map +1 -0
- package/dist/decibelPaths.js +150 -0
- package/dist/decibelPaths.js.map +1 -0
- package/dist/facades/definitions.d.ts +6 -0
- package/dist/facades/definitions.d.ts.map +1 -0
- package/dist/facades/definitions.js +450 -0
- package/dist/facades/definitions.js.map +1 -0
- package/dist/facades/index.d.ts +27 -0
- package/dist/facades/index.d.ts.map +1 -0
- package/dist/facades/index.js +124 -0
- package/dist/facades/index.js.map +1 -0
- package/dist/facades/types.d.ts +38 -0
- package/dist/facades/types.d.ts.map +1 -0
- package/dist/facades/types.js +8 -0
- package/dist/facades/types.js.map +1 -0
- package/dist/httpServer.d.ts +66 -0
- package/dist/httpServer.d.ts.map +1 -0
- package/dist/httpServer.js +1723 -0
- package/dist/httpServer.js.map +1 -0
- package/dist/kernel.d.ts +87 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +256 -0
- package/dist/kernel.js.map +1 -0
- package/dist/lib/agent-services/assumptions.d.ts +16 -0
- package/dist/lib/agent-services/assumptions.d.ts.map +1 -0
- package/dist/lib/agent-services/assumptions.js +284 -0
- package/dist/lib/agent-services/assumptions.js.map +1 -0
- package/dist/lib/agent-services/context-pack.d.ts +6 -0
- package/dist/lib/agent-services/context-pack.d.ts.map +1 -0
- package/dist/lib/agent-services/context-pack.js +354 -0
- package/dist/lib/agent-services/context-pack.js.map +1 -0
- package/dist/lib/agent-services/drift-guard.d.ts +14 -0
- package/dist/lib/agent-services/drift-guard.d.ts.map +1 -0
- package/dist/lib/agent-services/drift-guard.js +355 -0
- package/dist/lib/agent-services/drift-guard.js.map +1 -0
- package/dist/lib/agent-services/index.d.ts +5 -0
- package/dist/lib/agent-services/index.d.ts.map +1 -0
- package/dist/lib/agent-services/index.js +10 -0
- package/dist/lib/agent-services/index.js.map +1 -0
- package/dist/lib/benchmark.d.ts +110 -0
- package/dist/lib/benchmark.d.ts.map +1 -0
- package/dist/lib/benchmark.js +338 -0
- package/dist/lib/benchmark.js.map +1 -0
- package/dist/lib/supabase.d.ts +123 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +91 -0
- package/dist/lib/supabase.js.map +1 -0
- package/dist/license.d.ts +30 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +131 -0
- package/dist/license.js.map +1 -0
- package/dist/projectPaths.d.ts +27 -0
- package/dist/projectPaths.d.ts.map +1 -0
- package/dist/projectPaths.js +86 -0
- package/dist/projectPaths.js.map +1 -0
- package/dist/projectRegistry.d.ts +97 -0
- package/dist/projectRegistry.d.ts.map +1 -0
- package/dist/projectRegistry.js +374 -0
- package/dist/projectRegistry.js.map +1 -0
- package/dist/sentinelIssues.d.ts +65 -0
- package/dist/sentinelIssues.d.ts.map +1 -0
- package/dist/sentinelIssues.js +297 -0
- package/dist/sentinelIssues.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +195 -0
- package/dist/server.js.map +1 -0
- package/dist/test.d.ts +7 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +77 -0
- package/dist/test.js.map +1 -0
- package/dist/tools/agentic/index.d.ts +7 -0
- package/dist/tools/agentic/index.d.ts.map +1 -0
- package/dist/tools/agentic/index.js +203 -0
- package/dist/tools/agentic/index.js.map +1 -0
- package/dist/tools/architect/index.d.ts +11 -0
- package/dist/tools/architect/index.d.ts.map +1 -0
- package/dist/tools/architect/index.js +506 -0
- package/dist/tools/architect/index.js.map +1 -0
- package/dist/tools/architect.d.ts +19 -0
- package/dist/tools/architect.d.ts.map +1 -0
- package/dist/tools/architect.js +88 -0
- package/dist/tools/architect.js.map +1 -0
- package/dist/tools/auditor/index.d.ts +10 -0
- package/dist/tools/auditor/index.d.ts.map +1 -0
- package/dist/tools/auditor/index.js +310 -0
- package/dist/tools/auditor/index.js.map +1 -0
- package/dist/tools/auditor.d.ts +149 -0
- package/dist/tools/auditor.d.ts.map +1 -0
- package/dist/tools/auditor.js +775 -0
- package/dist/tools/auditor.js.map +1 -0
- package/dist/tools/bench/index.d.ts +3 -0
- package/dist/tools/bench/index.d.ts.map +1 -0
- package/dist/tools/bench/index.js +220 -0
- package/dist/tools/bench/index.js.map +1 -0
- package/dist/tools/bench.d.ts +89 -0
- package/dist/tools/bench.d.ts.map +1 -0
- package/dist/tools/bench.js +826 -0
- package/dist/tools/bench.js.map +1 -0
- package/dist/tools/context/index.d.ts +11 -0
- package/dist/tools/context/index.d.ts.map +1 -0
- package/dist/tools/context/index.js +482 -0
- package/dist/tools/context/index.js.map +1 -0
- package/dist/tools/context.d.ts +146 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +481 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/coordinator/coordinator.d.ts +168 -0
- package/dist/tools/coordinator/coordinator.d.ts.map +1 -0
- package/dist/tools/coordinator/coordinator.js +535 -0
- package/dist/tools/coordinator/coordinator.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +12 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -0
- package/dist/tools/coordinator/index.js +381 -0
- package/dist/tools/coordinator/index.js.map +1 -0
- package/dist/tools/corpus/index.d.ts +5 -0
- package/dist/tools/corpus/index.d.ts.map +1 -0
- package/dist/tools/corpus/index.js +105 -0
- package/dist/tools/corpus/index.js.map +1 -0
- package/dist/tools/corpus.d.ts +33 -0
- package/dist/tools/corpus.d.ts.map +1 -0
- package/dist/tools/corpus.js +180 -0
- package/dist/tools/corpus.js.map +1 -0
- package/dist/tools/crit.d.ts +63 -0
- package/dist/tools/crit.d.ts.map +1 -0
- package/dist/tools/crit.js +159 -0
- package/dist/tools/crit.js.map +1 -0
- package/dist/tools/data-inspector.d.ts +189 -0
- package/dist/tools/data-inspector.d.ts.map +1 -0
- package/dist/tools/data-inspector.js +669 -0
- package/dist/tools/data-inspector.js.map +1 -0
- package/dist/tools/deck.d.ts +11 -0
- package/dist/tools/deck.d.ts.map +1 -0
- package/dist/tools/deck.js +188 -0
- package/dist/tools/deck.js.map +1 -0
- package/dist/tools/designer/index.d.ts +11 -0
- package/dist/tools/designer/index.d.ts.map +1 -0
- package/dist/tools/designer/index.js +442 -0
- package/dist/tools/designer/index.js.map +1 -0
- package/dist/tools/designer/lateral-tools.d.ts +6 -0
- package/dist/tools/designer/lateral-tools.d.ts.map +1 -0
- package/dist/tools/designer/lateral-tools.js +190 -0
- package/dist/tools/designer/lateral-tools.js.map +1 -0
- package/dist/tools/designer.d.ts +122 -0
- package/dist/tools/designer.d.ts.map +1 -0
- package/dist/tools/designer.js +495 -0
- package/dist/tools/designer.js.map +1 -0
- package/dist/tools/dojo/index.d.ts +13 -0
- package/dist/tools/dojo/index.d.ts.map +1 -0
- package/dist/tools/dojo/index.js +613 -0
- package/dist/tools/dojo/index.js.map +1 -0
- package/dist/tools/dojo.d.ts +254 -0
- package/dist/tools/dojo.d.ts.map +1 -0
- package/dist/tools/dojo.js +933 -0
- package/dist/tools/dojo.js.map +1 -0
- package/dist/tools/dojoBench.d.ts +49 -0
- package/dist/tools/dojoBench.d.ts.map +1 -0
- package/dist/tools/dojoBench.js +205 -0
- package/dist/tools/dojoBench.js.map +1 -0
- package/dist/tools/dojoGraduated.d.ts +50 -0
- package/dist/tools/dojoGraduated.d.ts.map +1 -0
- package/dist/tools/dojoGraduated.js +174 -0
- package/dist/tools/dojoGraduated.js.map +1 -0
- package/dist/tools/dojoPolicy.d.ts +65 -0
- package/dist/tools/dojoPolicy.d.ts.map +1 -0
- package/dist/tools/dojoPolicy.js +263 -0
- package/dist/tools/dojoPolicy.js.map +1 -0
- package/dist/tools/feedback/index.d.ts +5 -0
- package/dist/tools/feedback/index.d.ts.map +1 -0
- package/dist/tools/feedback/index.js +153 -0
- package/dist/tools/feedback/index.js.map +1 -0
- package/dist/tools/feedback.d.ts +61 -0
- package/dist/tools/feedback.d.ts.map +1 -0
- package/dist/tools/feedback.js +209 -0
- package/dist/tools/feedback.js.map +1 -0
- package/dist/tools/forecast/index.d.ts +8 -0
- package/dist/tools/forecast/index.d.ts.map +1 -0
- package/dist/tools/forecast/index.js +283 -0
- package/dist/tools/forecast/index.js.map +1 -0
- package/dist/tools/forecast.d.ts +147 -0
- package/dist/tools/forecast.d.ts.map +1 -0
- package/dist/tools/forecast.js +417 -0
- package/dist/tools/forecast.js.map +1 -0
- package/dist/tools/friction/index.d.ts +7 -0
- package/dist/tools/friction/index.d.ts.map +1 -0
- package/dist/tools/friction/index.js +265 -0
- package/dist/tools/friction/index.js.map +1 -0
- package/dist/tools/friction.d.ts +82 -0
- package/dist/tools/friction.d.ts.map +1 -0
- package/dist/tools/friction.js +331 -0
- package/dist/tools/friction.js.map +1 -0
- package/dist/tools/git/index.d.ts +9 -0
- package/dist/tools/git/index.d.ts.map +1 -0
- package/dist/tools/git/index.js +237 -0
- package/dist/tools/git/index.js.map +1 -0
- package/dist/tools/git-sentinel/index.d.ts +7 -0
- package/dist/tools/git-sentinel/index.d.ts.map +1 -0
- package/dist/tools/git-sentinel/index.js +178 -0
- package/dist/tools/git-sentinel/index.js.map +1 -0
- package/dist/tools/git-sentinel.d.ts +78 -0
- package/dist/tools/git-sentinel.d.ts.map +1 -0
- package/dist/tools/git-sentinel.js +391 -0
- package/dist/tools/git-sentinel.js.map +1 -0
- package/dist/tools/git.d.ts +134 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/git.js +374 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/guardian/index.d.ts +8 -0
- package/dist/tools/guardian/index.d.ts.map +1 -0
- package/dist/tools/guardian/index.js +171 -0
- package/dist/tools/guardian/index.js.map +1 -0
- package/dist/tools/guardian.d.ts +62 -0
- package/dist/tools/guardian.d.ts.map +1 -0
- package/dist/tools/guardian.js +332 -0
- package/dist/tools/guardian.js.map +1 -0
- package/dist/tools/hygiene/codebase-scanner.d.ts +38 -0
- package/dist/tools/hygiene/codebase-scanner.d.ts.map +1 -0
- package/dist/tools/hygiene/codebase-scanner.js +411 -0
- package/dist/tools/hygiene/codebase-scanner.js.map +1 -0
- package/dist/tools/hygiene/config-scanner.d.ts +33 -0
- package/dist/tools/hygiene/config-scanner.d.ts.map +1 -0
- package/dist/tools/hygiene/config-scanner.js +482 -0
- package/dist/tools/hygiene/config-scanner.js.map +1 -0
- package/dist/tools/hygiene/coverage-scanner.d.ts +41 -0
- package/dist/tools/hygiene/coverage-scanner.d.ts.map +1 -0
- package/dist/tools/hygiene/coverage-scanner.js +331 -0
- package/dist/tools/hygiene/coverage-scanner.js.map +1 -0
- package/dist/tools/hygiene/index.d.ts +7 -0
- package/dist/tools/hygiene/index.d.ts.map +1 -0
- package/dist/tools/hygiene/index.js +291 -0
- package/dist/tools/hygiene/index.js.map +1 -0
- package/dist/tools/hygiene/oracle-hygiene.d.ts +68 -0
- package/dist/tools/hygiene/oracle-hygiene.d.ts.map +1 -0
- package/dist/tools/hygiene/oracle-hygiene.js +324 -0
- package/dist/tools/hygiene/oracle-hygiene.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +130 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/lateral.d.ts +114 -0
- package/dist/tools/lateral.d.ts.map +1 -0
- package/dist/tools/lateral.js +536 -0
- package/dist/tools/lateral.js.map +1 -0
- package/dist/tools/learnings/index.d.ts +5 -0
- package/dist/tools/learnings/index.d.ts.map +1 -0
- package/dist/tools/learnings/index.js +138 -0
- package/dist/tools/learnings/index.js.map +1 -0
- package/dist/tools/learnings.d.ts +41 -0
- package/dist/tools/learnings.d.ts.map +1 -0
- package/dist/tools/learnings.js +149 -0
- package/dist/tools/learnings.js.map +1 -0
- package/dist/tools/oracle/index.d.ts +6 -0
- package/dist/tools/oracle/index.d.ts.map +1 -0
- package/dist/tools/oracle/index.js +217 -0
- package/dist/tools/oracle/index.js.map +1 -0
- package/dist/tools/oracle.d.ts +90 -0
- package/dist/tools/oracle.d.ts.map +1 -0
- package/dist/tools/oracle.js +529 -0
- package/dist/tools/oracle.js.map +1 -0
- package/dist/tools/policy.d.ts +119 -0
- package/dist/tools/policy.d.ts.map +1 -0
- package/dist/tools/policy.js +406 -0
- package/dist/tools/policy.js.map +1 -0
- package/dist/tools/provenance/index.d.ts +4 -0
- package/dist/tools/provenance/index.d.ts.map +1 -0
- package/dist/tools/provenance/index.js +63 -0
- package/dist/tools/provenance/index.js.map +1 -0
- package/dist/tools/provenance.d.ts +75 -0
- package/dist/tools/provenance.d.ts.map +1 -0
- package/dist/tools/provenance.js +224 -0
- package/dist/tools/provenance.js.map +1 -0
- package/dist/tools/rateLimiter.d.ts +45 -0
- package/dist/tools/rateLimiter.d.ts.map +1 -0
- package/dist/tools/rateLimiter.js +91 -0
- package/dist/tools/rateLimiter.js.map +1 -0
- package/dist/tools/registry/index.d.ts +10 -0
- package/dist/tools/registry/index.d.ts.map +1 -0
- package/dist/tools/registry/index.js +506 -0
- package/dist/tools/registry/index.js.map +1 -0
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +189 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/roadmap/index.d.ts +11 -0
- package/dist/tools/roadmap/index.d.ts.map +1 -0
- package/dist/tools/roadmap/index.js +364 -0
- package/dist/tools/roadmap/index.js.map +1 -0
- package/dist/tools/roadmap.d.ts +103 -0
- package/dist/tools/roadmap.d.ts.map +1 -0
- package/dist/tools/roadmap.js +407 -0
- package/dist/tools/roadmap.js.map +1 -0
- package/dist/tools/senken.d.ts +11 -0
- package/dist/tools/senken.d.ts.map +1 -0
- package/dist/tools/senken.js +482 -0
- package/dist/tools/senken.js.map +1 -0
- package/dist/tools/sentinel/index.d.ts +21 -0
- package/dist/tools/sentinel/index.d.ts.map +1 -0
- package/dist/tools/sentinel/index.js +1067 -0
- package/dist/tools/sentinel/index.js.map +1 -0
- package/dist/tools/sentinel-scan-data.d.ts +90 -0
- package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
- package/dist/tools/sentinel-scan-data.js +122 -0
- package/dist/tools/sentinel-scan-data.js.map +1 -0
- package/dist/tools/sentinel.d.ts +156 -0
- package/dist/tools/sentinel.d.ts.map +1 -0
- package/dist/tools/sentinel.js +603 -0
- package/dist/tools/sentinel.js.map +1 -0
- package/dist/tools/shared/index.d.ts +5 -0
- package/dist/tools/shared/index.d.ts.map +1 -0
- package/dist/tools/shared/index.js +8 -0
- package/dist/tools/shared/index.js.map +1 -0
- package/dist/tools/shared/project.d.ts +17 -0
- package/dist/tools/shared/project.d.ts.map +1 -0
- package/dist/tools/shared/project.js +36 -0
- package/dist/tools/shared/project.js.map +1 -0
- package/dist/tools/shared/response.d.ts +15 -0
- package/dist/tools/shared/response.d.ts.map +1 -0
- package/dist/tools/shared/response.js +77 -0
- package/dist/tools/shared/response.js.map +1 -0
- package/dist/tools/shared/runTracker.d.ts +87 -0
- package/dist/tools/shared/runTracker.d.ts.map +1 -0
- package/dist/tools/shared/runTracker.js +225 -0
- package/dist/tools/shared/runTracker.js.map +1 -0
- package/dist/tools/shared/validation.d.ts +10 -0
- package/dist/tools/shared/validation.d.ts.map +1 -0
- package/dist/tools/shared/validation.js +26 -0
- package/dist/tools/shared/validation.js.map +1 -0
- package/dist/tools/studio/cloud-spine.d.ts +27 -0
- package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
- package/dist/tools/studio/cloud-spine.js +845 -0
- package/dist/tools/studio/cloud-spine.js.map +1 -0
- package/dist/tools/studio/index.d.ts +154 -0
- package/dist/tools/studio/index.d.ts.map +1 -0
- package/dist/tools/studio/index.js +541 -0
- package/dist/tools/studio/index.js.map +1 -0
- package/dist/tools/testSpec.d.ts +122 -0
- package/dist/tools/testSpec.d.ts.map +1 -0
- package/dist/tools/testSpec.js +525 -0
- package/dist/tools/testSpec.js.map +1 -0
- package/dist/tools/toolsIndex.d.ts +5 -0
- package/dist/tools/toolsIndex.d.ts.map +1 -0
- package/dist/tools/toolsIndex.js +37 -0
- package/dist/tools/toolsIndex.js.map +1 -0
- package/dist/tools/types.d.ts +47 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/vector/index.d.ts +13 -0
- package/dist/tools/vector/index.d.ts.map +1 -0
- package/dist/tools/vector/index.js +592 -0
- package/dist/tools/vector/index.js.map +1 -0
- package/dist/tools/vector.d.ts +189 -0
- package/dist/tools/vector.d.ts.map +1 -0
- package/dist/tools/vector.js +570 -0
- package/dist/tools/vector.js.map +1 -0
- package/dist/tools/velocity/index.d.ts +9 -0
- package/dist/tools/velocity/index.d.ts.map +1 -0
- package/dist/tools/velocity/index.js +306 -0
- package/dist/tools/velocity/index.js.map +1 -0
- package/dist/tools/velocity.d.ts +143 -0
- package/dist/tools/velocity.d.ts.map +1 -0
- package/dist/tools/velocity.js +628 -0
- package/dist/tools/velocity.js.map +1 -0
- package/dist/tools/voice/index.d.ts +8 -0
- package/dist/tools/voice/index.d.ts.map +1 -0
- package/dist/tools/voice/index.js +203 -0
- package/dist/tools/voice/index.js.map +1 -0
- package/dist/tools/voice.d.ts +291 -0
- package/dist/tools/voice.d.ts.map +1 -0
- package/dist/tools/voice.js +734 -0
- package/dist/tools/voice.js.map +1 -0
- package/dist/tools/workflow/index.d.ts +8 -0
- package/dist/tools/workflow/index.d.ts.map +1 -0
- package/dist/tools/workflow/index.js +199 -0
- package/dist/tools/workflow/index.js.map +1 -0
- package/dist/tools/workflow.d.ts +123 -0
- package/dist/tools/workflow.d.ts.map +1 -0
- package/dist/tools/workflow.js +647 -0
- package/dist/tools/workflow.js.map +1 -0
- package/dist/transports/bridge.d.ts +22 -0
- package/dist/transports/bridge.d.ts.map +1 -0
- package/dist/transports/bridge.js +177 -0
- package/dist/transports/bridge.js.map +1 -0
- package/dist/transports/http.d.ts +9 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +35 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/index.d.ts +6 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +8 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/transports/mcp.d.ts +9 -0
- package/dist/transports/mcp.d.ts.map +1 -0
- package/dist/transports/mcp.js +51 -0
- package/dist/transports/mcp.js.map +1 -0
- package/dist/transports/stdio.d.ts +9 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +26 -0
- package/dist/transports/stdio.js.map +1 -0
- package/dist/transports/types.d.ts +27 -0
- package/dist/transports/types.d.ts.map +1 -0
- package/dist/transports/types.js +8 -0
- package/dist/transports/types.js.map +1 -0
- package/dist/types/agent-services.d.ts +193 -0
- package/dist/types/agent-services.d.ts.map +1 -0
- package/dist/types/agent-services.js +8 -0
- package/dist/types/agent-services.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +72 -0
- package/templates/AGENT.md +87 -0
- package/templates/com.decibel.daemon.plist +47 -0
- package/templates/sentinel/ISSUE_TEMPLATE.md +20 -0
|
@@ -0,0 +1,1723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Server Mode for Decibel MCP
|
|
3
|
+
*
|
|
4
|
+
* Exposes the MCP server over HTTP for remote access (e.g., ChatGPT, external agents).
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node dist/server.js --http --port 8787
|
|
8
|
+
* node dist/server.js --http --port 8787 --auth-token YOUR_SECRET
|
|
9
|
+
*
|
|
10
|
+
* Endpoints:
|
|
11
|
+
* GET /health - Health check
|
|
12
|
+
* GET /tools - List available tools
|
|
13
|
+
* POST /call - Execute any tool: { tool: string, arguments: object }
|
|
14
|
+
* POST /dojo/wish - Shorthand for dojo_add_wish
|
|
15
|
+
* POST /dojo/propose - Shorthand for dojo_create_proposal
|
|
16
|
+
* POST /dojo/scaffold - Shorthand for dojo_scaffold_experiment
|
|
17
|
+
* POST /dojo/run - Shorthand for dojo_run_experiment
|
|
18
|
+
* POST /dojo/results - Shorthand for dojo_get_results
|
|
19
|
+
* POST /dojo/artifact - Shorthand for dojo_read_artifact
|
|
20
|
+
* GET /dojo/list - Shorthand for dojo_list
|
|
21
|
+
* POST /mcp - Full MCP protocol endpoint
|
|
22
|
+
*
|
|
23
|
+
* All responses use status envelope:
|
|
24
|
+
* { "status": "executed", ...data }
|
|
25
|
+
* { "status": "error", "error": "...", "code": "..." }
|
|
26
|
+
*/
|
|
27
|
+
import { createServer } from 'http';
|
|
28
|
+
import { timingSafeEqual } from 'crypto';
|
|
29
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
30
|
+
import { readFileSync } from 'fs';
|
|
31
|
+
import { fileURLToPath } from 'url';
|
|
32
|
+
import { dirname, join } from 'path';
|
|
33
|
+
import { log } from './config.js';
|
|
34
|
+
import { isSupabaseConfigured } from './lib/supabase.js';
|
|
35
|
+
import { getLicenseValidator } from './license.js';
|
|
36
|
+
import { listProjects } from './projectRegistry.js';
|
|
37
|
+
import { listEpics, listRepoIssues, isProjectResolutionError, } from './tools/sentinel.js';
|
|
38
|
+
import { voiceInboxAdd, } from './tools/voice.js';
|
|
39
|
+
import { generateImage, getImageStatus, meshyGenerate, getMeshyStatus, meshyDownload, tripoGenerate, getTripoStatus, tripoDownload, klingGenerateVideo, klingGenerateTextVideo, klingGenerateAvatar, getKlingStatus, listTasks, } from './tools/studio/index.js';
|
|
40
|
+
// Module-level kernel reference — set by startHttpServer()
|
|
41
|
+
let kernel;
|
|
42
|
+
let landingPageHtml = '';
|
|
43
|
+
let startedAt = 0;
|
|
44
|
+
let sseConnectionCount = 0;
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Security: Body Size Limit
|
|
47
|
+
// ============================================================================
|
|
48
|
+
const MAX_BODY_BYTES = 1_048_576; // 1MB
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Security: Rate Limiter
|
|
51
|
+
// ============================================================================
|
|
52
|
+
class RateLimiter {
|
|
53
|
+
windows = new Map();
|
|
54
|
+
maxRpm;
|
|
55
|
+
constructor(maxRpm) {
|
|
56
|
+
this.maxRpm = maxRpm;
|
|
57
|
+
}
|
|
58
|
+
/** Returns true if the request should be allowed */
|
|
59
|
+
check(ip) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const entry = this.windows.get(ip);
|
|
62
|
+
if (!entry || now - entry.start > 60_000) {
|
|
63
|
+
this.windows.set(ip, { count: 1, start: now });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
entry.count++;
|
|
67
|
+
return entry.count <= this.maxRpm;
|
|
68
|
+
}
|
|
69
|
+
/** Update the max RPM (e.g. on config reload) */
|
|
70
|
+
setMaxRpm(rpm) {
|
|
71
|
+
this.maxRpm = rpm;
|
|
72
|
+
}
|
|
73
|
+
/** Periodic cleanup of expired windows */
|
|
74
|
+
cleanup() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
for (const [ip, entry] of this.windows) {
|
|
77
|
+
if (now - entry.start > 60_000) {
|
|
78
|
+
this.windows.delete(ip);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Security: Timing-Safe Token Comparison
|
|
85
|
+
// ============================================================================
|
|
86
|
+
function timingSafeTokenCompare(provided, expected) {
|
|
87
|
+
const a = Buffer.from(provided, 'utf-8');
|
|
88
|
+
const b = Buffer.from(expected, 'utf-8');
|
|
89
|
+
if (a.length !== b.length) {
|
|
90
|
+
// Still do a comparison to keep timing constant
|
|
91
|
+
timingSafeEqual(a, a);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return timingSafeEqual(a, b);
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Version Info
|
|
98
|
+
// ============================================================================
|
|
99
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
100
|
+
const __dirname = dirname(__filename);
|
|
101
|
+
function getVersion() {
|
|
102
|
+
try {
|
|
103
|
+
// Try to read from package.json (works in both dev and prod)
|
|
104
|
+
const pkgPath = join(__dirname, '..', 'package.json');
|
|
105
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
106
|
+
return { version: pkg.version || '0.0.0', name: pkg.name || '@decibel/tools' };
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { version: '2.0.0', name: '@decibel/tools' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const PKG = getVersion();
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Landing Page HTML
|
|
115
|
+
// ============================================================================
|
|
116
|
+
/**
|
|
117
|
+
* Generate landing page HTML from facade definitions.
|
|
118
|
+
*/
|
|
119
|
+
function buildLandingPageHtml(facades) {
|
|
120
|
+
const escHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
121
|
+
const totalActions = facades.reduce((sum, f) => sum + f.actions.length, 0);
|
|
122
|
+
const toolSections = facades
|
|
123
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
124
|
+
.map(f => {
|
|
125
|
+
const rows = f.actions
|
|
126
|
+
.sort()
|
|
127
|
+
.map(action => {
|
|
128
|
+
return ` <tr><td class="tool-name">${escHtml(f.name)}(${escHtml(action)})</td><td class="tool-desc"></td></tr>`;
|
|
129
|
+
})
|
|
130
|
+
.join('\n');
|
|
131
|
+
return ` <div class="module-section">
|
|
132
|
+
<h3>${escHtml(f.name)} <span class="tool-count">${f.actions.length} actions</span></h3>
|
|
133
|
+
<p style="color:#888;font-size:0.85rem;margin-bottom:0.75rem">${escHtml(f.description.split('.')[0])}</p>
|
|
134
|
+
<table>${rows}</table>
|
|
135
|
+
</div>`;
|
|
136
|
+
}).join('\n');
|
|
137
|
+
return `<!DOCTYPE html>
|
|
138
|
+
<html lang="en">
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="UTF-8">
|
|
141
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
142
|
+
<title>Decibel Tools - ${facades.length} Facades</title>
|
|
143
|
+
<style>
|
|
144
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
145
|
+
body {
|
|
146
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
147
|
+
background: #0a0a0a;
|
|
148
|
+
color: #e5e5e5;
|
|
149
|
+
min-height: 100vh;
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
align-items: center;
|
|
153
|
+
padding: 2rem;
|
|
154
|
+
}
|
|
155
|
+
.container { max-width: 900px; width: 100%; }
|
|
156
|
+
h1 {
|
|
157
|
+
font-size: 2.5rem;
|
|
158
|
+
font-weight: 700;
|
|
159
|
+
margin-bottom: 0.5rem;
|
|
160
|
+
background: linear-gradient(135deg, #fff 0%, #888 100%);
|
|
161
|
+
-webkit-background-clip: text;
|
|
162
|
+
-webkit-text-fill-color: transparent;
|
|
163
|
+
}
|
|
164
|
+
.tagline { font-size: 1.25rem; color: #888; margin-bottom: 0.5rem; }
|
|
165
|
+
.tool-total { font-size: 0.9rem; color: #555; margin-bottom: 2rem; }
|
|
166
|
+
.features { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
|
167
|
+
.feature { background: #111; border: 1px solid #222; border-radius: 8px; padding: 1.25rem; }
|
|
168
|
+
.feature h3 { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.5rem; color: #fff; }
|
|
169
|
+
.feature p { font-size: 0.85rem; color: #888; line-height: 1.5; }
|
|
170
|
+
.module-section { background: #111; border: 1px solid #222; border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
|
|
171
|
+
.module-section h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.75rem; color: #fff; text-transform: capitalize; }
|
|
172
|
+
.tool-count { font-size: 0.75rem; color: #555; font-weight: 400; margin-left: 0.5rem; }
|
|
173
|
+
table { width: 100%; border-collapse: collapse; }
|
|
174
|
+
tr { border-bottom: 1px solid #1a1a1a; }
|
|
175
|
+
tr:last-child { border-bottom: none; }
|
|
176
|
+
td { padding: 0.4rem 0; vertical-align: top; font-size: 0.8rem; }
|
|
177
|
+
.tool-name { color: #ccc; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; white-space: nowrap; padding-right: 1rem; width: 1%; }
|
|
178
|
+
.tool-desc { color: #666; }
|
|
179
|
+
footer { margin-top: 2rem; padding-top: 2rem; font-size: 0.8rem; color: #444; }
|
|
180
|
+
footer a { color: #666; text-decoration: none; }
|
|
181
|
+
footer a:hover { color: #888; }
|
|
182
|
+
@media (max-width: 600px) { .features { grid-template-columns: 1fr; } }
|
|
183
|
+
</style>
|
|
184
|
+
</head>
|
|
185
|
+
<body>
|
|
186
|
+
<div class="container">
|
|
187
|
+
<h1>Decibel Tools</h1>
|
|
188
|
+
<p class="tagline">Project intelligence for AI coding agents</p>
|
|
189
|
+
<p class="tool-total">${facades.length} facades, ${totalActions} actions · v${PKG.version}</p>
|
|
190
|
+
<div class="features">
|
|
191
|
+
<div class="feature"><h3>Sentinel</h3><p>Track epics, issues, and incidents. Your agent knows what's in flight.</p></div>
|
|
192
|
+
<div class="feature"><h3>Architect</h3><p>Record ADRs and decisions. Context persists across sessions.</p></div>
|
|
193
|
+
<div class="feature"><h3>Dojo</h3><p>Incubate ideas with wishes, proposals, and experiments.</p></div>
|
|
194
|
+
<div class="feature"><h3>Oracle</h3><p>Get AI-powered recommendations on what to work on next.</p></div>
|
|
195
|
+
</div>
|
|
196
|
+
${toolSections}
|
|
197
|
+
<footer>
|
|
198
|
+
<a href="https://github.com/decibelsystems/decibel-tools-beta">GitHub</a> ·
|
|
199
|
+
<a href="https://modelcontextprotocol.io">MCP Protocol</a>
|
|
200
|
+
</footer>
|
|
201
|
+
</div>
|
|
202
|
+
</body>
|
|
203
|
+
</html>`;
|
|
204
|
+
}
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// Response Helpers
|
|
207
|
+
// ============================================================================
|
|
208
|
+
/**
|
|
209
|
+
* Format milliseconds into human-readable uptime string.
|
|
210
|
+
*/
|
|
211
|
+
function formatUptime(ms) {
|
|
212
|
+
const seconds = Math.floor(ms / 1000);
|
|
213
|
+
const minutes = Math.floor(seconds / 60);
|
|
214
|
+
const hours = Math.floor(minutes / 60);
|
|
215
|
+
const days = Math.floor(hours / 24);
|
|
216
|
+
if (days > 0)
|
|
217
|
+
return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
|
218
|
+
if (hours > 0)
|
|
219
|
+
return `${hours}h ${minutes % 60}m`;
|
|
220
|
+
if (minutes > 0)
|
|
221
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
222
|
+
return `${seconds}s`;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Wrap a successful result in status envelope
|
|
226
|
+
*/
|
|
227
|
+
function wrapSuccess(data) {
|
|
228
|
+
return { status: 'executed', ...data };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Wrap an error in status envelope
|
|
232
|
+
*/
|
|
233
|
+
function wrapError(error, code) {
|
|
234
|
+
return { status: 'error', error, ...(code && { code }) };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Send JSON response with status envelope
|
|
238
|
+
*/
|
|
239
|
+
function sendJson(res, statusCode, data) {
|
|
240
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
241
|
+
res.end(JSON.stringify(data));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse JSON body from request (with 1MB size limit)
|
|
245
|
+
*/
|
|
246
|
+
async function parseBody(req) {
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
let body = '';
|
|
249
|
+
let bytes = 0;
|
|
250
|
+
req.on('data', (chunk) => {
|
|
251
|
+
bytes += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
|
|
252
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
253
|
+
req.destroy();
|
|
254
|
+
reject(new Error('Request body too large (max 1MB)'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
body += chunk;
|
|
258
|
+
});
|
|
259
|
+
req.on('end', () => {
|
|
260
|
+
try {
|
|
261
|
+
resolve(body ? JSON.parse(body) : {});
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
reject(new Error('Invalid JSON'));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
req.on('error', reject);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Tool Executor — unified dispatch through modular tool registry
|
|
272
|
+
// ============================================================================
|
|
273
|
+
/**
|
|
274
|
+
* Execute any tool via the kernel's dispatch.
|
|
275
|
+
* Extracts agent context from HTTP headers when present.
|
|
276
|
+
*/
|
|
277
|
+
async function executeTool(tool, args, req, tierOverride) {
|
|
278
|
+
try {
|
|
279
|
+
// Extract agent context from HTTP headers
|
|
280
|
+
const context = req ? {
|
|
281
|
+
agentId: req.headers['x-agent-id'],
|
|
282
|
+
runId: req.headers['x-run-id'],
|
|
283
|
+
parentCallId: req.headers['x-parent-call-id'],
|
|
284
|
+
scope: req.headers['x-scope'],
|
|
285
|
+
tier: tierOverride,
|
|
286
|
+
} : tierOverride ? { tier: tierOverride } : undefined;
|
|
287
|
+
const toolResult = await kernel.dispatch(tool, args, context);
|
|
288
|
+
const text = toolResult.content[0]?.text;
|
|
289
|
+
if (toolResult.isError) {
|
|
290
|
+
return wrapError(text || 'Tool execution failed', 'TOOL_ERROR');
|
|
291
|
+
}
|
|
292
|
+
// Parse JSON result or wrap as message
|
|
293
|
+
let result;
|
|
294
|
+
if (text) {
|
|
295
|
+
try {
|
|
296
|
+
result = JSON.parse(text);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
result = { message: text };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
result = { success: true };
|
|
304
|
+
}
|
|
305
|
+
return wrapSuccess(result);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
309
|
+
if (message.includes('Rate limit')) {
|
|
310
|
+
return wrapError(message, 'RATE_LIMITED');
|
|
311
|
+
}
|
|
312
|
+
if (message.includes('Access denied')) {
|
|
313
|
+
return wrapError(message, 'ACCESS_DENIED');
|
|
314
|
+
}
|
|
315
|
+
if (message.includes('not found')) {
|
|
316
|
+
return wrapError(message, 'NOT_FOUND');
|
|
317
|
+
}
|
|
318
|
+
return wrapError(message, 'EXECUTION_ERROR');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get list of available facades — public API for tool discovery
|
|
323
|
+
*/
|
|
324
|
+
function getAvailableTools() {
|
|
325
|
+
return kernel.facades.map(f => ({
|
|
326
|
+
name: f.name,
|
|
327
|
+
description: f.description,
|
|
328
|
+
actions: Object.keys(f.actions),
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get tools in OpenAI function calling format (facade-based)
|
|
333
|
+
*/
|
|
334
|
+
function getOpenAITools() {
|
|
335
|
+
return kernel.getMcpToolDefinitions('full').map(def => ({
|
|
336
|
+
type: 'function',
|
|
337
|
+
function: {
|
|
338
|
+
name: def.name,
|
|
339
|
+
description: def.description,
|
|
340
|
+
parameters: {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: def.inputSchema.properties,
|
|
343
|
+
required: def.inputSchema.required,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
// ============================================================================
|
|
349
|
+
// License Tier Resolution
|
|
350
|
+
// ============================================================================
|
|
351
|
+
/**
|
|
352
|
+
* Resolve the caller's license tier from the request.
|
|
353
|
+
* - DECIBEL_PRO=1 env var → skip validation (dev mode)
|
|
354
|
+
* - Authorization header with DCBL-XXXX key → validate via LicenseValidator
|
|
355
|
+
* - No key → 'core' tier (only core facades)
|
|
356
|
+
* - Config-level key → use that as default
|
|
357
|
+
*/
|
|
358
|
+
async function resolveTier(req, configLicenseKey) {
|
|
359
|
+
// Dev mode bypass
|
|
360
|
+
if (process.env.DECIBEL_PRO === '1' || process.env.NODE_ENV !== 'production') {
|
|
361
|
+
return 'pro';
|
|
362
|
+
}
|
|
363
|
+
// Extract license key from Authorization header (separate from auth token)
|
|
364
|
+
// Format: X-License-Key: DCBL-XXXX-XXXX-XXXX
|
|
365
|
+
const licenseHeader = req.headers['x-license-key'];
|
|
366
|
+
const key = licenseHeader || configLicenseKey;
|
|
367
|
+
if (!key)
|
|
368
|
+
return 'core';
|
|
369
|
+
const validator = getLicenseValidator();
|
|
370
|
+
const result = await validator.validate(key);
|
|
371
|
+
return result.valid ? result.tier : 'core';
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Start an HTTP server that handles MCP requests
|
|
375
|
+
*
|
|
376
|
+
* Note: This creates a single stateless transport. Each request is handled
|
|
377
|
+
* independently. For full session support, this would need to be expanded.
|
|
378
|
+
*/
|
|
379
|
+
export async function startHttpServer(server, kernelInstance, options) {
|
|
380
|
+
const { port, authToken, host = '0.0.0.0', sseKeepaliveMs = 30000, // Send keepalive every 30s
|
|
381
|
+
timeoutMs = 120000, // 2 minute default timeout
|
|
382
|
+
retryIntervalMs = 3000, // 3s retry for SSE clients
|
|
383
|
+
rateLimitRpm = 100, // 100 req/min per IP default
|
|
384
|
+
isDaemon = false, configLicenseKey, } = options;
|
|
385
|
+
// Set module-level references
|
|
386
|
+
kernel = kernelInstance;
|
|
387
|
+
startedAt = Date.now();
|
|
388
|
+
log(`HTTP: Using kernel with ${kernel.toolCount} tools`);
|
|
389
|
+
// Rate limiter (clean up stale entries every 60s)
|
|
390
|
+
const rateLimiter = new RateLimiter(rateLimitRpm);
|
|
391
|
+
const rateLimiterCleanup = setInterval(() => rateLimiter.cleanup(), 60_000);
|
|
392
|
+
// Build landing page from actual tool list
|
|
393
|
+
landingPageHtml = buildLandingPageHtml(getAvailableTools());
|
|
394
|
+
// Create transport in STATELESS mode (better for ChatGPT compatibility)
|
|
395
|
+
// Setting sessionIdGenerator to undefined disables session tracking
|
|
396
|
+
const transport = new StreamableHTTPServerTransport({
|
|
397
|
+
sessionIdGenerator: undefined, // Stateless mode
|
|
398
|
+
enableJsonResponse: true, // Enable JSON fallback for non-streaming clients
|
|
399
|
+
retryInterval: retryIntervalMs, // Tell clients how long to wait before retry
|
|
400
|
+
});
|
|
401
|
+
// Connect the MCP server to the transport
|
|
402
|
+
await server.connect(transport);
|
|
403
|
+
// Track active SSE connections for keepalive
|
|
404
|
+
const activeSseConnections = new Set();
|
|
405
|
+
// Track active in-flight requests for graceful shutdown
|
|
406
|
+
const activeRequests = new Set();
|
|
407
|
+
// Start SSE keepalive heartbeat
|
|
408
|
+
const keepaliveInterval = setInterval(() => {
|
|
409
|
+
if (activeSseConnections.size > 0) {
|
|
410
|
+
log(`SSE keepalive: pinging ${activeSseConnections.size} connection(s)`);
|
|
411
|
+
}
|
|
412
|
+
for (const res of activeSseConnections) {
|
|
413
|
+
try {
|
|
414
|
+
if (!res.writableEnded) {
|
|
415
|
+
// Send SSE comment as keepalive (standard pattern)
|
|
416
|
+
res.write(': keepalive\n\n');
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
activeSseConnections.delete(res);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
// Connection likely closed
|
|
424
|
+
activeSseConnections.delete(res);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}, sseKeepaliveMs);
|
|
428
|
+
// Clean up on process exit
|
|
429
|
+
process.on('SIGTERM', () => {
|
|
430
|
+
clearInterval(keepaliveInterval);
|
|
431
|
+
});
|
|
432
|
+
const httpServer = createServer(async (req, res) => {
|
|
433
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
434
|
+
const path = url.pathname;
|
|
435
|
+
log(`HTTP: ${req.method} ${path}`);
|
|
436
|
+
// CORS headers — /mcp needs '*' for ChatGPT; REST endpoints restrict to localhost in daemon mode
|
|
437
|
+
const isMcpRoute = path === '/mcp' || path === '/sse' || path === '/sse/';
|
|
438
|
+
if (isMcpRoute || !isDaemon) {
|
|
439
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Daemon mode: restrict REST endpoints to localhost origins
|
|
443
|
+
const origin = req.headers.origin || '';
|
|
444
|
+
const localhostOrigins = ['http://localhost', 'http://127.0.0.1', 'https://localhost', 'https://127.0.0.1'];
|
|
445
|
+
if (localhostOrigins.some(lo => origin.startsWith(lo)) || !origin) {
|
|
446
|
+
res.setHeader('Access-Control-Allow-Origin', origin || 'http://localhost');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
450
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Accept');
|
|
451
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
452
|
+
// (a) Handle preflight OPTIONS requests
|
|
453
|
+
if (req.method === 'OPTIONS') {
|
|
454
|
+
res.writeHead(204);
|
|
455
|
+
res.end();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Rate limiting (check before auth to prevent brute force)
|
|
459
|
+
const clientIp = (req.socket.remoteAddress || '127.0.0.1').replace('::ffff:', '');
|
|
460
|
+
if (!rateLimiter.check(clientIp)) {
|
|
461
|
+
log(`HTTP: Rate limited ${clientIp}`);
|
|
462
|
+
sendJson(res, 429, wrapError('Too many requests (rate limit exceeded)', 'RATE_LIMITED'));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Track in-flight request
|
|
466
|
+
activeRequests.add(req);
|
|
467
|
+
res.on('finish', () => activeRequests.delete(req));
|
|
468
|
+
res.on('close', () => activeRequests.delete(req));
|
|
469
|
+
// (c) Root health check - GET / returns 200
|
|
470
|
+
if (path === '/' && req.method === 'GET') {
|
|
471
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
472
|
+
res.end(JSON.stringify({
|
|
473
|
+
status: 'ok',
|
|
474
|
+
name: PKG.name,
|
|
475
|
+
version: PKG.version,
|
|
476
|
+
api_version: 'v1',
|
|
477
|
+
}));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Health check at /health too
|
|
481
|
+
if (path === '/health') {
|
|
482
|
+
const uptimeMs = Date.now() - startedAt;
|
|
483
|
+
// Determine pro status from config license key
|
|
484
|
+
const proEnabled = process.env.DECIBEL_PRO === '1' || process.env.NODE_ENV !== 'production';
|
|
485
|
+
let licenseTier = proEnabled ? 'pro' : 'core';
|
|
486
|
+
if (configLicenseKey && !proEnabled) {
|
|
487
|
+
const cached = getLicenseValidator().getCachedResult(configLicenseKey);
|
|
488
|
+
if (cached)
|
|
489
|
+
licenseTier = cached.tier;
|
|
490
|
+
}
|
|
491
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
492
|
+
res.end(JSON.stringify({
|
|
493
|
+
status: 'ok',
|
|
494
|
+
version: PKG.version,
|
|
495
|
+
api_version: 'v1',
|
|
496
|
+
uptime_ms: uptimeMs,
|
|
497
|
+
uptime_human: formatUptime(uptimeMs),
|
|
498
|
+
pid: process.pid,
|
|
499
|
+
facade_count: kernel.facadeCount,
|
|
500
|
+
internal_tool_count: kernel.toolCount,
|
|
501
|
+
connected_clients: activeSseConnections.size,
|
|
502
|
+
active_requests: activeRequests.size,
|
|
503
|
+
pro: licenseTier !== 'core',
|
|
504
|
+
license_tier: licenseTier,
|
|
505
|
+
supabase_configured: isSupabaseConfigured(),
|
|
506
|
+
}));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Readiness probe at /ready
|
|
510
|
+
if (path === '/ready') {
|
|
511
|
+
// Ready if kernel loaded and at least one facade is available
|
|
512
|
+
const ready = kernel && kernel.facadeCount > 0;
|
|
513
|
+
res.writeHead(ready ? 200 : 503, { 'Content-Type': 'application/json' });
|
|
514
|
+
res.end(JSON.stringify({
|
|
515
|
+
ready,
|
|
516
|
+
facade_count: kernel?.facadeCount || 0,
|
|
517
|
+
}));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
// GET /events — query dispatch event log (dispatch.jsonl)
|
|
521
|
+
if (path === '/events' && req.method === 'GET') {
|
|
522
|
+
const dispatchLogPath = join(process.env.HOME || '~', '.decibel', 'logs', 'dispatch.jsonl');
|
|
523
|
+
try {
|
|
524
|
+
const content = readFileSync(dispatchLogPath, 'utf-8');
|
|
525
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
526
|
+
// Parse query params from URL
|
|
527
|
+
const since = url.searchParams.get('since');
|
|
528
|
+
const agentFilter = url.searchParams.get('agent_id');
|
|
529
|
+
const limitParam = url.searchParams.get('limit');
|
|
530
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 100;
|
|
531
|
+
let events = lines.map(line => {
|
|
532
|
+
try {
|
|
533
|
+
return JSON.parse(line);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}).filter(Boolean);
|
|
539
|
+
// Filter by timestamp
|
|
540
|
+
if (since) {
|
|
541
|
+
events = events.filter((e) => e.timestamp >= since);
|
|
542
|
+
}
|
|
543
|
+
// Filter by agent
|
|
544
|
+
if (agentFilter) {
|
|
545
|
+
events = events.filter((e) => e.agentId === agentFilter);
|
|
546
|
+
}
|
|
547
|
+
// Limit + return most recent
|
|
548
|
+
const recent = events.slice(-limit);
|
|
549
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
550
|
+
res.end(JSON.stringify({ events: recent, total: events.length }));
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// No dispatch log yet — empty response
|
|
554
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
555
|
+
res.end(JSON.stringify({ events: [], total: 0 }));
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// Landing page at /docs (always HTML)
|
|
560
|
+
if (path === '/docs' && req.method === 'GET') {
|
|
561
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
562
|
+
res.end(landingPageHtml);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// GET /tools — HTML for browsers, JSON for API clients
|
|
566
|
+
if (path === '/tools' && req.method === 'GET') {
|
|
567
|
+
const accept = req.headers.accept || '';
|
|
568
|
+
if (accept.includes('text/html')) {
|
|
569
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
570
|
+
res.end(landingPageHtml);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
// JSON response (for curl, agents, etc.) — falls through to auth + tool list below
|
|
574
|
+
}
|
|
575
|
+
// Serve OpenAPI spec for ChatGPT Actions (handle GET and POST)
|
|
576
|
+
if ((path === '/openapi.yaml' || path === '/openapi.json') && (req.method === 'GET' || req.method === 'POST')) {
|
|
577
|
+
try {
|
|
578
|
+
const specPath = join(__dirname, '..', 'openapi.yaml');
|
|
579
|
+
const spec = readFileSync(specPath, 'utf-8');
|
|
580
|
+
if (path === '/openapi.json') {
|
|
581
|
+
// Convert YAML to JSON if requested
|
|
582
|
+
const yaml = await import('yaml');
|
|
583
|
+
const parsed = yaml.parse(spec);
|
|
584
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
585
|
+
res.end(JSON.stringify(parsed, null, 2));
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
res.writeHead(200, { 'Content-Type': 'text/yaml' });
|
|
589
|
+
res.end(spec);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
594
|
+
res.end(JSON.stringify({ error: 'OpenAPI spec not found' }));
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// (d) OAuth discovery routes - return 404 (not 400) to keep connector wizard happy
|
|
599
|
+
if (path === '/.well-known/oauth-authorization-server' ||
|
|
600
|
+
path === '/.well-known/openid-configuration' ||
|
|
601
|
+
path === '/oauth/authorize' ||
|
|
602
|
+
path === '/oauth/token' ||
|
|
603
|
+
path === '/oauth/register') {
|
|
604
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
605
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Auth check (timing-safe comparison to prevent timing attacks)
|
|
609
|
+
if (authToken) {
|
|
610
|
+
const authHeader = req.headers.authorization;
|
|
611
|
+
if (!authHeader || !timingSafeTokenCompare(authHeader, `Bearer ${authToken}`)) {
|
|
612
|
+
log('HTTP: Unauthorized request');
|
|
613
|
+
sendJson(res, 401, wrapError('Unauthorized', 'UNAUTHORIZED'));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// ========================================================================
|
|
618
|
+
// Simple REST Endpoints (for external AI agents)
|
|
619
|
+
// ========================================================================
|
|
620
|
+
// GET /tools - List available tools
|
|
621
|
+
if (path === '/tools' && req.method === 'GET') {
|
|
622
|
+
sendJson(res, 200, wrapSuccess({
|
|
623
|
+
version: PKG.version,
|
|
624
|
+
api_version: 'v1',
|
|
625
|
+
tools: getAvailableTools(),
|
|
626
|
+
}));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// GET /facades - Facade registry for agent bootstrap
|
|
630
|
+
if (path === '/facades' && req.method === 'GET') {
|
|
631
|
+
const tier = (url.searchParams.get('tier') || 'full');
|
|
632
|
+
sendJson(res, 200, wrapSuccess({
|
|
633
|
+
facades: kernel.facades
|
|
634
|
+
.filter(f => tier !== 'micro' || f.microEligible)
|
|
635
|
+
.map(f => ({
|
|
636
|
+
name: f.name,
|
|
637
|
+
description: tier === 'compact' ? f.compactDescription : f.description,
|
|
638
|
+
actions: Object.keys(f.actions),
|
|
639
|
+
tier: f.tier,
|
|
640
|
+
})),
|
|
641
|
+
tier,
|
|
642
|
+
facade_count: kernel.facadeCount,
|
|
643
|
+
internal_tool_count: kernel.toolCount,
|
|
644
|
+
}));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// POST /call - Execute any tool
|
|
648
|
+
if (path === '/call' && req.method === 'POST') {
|
|
649
|
+
try {
|
|
650
|
+
const body = await parseBody(req);
|
|
651
|
+
const tool = body.tool;
|
|
652
|
+
const args = (body.arguments || {});
|
|
653
|
+
if (!tool) {
|
|
654
|
+
sendJson(res, 400, wrapError('Missing "tool" field', 'MISSING_TOOL'));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const tier = await resolveTier(req, configLicenseKey);
|
|
658
|
+
log(`HTTP: /call tool=${tool} tier=${tier}`);
|
|
659
|
+
const result = await executeTool(tool, args, req, tier);
|
|
660
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
664
|
+
if (message.includes('too large')) {
|
|
665
|
+
sendJson(res, 413, wrapError(message, 'BODY_TOO_LARGE'));
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// POST /batch - Dispatch multiple independent calls in parallel
|
|
674
|
+
if (path === '/batch' && req.method === 'POST') {
|
|
675
|
+
try {
|
|
676
|
+
const body = await parseBody(req);
|
|
677
|
+
const calls = body.calls;
|
|
678
|
+
if (!Array.isArray(calls) || calls.length === 0) {
|
|
679
|
+
sendJson(res, 400, wrapError('Missing or empty "calls" array', 'INVALID_BATCH'));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (calls.length > 20) {
|
|
683
|
+
sendJson(res, 400, wrapError('Batch limited to 20 calls', 'BATCH_TOO_LARGE'));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const tier = await resolveTier(req, configLicenseKey);
|
|
687
|
+
// Build context from headers + optional body context
|
|
688
|
+
const bodyContext = (body.context || {});
|
|
689
|
+
const context = {
|
|
690
|
+
agentId: req.headers['x-agent-id'] || bodyContext.agentId,
|
|
691
|
+
runId: req.headers['x-run-id'] || bodyContext.runId,
|
|
692
|
+
parentCallId: req.headers['x-parent-call-id'] || bodyContext.parentCallId,
|
|
693
|
+
scope: req.headers['x-scope'] || bodyContext.scope,
|
|
694
|
+
allowedFacades: bodyContext.allowedFacades,
|
|
695
|
+
tier,
|
|
696
|
+
};
|
|
697
|
+
log(`HTTP: /batch — ${calls.length} calls (agent=${context.agentId || 'anonymous'}, tier=${tier})`);
|
|
698
|
+
const results = await kernel.batch(calls, context);
|
|
699
|
+
sendJson(res, 200, { status: 'executed', results });
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
703
|
+
sendJson(res, 400, wrapError(message, 'BATCH_ERROR'));
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
// ========================================================================
|
|
708
|
+
// OpenAI-Compatible REST API (for SDK function calling)
|
|
709
|
+
// ========================================================================
|
|
710
|
+
// GET /api/tools - List tools in OpenAI function calling format
|
|
711
|
+
if (path === '/api/tools' && req.method === 'GET') {
|
|
712
|
+
const tools = getOpenAITools();
|
|
713
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
714
|
+
res.end(JSON.stringify(tools));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
// POST /api/tools/{name} - Execute a tool by name
|
|
718
|
+
if (path.startsWith('/api/tools/') && req.method === 'POST') {
|
|
719
|
+
try {
|
|
720
|
+
const toolName = path.replace('/api/tools/', '');
|
|
721
|
+
if (!toolName) {
|
|
722
|
+
sendJson(res, 400, wrapError('Missing tool name in path', 'MISSING_TOOL_NAME'));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const body = await parseBody(req);
|
|
726
|
+
log(`HTTP: /api/tools/${toolName}`);
|
|
727
|
+
const result = await executeTool(toolName, body);
|
|
728
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
732
|
+
sendJson(res, 400, wrapError(message, 'EXECUTION_ERROR'));
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// ========================================================================
|
|
737
|
+
// Dojo Convenience Endpoints
|
|
738
|
+
// ========================================================================
|
|
739
|
+
// POST /dojo/wish - Add a wish
|
|
740
|
+
if (path === '/dojo/wish' && req.method === 'POST') {
|
|
741
|
+
try {
|
|
742
|
+
const body = await parseBody(req);
|
|
743
|
+
log('HTTP: /dojo/wish');
|
|
744
|
+
const result = await executeTool('dojo_add_wish', body);
|
|
745
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
749
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
// POST /dojo/propose - Create a proposal
|
|
754
|
+
if (path === '/dojo/propose' && req.method === 'POST') {
|
|
755
|
+
try {
|
|
756
|
+
const body = await parseBody(req);
|
|
757
|
+
log('HTTP: /dojo/propose');
|
|
758
|
+
const result = await executeTool('dojo_create_proposal', body);
|
|
759
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
763
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
// POST /dojo/scaffold - Scaffold experiment
|
|
768
|
+
if (path === '/dojo/scaffold' && req.method === 'POST') {
|
|
769
|
+
try {
|
|
770
|
+
const body = await parseBody(req);
|
|
771
|
+
log('HTTP: /dojo/scaffold');
|
|
772
|
+
const result = await executeTool('dojo_scaffold_experiment', body);
|
|
773
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
777
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
// POST /dojo/run - Run experiment
|
|
782
|
+
if (path === '/dojo/run' && req.method === 'POST') {
|
|
783
|
+
try {
|
|
784
|
+
const body = await parseBody(req);
|
|
785
|
+
log('HTTP: /dojo/run');
|
|
786
|
+
const result = await executeTool('dojo_run_experiment', body);
|
|
787
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
791
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
// POST /dojo/results - Get experiment results
|
|
796
|
+
if (path === '/dojo/results' && req.method === 'POST') {
|
|
797
|
+
try {
|
|
798
|
+
const body = await parseBody(req);
|
|
799
|
+
log('HTTP: /dojo/results');
|
|
800
|
+
const result = await executeTool('dojo_read_results', body);
|
|
801
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
805
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// GET /dojo/list - List all (or POST with filter)
|
|
810
|
+
if (path === '/dojo/list') {
|
|
811
|
+
try {
|
|
812
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
813
|
+
// For GET, try to get project_id from query params
|
|
814
|
+
if (req.method === 'GET') {
|
|
815
|
+
const projectId = url.searchParams.get('project_id');
|
|
816
|
+
if (projectId) {
|
|
817
|
+
body.project_id = projectId;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
log('HTTP: /dojo/list');
|
|
821
|
+
const result = await executeTool('dojo_list', body);
|
|
822
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
826
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
827
|
+
}
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
// GET /dojo/wishes - List wishes
|
|
831
|
+
if (path === '/dojo/wishes') {
|
|
832
|
+
try {
|
|
833
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
834
|
+
if (req.method === 'GET') {
|
|
835
|
+
const projectId = url.searchParams.get('project_id');
|
|
836
|
+
if (projectId) {
|
|
837
|
+
body.project_id = projectId;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
log('HTTP: /dojo/wishes');
|
|
841
|
+
const result = await executeTool('dojo_list_wishes', body);
|
|
842
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
846
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
// POST /dojo/can-graduate - Check graduation eligibility
|
|
851
|
+
if (path === '/dojo/can-graduate' && req.method === 'POST') {
|
|
852
|
+
try {
|
|
853
|
+
const body = await parseBody(req);
|
|
854
|
+
log('HTTP: /dojo/can-graduate');
|
|
855
|
+
const result = await executeTool('dojo_can_graduate', body);
|
|
856
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
860
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
// POST /dojo/artifact - Read artifact from experiment results
|
|
865
|
+
if (path === '/dojo/artifact' && req.method === 'POST') {
|
|
866
|
+
try {
|
|
867
|
+
const body = await parseBody(req);
|
|
868
|
+
log('HTTP: /dojo/artifact');
|
|
869
|
+
const result = await executeTool('dojo_read_artifact', body);
|
|
870
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
874
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
// POST /dojo/bench - Run benchmark on experiment
|
|
879
|
+
if (path === '/dojo/bench' && req.method === 'POST') {
|
|
880
|
+
try {
|
|
881
|
+
const body = await parseBody(req);
|
|
882
|
+
log('HTTP: /dojo/bench');
|
|
883
|
+
const result = await executeTool('dojo_bench', body);
|
|
884
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
888
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
889
|
+
}
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
// ========================================================================
|
|
893
|
+
// Benchmark Endpoints (ISS-0014)
|
|
894
|
+
// ========================================================================
|
|
895
|
+
// POST /bench/run - Run a benchmark suite
|
|
896
|
+
if (path === '/bench/run' && req.method === 'POST') {
|
|
897
|
+
try {
|
|
898
|
+
const body = await parseBody(req);
|
|
899
|
+
log('HTTP: /bench/run');
|
|
900
|
+
const result = await executeTool('decibel_bench', body);
|
|
901
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
902
|
+
}
|
|
903
|
+
catch (error) {
|
|
904
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
905
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
// POST /bench/compare - Compare two baselines
|
|
910
|
+
if (path === '/bench/compare' && req.method === 'POST') {
|
|
911
|
+
try {
|
|
912
|
+
const body = await parseBody(req);
|
|
913
|
+
log('HTTP: /bench/compare');
|
|
914
|
+
const result = await executeTool('decibel_bench_compare', body);
|
|
915
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
919
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
// ========================================================================
|
|
924
|
+
// Context Pack Endpoints (ADR-002)
|
|
925
|
+
// ========================================================================
|
|
926
|
+
// POST /context/refresh - Compile full context pack
|
|
927
|
+
if (path === '/context/refresh' && req.method === 'POST') {
|
|
928
|
+
try {
|
|
929
|
+
const body = await parseBody(req);
|
|
930
|
+
log('HTTP: /context/refresh');
|
|
931
|
+
const result = await executeTool('decibel_context_refresh', body);
|
|
932
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
936
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
// POST /context/pin - Pin a fact
|
|
941
|
+
if (path === '/context/pin' && req.method === 'POST') {
|
|
942
|
+
try {
|
|
943
|
+
const body = await parseBody(req);
|
|
944
|
+
log('HTTP: /context/pin');
|
|
945
|
+
const result = await executeTool('decibel_context_pin', body);
|
|
946
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
950
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
// POST /context/unpin - Unpin a fact
|
|
955
|
+
if (path === '/context/unpin' && req.method === 'POST') {
|
|
956
|
+
try {
|
|
957
|
+
const body = await parseBody(req);
|
|
958
|
+
log('HTTP: /context/unpin');
|
|
959
|
+
const result = await executeTool('decibel_context_unpin', body);
|
|
960
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
964
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
965
|
+
}
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
// GET/POST /context/list - List pinned facts
|
|
969
|
+
if (path === '/context/list' && (req.method === 'GET' || req.method === 'POST')) {
|
|
970
|
+
try {
|
|
971
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
972
|
+
if (req.method === 'GET') {
|
|
973
|
+
const projectId = url.searchParams.get('project_id');
|
|
974
|
+
if (projectId) {
|
|
975
|
+
body.project_id = projectId;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
log('HTTP: /context/list');
|
|
979
|
+
const result = await executeTool('decibel_context_list', body);
|
|
980
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
984
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
// POST /event/append - Append event to journal
|
|
989
|
+
if (path === '/event/append' && req.method === 'POST') {
|
|
990
|
+
try {
|
|
991
|
+
const body = await parseBody(req);
|
|
992
|
+
log('HTTP: /event/append');
|
|
993
|
+
const result = await executeTool('decibel_event_append', body);
|
|
994
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
998
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// GET/POST /event/search - Search events
|
|
1003
|
+
if (path === '/event/search' && (req.method === 'GET' || req.method === 'POST')) {
|
|
1004
|
+
try {
|
|
1005
|
+
const body = req.method === 'POST' ? await parseBody(req) : {};
|
|
1006
|
+
if (req.method === 'GET') {
|
|
1007
|
+
const projectId = url.searchParams.get('project_id');
|
|
1008
|
+
const query = url.searchParams.get('query');
|
|
1009
|
+
const limit = url.searchParams.get('limit');
|
|
1010
|
+
if (projectId) {
|
|
1011
|
+
body.project_id = projectId;
|
|
1012
|
+
}
|
|
1013
|
+
if (query) {
|
|
1014
|
+
body.query = query;
|
|
1015
|
+
}
|
|
1016
|
+
if (limit) {
|
|
1017
|
+
body.limit = parseInt(limit, 10);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
log('HTTP: /event/search');
|
|
1021
|
+
const result = await executeTool('decibel_event_search', body);
|
|
1022
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
1023
|
+
}
|
|
1024
|
+
catch (error) {
|
|
1025
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1026
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
1027
|
+
}
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
// POST /artifact/list - List artifacts for a run
|
|
1031
|
+
if (path === '/artifact/list' && req.method === 'POST') {
|
|
1032
|
+
try {
|
|
1033
|
+
const body = await parseBody(req);
|
|
1034
|
+
log('HTTP: /artifact/list');
|
|
1035
|
+
const result = await executeTool('decibel_artifact_list', body);
|
|
1036
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1040
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
1041
|
+
}
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
// POST /artifact/read - Read artifact by run_id and name
|
|
1045
|
+
if (path === '/artifact/read' && req.method === 'POST') {
|
|
1046
|
+
try {
|
|
1047
|
+
const body = await parseBody(req);
|
|
1048
|
+
log('HTTP: /artifact/read');
|
|
1049
|
+
const result = await executeTool('decibel_artifact_read', body);
|
|
1050
|
+
sendJson(res, result.status === 'error' ? 400 : 200, result);
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1054
|
+
sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
|
|
1055
|
+
}
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// ========================================================================
|
|
1059
|
+
// iOS Mobile App Endpoint
|
|
1060
|
+
// ========================================================================
|
|
1061
|
+
// Helper: Call ML classifier sidecar (optional, graceful fallback)
|
|
1062
|
+
async function classifyWithML(transcript) {
|
|
1063
|
+
try {
|
|
1064
|
+
const controller = new AbortController();
|
|
1065
|
+
const timeout = setTimeout(() => controller.abort(), 1000); // 1s timeout
|
|
1066
|
+
const resp = await fetch('http://127.0.0.1:8790/classify', {
|
|
1067
|
+
method: 'POST',
|
|
1068
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1069
|
+
body: JSON.stringify({ transcript }),
|
|
1070
|
+
signal: controller.signal,
|
|
1071
|
+
});
|
|
1072
|
+
clearTimeout(timeout);
|
|
1073
|
+
if (resp.ok) {
|
|
1074
|
+
return await resp.json();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
catch {
|
|
1078
|
+
// Classifier not running or timed out - that's fine
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
// Helper: Log training sample to ML classifier
|
|
1083
|
+
async function logTrainingSample(data) {
|
|
1084
|
+
try {
|
|
1085
|
+
await fetch('http://127.0.0.1:8790/log', {
|
|
1086
|
+
method: 'POST',
|
|
1087
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1088
|
+
body: JSON.stringify(data),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
catch {
|
|
1092
|
+
// Best effort logging
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// POST /api/inbox - Receive voice transcript from iOS app
|
|
1096
|
+
if (path === '/api/inbox' && req.method === 'POST') {
|
|
1097
|
+
try {
|
|
1098
|
+
const body = await parseBody(req);
|
|
1099
|
+
log('HTTP: /api/inbox (iOS)');
|
|
1100
|
+
// Validate required field
|
|
1101
|
+
const transcript = body.transcript;
|
|
1102
|
+
if (!transcript) {
|
|
1103
|
+
sendJson(res, 400, wrapError('Missing "transcript" field', 'MISSING_TRANSCRIPT'));
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
// Build tags array
|
|
1107
|
+
const tags = [];
|
|
1108
|
+
if (body.device)
|
|
1109
|
+
tags.push(`device:${body.device}`);
|
|
1110
|
+
// User's explicit intent (from button tap)
|
|
1111
|
+
// iOS sends as "event_type", also accept "intent" for compatibility
|
|
1112
|
+
const userIntent = (body.event_type || body.intent);
|
|
1113
|
+
// ML classification (optional - graceful fallback if not running)
|
|
1114
|
+
const mlResult = await classifyWithML(transcript);
|
|
1115
|
+
let finalIntent = userIntent;
|
|
1116
|
+
let wasOverridden = false;
|
|
1117
|
+
let mlConfidence = 0;
|
|
1118
|
+
if (mlResult) {
|
|
1119
|
+
mlConfidence = mlResult.confidence;
|
|
1120
|
+
log(`HTTP: ML classified as "${mlResult.intent}" (${(mlResult.confidence * 100).toFixed(0)}%)`);
|
|
1121
|
+
if (userIntent) {
|
|
1122
|
+
// User provided intent - ML can override if confident and disagrees
|
|
1123
|
+
if (mlResult.intent !== userIntent && mlResult.confidence > 0.75) {
|
|
1124
|
+
finalIntent = mlResult.intent;
|
|
1125
|
+
wasOverridden = true;
|
|
1126
|
+
tags.push('ml:overridden');
|
|
1127
|
+
log(`HTTP: ML overriding user intent "${userIntent}" → "${mlResult.intent}"`);
|
|
1128
|
+
}
|
|
1129
|
+
// Log training sample (user label = ground truth)
|
|
1130
|
+
logTrainingSample({
|
|
1131
|
+
transcript,
|
|
1132
|
+
user_label: userIntent,
|
|
1133
|
+
predicted: mlResult.intent,
|
|
1134
|
+
confidence: mlResult.confidence,
|
|
1135
|
+
was_overridden: wasOverridden,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
// No user intent - use ML prediction
|
|
1140
|
+
finalIntent = mlResult.intent;
|
|
1141
|
+
tags.push('ml:predicted');
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
// Mark as human-labeled if user provided intent
|
|
1145
|
+
if (userIntent) {
|
|
1146
|
+
tags.push('labeled:human');
|
|
1147
|
+
tags.push(`user_intent:${userIntent}`);
|
|
1148
|
+
}
|
|
1149
|
+
// Map iOS payload to VoiceInboxAddInput
|
|
1150
|
+
const voiceInput = {
|
|
1151
|
+
transcript,
|
|
1152
|
+
source: 'mobile_app',
|
|
1153
|
+
project_id: body.project_id,
|
|
1154
|
+
process_immediately: true, // Process on receipt
|
|
1155
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
1156
|
+
// Pass final intent (may be ML-overridden)
|
|
1157
|
+
explicit_intent: finalIntent,
|
|
1158
|
+
};
|
|
1159
|
+
const result = await voiceInboxAdd(voiceInput);
|
|
1160
|
+
sendJson(res, 200, wrapSuccess({
|
|
1161
|
+
inbox_id: result.inbox_id,
|
|
1162
|
+
transcript: result.transcript,
|
|
1163
|
+
intent: result.intent,
|
|
1164
|
+
intent_confidence: result.intent_confidence,
|
|
1165
|
+
inbox_status: result.status,
|
|
1166
|
+
immediate_result: result.immediate_result,
|
|
1167
|
+
// ML metadata
|
|
1168
|
+
labeled: !!userIntent,
|
|
1169
|
+
user_intent: userIntent || null,
|
|
1170
|
+
ml_intent: mlResult?.intent || null,
|
|
1171
|
+
ml_confidence: mlResult ? Math.round(mlResult.confidence * 100) / 100 : null,
|
|
1172
|
+
was_overridden: wasOverridden,
|
|
1173
|
+
}));
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1177
|
+
log(`HTTP: /api/inbox error: ${message}`);
|
|
1178
|
+
sendJson(res, 400, wrapError(message, 'VOICE_INBOX_ERROR'));
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
// ========================================================================
|
|
1183
|
+
// iOS App API Endpoints (StatusSnapshot compatible)
|
|
1184
|
+
// ========================================================================
|
|
1185
|
+
// GET /api/projects - List registered projects for iOS project picker
|
|
1186
|
+
if (path === '/api/projects' && req.method === 'GET') {
|
|
1187
|
+
try {
|
|
1188
|
+
log('HTTP: /api/projects');
|
|
1189
|
+
const projects = listProjects();
|
|
1190
|
+
sendJson(res, 200, wrapSuccess({
|
|
1191
|
+
projects: projects.map(p => ({
|
|
1192
|
+
id: p.id,
|
|
1193
|
+
name: p.name || p.id,
|
|
1194
|
+
aliases: p.aliases || [],
|
|
1195
|
+
})),
|
|
1196
|
+
}));
|
|
1197
|
+
}
|
|
1198
|
+
catch (error) {
|
|
1199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1200
|
+
log(`HTTP: /api/projects error: ${message}`);
|
|
1201
|
+
sendJson(res, 500, wrapError(message, 'PROJECTS_ERROR'));
|
|
1202
|
+
}
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
// GET /api/status - StatusSnapshot for iOS StatusView
|
|
1206
|
+
if (path === '/api/status' && req.method === 'GET') {
|
|
1207
|
+
try {
|
|
1208
|
+
log('HTTP: /api/status');
|
|
1209
|
+
const projects = listProjects();
|
|
1210
|
+
// Check system health by listing each project's data
|
|
1211
|
+
const systemsHealth = {
|
|
1212
|
+
sentinel: { status: 'healthy', message: null },
|
|
1213
|
+
oracle: { status: 'healthy', message: null },
|
|
1214
|
+
dojo: { status: 'healthy', message: null },
|
|
1215
|
+
architect: { status: 'healthy', message: null },
|
|
1216
|
+
};
|
|
1217
|
+
// Build project summaries
|
|
1218
|
+
const projectSummaries = [];
|
|
1219
|
+
for (const project of projects) {
|
|
1220
|
+
try {
|
|
1221
|
+
// Get epic count
|
|
1222
|
+
const epicsResult = await listEpics({ projectId: project.id });
|
|
1223
|
+
const epicCount = isProjectResolutionError(epicsResult)
|
|
1224
|
+
? 0
|
|
1225
|
+
: epicsResult.epics?.length || 0;
|
|
1226
|
+
// Get open issues count
|
|
1227
|
+
const issuesResult = await listRepoIssues({ projectId: project.id, status: 'open' });
|
|
1228
|
+
const openIssueCount = isProjectResolutionError(issuesResult)
|
|
1229
|
+
? 0
|
|
1230
|
+
: issuesResult.issues?.length || 0;
|
|
1231
|
+
projectSummaries.push({
|
|
1232
|
+
project_id: project.id,
|
|
1233
|
+
name: project.name || project.id,
|
|
1234
|
+
active_epics: epicCount,
|
|
1235
|
+
open_issues: openIssueCount,
|
|
1236
|
+
last_activity: null, // Would need to scan files for timestamps
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
// If we can't get data for a project, still include it with zeros
|
|
1241
|
+
projectSummaries.push({
|
|
1242
|
+
project_id: project.id,
|
|
1243
|
+
name: project.name || project.id,
|
|
1244
|
+
active_epics: 0,
|
|
1245
|
+
open_issues: 0,
|
|
1246
|
+
last_activity: null,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const snapshot = {
|
|
1251
|
+
snapshot_id: crypto.randomUUID(),
|
|
1252
|
+
generated_at: new Date().toISOString(),
|
|
1253
|
+
source: {
|
|
1254
|
+
generator: 'mcp-server',
|
|
1255
|
+
version: PKG.version,
|
|
1256
|
+
},
|
|
1257
|
+
systems: systemsHealth,
|
|
1258
|
+
projects: projectSummaries,
|
|
1259
|
+
builds: [],
|
|
1260
|
+
alerts: [],
|
|
1261
|
+
};
|
|
1262
|
+
sendJson(res, 200, wrapSuccess(snapshot));
|
|
1263
|
+
}
|
|
1264
|
+
catch (error) {
|
|
1265
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1266
|
+
log(`HTTP: /api/status error: ${message}`);
|
|
1267
|
+
sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
|
|
1268
|
+
}
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
// ========================================================================
|
|
1272
|
+
// Studio API Endpoints (frontend_v0.2 compatible)
|
|
1273
|
+
// ========================================================================
|
|
1274
|
+
// POST /api/generate-flux-kontext-image - Start image generation
|
|
1275
|
+
if (path === '/api/generate-flux-kontext-image' && req.method === 'POST') {
|
|
1276
|
+
try {
|
|
1277
|
+
const body = await parseBody(req);
|
|
1278
|
+
log('HTTP: /api/generate-flux-kontext-image');
|
|
1279
|
+
// Validate required fields
|
|
1280
|
+
if (!body.prompt) {
|
|
1281
|
+
sendJson(res, 400, wrapError('Missing "prompt" field', 'MISSING_PROMPT'));
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const input = {
|
|
1285
|
+
asset_id: body.asset_id || `asset_${Date.now()}`,
|
|
1286
|
+
user_id: body.user_id || 'anonymous',
|
|
1287
|
+
prompt: body.prompt,
|
|
1288
|
+
input_image: body.input_image,
|
|
1289
|
+
aspect_ratio: body.aspect_ratio || '16:9',
|
|
1290
|
+
model: body.model || 'flux-kontext-pro',
|
|
1291
|
+
};
|
|
1292
|
+
const result = await generateImage(input);
|
|
1293
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1294
|
+
}
|
|
1295
|
+
catch (error) {
|
|
1296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1297
|
+
log(`HTTP: /api/generate-flux-kontext-image error: ${message}`);
|
|
1298
|
+
sendJson(res, 500, wrapError(message, 'GENERATION_ERROR'));
|
|
1299
|
+
}
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// GET /api/flux-kontext-status/:taskId - Check image generation status
|
|
1303
|
+
if (path.startsWith('/api/flux-kontext-status/') && req.method === 'GET') {
|
|
1304
|
+
try {
|
|
1305
|
+
const taskId = path.replace('/api/flux-kontext-status/', '');
|
|
1306
|
+
log(`HTTP: /api/flux-kontext-status/${taskId}`);
|
|
1307
|
+
if (!taskId) {
|
|
1308
|
+
sendJson(res, 400, wrapError('Missing task ID', 'MISSING_TASK_ID'));
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const status = getImageStatus(taskId);
|
|
1312
|
+
if (!status) {
|
|
1313
|
+
sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1317
|
+
res.end(JSON.stringify(status));
|
|
1318
|
+
}
|
|
1319
|
+
catch (error) {
|
|
1320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1321
|
+
log(`HTTP: /api/flux-kontext-status error: ${message}`);
|
|
1322
|
+
sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
|
|
1323
|
+
}
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
// ========================================================================
|
|
1327
|
+
// Meshy 3D Generation Endpoints
|
|
1328
|
+
// ========================================================================
|
|
1329
|
+
// POST /api/meshy/generate - Start 3D generation
|
|
1330
|
+
if (path === '/api/meshy/generate' && req.method === 'POST') {
|
|
1331
|
+
try {
|
|
1332
|
+
const body = await parseBody(req);
|
|
1333
|
+
log('HTTP: /api/meshy/generate');
|
|
1334
|
+
if (!body.mode) {
|
|
1335
|
+
sendJson(res, 400, wrapError('Missing "mode" field', 'MISSING_MODE'));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const input = {
|
|
1339
|
+
mode: body.mode,
|
|
1340
|
+
prompt: body.prompt,
|
|
1341
|
+
image_url: body.image_url,
|
|
1342
|
+
image_urls: body.image_urls,
|
|
1343
|
+
preview_task_id: body.preview_task_id,
|
|
1344
|
+
model_input: body.model_input,
|
|
1345
|
+
parameters: body.parameters,
|
|
1346
|
+
asset_id: body.asset_id,
|
|
1347
|
+
user_id: body.user_id,
|
|
1348
|
+
};
|
|
1349
|
+
const result = await meshyGenerate(input);
|
|
1350
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1351
|
+
}
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1354
|
+
log(`HTTP: /api/meshy/generate error: ${message}`);
|
|
1355
|
+
sendJson(res, 500, wrapError(message, 'MESHY_ERROR'));
|
|
1356
|
+
}
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
// GET /api/meshy/status/:taskId - Check 3D generation status
|
|
1360
|
+
if (path.startsWith('/api/meshy/status/') && req.method === 'GET') {
|
|
1361
|
+
try {
|
|
1362
|
+
const taskId = path.replace('/api/meshy/status/', '').split('?')[0];
|
|
1363
|
+
log(`HTTP: /api/meshy/status/${taskId}`);
|
|
1364
|
+
const status = getMeshyStatus(taskId);
|
|
1365
|
+
if (!status) {
|
|
1366
|
+
sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1370
|
+
res.end(JSON.stringify(status));
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1374
|
+
sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
// POST /api/meshy/download - Download completed model
|
|
1379
|
+
if (path === '/api/meshy/download' && req.method === 'POST') {
|
|
1380
|
+
try {
|
|
1381
|
+
const body = await parseBody(req);
|
|
1382
|
+
log('HTTP: /api/meshy/download');
|
|
1383
|
+
if (!body.task_id) {
|
|
1384
|
+
sendJson(res, 400, wrapError('Missing "task_id" field', 'MISSING_TASK_ID'));
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const result = await meshyDownload(body.task_id, body.asset_id || `asset_${Date.now()}`, body.user_id || 'anonymous');
|
|
1388
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1392
|
+
sendJson(res, 500, wrapError(message, 'DOWNLOAD_ERROR'));
|
|
1393
|
+
}
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
// ========================================================================
|
|
1397
|
+
// Tripo 3D Generation Endpoints
|
|
1398
|
+
// ========================================================================
|
|
1399
|
+
// POST /api/tripo/generate - Start Tripo 3D generation
|
|
1400
|
+
if (path === '/api/tripo/generate' && req.method === 'POST') {
|
|
1401
|
+
try {
|
|
1402
|
+
const body = await parseBody(req);
|
|
1403
|
+
log('HTTP: /api/tripo/generate');
|
|
1404
|
+
if (!body.type) {
|
|
1405
|
+
sendJson(res, 400, wrapError('Missing "type" field', 'MISSING_TYPE'));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const input = {
|
|
1409
|
+
type: body.type,
|
|
1410
|
+
prompt: body.prompt,
|
|
1411
|
+
image_url: body.image_url,
|
|
1412
|
+
image_urls: body.image_urls,
|
|
1413
|
+
parameters: body.parameters,
|
|
1414
|
+
asset_id: body.asset_id,
|
|
1415
|
+
user_id: body.user_id,
|
|
1416
|
+
};
|
|
1417
|
+
const result = await tripoGenerate(input);
|
|
1418
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1422
|
+
log(`HTTP: /api/tripo/generate error: ${message}`);
|
|
1423
|
+
sendJson(res, 500, wrapError(message, 'TRIPO_ERROR'));
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
// GET /api/tripo/task/:taskId - Check Tripo task status
|
|
1428
|
+
if (path.startsWith('/api/tripo/task/') && req.method === 'GET') {
|
|
1429
|
+
try {
|
|
1430
|
+
const taskId = path.replace('/api/tripo/task/', '');
|
|
1431
|
+
log(`HTTP: /api/tripo/task/${taskId}`);
|
|
1432
|
+
const status = getTripoStatus(taskId);
|
|
1433
|
+
if (!status) {
|
|
1434
|
+
sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1438
|
+
res.end(JSON.stringify(status));
|
|
1439
|
+
}
|
|
1440
|
+
catch (error) {
|
|
1441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1442
|
+
sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
// POST /api/tripo/download/:taskId - Download Tripo model
|
|
1447
|
+
if (path.startsWith('/api/tripo/download/') && req.method === 'POST') {
|
|
1448
|
+
try {
|
|
1449
|
+
const taskId = path.replace('/api/tripo/download/', '');
|
|
1450
|
+
const body = await parseBody(req);
|
|
1451
|
+
log(`HTTP: /api/tripo/download/${taskId}`);
|
|
1452
|
+
const result = await tripoDownload(taskId, body.asset_id || `asset_${Date.now()}`, body.user_id || 'anonymous');
|
|
1453
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1454
|
+
}
|
|
1455
|
+
catch (error) {
|
|
1456
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1457
|
+
sendJson(res, 500, wrapError(message, 'DOWNLOAD_ERROR'));
|
|
1458
|
+
}
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
// ========================================================================
|
|
1462
|
+
// Kling Video Generation Endpoints
|
|
1463
|
+
// ========================================================================
|
|
1464
|
+
// POST /api/generate-kling-video - Image to video
|
|
1465
|
+
if (path === '/api/generate-kling-video' && req.method === 'POST') {
|
|
1466
|
+
try {
|
|
1467
|
+
const body = await parseBody(req);
|
|
1468
|
+
log('HTTP: /api/generate-kling-video');
|
|
1469
|
+
if (!body.image_url || !body.prompt) {
|
|
1470
|
+
sendJson(res, 400, wrapError('Missing "image_url" or "prompt" field', 'MISSING_FIELDS'));
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const input = {
|
|
1474
|
+
asset_id: body.asset_id || `asset_${Date.now()}`,
|
|
1475
|
+
image_url: body.image_url,
|
|
1476
|
+
prompt: body.prompt,
|
|
1477
|
+
negative_prompt: body.negative_prompt,
|
|
1478
|
+
duration: body.duration || 5,
|
|
1479
|
+
aspect_ratio: body.aspect_ratio || '16:9',
|
|
1480
|
+
cfg_scale: body.cfg_scale,
|
|
1481
|
+
seed: body.seed,
|
|
1482
|
+
user_id: body.user_id,
|
|
1483
|
+
model: body.model,
|
|
1484
|
+
sound: body.sound,
|
|
1485
|
+
};
|
|
1486
|
+
const result = await klingGenerateVideo(input);
|
|
1487
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1491
|
+
log(`HTTP: /api/generate-kling-video error: ${message}`);
|
|
1492
|
+
sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
|
|
1493
|
+
}
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
// POST /api/generate-kling-text-video - Text to video
|
|
1497
|
+
if (path === '/api/generate-kling-text-video' && req.method === 'POST') {
|
|
1498
|
+
try {
|
|
1499
|
+
const body = await parseBody(req);
|
|
1500
|
+
log('HTTP: /api/generate-kling-text-video');
|
|
1501
|
+
if (!body.prompt) {
|
|
1502
|
+
sendJson(res, 400, wrapError('Missing "prompt" field', 'MISSING_PROMPT'));
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const input = {
|
|
1506
|
+
asset_id: body.asset_id || `asset_${Date.now()}`,
|
|
1507
|
+
prompt: body.prompt,
|
|
1508
|
+
negative_prompt: body.negative_prompt,
|
|
1509
|
+
duration: body.duration || 5,
|
|
1510
|
+
aspect_ratio: body.aspect_ratio || '16:9',
|
|
1511
|
+
cfg_scale: body.cfg_scale,
|
|
1512
|
+
user_id: body.user_id,
|
|
1513
|
+
model: body.model,
|
|
1514
|
+
sound: body.sound,
|
|
1515
|
+
};
|
|
1516
|
+
const result = await klingGenerateTextVideo(input);
|
|
1517
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1521
|
+
log(`HTTP: /api/generate-kling-text-video error: ${message}`);
|
|
1522
|
+
sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
|
|
1523
|
+
}
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
// POST /api/generate-kling-avatar - Avatar/lip-sync video
|
|
1527
|
+
if (path === '/api/generate-kling-avatar' && req.method === 'POST') {
|
|
1528
|
+
try {
|
|
1529
|
+
const body = await parseBody(req);
|
|
1530
|
+
log('HTTP: /api/generate-kling-avatar');
|
|
1531
|
+
if (!body.image_url || !body.audio_url) {
|
|
1532
|
+
sendJson(res, 400, wrapError('Missing "image_url" or "audio_url" field', 'MISSING_FIELDS'));
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const input = {
|
|
1536
|
+
asset_id: body.asset_id || `asset_${Date.now()}`,
|
|
1537
|
+
image_url: body.image_url,
|
|
1538
|
+
audio_url: body.audio_url,
|
|
1539
|
+
prompt: body.prompt,
|
|
1540
|
+
user_id: body.user_id,
|
|
1541
|
+
model: body.model,
|
|
1542
|
+
};
|
|
1543
|
+
const result = await klingGenerateAvatar(input);
|
|
1544
|
+
sendJson(res, 200, wrapSuccess(result));
|
|
1545
|
+
}
|
|
1546
|
+
catch (error) {
|
|
1547
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1548
|
+
log(`HTTP: /api/generate-kling-avatar error: ${message}`);
|
|
1549
|
+
sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
|
|
1550
|
+
}
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
// GET /api/kling-video-status/:taskId - Check video generation status
|
|
1554
|
+
if (path.startsWith('/api/kling-video-status/') && req.method === 'GET') {
|
|
1555
|
+
try {
|
|
1556
|
+
const taskId = path.replace('/api/kling-video-status/', '');
|
|
1557
|
+
log(`HTTP: /api/kling-video-status/${taskId}`);
|
|
1558
|
+
const status = getKlingStatus(taskId);
|
|
1559
|
+
if (!status) {
|
|
1560
|
+
sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1564
|
+
res.end(JSON.stringify(status));
|
|
1565
|
+
}
|
|
1566
|
+
catch (error) {
|
|
1567
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1568
|
+
sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
|
|
1569
|
+
}
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
// GET /api/studio/tasks - List all tasks (debug endpoint)
|
|
1573
|
+
if (path === '/api/studio/tasks' && req.method === 'GET') {
|
|
1574
|
+
try {
|
|
1575
|
+
log('HTTP: /api/studio/tasks');
|
|
1576
|
+
const tasks = listTasks();
|
|
1577
|
+
sendJson(res, 200, wrapSuccess({ tasks, count: tasks.length }));
|
|
1578
|
+
}
|
|
1579
|
+
catch (error) {
|
|
1580
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1581
|
+
sendJson(res, 500, wrapError(message, 'LIST_ERROR'));
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
// ========================================================================
|
|
1586
|
+
// Full MCP Protocol Endpoint
|
|
1587
|
+
// ========================================================================
|
|
1588
|
+
// (b) MCP endpoint - supports GET, POST, DELETE via StreamableHTTPServerTransport
|
|
1589
|
+
// Handle at /mcp, /sse, /sse/ (ChatGPT uses trailing slash), and root / for compatibility
|
|
1590
|
+
if (path === '/mcp' || path === '/sse' || path === '/sse/' || (path === '/' && (req.method === 'POST' || req.method === 'DELETE'))) {
|
|
1591
|
+
try {
|
|
1592
|
+
// Track SSE connections for keepalive (GET requests establish SSE streams)
|
|
1593
|
+
if (req.method === 'GET') {
|
|
1594
|
+
activeSseConnections.add(res);
|
|
1595
|
+
log(`HTTP: SSE stream opened via GET ${path} (${activeSseConnections.size} active) - keepalive enabled`);
|
|
1596
|
+
// Clean up when connection closes
|
|
1597
|
+
res.on('close', () => {
|
|
1598
|
+
activeSseConnections.delete(res);
|
|
1599
|
+
log(`HTTP: SSE stream closed (${activeSseConnections.size} active)`);
|
|
1600
|
+
});
|
|
1601
|
+
res.on('error', (err) => {
|
|
1602
|
+
activeSseConnections.delete(res);
|
|
1603
|
+
log(`HTTP: SSE stream error: ${err.message}`);
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
else if (req.method === 'POST') {
|
|
1607
|
+
log(`HTTP: StreamableHTTP request via POST ${path} - no keepalive needed`);
|
|
1608
|
+
}
|
|
1609
|
+
await transport.handleRequest(req, res);
|
|
1610
|
+
}
|
|
1611
|
+
catch (error) {
|
|
1612
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1613
|
+
log(`HTTP: Error handling MCP request: ${message}`);
|
|
1614
|
+
// Remove from active connections on error
|
|
1615
|
+
activeSseConnections.delete(res);
|
|
1616
|
+
if (!res.writableEnded) {
|
|
1617
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1618
|
+
res.end(JSON.stringify({ error: message }));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
// 404 for all other paths
|
|
1624
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1625
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
1626
|
+
});
|
|
1627
|
+
// Configure HTTP server timeouts to prevent premature connection drops
|
|
1628
|
+
httpServer.keepAliveTimeout = timeoutMs;
|
|
1629
|
+
httpServer.headersTimeout = timeoutMs + 1000; // Slightly longer than keepAliveTimeout
|
|
1630
|
+
httpServer.listen(port, host, () => {
|
|
1631
|
+
log(`HTTP Server listening on http://${host}:${port}`);
|
|
1632
|
+
console.log(`
|
|
1633
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
1634
|
+
║ Decibel MCP Server - HTTP Mode v${PKG.version}${' '.repeat(Math.max(0, 24 - PKG.version.length))}║
|
|
1635
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
1636
|
+
║ Endpoints: ║
|
|
1637
|
+
║ GET /health Health check ║
|
|
1638
|
+
║ GET /tools List tools ║
|
|
1639
|
+
║ POST /call Execute tool (generic) ║
|
|
1640
|
+
║ POST /batch Batch dispatch (parallel) ║
|
|
1641
|
+
║ GET /events Dispatch event log (query) ║
|
|
1642
|
+
║ POST /dojo/wish Add wish ║
|
|
1643
|
+
║ POST /dojo/propose Create proposal ║
|
|
1644
|
+
║ POST /dojo/scaffold Scaffold experiment ║
|
|
1645
|
+
║ POST /dojo/run Run experiment ║
|
|
1646
|
+
║ POST /dojo/results Get results ║
|
|
1647
|
+
║ POST /dojo/artifact Read artifact file ║
|
|
1648
|
+
║ GET /dojo/list List all ║
|
|
1649
|
+
║ POST /mcp Full MCP protocol ║
|
|
1650
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
1651
|
+
║ Base URL: http://${host}:${port}${' '.repeat(Math.max(0, 40 - port.toString().length - host.length))}║
|
|
1652
|
+
${authToken ? '║ Auth: Bearer token required ║' : '║ Auth: None (use --auth-token for security) ║'}
|
|
1653
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
1654
|
+
║ SSE Settings: ║
|
|
1655
|
+
║ Keepalive: ${sseKeepaliveMs}ms${' '.repeat(Math.max(0, 43 - sseKeepaliveMs.toString().length))}║
|
|
1656
|
+
║ Timeout: ${timeoutMs}ms${' '.repeat(Math.max(0, 43 - timeoutMs.toString().length))}║
|
|
1657
|
+
║ Retry: ${retryIntervalMs}ms${' '.repeat(Math.max(0, 43 - retryIntervalMs.toString().length))}║
|
|
1658
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
1659
|
+
║ Response format: {"status": "executed"|"error", ...} ║
|
|
1660
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
1661
|
+
`);
|
|
1662
|
+
});
|
|
1663
|
+
// Return lifecycle handle for TransportAdapter.stop()
|
|
1664
|
+
return {
|
|
1665
|
+
async stop() {
|
|
1666
|
+
clearInterval(keepaliveInterval);
|
|
1667
|
+
clearInterval(rateLimiterCleanup);
|
|
1668
|
+
// Stop accepting new connections
|
|
1669
|
+
httpServer.close(() => { });
|
|
1670
|
+
// Wait for in-flight requests to drain (max 10s)
|
|
1671
|
+
if (activeRequests.size > 0) {
|
|
1672
|
+
log(`HTTP: Waiting for ${activeRequests.size} in-flight request(s) to drain...`);
|
|
1673
|
+
const drainStart = Date.now();
|
|
1674
|
+
while (activeRequests.size > 0 && Date.now() - drainStart < 10_000) {
|
|
1675
|
+
await new Promise(r => setTimeout(r, 100));
|
|
1676
|
+
}
|
|
1677
|
+
if (activeRequests.size > 0) {
|
|
1678
|
+
log(`HTTP: ${activeRequests.size} request(s) still active after 10s drain timeout`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// Close SSE connections
|
|
1682
|
+
for (const conn of activeSseConnections) {
|
|
1683
|
+
try {
|
|
1684
|
+
if (!conn.writableEnded)
|
|
1685
|
+
conn.end();
|
|
1686
|
+
}
|
|
1687
|
+
catch { /* ignore */ }
|
|
1688
|
+
}
|
|
1689
|
+
activeSseConnections.clear();
|
|
1690
|
+
// Final close
|
|
1691
|
+
await new Promise((resolve) => {
|
|
1692
|
+
// Server may already be closed from above — handle gracefully
|
|
1693
|
+
httpServer.close(() => resolve());
|
|
1694
|
+
// If already closed, resolve immediately
|
|
1695
|
+
setTimeout(resolve, 100);
|
|
1696
|
+
});
|
|
1697
|
+
log('HTTP server stopped');
|
|
1698
|
+
},
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Parse command line arguments for HTTP mode
|
|
1703
|
+
*/
|
|
1704
|
+
export function parseHttpArgs(args) {
|
|
1705
|
+
const httpMode = args.includes('--http');
|
|
1706
|
+
const portIndex = args.indexOf('--port');
|
|
1707
|
+
// Render sets PORT env var - use it if available
|
|
1708
|
+
const defaultPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 8787;
|
|
1709
|
+
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : defaultPort;
|
|
1710
|
+
const authIndex = args.indexOf('--auth-token');
|
|
1711
|
+
const authToken = authIndex !== -1 ? args[authIndex + 1] : undefined;
|
|
1712
|
+
const hostIndex = args.indexOf('--host');
|
|
1713
|
+
const host = hostIndex !== -1 ? args[hostIndex + 1] : '0.0.0.0';
|
|
1714
|
+
// SSE/Connection tuning arguments
|
|
1715
|
+
const keepaliveIndex = args.indexOf('--sse-keepalive');
|
|
1716
|
+
const sseKeepaliveMs = keepaliveIndex !== -1 ? parseInt(args[keepaliveIndex + 1], 10) : undefined;
|
|
1717
|
+
const timeoutIndex = args.indexOf('--timeout');
|
|
1718
|
+
const timeoutMs = timeoutIndex !== -1 ? parseInt(args[timeoutIndex + 1], 10) : undefined;
|
|
1719
|
+
const retryIndex = args.indexOf('--sse-retry');
|
|
1720
|
+
const retryIntervalMs = retryIndex !== -1 ? parseInt(args[retryIndex + 1], 10) : undefined;
|
|
1721
|
+
return { httpMode, port, authToken, host, sseKeepaliveMs, timeoutMs, retryIntervalMs };
|
|
1722
|
+
}
|
|
1723
|
+
//# sourceMappingURL=httpServer.js.map
|