@geraldmaron/construct 1.2.3 → 1.3.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/README.md +7 -10
- package/bin/construct +235 -209
- package/bin/construct-postinstall.mjs +0 -21
- package/commands/work/optimize-prompts.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/artifact-loop-core.mjs +412 -0
- package/lib/artifact-manifest.mjs +6 -0
- package/lib/artifact-release-gate.mjs +115 -49
- package/lib/audit-rules.mjs +2 -4
- package/lib/audit-skills.mjs +32 -20
- package/lib/audit-specialists.mjs +44 -26
- package/lib/auto-docs.mjs +6 -5
- package/lib/brand-prose.mjs +3 -3
- package/lib/brand-tokens.mjs +2 -2
- package/lib/certification/artifact-fixtures.mjs +4 -1
- package/lib/certification/demo-parity.mjs +6 -32
- package/lib/certification/real-llm-scenarios.mjs +58 -13
- package/lib/certification/role-cards.mjs +7 -6
- package/lib/certification/role-overlays.mjs +4 -4
- package/lib/certification/runner.mjs +9 -2
- package/lib/certification/skill-inventory.mjs +34 -22
- package/lib/certification/skill-scenarios.mjs +21 -8
- package/lib/certification/specialist-contracts.mjs +4 -3
- package/lib/certification/specialist-scenarios.mjs +7 -6
- package/lib/certification/status.mjs +5 -4
- package/lib/cli-commands.mjs +55 -54
- package/lib/comment-lint.mjs +1 -5
- package/lib/completions.mjs +20 -2
- package/lib/config/legacy-config-migration.mjs +90 -24
- package/lib/config/schema.mjs +5 -5
- package/lib/config/source-targets.mjs +80 -0
- package/lib/contracts/validate.mjs +119 -51
- package/lib/decisions/golden.mjs +3 -2
- package/lib/decisions/registry.mjs +11 -6
- package/lib/demo-project.mjs +188 -0
- package/lib/demo-recording.mjs +9 -34
- package/lib/demo-script.mjs +2 -2
- package/lib/demo-surface.mjs +13 -131
- package/lib/demo-tour-renderer.mjs +92 -0
- package/lib/demo.mjs +69 -54
- package/lib/diagram-export.mjs +63 -14
- package/lib/doctor/source-checkout.mjs +3 -4
- package/lib/doctor/watchers/consistency.mjs +90 -22
- package/lib/doctor/watchers/credential-parity.mjs +97 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +1 -1
- package/lib/doctor/watchers/service-health.mjs +4 -32
- package/lib/document-export.mjs +73 -9
- package/lib/document-extract.mjs +10 -198
- package/lib/embed/artifact.mjs +2 -3
- package/lib/embed/cli.mjs +1 -1
- package/lib/embed/config.mjs +1 -2
- package/lib/embed/customer-profiles.mjs +1 -1
- package/lib/embed/daemon.mjs +5 -6
- package/lib/embed/demand-fetch.mjs +114 -2
- package/lib/embed/docs-lifecycle.mjs +9 -5
- package/lib/embed/intake-metrics.mjs +2 -2
- package/lib/embed/notifications.mjs +2 -3
- package/lib/embed/recommendation-store.mjs +25 -22
- package/lib/embed/role-framing.mjs +4 -9
- package/lib/embedded-contract/capability.mjs +4 -2
- package/lib/embedded-contract/contract-version.mjs +1 -1
- package/lib/env-config.mjs +2 -2
- package/lib/flavors/loader.mjs +21 -19
- package/lib/graph/build-from-registry.mjs +31 -7
- package/lib/graph/staleness.mjs +1 -1
- package/lib/headhunt.mjs +82 -35
- package/lib/hooks/agent-tracker.mjs +2 -2
- package/lib/hooks/config-protection.mjs +1 -1
- package/lib/hooks/registry-sync.mjs +3 -3
- package/lib/host-capabilities.mjs +5 -5
- package/lib/improvement/controller.mjs +2 -2
- package/lib/init-unified.mjs +19 -21
- package/lib/intake/classify.mjs +26 -0
- package/lib/intake/daemon.mjs +2 -3
- package/lib/intake/prepare.mjs +10 -3
- package/lib/intake/session-prelude.mjs +1 -1
- package/lib/intake/tables/creative.mjs +9 -9
- package/lib/intake/tables/operations.mjs +4 -4
- package/lib/intake/tables/research.mjs +2 -2
- package/lib/integrations/intake-integrations.mjs +9 -10
- package/lib/knowledge/research-store.mjs +2 -2
- package/lib/mcp/server.mjs +104 -61
- package/lib/mcp/tool-budget.mjs +1 -46
- package/lib/mcp/tool-recovery.mjs +44 -0
- package/lib/mcp/tools/artifact-author.mjs +36 -0
- package/lib/mcp/tools/find-tool.mjs +86 -0
- package/lib/mcp/tools/orchestration-run.mjs +107 -69
- package/lib/mcp/tools/project.mjs +4 -3
- package/lib/mcp/tools/{profile.mjs → scope.mjs} +54 -52
- package/lib/mcp/tools/skills.mjs +39 -22
- package/lib/mcp/tools/telemetry.mjs +2 -1
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/mcp-platform-config.mjs +7 -5
- package/lib/migrations/index.mjs +4 -2
- package/lib/migrations/v2-unified-registry.mjs +35 -0
- package/lib/model-router.mjs +203 -36
- package/lib/models/catalog.mjs +25 -9
- package/lib/models/execution-capability-profile.mjs +3 -3
- package/lib/models/execution-policy.mjs +7 -6
- package/lib/models/provider-poll.mjs +18 -4
- package/lib/opencode-config.mjs +56 -1
- package/lib/opencode-runtime-plugin.mjs +13 -10
- package/lib/oracle/artifact-gate.mjs +14 -4
- package/lib/oracle/cli.mjs +2 -0
- package/lib/oracle/dispatch.mjs +18 -1
- package/lib/oracle/execute.mjs +39 -7
- package/lib/oracle/index.mjs +1 -1
- package/lib/oracle/org-graph.mjs +10 -5
- package/lib/oracle/policy.mjs +6 -0
- package/lib/oracle/read-model.mjs +103 -6
- package/lib/oracle/reconcile.mjs +31 -4
- package/lib/oracle/remediation-dispatch.mjs +53 -0
- package/lib/oracle/routing.mjs +5 -0
- package/lib/oracle/synthesize.mjs +80 -3
- package/lib/orchestration/routing-tables.mjs +44 -10
- package/lib/orchestration/runtime.mjs +2 -0
- package/lib/orchestration/worker.mjs +1 -1
- package/lib/orchestration-policy.mjs +351 -62
- package/lib/overrides/resolver.mjs +1 -12
- package/lib/parity.mjs +37 -11
- package/lib/policy/engine.mjs +80 -15
- package/lib/prompt-composer.js +51 -15
- package/lib/prompt-metadata.mjs +3 -2
- package/lib/providers/connection-probe.mjs +58 -0
- package/lib/providers/copilot-auth.mjs +1 -1
- package/lib/providers/credential-bootstrap.mjs +1 -1
- package/lib/providers/op-run.mjs +3 -4
- package/lib/providers/secret-resolver.mjs +31 -0
- package/lib/publish-tooling.mjs +3 -11
- package/lib/publish.mjs +12 -29
- package/lib/reconcile/mcp-entry-reconcile.mjs +14 -9
- package/lib/reflect.mjs +2 -2
- package/lib/registry/agent-manifest.mjs +190 -0
- package/lib/registry/assemble.mjs +133 -0
- package/lib/registry/catalog.mjs +171 -0
- package/lib/registry/cli.mjs +590 -6
- package/lib/registry/consolidation.mjs +5 -4
- package/lib/registry/docs-sync.mjs +67 -0
- package/lib/registry/loader.mjs +147 -0
- package/lib/registry/org-io.mjs +76 -0
- package/lib/registry/retired-paths.mjs +71 -0
- package/lib/registry/surface-map.mjs +5 -7
- package/lib/registry/validator.mjs +438 -0
- package/lib/research-execution-policy.mjs +173 -0
- package/lib/roles/catalog.mjs +4 -9
- package/lib/roles/fence.mjs +107 -1
- package/lib/roles/flavor-bindings.mjs +68 -0
- package/lib/roles/gateway.mjs +140 -29
- package/lib/roles/manifest.mjs +22 -13
- package/lib/roles/router.mjs +1 -1
- package/lib/scopes/enrich.mjs +67 -0
- package/lib/scopes/hooks.mjs +12 -0
- package/lib/{profiles → scopes}/lifecycle.mjs +70 -70
- package/lib/scopes/loader.mjs +104 -0
- package/lib/scopes/rebrand.mjs +32 -0
- package/lib/scopes/research-profile.mjs +16 -0
- package/lib/scopes/teams.mjs +98 -0
- package/lib/service-manager.mjs +1 -117
- package/lib/setup-prompts.mjs +1 -1
- package/lib/setup.mjs +6 -26
- package/lib/skills/composition-graph.mjs +98 -0
- package/lib/skills/router.mjs +80 -0
- package/lib/specialist-contracts.mjs +17 -18
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/specialists/prompt-schema.mjs +6 -5
- package/lib/specialists/roster.mjs +7 -12
- package/lib/specialists/schema.mjs +9 -6
- package/lib/status.mjs +25 -90
- package/lib/storage/file-lock.mjs +6 -3
- package/lib/telemetry/skill-calls.mjs +1 -1
- package/lib/templates/doc-presentation.mjs +63 -0
- package/lib/templates/visual-requirements.mjs +35 -2
- package/lib/term-format.mjs +107 -2
- package/lib/test-env-setup.mjs +21 -0
- package/lib/ui/components.mjs +42 -0
- package/lib/ui/glyphs.mjs +58 -0
- package/lib/ui/links.mjs +115 -0
- package/lib/ui/theme.mjs +108 -0
- package/lib/uninstall/uninstall.mjs +2 -1
- package/lib/validator.mjs +45 -8
- package/lib/validators/skill-effectiveness.mjs +174 -0
- package/lib/validators/skills.mjs +1 -1
- package/package.json +12 -8
- package/personas/construct.md +12 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/beads-hygiene.md +52 -0
- package/rules/common/neurodivergent-output.md +4 -7
- package/rules/common/research.md +2 -0
- package/scripts/sync-specialists.mjs +260 -49
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/brand/output-vibe.md +48 -0
- package/skills/docs/adr-workflow.md +3 -0
- package/skills/docs/backlog-proposal-workflow.md +3 -0
- package/skills/docs/codebase-research-workflow.md +5 -2
- package/skills/docs/customer-profile-workflow.md +3 -0
- package/skills/docs/document-ingest-workflow.md +3 -0
- package/skills/docs/evidence-ingest-workflow.md +4 -1
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +1 -1
- package/skills/docs/prfaq-workflow.md +3 -0
- package/skills/docs/product-intelligence-workflow.md +4 -1
- package/skills/docs/product-signal-workflow.md +3 -0
- package/skills/docs/research-workflow.md +13 -6
- package/skills/docs/runbook-workflow.md +3 -0
- package/skills/docs/strategy-workflow.md +3 -0
- package/skills/docs/user-research-workflow.md +6 -3
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/{engineer.ai.md → ai-engineer.md} +2 -2
- package/skills/roles/architect.ai-systems.md +1 -1
- package/skills/roles/architect.data.md +1 -1
- package/skills/roles/architect.enterprise.md +1 -1
- package/skills/roles/architect.integration.md +1 -1
- package/skills/roles/architect.md +1 -1
- package/skills/roles/architect.platform.md +1 -1
- package/skills/roles/{product-manager.business-strategy.md → business-strategist.md} +3 -3
- package/skills/roles/data-analyst.experiment.md +1 -1
- package/skills/roles/data-analyst.md +1 -1
- package/skills/roles/data-analyst.product-intelligence.md +1 -1
- package/skills/roles/data-analyst.product.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/{engineer.data.md → data-engineer.md} +2 -2
- package/skills/roles/data-engineer.pipeline.md +2 -2
- package/skills/roles/data-engineer.vector-retrieval.md +2 -2
- package/skills/roles/data-engineer.warehouse.md +2 -2
- package/skills/roles/debugger.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +1 -1
- package/skills/roles/{reviewer.devil-advocate.md → devil-advocate.md} +2 -2
- package/skills/roles/{operator.docs.md → docs-keeper.md} +3 -3
- package/skills/roles/engineer.md +3 -7
- package/skills/roles/{reviewer.evaluator.md → evaluator.md} +2 -2
- package/skills/roles/{researcher.explorer.md → explorer.md} +2 -2
- package/skills/roles/{operator.md → operations.md} +2 -5
- package/skills/roles/orchestrator.md +1 -1
- package/skills/roles/{engineer.platform.md → platform-engineer.md} +2 -2
- package/skills/roles/product-manager.ai-product.md +1 -1
- package/skills/roles/product-manager.enterprise.md +1 -1
- package/skills/roles/product-manager.growth.md +1 -2
- package/skills/roles/product-manager.md +1 -2
- package/skills/roles/product-manager.platform.md +1 -1
- package/skills/roles/product-manager.product.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -1
- package/skills/roles/qa.api-contract.md +1 -1
- package/skills/roles/qa.data-pipeline.md +1 -1
- package/skills/roles/qa.md +1 -1
- package/skills/roles/qa.web-ui.md +1 -1
- package/skills/roles/{operator.release.md → release-manager.md} +3 -3
- package/skills/roles/researcher.md +1 -1
- package/skills/roles/reviewer.md +1 -1
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.appsec.md +1 -1
- package/skills/roles/security.cloud.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +1 -1
- package/skills/roles/security.privacy.md +1 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/{operator.sre.md → sre.md} +3 -3
- package/skills/roles/{qa.test-automation.md → test-automation.md} +2 -2
- package/skills/roles/{reviewer.trace.md → trace-reviewer.md} +2 -2
- package/skills/roles/{researcher.ux.md → ux-researcher.md} +2 -2
- package/skills/routing.json +18 -0
- package/skills/routing.md +1 -1
- package/specialists/artifact-manifest.json +2 -1
- package/specialists/audit-enrichments.json +14 -14
- package/specialists/org/contracts/accessibility-to-qa.json +37 -0
- package/specialists/org/contracts/any-to-business-strategist.json +57 -0
- package/specialists/org/contracts/any-to-debugger.json +38 -0
- package/specialists/org/contracts/any-to-designer.json +33 -0
- package/specialists/org/contracts/any-to-docs-keeper.json +34 -0
- package/specialists/org/contracts/any-to-explorer.json +39 -0
- package/specialists/org/contracts/any-to-sre-incident.json +33 -0
- package/specialists/org/contracts/any-to-trace-reviewer.json +32 -0
- package/specialists/org/contracts/architect-to-ai-engineer.json +32 -0
- package/specialists/org/contracts/architect-to-data-engineer.json +52 -0
- package/specialists/org/contracts/architect-to-devil-advocate.json +38 -0
- package/specialists/org/contracts/architect-to-engineer.json +34 -0
- package/specialists/org/contracts/architect-to-evaluator.json +59 -0
- package/specialists/org/contracts/architect-to-legal-compliance.json +55 -0
- package/specialists/org/contracts/architect-to-operations.json +45 -0
- package/specialists/org/contracts/architect-to-platform-engineer.json +42 -0
- package/specialists/org/contracts/business-strategist-to-product-manager.json +39 -0
- package/specialists/org/contracts/construct-to-orchestrator.json +36 -0
- package/specialists/org/contracts/construct-to-rd-lead.json +53 -0
- package/specialists/org/contracts/data-analyst-to-product-manager.json +43 -0
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +36 -0
- package/specialists/org/contracts/designer-to-accessibility.json +36 -0
- package/specialists/org/contracts/engineer-to-qa.json +34 -0
- package/specialists/org/contracts/engineer-to-reviewer.json +52 -0
- package/specialists/org/contracts/explorer-to-engineer.json +38 -0
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +43 -0
- package/specialists/org/contracts/platform-engineer-to-engineer.json +31 -0
- package/specialists/org/contracts/product-manager-to-architect.json +71 -0
- package/specialists/org/contracts/product-manager-to-data-analyst.json +49 -0
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +51 -0
- package/specialists/org/contracts/qa-to-release-manager.json +42 -0
- package/specialists/org/contracts/qa-to-test-automation.json +42 -0
- package/specialists/org/contracts/rd-lead-to-architect.json +38 -0
- package/specialists/org/contracts/researcher-to-architect.json +40 -0
- package/specialists/org/contracts/researcher-to-product-manager.json +57 -0
- package/specialists/org/contracts/reviewer-to-security.json +41 -0
- package/specialists/org/contracts/sre-to-release-manager.json +36 -0
- package/specialists/org/contracts/test-automation-to-engineer.json +34 -0
- package/specialists/org/contracts/trace-reviewer-to-sre.json +41 -0
- package/specialists/org/contracts/user-to-construct.json +30 -0
- package/specialists/org/groups/engineering-group.json +43 -0
- package/specialists/org/groups/governance-group.json +38 -0
- package/specialists/org/groups/operations-group.json +41 -0
- package/specialists/org/groups/product-group.json +46 -0
- package/specialists/org/groups/quality-group.json +42 -0
- package/specialists/org/groups/strategy-group.json +41 -0
- package/specialists/org/policies/action-approval.json +13 -0
- package/specialists/org/policies/agents-routing.json +13 -0
- package/specialists/org/policies/architecture.json +20 -0
- package/specialists/org/policies/bash-safety.json +13 -0
- package/specialists/org/policies/beads-hygiene.json +13 -0
- package/specialists/org/policies/bootstrap-state.json +13 -0
- package/specialists/org/policies/code-review.json +13 -0
- package/specialists/org/policies/coding-style.json +13 -0
- package/specialists/org/policies/comments.json +13 -0
- package/specialists/org/policies/commit-approval.json +13 -0
- package/specialists/org/policies/contract-preconditions.json +13 -0
- package/specialists/org/policies/deployment.json +20 -0
- package/specialists/org/policies/description.json +14 -0
- package/specialists/org/policies/design-approval.json +19 -0
- package/specialists/org/policies/doc-ownership.json +13 -0
- package/specialists/org/policies/file-path-fence.json +13 -0
- package/specialists/org/policies/framing.json +13 -0
- package/specialists/org/policies/git-workflow.json +13 -0
- package/specialists/org/policies/incident-response.json +15 -0
- package/specialists/org/policies/intake-triage.json +15 -0
- package/specialists/org/policies/neurodivergent-output.json +13 -0
- package/specialists/org/policies/no-fabrication.json +13 -0
- package/specialists/org/policies/patterns.json +13 -0
- package/specialists/org/policies/quality-gate-approval.json +17 -0
- package/specialists/org/policies/release-gates.json +13 -0
- package/specialists/org/policies/research-evidence.json +13 -0
- package/specialists/org/policies/review-before-change.json +13 -0
- package/specialists/org/policies/rollback.json +15 -0
- package/specialists/org/policies/scope-change.json +20 -0
- package/specialists/org/policies/secret-scan.json +13 -0
- package/specialists/org/policies/security-approval.json +17 -0
- package/specialists/org/policies/security.json +13 -0
- package/specialists/org/policies/session-efficiency.json +13 -0
- package/specialists/org/policies/skill-routing.json +13 -0
- package/specialists/org/policies/strategic-prioritization.json +17 -0
- package/specialists/org/policies/testing.json +13 -0
- package/specialists/org/policies/tool-invisibility.json +14 -0
- package/specialists/org/scopes/creative.json +54 -0
- package/specialists/org/scopes/operations.json +51 -0
- package/specialists/org/scopes/research.json +60 -0
- package/specialists/org/scopes/rnd.json +81 -0
- package/specialists/org/specialists/cx-accessibility.json +69 -0
- package/specialists/org/specialists/cx-ai-engineer.json +75 -0
- package/specialists/org/specialists/cx-architect.json +89 -0
- package/specialists/org/specialists/cx-business-strategist.json +72 -0
- package/specialists/org/specialists/cx-data-analyst.json +68 -0
- package/specialists/org/specialists/cx-data-engineer.json +71 -0
- package/specialists/org/specialists/cx-debugger.json +75 -0
- package/specialists/org/specialists/cx-designer.json +85 -0
- package/specialists/org/specialists/cx-devil-advocate.json +71 -0
- package/specialists/org/specialists/cx-docs-keeper.json +82 -0
- package/specialists/org/specialists/cx-engineer.json +106 -0
- package/specialists/org/specialists/cx-evaluator.json +69 -0
- package/specialists/org/specialists/cx-explorer.json +69 -0
- package/specialists/org/specialists/cx-legal-compliance.json +74 -0
- package/specialists/org/specialists/cx-operations.json +72 -0
- package/specialists/org/specialists/cx-oracle.json +46 -0
- package/specialists/org/specialists/cx-orchestrator.json +81 -0
- package/specialists/org/specialists/cx-platform-engineer.json +80 -0
- package/specialists/org/specialists/cx-product-manager.json +102 -0
- package/specialists/org/specialists/cx-qa.json +79 -0
- package/specialists/org/specialists/cx-rd-lead.json +73 -0
- package/specialists/org/specialists/cx-release-manager.json +77 -0
- package/specialists/org/specialists/cx-researcher.json +82 -0
- package/specialists/org/specialists/cx-reviewer.json +71 -0
- package/specialists/org/specialists/cx-security.json +91 -0
- package/specialists/org/specialists/cx-sre.json +94 -0
- package/specialists/org/specialists/cx-test-automation.json +69 -0
- package/specialists/org/specialists/cx-trace-reviewer.json +69 -0
- package/specialists/org/specialists/cx-ux-researcher.json +68 -0
- package/specialists/org/teams/accessibility-team.json +39 -0
- package/specialists/org/teams/design-team.json +40 -0
- package/specialists/org/teams/engineering-team.json +50 -0
- package/specialists/org/teams/governance-team.json +41 -0
- package/specialists/org/teams/operations-team.json +46 -0
- package/specialists/org/teams/product-management-team.json +45 -0
- package/specialists/org/teams/quality-team.json +49 -0
- package/specialists/org/teams/research-team.json +39 -0
- package/specialists/org/teams/strategy-team.json +48 -0
- package/specialists/org/teams/ux-research-team.json +39 -0
- package/specialists/prompts/_shared/validation-contract.md +1 -1
- package/specialists/prompts/_team-template.md +10 -0
- package/specialists/prompts/cx-accessibility.md +1 -0
- package/specialists/prompts/cx-ai-engineer.md +1 -1
- package/specialists/prompts/cx-business-strategist.md +1 -1
- package/specialists/prompts/cx-data-engineer.md +1 -1
- package/specialists/prompts/cx-designer.md +1 -0
- package/specialists/prompts/cx-devil-advocate.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-evaluator.md +1 -1
- package/specialists/prompts/cx-explorer.md +1 -1
- package/specialists/prompts/cx-operations.md +1 -1
- package/specialists/prompts/cx-oracle.md +7 -3
- package/specialists/prompts/cx-orchestrator.md +17 -1
- package/specialists/prompts/cx-platform-engineer.md +1 -1
- package/specialists/prompts/cx-product-manager.md +2 -0
- package/specialists/prompts/cx-release-manager.md +1 -1
- package/specialists/prompts/cx-researcher.md +7 -4
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/prompts/cx-test-automation.md +2 -2
- package/specialists/prompts/cx-trace-reviewer.md +3 -3
- package/specialists/prompts/cx-ux-researcher.md +2 -1
- package/templates/demos/scripts/architecture-review-adr.json +30 -0
- package/templates/demos/scripts/capability-contract.json +25 -0
- package/templates/demos/scripts/intake-triage.json +25 -0
- package/templates/demos/scripts/profile-doctor-health.json +25 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +2 -2
- package/templates/demos/tapes/architecture-review-adr.tape +44 -0
- package/templates/demos/tapes/capability-contract.tape +39 -0
- package/templates/demos/tapes/intake-triage.tape +39 -0
- package/templates/demos/tapes/profile-doctor-health.tape +39 -0
- package/templates/distribution/construct-brand.typ +23 -18
- package/templates/distribution/construct-decision.typ +1 -1
- package/templates/distribution/construct-pdf.typ +1 -1
- package/templates/distribution/construct-prd.typ +1 -1
- package/templates/distribution/construct-research.typ +1 -1
- package/templates/distribution/mermaid-puppeteer.json +8 -0
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prd.md +6 -0
- package/apps/chat/engine/ai-sdk-agent.mjs +0 -183
- package/apps/chat/engine/loop-driver.mjs +0 -211
- package/apps/chat/engine/models.mjs +0 -122
- package/apps/chat/engine/provider-adapters.mjs +0 -171
- package/apps/chat/engine/tools/permission.mjs +0 -54
- package/apps/chat/engine/tools/primitives.mjs +0 -180
- package/apps/chat/engine/tools/registry.mjs +0 -122
- package/apps/chat/engine/turn-controls.mjs +0 -70
- package/lib/boundary.mjs +0 -127
- package/lib/certification/dashboard-api.mjs +0 -71
- package/lib/chat/cli.mjs +0 -333
- package/lib/chat/command-suggest.mjs +0 -161
- package/lib/chat/commands.mjs +0 -215
- package/lib/chat/config.mjs +0 -142
- package/lib/chat/context-compactor.mjs +0 -250
- package/lib/chat/context-continuation.mjs +0 -253
- package/lib/chat/continuation-source.mjs +0 -58
- package/lib/chat/demo-guide.mjs +0 -61
- package/lib/chat/design-tokens.mjs +0 -91
- package/lib/chat/desktop-binary.mjs +0 -81
- package/lib/chat/desktop-build.mjs +0 -130
- package/lib/chat/desktop-launcher.mjs +0 -133
- package/lib/chat/evidence.mjs +0 -145
- package/lib/chat/export.mjs +0 -74
- package/lib/chat/harness/driver.mjs +0 -91
- package/lib/chat/list-picker.mjs +0 -112
- package/lib/chat/model-picker.mjs +0 -356
- package/lib/chat/openrouter-fallback.mjs +0 -151
- package/lib/chat/permission-prompt.mjs +0 -33
- package/lib/chat/picker-catalog.mjs +0 -45
- package/lib/chat/policy-telemetry.mjs +0 -34
- package/lib/chat/present.mjs +0 -246
- package/lib/chat/session-context.mjs +0 -39
- package/lib/chat/session-persist.mjs +0 -73
- package/lib/chat/session-restore.mjs +0 -71
- package/lib/chat/session-settings.mjs +0 -53
- package/lib/chat/system-prompt.mjs +0 -52
- package/lib/chat/transparency.mjs +0 -93
- package/lib/chat/tui/color-scheme.mjs +0 -42
- package/lib/chat/tui/markdown.mjs +0 -123
- package/lib/chat/tui/presentation.mjs +0 -100
- package/lib/chat/tui/render.mjs +0 -500
- package/lib/chat/tui/turn-block.mjs +0 -284
- package/lib/chat/tui/turn-present.mjs +0 -18
- package/lib/chat/tui/turn-state.mjs +0 -88
- package/lib/chat/tui/usage.mjs +0 -122
- package/lib/chat/web-commands.mjs +0 -146
- package/lib/chat/web-launcher.mjs +0 -63
- package/lib/chat/web-picker-keys.mjs +0 -46
- package/lib/chat/web-session.mjs +0 -159
- package/lib/config/alias.mjs +0 -57
- package/lib/dashboard-demo.mjs +0 -71
- package/lib/dashboard-static.mjs +0 -175
- package/lib/install/desktop-binary-download.mjs +0 -88
- package/lib/profiles/loader.mjs +0 -123
- package/lib/profiles/rebrand.mjs +0 -46
- package/lib/server/auth.mjs +0 -169
- package/lib/server/chat-loop.mjs +0 -622
- package/lib/server/chat.mjs +0 -336
- package/lib/server/cors.mjs +0 -77
- package/lib/server/csrf.mjs +0 -103
- package/lib/server/demo-preview.mjs +0 -63
- package/lib/server/index.mjs +0 -2641
- package/lib/server/insights.mjs +0 -780
- package/lib/server/langfuse-login.mjs +0 -58
- package/lib/server/rate-limit.mjs +0 -93
- package/lib/server/telemetry-login.mjs +0 -100
- package/lib/server/webhook.mjs +0 -512
- package/specialists/contracts.json +0 -1032
- package/specialists/contracts.schema.json +0 -83
- package/specialists/policy-inventory.json +0 -188
- package/specialists/registry.json +0 -1412
- package/specialists/role-manifests.json +0 -217
- package/specialists/teams.json +0 -94
package/lib/server/chat.mjs
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/server/chat.mjs — Dashboard chat endpoint handler.
|
|
3
|
-
*
|
|
4
|
-
* Handles POST /api/chat (non-streaming JSON) and GET /api/chat/stream (SSE).
|
|
5
|
-
* Shells out to the `claude --print` CLI and streams the response back as
|
|
6
|
-
* Server-Sent Events. Returns a structured fallback if the CLI is unavailable.
|
|
7
|
-
*
|
|
8
|
-
* Message history is kept per conversation ID in memory (cleared on restart).
|
|
9
|
-
* The client sends { id?, message } — omitting id starts a new conversation.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { spawn, execSync } from 'child_process';
|
|
13
|
-
import { randomBytes } from 'crypto';
|
|
14
|
-
import { existsSync, readFileSync } from 'fs';
|
|
15
|
-
import { join } from 'path';
|
|
16
|
-
import { homedir } from 'os';
|
|
17
|
-
import { fileURLToPath } from 'url';
|
|
18
|
-
import { dirname, resolve } from 'path';
|
|
19
|
-
|
|
20
|
-
const HOME = homedir();
|
|
21
|
-
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
22
|
-
|
|
23
|
-
// ── Session-end optimize hook ──────────────────────────────────────────────
|
|
24
|
-
// After a chat session closes, check whether enough new quality scores have
|
|
25
|
-
// accumulated in the performance review to warrant running optimize.
|
|
26
|
-
// Fires asynchronously — never blocks the response.
|
|
27
|
-
|
|
28
|
-
let _sessionsSinceOptimize = 0;
|
|
29
|
-
const OPTIMIZE_EVERY_N_SESSIONS = parseInt(process.env.CX_OPTIMIZE_INTERVAL ?? '5', 10);
|
|
30
|
-
|
|
31
|
-
function maybeRunOptimize() {
|
|
32
|
-
_sessionsSinceOptimize++;
|
|
33
|
-
if (_sessionsSinceOptimize < OPTIMIZE_EVERY_N_SESSIONS) return;
|
|
34
|
-
_sessionsSinceOptimize = 0;
|
|
35
|
-
|
|
36
|
-
// Spawn optimize --list to see if any agents are below threshold
|
|
37
|
-
const script = join(ROOT_DIR, 'scripts', 'optimize.mjs');
|
|
38
|
-
if (!existsSync(script)) return;
|
|
39
|
-
|
|
40
|
-
const proc = spawn(process.execPath, [script, '--list', '--threshold=0.7'], {
|
|
41
|
-
cwd: ROOT_DIR,
|
|
42
|
-
env: { ...process.env },
|
|
43
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
let out = '';
|
|
47
|
-
proc.stdout.on('data', d => { out += d; });
|
|
48
|
-
proc.on('close', () => {
|
|
49
|
-
// Find agents flagged below threshold and run optimize on each
|
|
50
|
-
const flagged = out.split('\n')
|
|
51
|
-
.filter(l => l.includes('← below threshold'))
|
|
52
|
-
.map(l => l.trim().split(/\s+/)[0])
|
|
53
|
-
.filter(Boolean);
|
|
54
|
-
|
|
55
|
-
for (const agent of flagged) {
|
|
56
|
-
const opt = spawn(process.execPath, [script, agent, `--threshold=0.7`], {
|
|
57
|
-
cwd: ROOT_DIR,
|
|
58
|
-
env: { ...process.env },
|
|
59
|
-
stdio: 'ignore',
|
|
60
|
-
detached: true,
|
|
61
|
-
});
|
|
62
|
-
opt.unref();
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
proc.unref();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Conversation store ─────────────────────────────────────────────────────
|
|
69
|
-
const conversations = new Map();
|
|
70
|
-
const CONV_TTL_MS = 2 * 60 * 60 * 1000;
|
|
71
|
-
|
|
72
|
-
function pruneConversations() {
|
|
73
|
-
const now = Date.now();
|
|
74
|
-
for (const [id, conv] of conversations) {
|
|
75
|
-
if (now - conv.lastAt > CONV_TTL_MS) conversations.delete(id);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function getOrCreateConversation(id) {
|
|
80
|
-
pruneConversations();
|
|
81
|
-
if (id && conversations.has(id)) {
|
|
82
|
-
const conv = conversations.get(id);
|
|
83
|
-
conv.lastAt = Date.now();
|
|
84
|
-
return conv;
|
|
85
|
-
}
|
|
86
|
-
const newId = randomBytes(12).toString('hex');
|
|
87
|
-
const conv = { id: newId, messages: [], createdAt: Date.now(), lastAt: Date.now() };
|
|
88
|
-
conversations.set(newId, conv);
|
|
89
|
-
return conv;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── CLI detection ──────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
let _cliCmd = undefined; // undefined = not yet checked, false = not found, string = found
|
|
95
|
-
|
|
96
|
-
function detectCli() {
|
|
97
|
-
if (_cliCmd !== undefined) return _cliCmd;
|
|
98
|
-
for (const cmd of ['claude', 'anthropic']) {
|
|
99
|
-
try {
|
|
100
|
-
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
101
|
-
_cliCmd = cmd;
|
|
102
|
-
return _cliCmd;
|
|
103
|
-
} catch { /* not found */ }
|
|
104
|
-
}
|
|
105
|
-
_cliCmd = false;
|
|
106
|
-
return _cliCmd;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Read-only Construct MCP tools that are pre-authorized in dashboard chat.
|
|
110
|
-
// Destructive tools (storage_reset, delete_ingested_artifacts, ingest_document)
|
|
111
|
-
// are deliberately omitted and still require explicit approval.
|
|
112
|
-
const ALLOWED_CONSTRUCT_TOOLS = [
|
|
113
|
-
'mcp__construct-mcp__provider_fetch',
|
|
114
|
-
'mcp__construct-mcp__knowledge_search',
|
|
115
|
-
'mcp__construct-mcp__memory_search',
|
|
116
|
-
'mcp__construct-mcp__memory_recent',
|
|
117
|
-
'mcp__construct-mcp__project_context',
|
|
118
|
-
'mcp__construct-mcp__session_list',
|
|
119
|
-
'mcp__construct-mcp__session_load',
|
|
120
|
-
'mcp__construct-mcp__session_search',
|
|
121
|
-
'mcp__construct-mcp__agent_health',
|
|
122
|
-
'mcp__construct-mcp__storage_status',
|
|
123
|
-
'mcp__construct-mcp__list_skills',
|
|
124
|
-
'mcp__construct-mcp__get_skill',
|
|
125
|
-
'mcp__construct-mcp__list_templates',
|
|
126
|
-
'mcp__construct-mcp__get_template',
|
|
127
|
-
'mcp__construct-mcp__list_teams',
|
|
128
|
-
'mcp__construct-mcp__get_team',
|
|
129
|
-
'mcp__construct-mcp__agent_contract',
|
|
130
|
-
'mcp__construct-mcp__orchestration_policy',
|
|
131
|
-
'mcp__construct-mcp__search_skills',
|
|
132
|
-
'mcp__construct-mcp__efficiency_snapshot',
|
|
133
|
-
'mcp__construct-mcp__session_usage',
|
|
134
|
-
'mcp__construct-mcp__list_schema_artifacts',
|
|
135
|
-
'mcp__construct-mcp__extract_document_text',
|
|
136
|
-
'mcp__construct-mcp__summarize_diff',
|
|
137
|
-
'mcp__construct-mcp__scan_file',
|
|
138
|
-
'Read', 'Glob', 'Grep',
|
|
139
|
-
];
|
|
140
|
-
|
|
141
|
-
function buildCliArgs() {
|
|
142
|
-
return [
|
|
143
|
-
'--print',
|
|
144
|
-
'--permission-mode', 'default',
|
|
145
|
-
'--allowedTools', ...ALLOWED_CONSTRUCT_TOOLS,
|
|
146
|
-
];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── Prompt builder ─────────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
function buildPrompt(messages, newMessage, rootDir) {
|
|
152
|
-
const lines = [];
|
|
153
|
-
|
|
154
|
-
// Attach project context prefix on first message
|
|
155
|
-
if (messages.length === 0) {
|
|
156
|
-
lines.push(
|
|
157
|
-
'[System]',
|
|
158
|
-
'You are Construct, the operator-facing chat for this project. The MCP tools',
|
|
159
|
-
'`provider_fetch`, `knowledge_search`, `memory_search`, `memory_recent`,',
|
|
160
|
-
'`project_context`, and `session_*` are pre-authorized read-only lookups.',
|
|
161
|
-
'Call them directly without asking for approval. They route to the configured',
|
|
162
|
-
'GitHub/Jira/Slack/local sources via authority-guarded providers.',
|
|
163
|
-
'For "what is X" / "tell me about X" queries about a project name, repo, or',
|
|
164
|
-
'concept, your first action is `provider_fetch({ query: "<user query>" })`.',
|
|
165
|
-
'',
|
|
166
|
-
);
|
|
167
|
-
const contextFile = join(rootDir || process.cwd(), '.cx', 'context.md');
|
|
168
|
-
if (existsSync(contextFile)) {
|
|
169
|
-
try {
|
|
170
|
-
const ctx = readFileSync(contextFile, 'utf8').slice(0, 1500);
|
|
171
|
-
lines.push(`[Project context]\n${ctx}\n`);
|
|
172
|
-
} catch { /* ignore */ }
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Include last 6 turns of history
|
|
177
|
-
for (const m of messages.slice(-6)) {
|
|
178
|
-
lines.push(`${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content}`);
|
|
179
|
-
}
|
|
180
|
-
lines.push(`Human: ${newMessage}`, '', 'Assistant:');
|
|
181
|
-
return lines.join('\n');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── SSE helpers ────────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
function sseWrite(res, type, text, id) {
|
|
187
|
-
res.write(`data: ${JSON.stringify({ type, text, id })}\n\n`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function cliMissingResponse(res, conv) {
|
|
191
|
-
sseWrite(res, 'chunk',
|
|
192
|
-
'**Construct chat** requires the `claude` CLI.\n\n' +
|
|
193
|
-
'Install: `npm install -g @anthropic-ai/claude-code`\n\n' +
|
|
194
|
-
'Then authenticate once with `claude` before using the dashboard chat.',
|
|
195
|
-
conv.id);
|
|
196
|
-
sseWrite(res, 'done', '', conv.id);
|
|
197
|
-
conv.messages.push({ role: 'assistant', content: '[claude CLI not available]' });
|
|
198
|
-
res.end();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── Handlers ───────────────────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* GET /api/chat/stream?message=<text>[&id=<convId>]
|
|
205
|
-
* Streams response as SSE: data: { type, text, id }
|
|
206
|
-
* type = 'chunk' | 'done' | 'error'
|
|
207
|
-
*/
|
|
208
|
-
export function handleChatStream(req, res, { rootDir } = {}) {
|
|
209
|
-
const url = new URL(req.url, 'http://localhost');
|
|
210
|
-
const message = url.searchParams.get('message');
|
|
211
|
-
const convId = url.searchParams.get('id') || undefined;
|
|
212
|
-
|
|
213
|
-
if (!message) {
|
|
214
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
215
|
-
res.end(JSON.stringify({ error: 'message query param required' }));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
res.writeHead(200, {
|
|
220
|
-
'Content-Type': 'text/event-stream',
|
|
221
|
-
'Cache-Control': 'no-cache',
|
|
222
|
-
'Connection': 'keep-alive',
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const conv = getOrCreateConversation(convId);
|
|
226
|
-
conv.messages.push({ role: 'user', content: message });
|
|
227
|
-
|
|
228
|
-
const cli = detectCli();
|
|
229
|
-
if (!cli) { cliMissingResponse(res, conv); return; }
|
|
230
|
-
|
|
231
|
-
const prompt = buildPrompt(conv.messages.slice(0, -1), message, rootDir);
|
|
232
|
-
const proc = spawn(cli, buildCliArgs(), {
|
|
233
|
-
cwd: rootDir || process.cwd(),
|
|
234
|
-
env: { ...process.env },
|
|
235
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
proc.stdin.write(prompt);
|
|
239
|
-
proc.stdin.end();
|
|
240
|
-
|
|
241
|
-
let fullResponse = '';
|
|
242
|
-
|
|
243
|
-
proc.stdout.on('data', chunk => {
|
|
244
|
-
const text = chunk.toString();
|
|
245
|
-
fullResponse += text;
|
|
246
|
-
sseWrite(res, 'chunk', text, conv.id);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
proc.on('close', code => {
|
|
250
|
-
if (code !== 0 && !fullResponse) {
|
|
251
|
-
sseWrite(res, 'error', `Chat exited with code ${code}. Check that \`claude\` is authenticated.`, conv.id);
|
|
252
|
-
} else {
|
|
253
|
-
sseWrite(res, 'done', '', conv.id);
|
|
254
|
-
}
|
|
255
|
-
conv.messages.push({ role: 'assistant', content: fullResponse || '[no response]' });
|
|
256
|
-
res.end();
|
|
257
|
-
maybeRunOptimize();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
proc.on('error', err => {
|
|
261
|
-
sseWrite(res, 'error', 'Failed to start chat: ' + err.message, conv.id);
|
|
262
|
-
res.end();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
req.on('close', () => { try { proc.kill(); } catch { /* ignore */ } });
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* POST /api/chat — non-streaming, full response as JSON.
|
|
270
|
-
* Body: { id?, message } → { id, reply }
|
|
271
|
-
*/
|
|
272
|
-
export function handleChat(req, res, { rootDir } = {}) {
|
|
273
|
-
let body = '';
|
|
274
|
-
req.on('data', chunk => { body += chunk; });
|
|
275
|
-
req.on('end', () => {
|
|
276
|
-
try {
|
|
277
|
-
const data = JSON.parse(body || '{}');
|
|
278
|
-
if (!data.message) throw new Error('message required');
|
|
279
|
-
|
|
280
|
-
const conv = getOrCreateConversation(data.id);
|
|
281
|
-
conv.messages.push({ role: 'user', content: data.message });
|
|
282
|
-
|
|
283
|
-
const cli = detectCli();
|
|
284
|
-
if (!cli) {
|
|
285
|
-
const reply = 'The `claude` CLI is not installed. Run: npm install -g @anthropic-ai/claude-code';
|
|
286
|
-
conv.messages.push({ role: 'assistant', content: reply });
|
|
287
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
288
|
-
res.end(JSON.stringify({ id: conv.id, reply, cliMissing: true }));
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const prompt = buildPrompt(conv.messages.slice(0, -1), data.message, rootDir);
|
|
293
|
-
const proc = spawn(cli, buildCliArgs(), {
|
|
294
|
-
cwd: rootDir || process.cwd(),
|
|
295
|
-
env: { ...process.env },
|
|
296
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
proc.stdin.write(prompt);
|
|
300
|
-
proc.stdin.end();
|
|
301
|
-
|
|
302
|
-
let reply = '';
|
|
303
|
-
proc.stdout.on('data', c => { reply += c.toString(); });
|
|
304
|
-
proc.on('close', code => {
|
|
305
|
-
const text = reply.trim() || (code !== 0 ? `[exit ${code}]` : '[empty response]');
|
|
306
|
-
conv.messages.push({ role: 'assistant', content: text });
|
|
307
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
308
|
-
res.end(JSON.stringify({ id: conv.id, reply: text }));
|
|
309
|
-
maybeRunOptimize();
|
|
310
|
-
});
|
|
311
|
-
proc.on('error', err => {
|
|
312
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
313
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
314
|
-
});
|
|
315
|
-
} catch (err) {
|
|
316
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
317
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* GET /api/chat/history?id=<convId>
|
|
324
|
-
*/
|
|
325
|
-
export function handleChatHistory(req, res) {
|
|
326
|
-
const url = new URL(req.url, 'http://localhost');
|
|
327
|
-
const id = url.searchParams.get('id');
|
|
328
|
-
if (!id || !conversations.has(id)) {
|
|
329
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
330
|
-
res.end(JSON.stringify({ id: null, messages: [] }));
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
const conv = conversations.get(id);
|
|
334
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
335
|
-
res.end(JSON.stringify({ id: conv.id, messages: conv.messages }));
|
|
336
|
-
}
|
package/lib/server/cors.mjs
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/server/cors.mjs — origin allowlist for the dashboard server.
|
|
3
|
-
*
|
|
4
|
-
* The default is exact-match against the dashboard's own origin (single-host
|
|
5
|
-
* self-hosted install). Operators can pass extra origins via the env var
|
|
6
|
-
* `CONSTRUCT_DASHBOARD_ORIGINS` as a comma-separated list.
|
|
7
|
-
*
|
|
8
|
-
* CONSTRUCT_DASHBOARD_ORIGINS="https://my-laptop.tail-net.ts.net,https://construct.example.com"
|
|
9
|
-
*
|
|
10
|
-
* Wildcard origins are NOT supported. `Access-Control-Allow-Origin: *` is
|
|
11
|
-
* never set — paid deployments behind a private network or behind an ALB
|
|
12
|
-
* with a CloudFront distribution declare exactly which origins may issue
|
|
13
|
-
* requests.
|
|
14
|
-
*
|
|
15
|
-
* Exports:
|
|
16
|
-
* - applyCors(req, res, opts) : sets the right Access-Control-* headers
|
|
17
|
-
* on a request. Returns true when the
|
|
18
|
-
* request was a preflight (OPTIONS) and
|
|
19
|
-
* the caller should respond with 204.
|
|
20
|
-
* - originAllowed(req, opts) : true/false. Useful when middleware wants
|
|
21
|
-
* to enforce same-origin without setting
|
|
22
|
-
* any headers (e.g. on SSE streams).
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const HEADER_NAME = 'access-control-allow-origin';
|
|
26
|
-
|
|
27
|
-
function parseOrigins(env) {
|
|
28
|
-
const list = (env.CONSTRUCT_DASHBOARD_ORIGINS || '')
|
|
29
|
-
.split(',')
|
|
30
|
-
.map((s) => s.trim())
|
|
31
|
-
.filter(Boolean);
|
|
32
|
-
return new Set(list);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function selfOrigin(req) {
|
|
36
|
-
const proto = req.headers['x-forwarded-proto'] || (req.socket?.encrypted ? 'https' : 'http');
|
|
37
|
-
const host = req.headers['x-forwarded-host'] || req.headers.host;
|
|
38
|
-
if (!host) return null;
|
|
39
|
-
return `${proto}://${host}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function originAllowed(req, { env = process.env } = {}) {
|
|
43
|
-
const origin = req.headers.origin;
|
|
44
|
-
if (!origin) return true;
|
|
45
|
-
const allowed = parseOrigins(env);
|
|
46
|
-
allowed.add(selfOrigin(req));
|
|
47
|
-
return allowed.has(origin);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Set CORS headers on a response. Returns `true` for preflight requests
|
|
52
|
-
* that the caller should answer with 204 immediately.
|
|
53
|
-
*/
|
|
54
|
-
export function applyCors(req, res, { env = process.env } = {}) {
|
|
55
|
-
const origin = req.headers.origin;
|
|
56
|
-
if (origin && originAllowed(req, { env })) {
|
|
57
|
-
res.setHeader(HEADER_NAME, origin);
|
|
58
|
-
res.setHeader('Vary', 'Origin');
|
|
59
|
-
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
60
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
61
|
-
res.setHeader(
|
|
62
|
-
'Access-Control-Allow-Headers',
|
|
63
|
-
'Content-Type, Authorization, X-Construct-Csrf, X-Construct-Token'
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
if (req.method === 'OPTIONS') {
|
|
67
|
-
if (origin && !originAllowed(req, { env })) {
|
|
68
|
-
res.statusCode = 403;
|
|
69
|
-
res.end('origin not allowed');
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
res.statusCode = 204;
|
|
73
|
-
res.end();
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
return false;
|
|
77
|
-
}
|
package/lib/server/csrf.mjs
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/server/csrf.mjs — double-submit CSRF protection for state-mutating routes.
|
|
3
|
-
*
|
|
4
|
-
* Every non-GET request must include a `X-Construct-Csrf` header whose value
|
|
5
|
-
* matches the `cx_csrf` cookie. The dashboard mints the cookie on first
|
|
6
|
-
* authenticated load (the cookie value is a 256-bit random hex string,
|
|
7
|
-
* SameSite=Lax, HttpOnly=false so the SPA can read it). Issuing the header
|
|
8
|
-
* proves the request originated from a page that received the cookie, which
|
|
9
|
-
* a cross-origin attacker cannot forge.
|
|
10
|
-
*
|
|
11
|
-
* Routes that legitimately accept unauthenticated requests (webhooks,
|
|
12
|
-
* /api/auth/login) opt out by passing `{ skipMethods: ['POST'] }` or by
|
|
13
|
-
* checking `csrfRequired(url)` themselves.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
17
|
-
|
|
18
|
-
const COOKIE_NAME = 'cx_csrf';
|
|
19
|
-
const HEADER_NAME = 'x-construct-csrf';
|
|
20
|
-
const TOKEN_BYTES = 32;
|
|
21
|
-
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
22
|
-
|
|
23
|
-
function parseCookies(req) {
|
|
24
|
-
const out = {};
|
|
25
|
-
const header = req.headers.cookie;
|
|
26
|
-
if (!header) return out;
|
|
27
|
-
for (const part of header.split(';')) {
|
|
28
|
-
const [k, ...rest] = part.split('=');
|
|
29
|
-
if (!k) continue;
|
|
30
|
-
out[k.trim()] = rest.join('=').trim();
|
|
31
|
-
}
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function bufFromHex(s) {
|
|
36
|
-
if (typeof s !== 'string' || s.length % 2 !== 0) return null;
|
|
37
|
-
try { return Buffer.from(s, 'hex'); } catch { return null; }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function mintCsrfToken() {
|
|
41
|
-
return randomBytes(TOKEN_BYTES).toString('hex');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Ensure the response has the `cx_csrf` cookie. If the request already had
|
|
46
|
-
* one, leave it alone. Otherwise mint and set it.
|
|
47
|
-
*
|
|
48
|
-
* @returns {string} the token now in play
|
|
49
|
-
*/
|
|
50
|
-
export function ensureCsrfCookie(req, res) {
|
|
51
|
-
const existing = parseCookies(req)[COOKIE_NAME];
|
|
52
|
-
if (existing) return existing;
|
|
53
|
-
const token = mintCsrfToken();
|
|
54
|
-
const sec = req.socket?.encrypted ? '; Secure' : '';
|
|
55
|
-
res.setHeader(
|
|
56
|
-
'Set-Cookie',
|
|
57
|
-
`${COOKIE_NAME}=${token}; Path=/; SameSite=Lax; Max-Age=43200${sec}`
|
|
58
|
-
);
|
|
59
|
-
return token;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Verify the `X-Construct-Csrf` header matches the `cx_csrf` cookie. Returns
|
|
64
|
-
* true on safe methods (GET/HEAD/OPTIONS) without checking. Returns true on
|
|
65
|
-
* URLs that opt out via `skip(url)`.
|
|
66
|
-
*/
|
|
67
|
-
export function verifyCsrf(req, { skip } = {}) {
|
|
68
|
-
if (SAFE_METHODS.has(req.method)) return true;
|
|
69
|
-
// A request authenticated by an Authorization header (bearer token) is not
|
|
70
|
-
// cookie-driven, so it cannot be forged cross-origin — CSRF defends ambient
|
|
71
|
-
// cookie auth, not header tokens. This is what lets programmatic API clients
|
|
72
|
-
// (editor adapters, CLI, CI) POST without the browser CSRF dance.
|
|
73
|
-
if (req.headers && req.headers.authorization) return true;
|
|
74
|
-
if (typeof skip === 'function' && skip(req.url || '')) return true;
|
|
75
|
-
const headerVal = req.headers[HEADER_NAME];
|
|
76
|
-
const cookieVal = parseCookies(req)[COOKIE_NAME];
|
|
77
|
-
if (!headerVal || !cookieVal) return false;
|
|
78
|
-
if (headerVal.length !== cookieVal.length) return false;
|
|
79
|
-
const a = bufFromHex(headerVal);
|
|
80
|
-
const b = bufFromHex(cookieVal);
|
|
81
|
-
if (!a || !b || a.length !== b.length) return false;
|
|
82
|
-
try { return timingSafeEqual(a, b); } catch { return false; }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Default skip predicate — webhooks, auth/login, and the orchestration API
|
|
87
|
-
* legitimately accept POSTs without the CSRF dance. CSRF defends a browser
|
|
88
|
-
* cookie session; the `/api/orchestration/*` endpoints are consumed only by
|
|
89
|
-
* programmatic clients (the `--remote` CLI, the MCP tool, editor adapters) that
|
|
90
|
-
* carry a bearer token (or nothing in no-token mode), never the dashboard
|
|
91
|
-
* cookie — so CSRF is the wrong control there, and the auth gate still protects
|
|
92
|
-
* them in token mode. Without this, a no-token daemon rejects every orchestration
|
|
93
|
-
* run with a 403.
|
|
94
|
-
*/
|
|
95
|
-
export function defaultSkip(url) {
|
|
96
|
-
if (!url) return false;
|
|
97
|
-
return (
|
|
98
|
-
url.startsWith('/api/webhooks/') ||
|
|
99
|
-
url.startsWith('/api/orchestration/') ||
|
|
100
|
-
url === '/api/auth/login' ||
|
|
101
|
-
url === '/api/slack/commands'
|
|
102
|
-
);
|
|
103
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/server/demo-preview.mjs — gated static serve for Playwright demo artifacts.
|
|
3
|
-
*
|
|
4
|
-
* When CONSTRUCT_DEMO_ARTIFACT_DIR is set, GET /demo-preview/<filename> serves
|
|
5
|
-
* PDF/HTML (and sibling export assets) from that directory with path traversal
|
|
6
|
-
* guards. Inactive when the env var is unset.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
|
|
12
|
-
const ALLOWED_EXT = new Set(['.pdf', '.html', '.png', '.svg', '.webm', '.mp4', '.css', '.js']);
|
|
13
|
-
|
|
14
|
-
const MIME = {
|
|
15
|
-
'.pdf': 'application/pdf',
|
|
16
|
-
'.html': 'text/html; charset=utf-8',
|
|
17
|
-
'.png': 'image/png',
|
|
18
|
-
'.svg': 'image/svg+xml',
|
|
19
|
-
'.webm': 'video/webm',
|
|
20
|
-
'.mp4': 'video/mp4',
|
|
21
|
-
'.css': 'text/css; charset=utf-8',
|
|
22
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function demoPreviewRootFromEnv(env = process.env) {
|
|
26
|
-
const raw = env.CONSTRUCT_DEMO_ARTIFACT_DIR;
|
|
27
|
-
if (!raw || !String(raw).trim()) return null;
|
|
28
|
-
return path.resolve(String(raw));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function resolveDemoPreviewPath(pathname, env = process.env) {
|
|
32
|
-
const root = demoPreviewRootFromEnv(env);
|
|
33
|
-
if (!root) return { ok: false, status: 404, message: 'Not found' };
|
|
34
|
-
|
|
35
|
-
const prefix = '/demo-preview/';
|
|
36
|
-
if (!pathname.startsWith(prefix)) return { ok: false, status: 404, message: 'Not found' };
|
|
37
|
-
|
|
38
|
-
const name = decodeURIComponent(pathname.slice(prefix.length));
|
|
39
|
-
if (!name || name.includes('/') || name.includes('\\') || name.includes('..')) {
|
|
40
|
-
return { ok: false, status: 403, message: 'Forbidden' };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const ext = path.extname(name).toLowerCase();
|
|
44
|
-
if (!ALLOWED_EXT.has(ext)) {
|
|
45
|
-
return { ok: false, status: 403, message: 'Forbidden' };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const fullPath = path.resolve(path.join(root, name));
|
|
49
|
-
const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
50
|
-
if (!fullPath.startsWith(rootWithSep) && fullPath !== root) {
|
|
51
|
-
return { ok: false, status: 403, message: 'Forbidden' };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
|
|
55
|
-
return { ok: false, status: 404, message: 'Not found' };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { ok: true, filePath: fullPath, ext };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function demoPreviewMime(ext) {
|
|
62
|
-
return MIME[ext] ?? 'application/octet-stream';
|
|
63
|
-
}
|