@ag-eco/agentplate-cli 0.13.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 +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API handlers for `ap serve`.
|
|
3
|
+
*
|
|
4
|
+
* Registers read-only endpoints that surface data from existing SQLite stores
|
|
5
|
+
* (EventStore, MailStore, SessionStore, RunStore). No new persistence.
|
|
6
|
+
*
|
|
7
|
+
* Route registration via registerApiHandler — no changes to serve.ts required
|
|
8
|
+
* beyond the single registerRestApi() call.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type { DoctorCategory } from "../../doctor/types.ts";
|
|
15
|
+
import { AgentError, AgentplateError, ValidationError } from "../../errors.ts";
|
|
16
|
+
import { createEventStore } from "../../events/store.ts";
|
|
17
|
+
import { apiError, apiJson } from "../../json.ts";
|
|
18
|
+
import type { MailStore } from "../../mail/store.ts";
|
|
19
|
+
import { createMailStore } from "../../mail/store.ts";
|
|
20
|
+
import { createMergeQueue } from "../../merge/queue.ts";
|
|
21
|
+
import type { MetricsStore } from "../../metrics/store.ts";
|
|
22
|
+
import { createMetricsStore } from "../../metrics/store.ts";
|
|
23
|
+
import { generateSummary } from "../../metrics/summary.ts";
|
|
24
|
+
import type { SessionStore } from "../../sessions/store.ts";
|
|
25
|
+
import { createRunStore, createSessionStore } from "../../sessions/store.ts";
|
|
26
|
+
import type { EventStore, MergeEntry, RunStore } from "../../types.ts";
|
|
27
|
+
import { listWorktrees } from "../../worktree/manager.ts";
|
|
28
|
+
import { loadGroups } from "../group.ts";
|
|
29
|
+
import { discoverLogFiles, filterEvents, parseLogFile } from "../logs.ts";
|
|
30
|
+
import { registerApiHandler } from "../serve.ts";
|
|
31
|
+
import {
|
|
32
|
+
type AgentActionDeps,
|
|
33
|
+
mergePreview,
|
|
34
|
+
mergeRun,
|
|
35
|
+
slingAgent,
|
|
36
|
+
stopAgent,
|
|
37
|
+
} from "./agent-actions.ts";
|
|
38
|
+
import {
|
|
39
|
+
askCoordinatorAction,
|
|
40
|
+
ConflictError,
|
|
41
|
+
type CoordinatorActionDeps,
|
|
42
|
+
checkCoordinatorComplete,
|
|
43
|
+
getCoordinatorState,
|
|
44
|
+
sendToCoordinator,
|
|
45
|
+
startCoordinatorHeadless,
|
|
46
|
+
stopCoordinator,
|
|
47
|
+
} from "./coordinator-actions.ts";
|
|
48
|
+
import { deleteMail, replyMail, sendMail } from "./mail-actions.ts";
|
|
49
|
+
|
|
50
|
+
// ─── Cursor helpers ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
type Cursor = { ts: string; id: string };
|
|
53
|
+
|
|
54
|
+
function encodeCursor(c: Cursor): string {
|
|
55
|
+
return Buffer.from(JSON.stringify(c)).toString("base64url");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function decodeCursor(s: string): Cursor {
|
|
59
|
+
let parsed: unknown;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(Buffer.from(s, "base64url").toString("utf-8"));
|
|
62
|
+
} catch {
|
|
63
|
+
throw new ValidationError("Invalid cursor", { field: "cursor", value: s });
|
|
64
|
+
}
|
|
65
|
+
const p = parsed as Record<string, unknown>;
|
|
66
|
+
if (typeof p.ts !== "string" || typeof p.id !== "string") {
|
|
67
|
+
throw new ValidationError("Invalid cursor", { field: "cursor", value: s });
|
|
68
|
+
}
|
|
69
|
+
return { ts: p.ts, id: p.id };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseLimitAndCursor(params: URLSearchParams): { limit: number; cursor: Cursor | null } {
|
|
73
|
+
const limitStr = params.get("limit");
|
|
74
|
+
const limit = limitStr !== null ? Number.parseInt(limitStr, 10) : 100;
|
|
75
|
+
if (Number.isNaN(limit) || limit < 1 || limit > 500) {
|
|
76
|
+
throw new ValidationError(`Invalid limit: ${limitStr ?? "undefined"}`, {
|
|
77
|
+
field: "limit",
|
|
78
|
+
value: limitStr,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cursorStr = params.get("cursor");
|
|
83
|
+
const cursor = cursorStr !== null ? decodeCursor(cursorStr) : null;
|
|
84
|
+
|
|
85
|
+
return { limit, cursor };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Generic paginator (for string-id collections) ───────────────────────────
|
|
89
|
+
|
|
90
|
+
interface PaginateResult<T> {
|
|
91
|
+
page: T[];
|
|
92
|
+
nextCursor: string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Client-side paginator for pre-sorted collections with string IDs.
|
|
97
|
+
* direction="asc": keep items where (ts > cursorTs) OR (ts === cursorTs AND id > cursorId)
|
|
98
|
+
* direction="desc": keep items where (ts < cursorTs) OR (ts === cursorTs AND id < cursorId)
|
|
99
|
+
*/
|
|
100
|
+
function paginateItems<T extends { id: string }>(
|
|
101
|
+
items: T[],
|
|
102
|
+
cursor: Cursor | null,
|
|
103
|
+
limit: number,
|
|
104
|
+
getTs: (item: T) => string,
|
|
105
|
+
direction: "asc" | "desc",
|
|
106
|
+
): PaginateResult<T> {
|
|
107
|
+
let filtered = items;
|
|
108
|
+
|
|
109
|
+
if (cursor !== null) {
|
|
110
|
+
const { ts: cTs, id: cId } = cursor;
|
|
111
|
+
if (direction === "asc") {
|
|
112
|
+
filtered = items.filter((item) => {
|
|
113
|
+
const ts = getTs(item);
|
|
114
|
+
if (ts > cTs) return true;
|
|
115
|
+
if (ts === cTs && item.id > cId) return true;
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
filtered = items.filter((item) => {
|
|
120
|
+
const ts = getTs(item);
|
|
121
|
+
if (ts < cTs) return true;
|
|
122
|
+
if (ts === cTs && item.id < cId) return true;
|
|
123
|
+
return false;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const page = filtered.slice(0, limit);
|
|
129
|
+
const hasMore = filtered.length > limit;
|
|
130
|
+
const lastItem = page[page.length - 1];
|
|
131
|
+
const nextCursor =
|
|
132
|
+
hasMore && lastItem !== undefined
|
|
133
|
+
? encodeCursor({ ts: getTs(lastItem), id: lastItem.id })
|
|
134
|
+
: null;
|
|
135
|
+
|
|
136
|
+
return { page, nextCursor };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Error → HTTP status ──────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function statusFromError(err: AgentplateError): number {
|
|
142
|
+
if (err instanceof ValidationError) return 400;
|
|
143
|
+
if (err instanceof ConflictError) return 409;
|
|
144
|
+
// AgentError with "not running" message — surface as 409 (preconditions
|
|
145
|
+
// failed) so the UI can offer a "start coordinator" affordance instead of
|
|
146
|
+
// treating it as a server-side fault.
|
|
147
|
+
if (err instanceof AgentError && /not running/i.test(err.message)) return 409;
|
|
148
|
+
return 500;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Stores ───────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export interface RestApiDeps {
|
|
154
|
+
_runStore?: RunStore;
|
|
155
|
+
_sessionStore?: SessionStore;
|
|
156
|
+
_eventStore?: EventStore;
|
|
157
|
+
_mailStore?: MailStore;
|
|
158
|
+
_metricsStore?: MetricsStore;
|
|
159
|
+
_projectRoot?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Override coordinator action functions (used by tests). When omitted, the
|
|
162
|
+
* production functions imported from ./coordinator-actions.ts are called.
|
|
163
|
+
*/
|
|
164
|
+
_coordinatorActions?: {
|
|
165
|
+
getCoordinatorState?: typeof getCoordinatorState;
|
|
166
|
+
sendToCoordinator?: typeof sendToCoordinator;
|
|
167
|
+
askCoordinatorAction?: typeof askCoordinatorAction;
|
|
168
|
+
checkCoordinatorComplete?: typeof checkCoordinatorComplete;
|
|
169
|
+
startCoordinatorHeadless?: typeof startCoordinatorHeadless;
|
|
170
|
+
stopCoordinator?: typeof stopCoordinator;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Override the deps passed to coordinator-actions. When omitted, the actions
|
|
174
|
+
* use deps.projectRoot to open their own short-lived stores. Tests pass
|
|
175
|
+
* `_sessionStore` / `_mailStore` so actions reuse the test harness DBs.
|
|
176
|
+
*/
|
|
177
|
+
_coordinatorActionDeps?: Partial<CoordinatorActionDeps>;
|
|
178
|
+
/**
|
|
179
|
+
* Override the fleet-control action functions (sling/stop/merge). Tests
|
|
180
|
+
* inject fakes to avoid spawning real agents or invoking git.
|
|
181
|
+
*/
|
|
182
|
+
_agentActions?: {
|
|
183
|
+
slingAgent?: typeof slingAgent;
|
|
184
|
+
stopAgent?: typeof stopAgent;
|
|
185
|
+
mergePreview?: typeof mergePreview;
|
|
186
|
+
mergeRun?: typeof mergeRun;
|
|
187
|
+
};
|
|
188
|
+
/** Override the deps passed to agent-actions (e.g. injected SessionStore). */
|
|
189
|
+
_agentActionDeps?: Partial<AgentActionDeps>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface Stores {
|
|
193
|
+
run: RunStore;
|
|
194
|
+
session: SessionStore;
|
|
195
|
+
event: EventStore;
|
|
196
|
+
mail: MailStore;
|
|
197
|
+
metrics: MetricsStore;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function openStores(projectRoot: string): Stores {
|
|
201
|
+
const ovDir = join(projectRoot, ".agentplate");
|
|
202
|
+
const sessionsDb = join(ovDir, "sessions.db");
|
|
203
|
+
const eventsDb = join(ovDir, "events.db");
|
|
204
|
+
const mailDb = join(ovDir, "mail.db");
|
|
205
|
+
const metricsDb = join(ovDir, "metrics.db");
|
|
206
|
+
return {
|
|
207
|
+
run: createRunStore(sessionsDb),
|
|
208
|
+
session: createSessionStore(sessionsDb),
|
|
209
|
+
event: createEventStore(eventsDb),
|
|
210
|
+
mail: createMailStore(mailDb),
|
|
211
|
+
metrics: createMetricsStore(metricsDb),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Route table ─────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
type RouteHandler = (
|
|
218
|
+
req: Request,
|
|
219
|
+
match: RegExpMatchArray,
|
|
220
|
+
params: URLSearchParams,
|
|
221
|
+
stores: Stores,
|
|
222
|
+
ctx: HandlerContext,
|
|
223
|
+
) => Promise<Response>;
|
|
224
|
+
|
|
225
|
+
interface Route {
|
|
226
|
+
method: string;
|
|
227
|
+
pattern: RegExp;
|
|
228
|
+
handler: RouteHandler;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Per-request context passed to every handler. Holds the resolved coordinator
|
|
233
|
+
* action functions (live or DI-overridden) and the deps that drive them.
|
|
234
|
+
*/
|
|
235
|
+
interface HandlerContext {
|
|
236
|
+
projectRoot: string;
|
|
237
|
+
coordinatorActions: {
|
|
238
|
+
getCoordinatorState: typeof getCoordinatorState;
|
|
239
|
+
sendToCoordinator: typeof sendToCoordinator;
|
|
240
|
+
askCoordinatorAction: typeof askCoordinatorAction;
|
|
241
|
+
checkCoordinatorComplete: typeof checkCoordinatorComplete;
|
|
242
|
+
startCoordinatorHeadless: typeof startCoordinatorHeadless;
|
|
243
|
+
stopCoordinator: typeof stopCoordinator;
|
|
244
|
+
};
|
|
245
|
+
coordinatorActionDeps: CoordinatorActionDeps;
|
|
246
|
+
agentActions: {
|
|
247
|
+
slingAgent: typeof slingAgent;
|
|
248
|
+
stopAgent: typeof stopAgent;
|
|
249
|
+
mergePreview: typeof mergePreview;
|
|
250
|
+
mergeRun: typeof mergeRun;
|
|
251
|
+
};
|
|
252
|
+
agentActionDeps: AgentActionDeps;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Body parsing ────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async function readJsonBody<T>(req: Request, validate: (v: unknown) => T): Promise<T> {
|
|
258
|
+
let raw: unknown;
|
|
259
|
+
try {
|
|
260
|
+
raw = await req.json();
|
|
261
|
+
} catch {
|
|
262
|
+
throw new ValidationError("Invalid JSON body");
|
|
263
|
+
}
|
|
264
|
+
return validate(raw);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function asObject(v: unknown, msg: string): Record<string, unknown> {
|
|
268
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) {
|
|
269
|
+
throw new ValidationError(msg);
|
|
270
|
+
}
|
|
271
|
+
return v as Record<string, unknown>;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function requireString(obj: Record<string, unknown>, field: string): string {
|
|
275
|
+
const v = obj[field];
|
|
276
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
277
|
+
throw new ValidationError(`Missing or invalid '${field}' (string)`, { field, value: v });
|
|
278
|
+
}
|
|
279
|
+
return v;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function optionalString(obj: Record<string, unknown>, field: string): string | undefined {
|
|
283
|
+
const v = obj[field];
|
|
284
|
+
if (v === undefined || v === null) return undefined;
|
|
285
|
+
if (typeof v !== "string") {
|
|
286
|
+
throw new ValidationError(`Invalid '${field}' (must be string)`, { field, value: v });
|
|
287
|
+
}
|
|
288
|
+
return v;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function optionalTimeoutSec(obj: Record<string, unknown>): number {
|
|
292
|
+
const v = obj.timeoutSec;
|
|
293
|
+
if (v === undefined || v === null) return 120;
|
|
294
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 1 || v > 600) {
|
|
295
|
+
throw new ValidationError("Invalid 'timeoutSec' (must be a number 1..600)", {
|
|
296
|
+
field: "timeoutSec",
|
|
297
|
+
value: v,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return v;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
|
304
|
+
let parsed: unknown;
|
|
305
|
+
try {
|
|
306
|
+
parsed = await req.json();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
throw new ValidationError(
|
|
309
|
+
`Invalid JSON body: ${err instanceof Error ? err.message : String(err)}`,
|
|
310
|
+
{
|
|
311
|
+
field: "body",
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
316
|
+
throw new ValidationError("Request body must be a JSON object", { field: "body" });
|
|
317
|
+
}
|
|
318
|
+
return parsed as Record<string, unknown>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Individual handlers ──────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
async function handleGetRuns(
|
|
324
|
+
_req: Request,
|
|
325
|
+
_match: RegExpMatchArray,
|
|
326
|
+
params: URLSearchParams,
|
|
327
|
+
stores: Stores,
|
|
328
|
+
): Promise<Response> {
|
|
329
|
+
const { limit, cursor } = parseLimitAndCursor(params);
|
|
330
|
+
const all = stores.run.listRuns();
|
|
331
|
+
// Sort DESC by (startedAt, id)
|
|
332
|
+
const sorted = [...all].sort((a, b) => {
|
|
333
|
+
if (b.startedAt !== a.startedAt) return b.startedAt < a.startedAt ? -1 : 1;
|
|
334
|
+
return b.id < a.id ? -1 : 1;
|
|
335
|
+
});
|
|
336
|
+
const { page, nextCursor } = paginateItems(sorted, cursor, limit, (r) => r.startedAt, "desc");
|
|
337
|
+
return apiJson(page, { nextCursor });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function handleGetRun(
|
|
341
|
+
_req: Request,
|
|
342
|
+
match: RegExpMatchArray,
|
|
343
|
+
_params: URLSearchParams,
|
|
344
|
+
stores: Stores,
|
|
345
|
+
): Promise<Response> {
|
|
346
|
+
const id = match[1];
|
|
347
|
+
if (id === undefined) return apiError("Run ID required", 400);
|
|
348
|
+
const run = stores.run.getRun(id);
|
|
349
|
+
if (run === null) return apiError(`Run not found: ${id}`, 404);
|
|
350
|
+
const agents = stores.session.getByRun(id);
|
|
351
|
+
// Sort agents ASC by (startedAt, id)
|
|
352
|
+
const sortedAgents = [...agents].sort((a, b) => {
|
|
353
|
+
if (a.startedAt !== b.startedAt) return a.startedAt < b.startedAt ? -1 : 1;
|
|
354
|
+
return a.id < b.id ? -1 : 1;
|
|
355
|
+
});
|
|
356
|
+
return apiJson({ ...run, agents: sortedAgents });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function handleGetAgents(
|
|
360
|
+
_req: Request,
|
|
361
|
+
_match: RegExpMatchArray,
|
|
362
|
+
params: URLSearchParams,
|
|
363
|
+
stores: Stores,
|
|
364
|
+
): Promise<Response> {
|
|
365
|
+
const { limit, cursor } = parseLimitAndCursor(params);
|
|
366
|
+
const runId = params.get("run");
|
|
367
|
+
|
|
368
|
+
const all = runId !== null ? stores.session.getByRun(runId) : stores.session.getAll();
|
|
369
|
+
// Sort ASC by (startedAt, id)
|
|
370
|
+
const sorted = [...all].sort((a, b) => {
|
|
371
|
+
if (a.startedAt !== b.startedAt) return a.startedAt < b.startedAt ? -1 : 1;
|
|
372
|
+
return a.id < b.id ? -1 : 1;
|
|
373
|
+
});
|
|
374
|
+
const { page, nextCursor } = paginateItems(sorted, cursor, limit, (a) => a.startedAt, "asc");
|
|
375
|
+
return apiJson(page, { nextCursor });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function handleGetAgent(
|
|
379
|
+
_req: Request,
|
|
380
|
+
match: RegExpMatchArray,
|
|
381
|
+
_params: URLSearchParams,
|
|
382
|
+
stores: Stores,
|
|
383
|
+
): Promise<Response> {
|
|
384
|
+
const name = match[1];
|
|
385
|
+
if (name === undefined) return apiError("Agent name required", 400);
|
|
386
|
+
const agent = stores.session.getByName(decodeURIComponent(name));
|
|
387
|
+
if (agent === null) return apiError(`Agent not found: ${name}`, 404);
|
|
388
|
+
return apiJson(agent);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function handleGetEvents(
|
|
392
|
+
_req: Request,
|
|
393
|
+
_match: RegExpMatchArray,
|
|
394
|
+
params: URLSearchParams,
|
|
395
|
+
stores: Stores,
|
|
396
|
+
): Promise<Response> {
|
|
397
|
+
const { limit, cursor } = parseLimitAndCursor(params);
|
|
398
|
+
const agentFilter = params.get("agent");
|
|
399
|
+
const runFilter = params.get("run");
|
|
400
|
+
const sinceParam = params.get("since");
|
|
401
|
+
|
|
402
|
+
// Validate sinceParam as an ISO date if provided
|
|
403
|
+
if (sinceParam !== null && Number.isNaN(Date.parse(sinceParam))) {
|
|
404
|
+
throw new ValidationError(`Invalid since timestamp: ${sinceParam}`, {
|
|
405
|
+
field: "since",
|
|
406
|
+
value: sinceParam,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Effective since: cursor.ts > explicit since > default epoch
|
|
411
|
+
const effectiveSince = cursor !== null ? cursor.ts : (sinceParam ?? "1970-01-01T00:00:00.000Z");
|
|
412
|
+
|
|
413
|
+
// Over-fetch by 1 to detect next page
|
|
414
|
+
const fetchOpts = { since: effectiveSince, limit: limit + 1 };
|
|
415
|
+
|
|
416
|
+
let rawItems =
|
|
417
|
+
agentFilter !== null
|
|
418
|
+
? stores.event.getByAgent(agentFilter, fetchOpts)
|
|
419
|
+
: runFilter !== null
|
|
420
|
+
? stores.event.getByRun(runFilter, fetchOpts)
|
|
421
|
+
: stores.event.getTimeline(fetchOpts);
|
|
422
|
+
|
|
423
|
+
// Drop entries <= cursor.id (ties at cursor.ts)
|
|
424
|
+
if (cursor !== null) {
|
|
425
|
+
const cursorIdNum = Number.parseInt(cursor.id, 10);
|
|
426
|
+
rawItems = rawItems.filter((e) => e.id > cursorIdNum);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const page = rawItems.slice(0, limit);
|
|
430
|
+
const hasMore = rawItems.length > limit;
|
|
431
|
+
const lastItem = page[page.length - 1];
|
|
432
|
+
const nextCursor =
|
|
433
|
+
hasMore && lastItem !== undefined
|
|
434
|
+
? encodeCursor({ ts: lastItem.createdAt, id: String(lastItem.id) })
|
|
435
|
+
: null;
|
|
436
|
+
|
|
437
|
+
return apiJson(page, { nextCursor });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function handleGetMetrics(
|
|
441
|
+
_req: Request,
|
|
442
|
+
_match: RegExpMatchArray,
|
|
443
|
+
params: URLSearchParams,
|
|
444
|
+
stores: Stores,
|
|
445
|
+
): Promise<Response> {
|
|
446
|
+
const limitStr = params.get("limit");
|
|
447
|
+
const limit = limitStr !== null ? Number.parseInt(limitStr, 10) : 10;
|
|
448
|
+
if (Number.isNaN(limit) || limit < 1 || limit > 500) {
|
|
449
|
+
throw new ValidationError(`Invalid limit: ${limitStr ?? "undefined"}`, {
|
|
450
|
+
field: "limit",
|
|
451
|
+
value: limitStr,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const summary = generateSummary(stores.metrics, limit);
|
|
455
|
+
return apiJson(summary);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function handleGetErrors(
|
|
459
|
+
_req: Request,
|
|
460
|
+
_match: RegExpMatchArray,
|
|
461
|
+
params: URLSearchParams,
|
|
462
|
+
stores: Stores,
|
|
463
|
+
): Promise<Response> {
|
|
464
|
+
const limitStr = params.get("limit");
|
|
465
|
+
const limit = limitStr !== null ? Number.parseInt(limitStr, 10) : 100;
|
|
466
|
+
if (Number.isNaN(limit) || limit < 1 || limit > 500) {
|
|
467
|
+
throw new ValidationError(`Invalid limit: ${limitStr ?? "undefined"}`, {
|
|
468
|
+
field: "limit",
|
|
469
|
+
value: limitStr,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const agentFilter = params.get("agent");
|
|
473
|
+
const runFilter = params.get("run");
|
|
474
|
+
const sinceParam = params.get("since");
|
|
475
|
+
if (sinceParam !== null && Number.isNaN(Date.parse(sinceParam))) {
|
|
476
|
+
throw new ValidationError(`Invalid since timestamp: ${sinceParam}`, {
|
|
477
|
+
field: "since",
|
|
478
|
+
value: sinceParam,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// getErrors has no agent/run filter, so over-fetch and post-filter. When a
|
|
483
|
+
// filter is active, widen the window so the client-side cut still fills a page.
|
|
484
|
+
const hasFilter = agentFilter !== null || runFilter !== null;
|
|
485
|
+
const errors = stores.event.getErrors({
|
|
486
|
+
...(sinceParam !== null ? { since: sinceParam } : {}),
|
|
487
|
+
limit: hasFilter ? Math.min(limit * 10, 500) : limit,
|
|
488
|
+
});
|
|
489
|
+
const filtered = errors.filter((e) => {
|
|
490
|
+
if (agentFilter !== null && e.agentName !== agentFilter) return false;
|
|
491
|
+
if (runFilter !== null && e.runId !== runFilter) return false;
|
|
492
|
+
return true;
|
|
493
|
+
});
|
|
494
|
+
return apiJson(filtered.slice(0, limit));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function handleGetMail(
|
|
498
|
+
_req: Request,
|
|
499
|
+
_match: RegExpMatchArray,
|
|
500
|
+
params: URLSearchParams,
|
|
501
|
+
stores: Stores,
|
|
502
|
+
): Promise<Response> {
|
|
503
|
+
const { limit, cursor } = parseLimitAndCursor(params);
|
|
504
|
+
const toFilter = params.get("to") ?? undefined;
|
|
505
|
+
const fromFilter = params.get("from") ?? undefined;
|
|
506
|
+
const unreadParam = params.get("unread");
|
|
507
|
+
const unreadFilter = unreadParam !== null ? unreadParam === "true" : undefined;
|
|
508
|
+
|
|
509
|
+
// Fetch a large window when cursor is present for client-side pagination
|
|
510
|
+
const fetchLimit = cursor !== null ? limit * 5 : undefined;
|
|
511
|
+
const all = stores.mail.getAll({
|
|
512
|
+
to: toFilter,
|
|
513
|
+
from: fromFilter,
|
|
514
|
+
unread: unreadFilter,
|
|
515
|
+
limit: fetchLimit,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Already sorted DESC by createdAt from the store; sort explicitly for stability
|
|
519
|
+
const sorted = [...all].sort((a, b) => {
|
|
520
|
+
if (b.createdAt !== a.createdAt) return b.createdAt < a.createdAt ? -1 : 1;
|
|
521
|
+
return b.id < a.id ? -1 : 1;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const { page, nextCursor } = paginateItems(sorted, cursor, limit, (m) => m.createdAt, "desc");
|
|
525
|
+
return apiJson(page, { nextCursor });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function handleGetMailMessage(
|
|
529
|
+
_req: Request,
|
|
530
|
+
match: RegExpMatchArray,
|
|
531
|
+
_params: URLSearchParams,
|
|
532
|
+
stores: Stores,
|
|
533
|
+
): Promise<Response> {
|
|
534
|
+
const id = match[1];
|
|
535
|
+
if (id === undefined) return apiError("Message ID required", 400);
|
|
536
|
+
const message = stores.mail.getById(id);
|
|
537
|
+
if (message === null) return apiError(`Message not found: ${id}`, 404);
|
|
538
|
+
const thread = message.threadId !== null ? stores.mail.getByThread(message.threadId) : [message];
|
|
539
|
+
return apiJson({ message, thread });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function handleMarkMailRead(
|
|
543
|
+
_req: Request,
|
|
544
|
+
match: RegExpMatchArray,
|
|
545
|
+
_params: URLSearchParams,
|
|
546
|
+
stores: Stores,
|
|
547
|
+
): Promise<Response> {
|
|
548
|
+
const id = match[1];
|
|
549
|
+
if (id === undefined) return apiError("Message ID required", 400);
|
|
550
|
+
const message = stores.mail.getById(id);
|
|
551
|
+
if (message === null) return apiError(`Message not found: ${id}`, 404);
|
|
552
|
+
stores.mail.markRead(id);
|
|
553
|
+
return apiJson({ id, read: true });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function handleSendMail(
|
|
557
|
+
req: Request,
|
|
558
|
+
_match: RegExpMatchArray,
|
|
559
|
+
_params: URLSearchParams,
|
|
560
|
+
stores: Stores,
|
|
561
|
+
): Promise<Response> {
|
|
562
|
+
const body = await parseJsonBody(req);
|
|
563
|
+
const result = sendMail({ mail: stores.mail, session: stores.session }, body);
|
|
564
|
+
return apiJson(result);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function handleReplyMail(
|
|
568
|
+
req: Request,
|
|
569
|
+
match: RegExpMatchArray,
|
|
570
|
+
_params: URLSearchParams,
|
|
571
|
+
stores: Stores,
|
|
572
|
+
): Promise<Response> {
|
|
573
|
+
const id = match[1];
|
|
574
|
+
if (id === undefined) return apiError("Message ID required", 400);
|
|
575
|
+
if (stores.mail.getById(id) === null) {
|
|
576
|
+
return apiError(`Message not found: ${id}`, 404);
|
|
577
|
+
}
|
|
578
|
+
const body = await parseJsonBody(req);
|
|
579
|
+
const result = replyMail({ mail: stores.mail, session: stores.session }, id, body);
|
|
580
|
+
return apiJson(result);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function handleDeleteMail(
|
|
584
|
+
_req: Request,
|
|
585
|
+
match: RegExpMatchArray,
|
|
586
|
+
_params: URLSearchParams,
|
|
587
|
+
stores: Stores,
|
|
588
|
+
): Promise<Response> {
|
|
589
|
+
const id = match[1];
|
|
590
|
+
if (id === undefined) return apiError("Message ID required", 400);
|
|
591
|
+
const result = deleteMail({ mail: stores.mail, session: stores.session }, id);
|
|
592
|
+
if (result === null) return apiError(`Message not found: ${id}`, 404);
|
|
593
|
+
return apiJson(result);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ─── Coordinator handlers ─────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
async function handleCoordinatorState(
|
|
599
|
+
_req: Request,
|
|
600
|
+
_match: RegExpMatchArray,
|
|
601
|
+
_params: URLSearchParams,
|
|
602
|
+
_stores: Stores,
|
|
603
|
+
ctx: HandlerContext,
|
|
604
|
+
): Promise<Response> {
|
|
605
|
+
const state = ctx.coordinatorActions.getCoordinatorState(ctx.coordinatorActionDeps);
|
|
606
|
+
return apiJson(state);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function handleCoordinatorSend(
|
|
610
|
+
req: Request,
|
|
611
|
+
_match: RegExpMatchArray,
|
|
612
|
+
_params: URLSearchParams,
|
|
613
|
+
_stores: Stores,
|
|
614
|
+
ctx: HandlerContext,
|
|
615
|
+
): Promise<Response> {
|
|
616
|
+
const { subject, body, from } = await readJsonBody(req, (raw) => {
|
|
617
|
+
const obj = asObject(raw, "Body must be a JSON object");
|
|
618
|
+
return {
|
|
619
|
+
subject: requireString(obj, "subject"),
|
|
620
|
+
body: requireString(obj, "body"),
|
|
621
|
+
from: optionalString(obj, "from"),
|
|
622
|
+
};
|
|
623
|
+
});
|
|
624
|
+
const result = await ctx.coordinatorActions.sendToCoordinator(
|
|
625
|
+
ctx.coordinatorActionDeps,
|
|
626
|
+
body,
|
|
627
|
+
from !== undefined ? { subject, from } : { subject },
|
|
628
|
+
);
|
|
629
|
+
return apiJson(result);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function handleCoordinatorAsk(
|
|
633
|
+
req: Request,
|
|
634
|
+
_match: RegExpMatchArray,
|
|
635
|
+
_params: URLSearchParams,
|
|
636
|
+
_stores: Stores,
|
|
637
|
+
ctx: HandlerContext,
|
|
638
|
+
): Promise<Response> {
|
|
639
|
+
const { subject, body, from, timeoutSec } = await readJsonBody(req, (raw) => {
|
|
640
|
+
const obj = asObject(raw, "Body must be a JSON object");
|
|
641
|
+
return {
|
|
642
|
+
subject: requireString(obj, "subject"),
|
|
643
|
+
body: requireString(obj, "body"),
|
|
644
|
+
from: optionalString(obj, "from"),
|
|
645
|
+
timeoutSec: optionalTimeoutSec(obj),
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
const result = await ctx.coordinatorActions.askCoordinatorAction(
|
|
649
|
+
ctx.coordinatorActionDeps,
|
|
650
|
+
body,
|
|
651
|
+
from !== undefined ? { subject, from, timeoutSec } : { subject, timeoutSec },
|
|
652
|
+
);
|
|
653
|
+
return apiJson(result);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function handleCoordinatorCheckComplete(
|
|
657
|
+
_req: Request,
|
|
658
|
+
_match: RegExpMatchArray,
|
|
659
|
+
_params: URLSearchParams,
|
|
660
|
+
_stores: Stores,
|
|
661
|
+
ctx: HandlerContext,
|
|
662
|
+
): Promise<Response> {
|
|
663
|
+
const result = await ctx.coordinatorActions.checkCoordinatorComplete(ctx.coordinatorActionDeps);
|
|
664
|
+
return apiJson(result);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function handleCoordinatorStart(
|
|
668
|
+
_req: Request,
|
|
669
|
+
_match: RegExpMatchArray,
|
|
670
|
+
_params: URLSearchParams,
|
|
671
|
+
_stores: Stores,
|
|
672
|
+
ctx: HandlerContext,
|
|
673
|
+
): Promise<Response> {
|
|
674
|
+
const result = await ctx.coordinatorActions.startCoordinatorHeadless(ctx.coordinatorActionDeps);
|
|
675
|
+
return apiJson(result);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function handleCoordinatorStop(
|
|
679
|
+
_req: Request,
|
|
680
|
+
_match: RegExpMatchArray,
|
|
681
|
+
_params: URLSearchParams,
|
|
682
|
+
_stores: Stores,
|
|
683
|
+
ctx: HandlerContext,
|
|
684
|
+
): Promise<Response> {
|
|
685
|
+
const result = await ctx.coordinatorActions.stopCoordinator(ctx.coordinatorActionDeps);
|
|
686
|
+
return apiJson(result);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ─── Fleet-control handlers (sling / stop / merge) ────────────────────────────
|
|
690
|
+
|
|
691
|
+
async function handleSlingAgent(
|
|
692
|
+
req: Request,
|
|
693
|
+
_match: RegExpMatchArray,
|
|
694
|
+
_params: URLSearchParams,
|
|
695
|
+
_stores: Stores,
|
|
696
|
+
ctx: HandlerContext,
|
|
697
|
+
): Promise<Response> {
|
|
698
|
+
const { taskId, capability, name, files, spec, runtime, skipTaskCheck } = await readJsonBody(
|
|
699
|
+
req,
|
|
700
|
+
(raw) => {
|
|
701
|
+
const obj = asObject(raw, "Body must be a JSON object");
|
|
702
|
+
return {
|
|
703
|
+
taskId: requireString(obj, "taskId"),
|
|
704
|
+
capability: optionalString(obj, "capability"),
|
|
705
|
+
name: optionalString(obj, "name"),
|
|
706
|
+
files: optionalString(obj, "files"),
|
|
707
|
+
spec: optionalString(obj, "spec"),
|
|
708
|
+
runtime: optionalString(obj, "runtime"),
|
|
709
|
+
skipTaskCheck: obj.skipTaskCheck === true,
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
);
|
|
713
|
+
const result = await ctx.agentActions.slingAgent(ctx.agentActionDeps, {
|
|
714
|
+
taskId,
|
|
715
|
+
...(capability !== undefined ? { capability } : {}),
|
|
716
|
+
...(name !== undefined ? { name } : {}),
|
|
717
|
+
...(files !== undefined ? { files } : {}),
|
|
718
|
+
...(spec !== undefined ? { spec } : {}),
|
|
719
|
+
...(runtime !== undefined ? { runtime } : {}),
|
|
720
|
+
skipTaskCheck,
|
|
721
|
+
});
|
|
722
|
+
return apiJson(result);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function handleStopAgent(
|
|
726
|
+
req: Request,
|
|
727
|
+
match: RegExpMatchArray,
|
|
728
|
+
_params: URLSearchParams,
|
|
729
|
+
_stores: Stores,
|
|
730
|
+
ctx: HandlerContext,
|
|
731
|
+
): Promise<Response> {
|
|
732
|
+
const name = match[1];
|
|
733
|
+
if (name === undefined) return apiError("Agent name required", 400);
|
|
734
|
+
// Body is optional; default cleanWorktree=false when absent/empty.
|
|
735
|
+
let cleanWorktree = false;
|
|
736
|
+
const text = await req.text();
|
|
737
|
+
if (text.trim().length > 0) {
|
|
738
|
+
try {
|
|
739
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
740
|
+
cleanWorktree = parsed.cleanWorktree === true;
|
|
741
|
+
} catch {
|
|
742
|
+
throw new ValidationError("Invalid JSON body", { field: "body" });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const result = await ctx.agentActions.stopAgent(ctx.agentActionDeps, decodeURIComponent(name), {
|
|
746
|
+
cleanWorktree,
|
|
747
|
+
});
|
|
748
|
+
return apiJson(result);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function handleMergePreview(
|
|
752
|
+
_req: Request,
|
|
753
|
+
_match: RegExpMatchArray,
|
|
754
|
+
params: URLSearchParams,
|
|
755
|
+
_stores: Stores,
|
|
756
|
+
ctx: HandlerContext,
|
|
757
|
+
): Promise<Response> {
|
|
758
|
+
const branch = params.get("branch");
|
|
759
|
+
if (branch === null || branch.length === 0) {
|
|
760
|
+
throw new ValidationError("Missing required query parameter 'branch'", { field: "branch" });
|
|
761
|
+
}
|
|
762
|
+
const into = params.get("into");
|
|
763
|
+
const result = await ctx.agentActions.mergePreview(ctx.agentActionDeps, {
|
|
764
|
+
branch,
|
|
765
|
+
...(into !== null ? { into } : {}),
|
|
766
|
+
});
|
|
767
|
+
return apiJson(result);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function handleMergeRun(
|
|
771
|
+
req: Request,
|
|
772
|
+
_match: RegExpMatchArray,
|
|
773
|
+
_params: URLSearchParams,
|
|
774
|
+
_stores: Stores,
|
|
775
|
+
ctx: HandlerContext,
|
|
776
|
+
): Promise<Response> {
|
|
777
|
+
const body = await parseJsonBody(req);
|
|
778
|
+
const branch = optionalString(body, "branch");
|
|
779
|
+
const into = optionalString(body, "into");
|
|
780
|
+
const all = body.all === true;
|
|
781
|
+
const result = await ctx.agentActions.mergeRun(ctx.agentActionDeps, {
|
|
782
|
+
...(branch !== undefined ? { branch } : {}),
|
|
783
|
+
...(into !== undefined ? { into } : {}),
|
|
784
|
+
all,
|
|
785
|
+
});
|
|
786
|
+
return apiJson(result);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ─── Infrastructure handlers (queue / worktrees / logs / doctor / groups / specs) ─
|
|
790
|
+
|
|
791
|
+
const MERGE_STATUSES: ReadonlySet<string> = new Set([
|
|
792
|
+
"pending",
|
|
793
|
+
"merging",
|
|
794
|
+
"merged",
|
|
795
|
+
"conflict",
|
|
796
|
+
"failed",
|
|
797
|
+
]);
|
|
798
|
+
|
|
799
|
+
async function handleGetMergeQueue(
|
|
800
|
+
_req: Request,
|
|
801
|
+
_match: RegExpMatchArray,
|
|
802
|
+
params: URLSearchParams,
|
|
803
|
+
_stores: Stores,
|
|
804
|
+
ctx: HandlerContext,
|
|
805
|
+
): Promise<Response> {
|
|
806
|
+
const statusParam = params.get("status");
|
|
807
|
+
if (statusParam !== null && !MERGE_STATUSES.has(statusParam)) {
|
|
808
|
+
throw new ValidationError(`Invalid status: ${statusParam}`, {
|
|
809
|
+
field: "status",
|
|
810
|
+
value: statusParam,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
const queuePath = join(ctx.projectRoot, ".agentplate", "merge-queue.db");
|
|
814
|
+
if (!existsSync(queuePath)) return apiJson([]);
|
|
815
|
+
const queue = createMergeQueue(queuePath);
|
|
816
|
+
try {
|
|
817
|
+
const entries = queue.list(
|
|
818
|
+
statusParam !== null ? (statusParam as MergeEntry["status"]) : undefined,
|
|
819
|
+
);
|
|
820
|
+
return apiJson(entries);
|
|
821
|
+
} finally {
|
|
822
|
+
queue.close();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
async function handleGetWorktrees(
|
|
827
|
+
_req: Request,
|
|
828
|
+
_match: RegExpMatchArray,
|
|
829
|
+
_params: URLSearchParams,
|
|
830
|
+
_stores: Stores,
|
|
831
|
+
ctx: HandlerContext,
|
|
832
|
+
): Promise<Response> {
|
|
833
|
+
const worktrees = await listWorktrees(ctx.projectRoot);
|
|
834
|
+
return apiJson(worktrees);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function handleGetLogs(
|
|
838
|
+
_req: Request,
|
|
839
|
+
_match: RegExpMatchArray,
|
|
840
|
+
params: URLSearchParams,
|
|
841
|
+
_stores: Stores,
|
|
842
|
+
ctx: HandlerContext,
|
|
843
|
+
): Promise<Response> {
|
|
844
|
+
const limitStr = params.get("limit");
|
|
845
|
+
const limit = limitStr !== null ? Number.parseInt(limitStr, 10) : 200;
|
|
846
|
+
if (Number.isNaN(limit) || limit < 1 || limit > 2000) {
|
|
847
|
+
throw new ValidationError(`Invalid limit: ${limitStr ?? "undefined"}`, {
|
|
848
|
+
field: "limit",
|
|
849
|
+
value: limitStr,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
const level = params.get("level") ?? undefined;
|
|
853
|
+
if (level !== undefined && !["debug", "info", "warn", "error"].includes(level)) {
|
|
854
|
+
throw new ValidationError(`Invalid level: ${level}`, { field: "level", value: level });
|
|
855
|
+
}
|
|
856
|
+
const agent = params.get("agent") ?? undefined;
|
|
857
|
+
const sinceParam = params.get("since");
|
|
858
|
+
if (sinceParam !== null && Number.isNaN(Date.parse(sinceParam))) {
|
|
859
|
+
throw new ValidationError(`Invalid since timestamp: ${sinceParam}`, {
|
|
860
|
+
field: "since",
|
|
861
|
+
value: sinceParam,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const logsDir = join(ctx.projectRoot, ".agentplate", "logs");
|
|
866
|
+
if (!existsSync(logsDir)) return apiJson([]);
|
|
867
|
+
|
|
868
|
+
const files = await discoverLogFiles(logsDir, agent);
|
|
869
|
+
const events = [];
|
|
870
|
+
for (const file of files) {
|
|
871
|
+
events.push(...(await parseLogFile(file.path)));
|
|
872
|
+
}
|
|
873
|
+
const filtered = filterEvents(events, {
|
|
874
|
+
...(level !== undefined ? { level } : {}),
|
|
875
|
+
...(sinceParam !== null ? { since: new Date(sinceParam) } : {}),
|
|
876
|
+
});
|
|
877
|
+
// Most-recent first, capped at limit.
|
|
878
|
+
filtered.sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : 0));
|
|
879
|
+
return apiJson(filtered.slice(0, limit));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const DOCTOR_CATEGORIES: ReadonlySet<string> = new Set([
|
|
883
|
+
"dependencies",
|
|
884
|
+
"structure",
|
|
885
|
+
"config",
|
|
886
|
+
"databases",
|
|
887
|
+
"consistency",
|
|
888
|
+
"agents",
|
|
889
|
+
"merge",
|
|
890
|
+
"logs",
|
|
891
|
+
"version",
|
|
892
|
+
"ecosystem",
|
|
893
|
+
"providers",
|
|
894
|
+
"watchdog",
|
|
895
|
+
"serve",
|
|
896
|
+
]);
|
|
897
|
+
|
|
898
|
+
async function handleGetDoctor(
|
|
899
|
+
_req: Request,
|
|
900
|
+
_match: RegExpMatchArray,
|
|
901
|
+
params: URLSearchParams,
|
|
902
|
+
_stores: Stores,
|
|
903
|
+
ctx: HandlerContext,
|
|
904
|
+
): Promise<Response> {
|
|
905
|
+
const category = params.get("category");
|
|
906
|
+
if (category !== null && !DOCTOR_CATEGORIES.has(category)) {
|
|
907
|
+
throw new ValidationError(`Invalid category: ${category}`, {
|
|
908
|
+
field: "category",
|
|
909
|
+
value: category,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
// Lazy import: `../doctor.ts` transitively imports `../commands/serve.ts`
|
|
913
|
+
// (via the serve doctor check), which would form a module-init cycle with
|
|
914
|
+
// serve.ts ↔ rest.ts and trip a TDZ on `_apiHandlers`. Importing on demand
|
|
915
|
+
// keeps doctor out of the static graph.
|
|
916
|
+
const { collectDoctorChecks } = await import("../doctor.ts");
|
|
917
|
+
const checks = await collectDoctorChecks(
|
|
918
|
+
ctx.projectRoot,
|
|
919
|
+
category !== null ? (category as DoctorCategory) : undefined,
|
|
920
|
+
);
|
|
921
|
+
const summary = {
|
|
922
|
+
pass: checks.filter((c) => c.status === "pass").length,
|
|
923
|
+
warn: checks.filter((c) => c.status === "warn").length,
|
|
924
|
+
fail: checks.filter((c) => c.status === "fail").length,
|
|
925
|
+
};
|
|
926
|
+
return apiJson({ checks, summary });
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function handleGetGroups(
|
|
930
|
+
_req: Request,
|
|
931
|
+
_match: RegExpMatchArray,
|
|
932
|
+
_params: URLSearchParams,
|
|
933
|
+
_stores: Stores,
|
|
934
|
+
ctx: HandlerContext,
|
|
935
|
+
): Promise<Response> {
|
|
936
|
+
const groups = await loadGroups(ctx.projectRoot);
|
|
937
|
+
return apiJson(groups);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async function handleGetSpecs(
|
|
941
|
+
_req: Request,
|
|
942
|
+
_match: RegExpMatchArray,
|
|
943
|
+
_params: URLSearchParams,
|
|
944
|
+
_stores: Stores,
|
|
945
|
+
ctx: HandlerContext,
|
|
946
|
+
): Promise<Response> {
|
|
947
|
+
const specsDir = join(ctx.projectRoot, ".agentplate", "specs");
|
|
948
|
+
if (!existsSync(specsDir)) return apiJson([]);
|
|
949
|
+
const entries = await readdir(specsDir);
|
|
950
|
+
const specs = [];
|
|
951
|
+
for (const file of entries) {
|
|
952
|
+
if (!file.endsWith(".md")) continue;
|
|
953
|
+
const info = await stat(join(specsDir, file));
|
|
954
|
+
specs.push({
|
|
955
|
+
taskId: file.replace(/\.md$/, ""),
|
|
956
|
+
sizeBytes: info.size,
|
|
957
|
+
modifiedAt: info.mtime.toISOString(),
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
specs.sort((a, b) => (a.modifiedAt < b.modifiedAt ? 1 : -1));
|
|
961
|
+
return apiJson(specs);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function handleGetSpec(
|
|
965
|
+
_req: Request,
|
|
966
|
+
match: RegExpMatchArray,
|
|
967
|
+
_params: URLSearchParams,
|
|
968
|
+
_stores: Stores,
|
|
969
|
+
ctx: HandlerContext,
|
|
970
|
+
): Promise<Response> {
|
|
971
|
+
const taskId = match[1];
|
|
972
|
+
if (taskId === undefined) return apiError("Task ID required", 400);
|
|
973
|
+
const decoded = decodeURIComponent(taskId);
|
|
974
|
+
// Guard against path traversal — task IDs are flat slugs.
|
|
975
|
+
if (decoded.includes("/") || decoded.includes("..")) {
|
|
976
|
+
return apiError("Invalid task ID", 400);
|
|
977
|
+
}
|
|
978
|
+
const specPath = join(ctx.projectRoot, ".agentplate", "specs", `${decoded}.md`);
|
|
979
|
+
if (!existsSync(specPath)) return apiError(`Spec not found: ${decoded}`, 404);
|
|
980
|
+
const content = await readFile(specPath, "utf-8");
|
|
981
|
+
return apiJson({ taskId: decoded, content });
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// ─── Route table ─────────────────────────────────────────────────────────────
|
|
985
|
+
|
|
986
|
+
const ROUTES: Route[] = [
|
|
987
|
+
{ method: "GET", pattern: /^\/api\/runs$/, handler: handleGetRuns },
|
|
988
|
+
{ method: "GET", pattern: /^\/api\/runs\/([^/]+)$/, handler: handleGetRun },
|
|
989
|
+
{ method: "GET", pattern: /^\/api\/agents$/, handler: handleGetAgents },
|
|
990
|
+
{ method: "POST", pattern: /^\/api\/agents\/sling$/, handler: handleSlingAgent },
|
|
991
|
+
{ method: "POST", pattern: /^\/api\/agents\/([^/]+)\/stop$/, handler: handleStopAgent },
|
|
992
|
+
{ method: "GET", pattern: /^\/api\/agents\/([^/]+)$/, handler: handleGetAgent },
|
|
993
|
+
{ method: "GET", pattern: /^\/api\/merge\/preview$/, handler: handleMergePreview },
|
|
994
|
+
{ method: "GET", pattern: /^\/api\/merge\/queue$/, handler: handleGetMergeQueue },
|
|
995
|
+
{ method: "POST", pattern: /^\/api\/merge$/, handler: handleMergeRun },
|
|
996
|
+
{ method: "GET", pattern: /^\/api\/worktrees$/, handler: handleGetWorktrees },
|
|
997
|
+
{ method: "GET", pattern: /^\/api\/logs$/, handler: handleGetLogs },
|
|
998
|
+
{ method: "GET", pattern: /^\/api\/doctor$/, handler: handleGetDoctor },
|
|
999
|
+
{ method: "GET", pattern: /^\/api\/groups$/, handler: handleGetGroups },
|
|
1000
|
+
{ method: "GET", pattern: /^\/api\/specs$/, handler: handleGetSpecs },
|
|
1001
|
+
{ method: "GET", pattern: /^\/api\/specs\/([^/]+)$/, handler: handleGetSpec },
|
|
1002
|
+
{ method: "GET", pattern: /^\/api\/events$/, handler: handleGetEvents },
|
|
1003
|
+
{ method: "GET", pattern: /^\/api\/metrics$/, handler: handleGetMetrics },
|
|
1004
|
+
{ method: "GET", pattern: /^\/api\/errors$/, handler: handleGetErrors },
|
|
1005
|
+
{ method: "GET", pattern: /^\/api\/mail$/, handler: handleGetMail },
|
|
1006
|
+
{ method: "POST", pattern: /^\/api\/mail$/, handler: handleSendMail },
|
|
1007
|
+
{ method: "GET", pattern: /^\/api\/mail\/([^/]+)$/, handler: handleGetMailMessage },
|
|
1008
|
+
{ method: "DELETE", pattern: /^\/api\/mail\/([^/]+)$/, handler: handleDeleteMail },
|
|
1009
|
+
{ method: "POST", pattern: /^\/api\/mail\/([^/]+)\/read$/, handler: handleMarkMailRead },
|
|
1010
|
+
{ method: "POST", pattern: /^\/api\/mail\/([^/]+)\/reply$/, handler: handleReplyMail },
|
|
1011
|
+
{ method: "GET", pattern: /^\/api\/coordinator\/state$/, handler: handleCoordinatorState },
|
|
1012
|
+
{ method: "POST", pattern: /^\/api\/coordinator\/send$/, handler: handleCoordinatorSend },
|
|
1013
|
+
{ method: "POST", pattern: /^\/api\/coordinator\/ask$/, handler: handleCoordinatorAsk },
|
|
1014
|
+
{
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
pattern: /^\/api\/coordinator\/check-complete$/,
|
|
1017
|
+
handler: handleCoordinatorCheckComplete,
|
|
1018
|
+
},
|
|
1019
|
+
{ method: "POST", pattern: /^\/api\/coordinator\/start$/, handler: handleCoordinatorStart },
|
|
1020
|
+
{ method: "POST", pattern: /^\/api\/coordinator\/stop$/, handler: handleCoordinatorStop },
|
|
1021
|
+
];
|
|
1022
|
+
|
|
1023
|
+
// ─── Public registration ──────────────────────────────────────────────────────
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Register all REST API handlers with the serve scaffold.
|
|
1027
|
+
* Deps allows injecting stores for testing; in production, stores are opened
|
|
1028
|
+
* from projectRoot/.agentplate/{sessions,events,mail}.db.
|
|
1029
|
+
*/
|
|
1030
|
+
export function registerRestApi(deps?: RestApiDeps): void {
|
|
1031
|
+
let stores: Stores | null = null;
|
|
1032
|
+
const projectRoot = deps?._projectRoot ?? process.cwd();
|
|
1033
|
+
|
|
1034
|
+
function getStores(): Stores {
|
|
1035
|
+
if (stores !== null) return stores;
|
|
1036
|
+
|
|
1037
|
+
if (
|
|
1038
|
+
deps?._runStore !== undefined &&
|
|
1039
|
+
deps._sessionStore !== undefined &&
|
|
1040
|
+
deps._eventStore !== undefined &&
|
|
1041
|
+
deps._mailStore !== undefined
|
|
1042
|
+
) {
|
|
1043
|
+
stores = {
|
|
1044
|
+
run: deps._runStore,
|
|
1045
|
+
session: deps._sessionStore,
|
|
1046
|
+
event: deps._eventStore,
|
|
1047
|
+
mail: deps._mailStore,
|
|
1048
|
+
// Metrics is optional in DI — tests that don't exercise /api/metrics
|
|
1049
|
+
// get an in-memory store so the field stays non-null.
|
|
1050
|
+
metrics: deps._metricsStore ?? createMetricsStore(":memory:"),
|
|
1051
|
+
};
|
|
1052
|
+
} else {
|
|
1053
|
+
stores = openStores(projectRoot);
|
|
1054
|
+
}
|
|
1055
|
+
return stores;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const overrides = deps?._coordinatorActions ?? {};
|
|
1059
|
+
const coordinatorActions = {
|
|
1060
|
+
getCoordinatorState: overrides.getCoordinatorState ?? getCoordinatorState,
|
|
1061
|
+
sendToCoordinator: overrides.sendToCoordinator ?? sendToCoordinator,
|
|
1062
|
+
askCoordinatorAction: overrides.askCoordinatorAction ?? askCoordinatorAction,
|
|
1063
|
+
checkCoordinatorComplete: overrides.checkCoordinatorComplete ?? checkCoordinatorComplete,
|
|
1064
|
+
startCoordinatorHeadless: overrides.startCoordinatorHeadless ?? startCoordinatorHeadless,
|
|
1065
|
+
stopCoordinator: overrides.stopCoordinator ?? stopCoordinator,
|
|
1066
|
+
};
|
|
1067
|
+
const coordinatorActionDeps: CoordinatorActionDeps = {
|
|
1068
|
+
projectRoot,
|
|
1069
|
+
...(deps?._coordinatorActionDeps ?? {}),
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
const agentOverrides = deps?._agentActions ?? {};
|
|
1073
|
+
const agentActions = {
|
|
1074
|
+
slingAgent: agentOverrides.slingAgent ?? slingAgent,
|
|
1075
|
+
stopAgent: agentOverrides.stopAgent ?? stopAgent,
|
|
1076
|
+
mergePreview: agentOverrides.mergePreview ?? mergePreview,
|
|
1077
|
+
mergeRun: agentOverrides.mergeRun ?? mergeRun,
|
|
1078
|
+
};
|
|
1079
|
+
const agentActionDeps: AgentActionDeps = {
|
|
1080
|
+
projectRoot,
|
|
1081
|
+
...(deps?._agentActionDeps ?? {}),
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
registerApiHandler((req: Request): Response | Promise<Response> | null => {
|
|
1085
|
+
const url = new URL(req.url);
|
|
1086
|
+
const path = url.pathname;
|
|
1087
|
+
|
|
1088
|
+
// Two passes: first try to match path+method exactly. If none matches
|
|
1089
|
+
// but the path matched some route, return 405 (method not allowed).
|
|
1090
|
+
let matchedRoute: { route: Route; match: RegExpMatchArray } | null = null;
|
|
1091
|
+
let pathMatched = false;
|
|
1092
|
+
for (const route of ROUTES) {
|
|
1093
|
+
const match = path.match(route.pattern);
|
|
1094
|
+
if (match === null) continue;
|
|
1095
|
+
pathMatched = true;
|
|
1096
|
+
if (req.method === route.method) {
|
|
1097
|
+
matchedRoute = { route, match };
|
|
1098
|
+
break;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (matchedRoute !== null) {
|
|
1103
|
+
const { route, match } = matchedRoute;
|
|
1104
|
+
return (async (): Promise<Response> => {
|
|
1105
|
+
try {
|
|
1106
|
+
const ctx: HandlerContext = {
|
|
1107
|
+
projectRoot,
|
|
1108
|
+
coordinatorActions,
|
|
1109
|
+
coordinatorActionDeps,
|
|
1110
|
+
agentActions,
|
|
1111
|
+
agentActionDeps,
|
|
1112
|
+
};
|
|
1113
|
+
return await route.handler(req, match, url.searchParams, getStores(), ctx);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
if (err instanceof AgentplateError) {
|
|
1116
|
+
return apiError(err.message, statusFromError(err));
|
|
1117
|
+
}
|
|
1118
|
+
process.stderr.write(`REST handler error: ${String(err)}\n`);
|
|
1119
|
+
return apiError("Internal server error", 500);
|
|
1120
|
+
}
|
|
1121
|
+
})();
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (pathMatched) {
|
|
1125
|
+
return apiError("Method not allowed", 405);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return null;
|
|
1129
|
+
});
|
|
1130
|
+
}
|