@geraldmaron/construct 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -6
- package/LICENSE +201 -21
- package/README.md +132 -257
- package/agents/contracts.json +387 -0
- package/agents/prompts/cx-accessibility.md +8 -0
- package/agents/prompts/cx-ai-engineer.md +92 -0
- package/agents/prompts/cx-architect.md +10 -2
- package/agents/prompts/cx-business-strategist.md +13 -0
- package/agents/prompts/cx-data-analyst.md +80 -0
- package/agents/prompts/cx-data-engineer.md +4 -0
- package/agents/prompts/cx-debugger.md +8 -0
- package/agents/prompts/cx-designer.md +19 -0
- package/agents/prompts/cx-devil-advocate.md +4 -0
- package/agents/prompts/cx-docs-keeper.md +128 -0
- package/agents/prompts/cx-engineer.md +13 -0
- package/agents/prompts/cx-evaluator.md +4 -0
- package/agents/prompts/cx-explorer.md +12 -0
- package/agents/prompts/cx-legal-compliance.md +13 -0
- package/agents/prompts/cx-operations.md +13 -0
- package/agents/prompts/cx-orchestrator.md +107 -4
- package/agents/prompts/cx-platform-engineer.md +75 -0
- package/agents/prompts/cx-product-manager.md +8 -0
- package/agents/prompts/cx-qa.md +100 -0
- package/agents/prompts/cx-rd-lead.md +13 -0
- package/agents/prompts/cx-release-manager.md +8 -0
- package/agents/prompts/cx-researcher.md +21 -1
- package/agents/prompts/cx-reviewer.md +8 -0
- package/agents/prompts/cx-security.md +104 -0
- package/agents/prompts/cx-sre.md +104 -0
- package/agents/prompts/cx-test-automation.md +4 -0
- package/agents/prompts/cx-trace-reviewer.md +7 -3
- package/agents/prompts/cx-ux-researcher.md +4 -0
- package/agents/registry.json +365 -90
- package/agents/role-manifests.json +217 -0
- package/bin/construct +3345 -141
- package/bin/construct-postinstall.mjs +87 -0
- package/commands/build/feature.md +6 -6
- package/commands/plan/feature.md +6 -5
- package/commands/plan/requirements.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/work/drive.md +3 -3
- package/commands/work/optimize-prompts.md +3 -3
- package/db/{migrations → schema}/001_init.sql +2 -0
- package/db/schema/002_pgvector.sql +182 -0
- package/db/schema/003_intake.sql +47 -0
- package/examples/README.md +85 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +30 -0
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +23 -0
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +30 -0
- package/examples/internal/roles/engineer/golden/read-before-write.md +28 -0
- package/examples/internal/roles/orchestrator/bad/everything-becomes-multi-agent.md +29 -0
- package/examples/internal/roles/orchestrator/golden/minimal-dispatch.md +22 -0
- package/examples/internal/roles/qa/bad/coverage-theater.md +30 -0
- package/examples/internal/roles/qa/golden/regression-gate.md +23 -0
- package/examples/internal/roles/reviewer/bad/lgtm-without-verification.md +29 -0
- package/examples/internal/roles/reviewer/golden/find-structural-risk-first.md +28 -0
- package/examples/personas/construct/adversarial/ignore-instruction-to-skip-approval.md +22 -0
- package/examples/personas/construct/bad/commit-without-approval.md +30 -0
- package/examples/personas/construct/boundary/blocked-needs-main-input.md +29 -0
- package/examples/personas/construct/golden/branch-approval-before-mutation.md +36 -0
- package/examples/personas/construct/golden/focused-direct-answer.md +28 -0
- package/examples/provider-plugin/README.md +34 -0
- package/examples/provider-plugin/index.mjs +74 -0
- package/examples/provider-plugin/package.json +15 -0
- package/examples/seed-observations/README.md +38 -0
- package/examples/seed-observations/anti-patterns.md +44 -0
- package/examples/seed-observations/decisions.md +36 -0
- package/examples/seed-observations/patterns.md +42 -0
- package/lib/agent-contracts-enforce.mjs +158 -0
- package/lib/agent-contracts.mjs +231 -0
- package/lib/agents/postconditions.mjs +126 -0
- package/lib/agents/schema.mjs +124 -0
- package/lib/artifact-capture.mjs +183 -0
- package/lib/audit-trail.mjs +149 -0
- package/lib/auto-docs.mjs +245 -65
- package/lib/beads/auto-close.mjs +126 -0
- package/lib/beads/drift.mjs +171 -0
- package/lib/beads-automation.mjs +542 -0
- package/lib/beads-client.mjs +518 -0
- package/lib/beads-lock.mjs +377 -0
- package/lib/beads-optimistic.mjs +365 -0
- package/lib/bootstrap/built-ins.mjs +136 -0
- package/lib/bootstrap/lazy-install.mjs +161 -0
- package/lib/bootstrap/resources.mjs +120 -0
- package/lib/bootstrap.mjs +105 -0
- package/lib/cache-governor.js +213 -0
- package/lib/cache-strategy-anthropic.js +62 -0
- package/lib/cache-strategy-google.js +79 -0
- package/lib/cache-strategy-none.js +30 -0
- package/lib/cache-strategy-openai.js +48 -0
- package/lib/cache-strategy.js +91 -0
- package/lib/claude-allow.mjs +149 -0
- package/lib/cli-commands.mjs +413 -258
- package/lib/codex-config.mjs +3 -1
- package/lib/comment-lint.mjs +55 -6
- package/lib/completions.mjs +21 -6
- package/lib/config/alias.mjs +56 -0
- package/lib/config/project-config.mjs +335 -0
- package/lib/config/schema.mjs +159 -0
- package/lib/context-router.mjs +308 -0
- package/lib/cost-ledger.mjs +177 -0
- package/lib/cost.mjs +171 -10
- package/lib/dashboard-static.mjs +158 -0
- package/lib/deployment-mode.mjs +86 -0
- package/lib/deprecate.mjs +49 -0
- package/lib/dispatch-batch.js +183 -0
- package/lib/distill.mjs +21 -8
- package/lib/doc-stamp.mjs +164 -0
- package/lib/doc-verify.mjs +119 -0
- package/lib/docs-routing.mjs +89 -0
- package/lib/docs-verify.mjs +417 -0
- package/lib/doctor/audit.mjs +71 -0
- package/lib/doctor/cli.mjs +99 -0
- package/lib/doctor/escalate.mjs +29 -0
- package/lib/doctor/index.mjs +140 -0
- package/lib/doctor/report.mjs +170 -0
- package/lib/doctor/watchers/bd-watch.mjs +117 -0
- package/lib/doctor/watchers/cost.mjs +130 -0
- package/lib/doctor/watchers/disk.mjs +122 -0
- package/lib/doctor/watchers/handoffs.mjs +33 -0
- package/lib/doctor/watchers/process-pressure.mjs +60 -0
- package/lib/doctor/watchers/service-health.mjs +188 -0
- package/lib/document-extract.mjs +288 -0
- package/lib/document-ingest.mjs +230 -0
- package/lib/drop.mjs +282 -0
- package/lib/embed/approval-queue.mjs +176 -0
- package/lib/embed/artifact.mjs +349 -0
- package/lib/embed/authority-guard.mjs +155 -0
- package/lib/embed/cli.mjs +408 -0
- package/lib/embed/config.mjs +355 -0
- package/lib/embed/conflict-detection.mjs +264 -0
- package/lib/embed/customer-profiles.mjs +480 -0
- package/lib/embed/daemon.mjs +1309 -0
- package/lib/embed/demand-fetch.mjs +449 -0
- package/lib/embed/docs-lifecycle.mjs +349 -0
- package/lib/embed/inbox-live-watcher.mjs +119 -0
- package/lib/embed/inbox.mjs +343 -0
- package/lib/embed/intake-metrics.mjs +190 -0
- package/lib/embed/jobs/vector-sync.mjs +198 -0
- package/lib/embed/notifications.mjs +75 -0
- package/lib/embed/output.mjs +79 -0
- package/lib/embed/providers/github.mjs +295 -0
- package/lib/embed/providers/jira.mjs +192 -0
- package/lib/embed/providers/linear.mjs +186 -0
- package/lib/embed/providers/registry.mjs +115 -0
- package/lib/embed/providers/slack.mjs +203 -0
- package/lib/embed/recommendation-store.mjs +378 -0
- package/lib/embed/roadmap.mjs +374 -0
- package/lib/embed/role-framing.mjs +110 -0
- package/lib/embed/scheduler.mjs +99 -0
- package/lib/embed/semantic.mjs +325 -0
- package/lib/embed/snapshot.mjs +191 -0
- package/lib/embed/supervision.mjs +235 -0
- package/lib/embed/target-resolver.mjs +186 -0
- package/lib/embed/worker.mjs +62 -0
- package/lib/embed/workspaces.mjs +297 -0
- package/lib/engine/chunker-headings.mjs +110 -0
- package/lib/engine/compressor-heuristic.mjs +100 -0
- package/lib/engine/consolidate.mjs +287 -0
- package/lib/engine/contracts.mjs +126 -0
- package/lib/engine/defaults.mjs +129 -0
- package/lib/engine/eval-retrieval.mjs +148 -0
- package/lib/engine/fuser-rrf.mjs +62 -0
- package/lib/engine/index.mjs +37 -0
- package/lib/engine/registry.mjs +146 -0
- package/lib/engine/reranker-mmr.mjs +90 -0
- package/lib/engine/tokens.mjs +77 -0
- package/lib/entity-store.mjs +280 -0
- package/lib/env-config.mjs +69 -4
- package/lib/evals/retrieval-bench.mjs +159 -0
- package/lib/evaluator-optimizer.mjs +317 -0
- package/lib/features.mjs +159 -29
- package/lib/gates-audit.mjs +236 -0
- package/lib/git-hooks/prepare-commit-msg +58 -0
- package/lib/handoffs/cleanup.mjs +159 -0
- package/lib/handoffs/contract.mjs +162 -0
- package/lib/handoffs/inventory.mjs +120 -0
- package/lib/headhunt.mjs +28 -47
- package/lib/health-check.mjs +399 -0
- package/lib/hook-health.mjs +442 -0
- package/lib/hooks/_lib/log.mjs +82 -0
- package/lib/hooks/adaptive-lint.mjs +26 -2
- package/lib/hooks/agent-tracker.mjs +171 -14
- package/lib/hooks/audit-reads.mjs +109 -0
- package/lib/hooks/audit-trail.mjs +153 -0
- package/lib/hooks/bash-output-logger.mjs +71 -0
- package/lib/hooks/block-no-verify.mjs +41 -0
- package/lib/hooks/ci-status-check.mjs +82 -0
- package/lib/hooks/comment-lint.mjs +29 -7
- package/lib/hooks/config-protection.mjs +39 -18
- package/lib/hooks/context-watch.mjs +137 -0
- package/lib/hooks/context-window-recovery.mjs +4 -20
- package/lib/hooks/dep-audit.mjs +16 -0
- package/lib/hooks/doc-coupling-check.mjs +80 -0
- package/lib/hooks/edit-accumulator.mjs +3 -0
- package/lib/hooks/edit-error-recovery.mjs +3 -0
- package/lib/hooks/edit-guard.mjs +45 -4
- package/lib/hooks/env-check.mjs +3 -0
- package/lib/hooks/guard-bash.mjs +58 -1
- package/lib/hooks/mcp-audit.mjs +18 -20
- package/lib/hooks/mcp-health-check.mjs +36 -0
- package/lib/hooks/model-fallback.mjs +42 -56
- package/lib/hooks/policy-engine.mjs +209 -0
- package/lib/hooks/post-merge-docs-check.mjs +63 -0
- package/lib/hooks/pre-compact.mjs +4 -24
- package/lib/hooks/pre-push-gate.mjs +200 -37
- package/lib/hooks/proactive-activation.mjs +284 -0
- package/lib/hooks/read-tracker.mjs +27 -4
- package/lib/hooks/readme-age-check.mjs +77 -0
- package/lib/hooks/registry-sync.mjs +12 -5
- package/lib/hooks/scan-secrets.mjs +50 -7
- package/lib/hooks/session-optimize.mjs +311 -0
- package/lib/hooks/session-start.mjs +287 -28
- package/lib/hooks/stop-notify.mjs +236 -96
- package/lib/hooks/stop-typecheck.mjs +13 -1
- package/lib/hooks/test-watch.mjs +68 -0
- package/lib/host-capabilities.mjs +31 -10
- package/lib/init-docs.mjs +731 -291
- package/lib/init-unified.mjs +1000 -0
- package/lib/init-update.mjs +168 -0
- package/lib/init.mjs +107 -0
- package/lib/install/first-invocation.mjs +119 -0
- package/lib/install/stage-project.mjs +69 -0
- package/lib/intake/classify.mjs +278 -0
- package/lib/intake/feedback.mjs +273 -0
- package/lib/intake/filesystem-queue.mjs +158 -0
- package/lib/intake/intake-config.mjs +132 -0
- package/lib/intake/postgres-queue.mjs +198 -0
- package/lib/intake/prepare.mjs +139 -0
- package/lib/intake/queue.mjs +83 -0
- package/lib/intake/session-prelude.mjs +50 -0
- package/lib/integrations/intake-integrations.mjs +740 -0
- package/lib/intent-classifier.mjs +253 -0
- package/lib/knowledge/layout.mjs +72 -0
- package/lib/knowledge/rag.mjs +331 -0
- package/lib/knowledge/search.mjs +315 -0
- package/lib/knowledge/trends.mjs +261 -0
- package/lib/logger.mjs +85 -0
- package/lib/mcp/broker.mjs +124 -0
- package/lib/mcp/server.mjs +554 -854
- package/lib/mcp/tools/document.mjs +132 -0
- package/lib/mcp/tools/memory.mjs +209 -0
- package/lib/mcp/tools/project.mjs +349 -0
- package/lib/mcp/tools/skills.mjs +409 -0
- package/lib/mcp/tools/storage.mjs +60 -0
- package/lib/mcp/tools/telemetry.mjs +315 -0
- package/lib/mcp/tools/workflow.mjs +112 -0
- package/lib/mcp-catalog.json +54 -3
- package/lib/mcp-manager.mjs +96 -48
- package/lib/mcp-platform-config.mjs +22 -22
- package/lib/memory-stats.mjs +121 -0
- package/lib/mode-commands.mjs +124 -0
- package/lib/model-free-selector.mjs +184 -0
- package/lib/model-pricing.mjs +152 -0
- package/lib/model-registry.mjs +226 -0
- package/lib/model-router.mjs +362 -386
- package/lib/observation-store.mjs +391 -0
- package/lib/ollama-manager.mjs +428 -0
- package/lib/opencode-config.mjs +13 -3
- package/lib/opencode-runtime-plugin.mjs +70 -38
- package/lib/opencode-telemetry.mjs +64 -6
- package/lib/orchestration-policy.mjs +509 -6
- package/lib/overrides/resolver.mjs +207 -0
- package/lib/parity.mjs +147 -0
- package/lib/paths.mjs +33 -0
- package/lib/performance/generate.mjs +212 -0
- package/lib/plugin-registry.mjs +268 -0
- package/lib/policy/engine.mjs +130 -0
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-detection.mjs +129 -0
- package/lib/project-init-shared.mjs +272 -0
- package/lib/project-profile.mjs +447 -0
- package/lib/prompt-composer.js +434 -0
- package/lib/prompt-metadata.mjs +1 -1
- package/lib/provider-capabilities-anthropic.js +44 -0
- package/lib/provider-capabilities-deepseek.js +37 -0
- package/lib/provider-capabilities-generic.js +26 -0
- package/lib/provider-capabilities-google.js +47 -0
- package/lib/provider-capabilities-openai.js +45 -0
- package/lib/provider-capabilities.js +142 -0
- package/lib/providers/atlassian-confluence/index.mjs +103 -0
- package/lib/providers/atlassian-jira/index.mjs +100 -0
- package/lib/providers/auth-manager.mjs +126 -0
- package/lib/providers/circuit-breaker.mjs +124 -0
- package/lib/providers/contract.mjs +93 -0
- package/lib/providers/github/index.mjs +126 -0
- package/lib/providers/registry.mjs +184 -0
- package/lib/providers/salesforce/index.mjs +100 -0
- package/lib/providers/slack/index.mjs +80 -0
- package/lib/reflect.mjs +137 -0
- package/lib/research-lint.mjs +164 -0
- package/lib/resources/budget.mjs +259 -0
- package/lib/role-preload.mjs +21 -6
- package/lib/roles/approval-surface.mjs +54 -0
- package/lib/roles/cli.mjs +118 -0
- package/lib/roles/event-bus.mjs +79 -0
- package/lib/roles/fence.mjs +84 -0
- package/lib/roles/gateway.mjs +260 -0
- package/lib/roles/hook-emit.mjs +37 -0
- package/lib/roles/manifest.mjs +48 -0
- package/lib/roles/router.mjs +27 -0
- package/lib/runtime-pressure.mjs +360 -0
- package/lib/schema-artifact.mjs +134 -0
- package/lib/schema-infer.mjs +551 -0
- package/lib/server/auth.mjs +168 -0
- package/lib/server/chat.mjs +336 -0
- package/lib/server/cors.mjs +77 -0
- package/lib/server/csrf.mjs +91 -0
- package/lib/server/index.mjs +1927 -78
- package/lib/server/insights.mjs +765 -0
- package/lib/server/rate-limit.mjs +91 -0
- package/lib/server/static/assets/index-ab25c707.js +70 -0
- package/lib/server/static/assets/index-f0c80a2b.css +1 -0
- package/lib/server/static/index.html +12 -817
- package/lib/server/telemetry-login.mjs +108 -0
- package/lib/server/webhook.mjs +510 -0
- package/lib/service-manager.mjs +522 -58
- package/lib/services/pattern-promotion-service.mjs +167 -0
- package/lib/services/telemetry-backend.mjs +178 -0
- package/lib/session-store.mjs +374 -0
- package/lib/setup-prompts.mjs +96 -0
- package/lib/setup.mjs +523 -36
- package/lib/skills-apply.mjs +280 -0
- package/lib/skills-scope.mjs +118 -0
- package/lib/status.mjs +261 -70
- package/lib/storage/admin.mjs +355 -0
- package/lib/storage/backend.mjs +2 -1
- package/lib/storage/backup.mjs +347 -0
- package/lib/storage/embeddings-engine.mjs +133 -0
- package/lib/storage/embeddings-legacy.mjs +85 -0
- package/lib/storage/embeddings-local.mjs +108 -0
- package/lib/storage/embeddings-ollama.mjs +78 -0
- package/lib/storage/embeddings-openai.mjs +85 -0
- package/lib/storage/embeddings.mjs +92 -33
- package/lib/storage/file-lock.mjs +130 -0
- package/lib/storage/fusion.mjs +95 -0
- package/lib/storage/hybrid-query.mjs +34 -27
- package/lib/storage/migrations.mjs +187 -0
- package/lib/storage/postgres-backup.mjs +124 -0
- package/lib/storage/sql-store.mjs +5 -15
- package/lib/storage/state-source.mjs +12 -13
- package/lib/storage/store-version.mjs +115 -0
- package/lib/storage/sync.mjs +144 -35
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/storage/vector-client.mjs +286 -0
- package/lib/storage/vector-store.mjs +71 -30
- package/lib/task-graph/generate.mjs +135 -0
- package/lib/task-graph/schema.mjs +81 -0
- package/lib/task-graph/store.mjs +71 -0
- package/lib/telemetry/backends/local.mjs +62 -0
- package/lib/telemetry/backends/{langfuse.mjs → remote.mjs} +27 -14
- package/lib/telemetry/backfill.mjs +180 -0
- package/lib/telemetry/eval-datasets.mjs +203 -0
- package/lib/telemetry/{langfuse-ingest.mjs → ingest.mjs} +26 -20
- package/lib/telemetry/intent-verifications.mjs +86 -0
- package/lib/telemetry/llm-judge.mjs +350 -0
- package/lib/telemetry/model-pricing-catalog.mjs +557 -0
- package/lib/telemetry/setup.mjs +151 -0
- package/lib/telemetry/skill-calls.mjs +78 -0
- package/lib/telemetry/team-rollup.mjs +4 -4
- package/lib/token-engine.js +117 -0
- package/lib/token-estimator-anthropic.js +15 -0
- package/lib/token-estimator-deepseek.js +13 -0
- package/lib/token-estimator-default.js +13 -0
- package/lib/token-estimator-google.js +13 -0
- package/lib/token-estimator-openai.js +13 -0
- package/lib/toolkit-env.mjs +1 -1
- package/lib/tty-prompts.mjs +211 -0
- package/lib/uninstall/uninstall.mjs +423 -0
- package/lib/update.mjs +115 -0
- package/lib/upgrade.mjs +141 -0
- package/lib/validator.mjs +51 -2
- package/lib/validators/skills.mjs +142 -0
- package/lib/wireframe.mjs +422 -0
- package/lib/worker/entrypoint.mjs +241 -0
- package/lib/worker/evidence.mjs +107 -0
- package/lib/worker/run.mjs +154 -0
- package/lib/worker/trace.mjs +182 -0
- package/lib/workflow-state.mjs +14 -18
- package/package.json +21 -8
- package/personas/construct.md +53 -51
- package/platforms/claude/CLAUDE.md +1 -1
- package/platforms/claude/settings.template.json +115 -68
- package/platforms/opencode/config.template.json +3 -7
- package/rules/common/agents.md +11 -38
- package/rules/common/beads-hygiene.md +75 -0
- package/rules/common/code-review.md +11 -100
- package/rules/common/coding-style.md +5 -3
- package/rules/common/comments.md +30 -111
- package/rules/common/commit-approval.md +53 -0
- package/rules/common/cx-agent-routing.md +1 -0
- package/rules/common/development-workflow.md +23 -41
- package/rules/common/doc-ownership.md +54 -0
- package/rules/common/efficiency.md +57 -0
- package/rules/common/framing.md +76 -0
- package/rules/common/git-workflow.md +2 -4
- package/rules/common/patterns.md +3 -7
- package/rules/common/performance.md +15 -31
- package/rules/common/release-gates.md +69 -0
- package/rules/common/research.md +107 -0
- package/rules/common/security.md +6 -6
- package/rules/common/skill-composition.md +67 -0
- package/rules/common/testing.md +15 -27
- package/rules/golang/hooks.md +0 -4
- package/rules/policy/bootstrap.yaml +11 -0
- package/rules/policy/drive.yaml +8 -0
- package/rules/policy/task.yaml +9 -0
- package/rules/policy/workflow.yaml +9 -0
- package/rules/python/hooks.md +0 -4
- package/rules/swift/hooks.md +0 -4
- package/rules/typescript/hooks.md +0 -4
- package/rules/web/hooks.md +0 -2
- package/{sync-agents.mjs → scripts/sync-agents.mjs} +306 -61
- package/skills/ai/prompt-optimizer.md +7 -7
- package/skills/compliance/ai-disclosure.md +58 -0
- package/skills/compliance/data-privacy.md +46 -0
- package/skills/compliance/license-audit.md +40 -0
- package/skills/compliance/regulatory-review.md +61 -0
- package/skills/docs/document-ingest-workflow.md +52 -0
- package/skills/docs/evidence-ingest-workflow.md +9 -0
- package/skills/docs/init-docs.md +51 -18
- package/skills/docs/product-intelligence-workflow.md +12 -0
- package/skills/docs/product-signal-workflow.md +4 -0
- package/skills/docs/research-workflow.md +23 -8
- package/skills/docs/runbook-workflow.md +1 -1
- package/skills/operating/orchestration-reference.md +151 -0
- package/skills/roles/architect.md +5 -0
- package/skills/roles/engineer.md +32 -0
- package/skills/roles/operator.md +12 -0
- package/skills/roles/reviewer.md +23 -0
- package/skills/routing.md +1 -1
- package/templates/devcontainer/Dockerfile.devcontainer +38 -0
- package/templates/devcontainer/devcontainer.json +31 -0
- package/templates/distribution/bootstrap.ps1 +142 -0
- package/templates/distribution/bootstrap.sh +196 -0
- package/templates/distribution/run.mjs +187 -0
- package/templates/docs/adr.md +44 -8
- package/templates/docs/changelog-entry.md +43 -0
- package/templates/docs/construct_guide.md +149 -0
- package/templates/docs/evidence-brief.md +2 -2
- package/templates/docs/meta-prd.md +140 -19
- package/templates/docs/onboarding.md +57 -0
- package/templates/docs/prd.md +159 -15
- package/templates/docs/research-brief.md +4 -4
- package/templates/homebrew/construct.rb +67 -0
- package/langfuse/docker-compose.yml +0 -82
- package/lib/eval-harness.mjs +0 -59
- package/lib/hooks/bootstrap-guard.mjs +0 -90
- package/lib/hooks/console-warn.mjs +0 -43
- package/lib/hooks/continuation-enforcer.mjs +0 -72
- package/lib/hooks/drive-guard.mjs +0 -89
- package/lib/hooks/mcp-task-scope.mjs +0 -47
- package/lib/hooks/task-completed-guard.mjs +0 -43
- package/lib/hooks/teammate-idle-guard.mjs +0 -54
- package/lib/hooks/workflow-guard.mjs +0 -62
- package/lib/prompt-composer.mjs +0 -196
- package/lib/review.mjs +0 -429
- package/lib/server/static/app.js +0 -841
- package/lib/telemetry/langfuse-model-sync.mjs +0 -270
- package/rules/common/hooks.md +0 -35
- package/rules/zh/README.md +0 -113
- package/rules/zh/agents.md +0 -55
- package/rules/zh/code-review.md +0 -129
- package/rules/zh/coding-style.md +0 -53
- package/rules/zh/development-workflow.md +0 -49
- package/rules/zh/git-workflow.md +0 -29
- package/rules/zh/hooks.md +0 -35
- package/rules/zh/patterns.md +0 -36
- package/rules/zh/performance.md +0 -60
- package/rules/zh/security.md +0 -34
- package/rules/zh/testing.md +0 -34
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-free-selector.mjs — Free model polling, scoring, and selection.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from model-router.mjs to keep each module under 800 lines.
|
|
5
|
+
* Polls OpenRouter for free models, scores them by tier, and provides
|
|
6
|
+
* helpers for free-preference modes (global, same-family).
|
|
7
|
+
*
|
|
8
|
+
* Consumed by:
|
|
9
|
+
* • bin/construct – model configuration commands (--auto, --apply).
|
|
10
|
+
* • lib/model-router.mjs – re-exports for backward compatibility.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
14
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the list of currently available free models from OpenRouter.
|
|
18
|
+
* Returns an array sorted by descending capability score.
|
|
19
|
+
* Returns `[]` (with a logged error) if the API key is missing or the request fails.
|
|
20
|
+
* @param {string} apiKey – OpenRouter bearer token.
|
|
21
|
+
* @returns {Promise<Array<{id:string,name:string,contextLength:number,isFree:boolean,score:number}>>}
|
|
22
|
+
*/
|
|
23
|
+
export async function pollFreeModels(apiKey) {
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
console.error(
|
|
26
|
+
"Error: OPENROUTER_API_KEY is not set. Cannot poll free models."
|
|
27
|
+
);
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
33
|
+
|
|
34
|
+
let data;
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(OPENROUTER_MODELS_URL, {
|
|
37
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
38
|
+
signal: controller.signal,
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
console.error(
|
|
42
|
+
`Error: OpenRouter models endpoint returned ${res.status} ${res.statusText}`
|
|
43
|
+
);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
data = await res.json();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.name === "AbortError") {
|
|
49
|
+
console.error(
|
|
50
|
+
"Error: OpenRouter request timed out after 10 seconds."
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
console.error(`Error: Failed to fetch OpenRouter models — ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const models = (data?.data ?? [])
|
|
61
|
+
.filter(
|
|
62
|
+
(m) =>
|
|
63
|
+
m?.pricing?.prompt === "0" && m?.pricing?.completion === "0"
|
|
64
|
+
)
|
|
65
|
+
.map((m) => ({
|
|
66
|
+
id: `openrouter/${m.id}`,
|
|
67
|
+
name: m.name ?? m.id,
|
|
68
|
+
contextLength: m.context_length ?? 0,
|
|
69
|
+
isFree: true,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return models.sort((a, b) => score(b, "standard") - score(a, "standard"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Score a model for a given tier (higher is better).
|
|
77
|
+
* @param {object} model – Model object as returned by `pollFreeModels`.
|
|
78
|
+
* @param {"reasoning"|"standard"|"fast"} tier – The tier to score for.
|
|
79
|
+
* @returns {number} – Numerical score used for ranking.
|
|
80
|
+
*/
|
|
81
|
+
export function score(model, tier) {
|
|
82
|
+
const { id, contextLength } = model;
|
|
83
|
+
let s = 0;
|
|
84
|
+
|
|
85
|
+
// Context-length weighting
|
|
86
|
+
if (contextLength >= 128_000) s += 40;
|
|
87
|
+
else if (contextLength >= 32_000) s += 20;
|
|
88
|
+
else if (contextLength >= 16_000) s += 10;
|
|
89
|
+
|
|
90
|
+
// Provider-agnostic capability weighting
|
|
91
|
+
if (id.includes("gemma-4")) s += 30;
|
|
92
|
+
if (id.includes("gemma-3")) s += 25;
|
|
93
|
+
if (id.includes("gpt-4o")) s += 25;
|
|
94
|
+
if (id.includes("nemotron-3-super")) s += 20;
|
|
95
|
+
if (id.includes("llama-3.3-70b")) s += 20;
|
|
96
|
+
if (id.includes("qwen3") && contextLength >= 32_000) s += 20;
|
|
97
|
+
if (id.includes("deepseek-r1")) s += 20;
|
|
98
|
+
if (id.includes("deepseek-v3")) s += 15;
|
|
99
|
+
// Extra boost for reasoning-heavy models when the tier is reasoning
|
|
100
|
+
if (
|
|
101
|
+
tier === "reasoning" &&
|
|
102
|
+
(id.includes("deepseek-r1") ||
|
|
103
|
+
id.includes("nemotron-3-super") ||
|
|
104
|
+
id.includes("qwen3"))
|
|
105
|
+
) {
|
|
106
|
+
s += 15;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return s;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Select the best free model for a given tier from an array of free models.
|
|
114
|
+
* Falls back to a registry-provided fallback chain if no suitable free model exists.
|
|
115
|
+
* @param {Array} freeModels – Output of `pollFreeModels`.
|
|
116
|
+
* @param {"reasoning"|"standard"|"fast"} tier – Target tier.
|
|
117
|
+
* @param {string[]} [registryFallbacks=[]] – Ordered list of fallback IDs.
|
|
118
|
+
* @returns {string|null} – Best model ID, or `null` if none found.
|
|
119
|
+
*/
|
|
120
|
+
export function selectForTier(freeModels, tier, registryFallbacks = []) {
|
|
121
|
+
const minContext = tier === "fast" ? 8_000 : tier === "standard" ? 16_000 : 32_000;
|
|
122
|
+
|
|
123
|
+
if (freeModels && freeModels.length > 0) {
|
|
124
|
+
const candidates = freeModels
|
|
125
|
+
.filter((m) => m.contextLength >= minContext)
|
|
126
|
+
.map((m) => ({ ...m, tierScore: score(m, tier) }))
|
|
127
|
+
.sort((a, b) => b.tierScore - a.tierScore);
|
|
128
|
+
|
|
129
|
+
if (candidates[0]?.id) return candidates[0].id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return registryFallbacks[0] ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Return the top N candidates for a given tier (used for UI lists).
|
|
137
|
+
* @param {Array} freeModels – Array of model objects.
|
|
138
|
+
* @param {"reasoning"|"standard"|"fast"} tier – Target tier.
|
|
139
|
+
* @param {number} n – Number of candidates to return (default = 3).
|
|
140
|
+
* @returns {Array} – Sorted candidate objects.
|
|
141
|
+
*/
|
|
142
|
+
export function topForTier(freeModels, tier, n = 3) {
|
|
143
|
+
if (!freeModels || freeModels.length === 0) return [];
|
|
144
|
+
|
|
145
|
+
const minContext = tier === "fast" ? 8_000 : tier === "standard" ? 16_000 : 32_000;
|
|
146
|
+
return freeModels
|
|
147
|
+
.filter((m) => m.contextLength >= minContext)
|
|
148
|
+
.map((m) => ({ ...m, tierScore: score(m, tier) }))
|
|
149
|
+
.sort((a, b) => b.tierScore - a.tierScore)
|
|
150
|
+
.slice(0, n);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a model ID ends with `:free`.
|
|
155
|
+
* @param {string} modelId – Model identifier.
|
|
156
|
+
* @returns {boolean} – True if the model is marked as free.
|
|
157
|
+
*/
|
|
158
|
+
export function isFreeModel(modelId = "") {
|
|
159
|
+
return /:free$/i.test(modelId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Choose a free model when multiple candidates exist.
|
|
164
|
+
* Preference order: explicit primary > fallback > registry default > builtin default.
|
|
165
|
+
* @param {string} primary – Explicitly selected model.
|
|
166
|
+
* @param {string} fallback – Second-choice candidate.
|
|
167
|
+
* @param {string} registryDefault – Default from registry.
|
|
168
|
+
* @param {string} builtinDefault – Built-in fallback.
|
|
169
|
+
* @returns {string|null} – The chosen model ID, or `null` if none selected.
|
|
170
|
+
*/
|
|
171
|
+
export function preferFreeValue(
|
|
172
|
+
primary,
|
|
173
|
+
fallback,
|
|
174
|
+
registryDefault,
|
|
175
|
+
builtinDefault
|
|
176
|
+
) {
|
|
177
|
+
if (primary && isFreeModel(primary)) return primary;
|
|
178
|
+
if (fallback && isFreeModel(fallback)) return fallback;
|
|
179
|
+
if (registryDefault && isFreeModel(registryDefault))
|
|
180
|
+
return registryDefault;
|
|
181
|
+
if (builtinDefault && isFreeModel(builtinDefault))
|
|
182
|
+
return builtinDefault;
|
|
183
|
+
return primary ?? fallback ?? registryDefault ?? builtinDefault ?? null;
|
|
184
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-pricing.mjs — best-effort live pricing for the model selector.
|
|
3
|
+
*
|
|
4
|
+
* OpenRouter exposes a free, unauthenticated `GET /api/v1/models` endpoint
|
|
5
|
+
* with per-1M-token pricing for every model in its catalog. We hit it once
|
|
6
|
+
* per process (with a 5-minute cache to ~/.cx/model-pricing.json) and fall
|
|
7
|
+
* back to a built-in pricing table for first-party endpoints (Anthropic,
|
|
8
|
+
* OpenAI) when their providers aren't proxied through OpenRouter.
|
|
9
|
+
*
|
|
10
|
+
* For local providers (ollama, local) we return zero pricing with a
|
|
11
|
+
* `runs locally` label so the UI can render `free · runs locally` without
|
|
12
|
+
* any network call.
|
|
13
|
+
*
|
|
14
|
+
* Pricing shape: { input: number, output: number, unit: '1M tokens', currency: 'USD', source }.
|
|
15
|
+
* Numbers are USD per 1M tokens for direct comparability across providers.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CACHE_FILE = path.join(os.homedir(), '.cx', 'model-pricing.json');
|
|
23
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
24
|
+
const OPENROUTER_ENDPOINT = 'https://openrouter.ai/api/v1/models';
|
|
25
|
+
|
|
26
|
+
const STATIC_PRICING = {
|
|
27
|
+
'anthropic/claude-opus-4-6': { input: 15.0, output: 75.0 },
|
|
28
|
+
'anthropic/claude-sonnet-4-6': { input: 3.0, output: 15.0 },
|
|
29
|
+
'anthropic/claude-haiku-4-5-20251001': { input: 0.8, output: 4.0 },
|
|
30
|
+
'openai/gpt-5.4': { input: 12.0, output: 60.0 },
|
|
31
|
+
'openai/gpt-5.1': { input: 2.5, output: 12.5 },
|
|
32
|
+
'openai/gpt-5.1-mini': { input: 0.6, output: 3.0 },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LOCAL_PRICING = { input: 0, output: 0, source: 'local', label: 'free · runs locally' };
|
|
36
|
+
|
|
37
|
+
function readCache(cacheFile) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(cacheFile, 'utf8');
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
if (parsed && typeof parsed === 'object' && parsed.fetchedAt) return parsed;
|
|
42
|
+
} catch { /* cache miss */ }
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeCache(cacheFile, payload) {
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
|
|
49
|
+
fs.writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
|
|
50
|
+
} catch { /* non-fatal */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isCacheFresh(cache, now = Date.now()) {
|
|
54
|
+
if (!cache || typeof cache.fetchedAt !== 'number') return false;
|
|
55
|
+
const age = now - cache.fetchedAt;
|
|
56
|
+
if (age < 0) return false;
|
|
57
|
+
return age < CACHE_TTL_MS;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch the OpenRouter catalog. Returns a Map of model id -> { input, output }
|
|
62
|
+
* in USD per 1M tokens. The endpoint returns pricing per token; we multiply
|
|
63
|
+
* by 1e6 so callers compare in the same unit as Anthropic / OpenAI lists.
|
|
64
|
+
*/
|
|
65
|
+
async function fetchOpenRouterPricing({ fetchImpl = globalThis.fetch, signal } = {}) {
|
|
66
|
+
if (typeof fetchImpl !== 'function') return null;
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetchImpl(OPENROUTER_ENDPOINT, { signal });
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (!res?.ok) return null;
|
|
74
|
+
let data;
|
|
75
|
+
try { data = await res.json(); } catch { return null; }
|
|
76
|
+
const list = Array.isArray(data?.data) ? data.data : [];
|
|
77
|
+
const map = {};
|
|
78
|
+
for (const entry of list) {
|
|
79
|
+
if (!entry?.id) continue;
|
|
80
|
+
const p = entry.pricing || {};
|
|
81
|
+
const input = Number(p.prompt);
|
|
82
|
+
const output = Number(p.completion);
|
|
83
|
+
if (!Number.isFinite(input) || !Number.isFinite(output)) continue;
|
|
84
|
+
map[`openrouter/${entry.id}`] = {
|
|
85
|
+
input: input * 1_000_000,
|
|
86
|
+
output: output * 1_000_000,
|
|
87
|
+
context: entry.context_length || null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return map;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve pricing for a list of model ids. Always returns an object keyed by
|
|
95
|
+
* model id; missing entries map to null so the UI can render an em-dash.
|
|
96
|
+
*/
|
|
97
|
+
export async function getPricingForModels(modelIds, { fetchImpl, signal, now = Date.now(), cacheFile = DEFAULT_CACHE_FILE } = {}) {
|
|
98
|
+
const result = {};
|
|
99
|
+
const needsRemote = [];
|
|
100
|
+
for (const id of modelIds || []) {
|
|
101
|
+
if (typeof id !== 'string' || !id) continue;
|
|
102
|
+
if (/^ollama\//.test(id) || /^local\//.test(id)) {
|
|
103
|
+
result[id] = { ...LOCAL_PRICING, unit: '1M tokens', currency: 'USD' };
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (STATIC_PRICING[id]) {
|
|
107
|
+
result[id] = { ...STATIC_PRICING[id], unit: '1M tokens', currency: 'USD', source: 'static' };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (/^openrouter\//.test(id)) {
|
|
111
|
+
needsRemote.push(id);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
result[id] = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (needsRemote.length === 0) return result;
|
|
118
|
+
|
|
119
|
+
let cache = readCache(cacheFile);
|
|
120
|
+
if (!isCacheFresh(cache, now)) {
|
|
121
|
+
const fresh = await fetchOpenRouterPricing({ fetchImpl, signal });
|
|
122
|
+
if (fresh) {
|
|
123
|
+
cache = { fetchedAt: now, models: fresh };
|
|
124
|
+
writeCache(cacheFile, cache);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const remoteMap = cache?.models ?? {};
|
|
129
|
+
for (const id of needsRemote) {
|
|
130
|
+
const hit = remoteMap[id];
|
|
131
|
+
result[id] = hit
|
|
132
|
+
? { input: hit.input, output: hit.output, context: hit.context, unit: '1M tokens', currency: 'USD', source: 'openrouter' }
|
|
133
|
+
: null;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format a pricing entry into a compact selector label like
|
|
140
|
+
* `$3.00 / $15.00 per 1M`. Returns null for missing or all-zero entries so
|
|
141
|
+
* callers can render `free` or an em-dash without branching on each field.
|
|
142
|
+
*/
|
|
143
|
+
export function formatPricingLabel(entry) {
|
|
144
|
+
if (!entry) return null;
|
|
145
|
+
if (entry.source === 'local' || (entry.input === 0 && entry.output === 0)) {
|
|
146
|
+
return entry.label || 'free · runs locally';
|
|
147
|
+
}
|
|
148
|
+
const input = Number(entry.input);
|
|
149
|
+
const output = Number(entry.output);
|
|
150
|
+
if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
|
|
151
|
+
return `$${input.toFixed(2)} in · $${output.toFixed(2)} out / 1M`;
|
|
152
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-registry.mjs — Single source of truth for model tier resolution.
|
|
3
|
+
*
|
|
4
|
+
* Addresses model tier complexity by:
|
|
5
|
+
* 1. ONE resolution path with clear precedence
|
|
6
|
+
* 2. Validated at config time, not runtime
|
|
7
|
+
* 3. Simple env-only overrides for emergencies
|
|
8
|
+
* 4. Clear error messages when config is missing
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Configuration precedence (single source of truth)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RESOLUTION ORDER (highest to lowest):
|
|
20
|
+
* 1. Environment variables (CX_MODEL_*): Emergency overrides
|
|
21
|
+
* 2. User config (~/.construct/config.env): Personal defaults
|
|
22
|
+
* 3. Registry (agents/registry.json): Project recommendations
|
|
23
|
+
* 4. Built-in defaults: Last resort fallbacks
|
|
24
|
+
*
|
|
25
|
+
* Primary resolution path. All code must use this function.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const BUILTIN_DEFAULTS = {
|
|
29
|
+
reasoning: 'anthropic/claude-3-5-sonnet',
|
|
30
|
+
standard: 'anthropic/claude-3-haiku',
|
|
31
|
+
fast: 'anthropic/claude-3-haiku',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ENV_PREFIX = 'CX_MODEL';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Core resolution function
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export function resolveModelTiers(options = {}) {
|
|
41
|
+
const {
|
|
42
|
+
env = process.env,
|
|
43
|
+
registryPath = null,
|
|
44
|
+
strict = false, // If true, require all tiers to be configured
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
const result = {
|
|
48
|
+
reasoning: null,
|
|
49
|
+
standard: null,
|
|
50
|
+
fast: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sources = {};
|
|
54
|
+
const errors = [];
|
|
55
|
+
|
|
56
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
57
|
+
const envKey = `${ENV_PREFIX}_${tier.toUpperCase()}`;
|
|
58
|
+
|
|
59
|
+
// Environment variable (highest priority)
|
|
60
|
+
if (env[envKey]) {
|
|
61
|
+
result[tier] = env[envKey];
|
|
62
|
+
sources[tier] = 'env';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// User config (read from env which was loaded by env-config)
|
|
67
|
+
const configKey = `CONSTRUCT_MODEL_${tier.toUpperCase()}`;
|
|
68
|
+
if (env[configKey]) {
|
|
69
|
+
result[tier] = env[configKey];
|
|
70
|
+
sources[tier] = 'config';
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Registry
|
|
75
|
+
if (registryPath && fs.existsSync(registryPath)) {
|
|
76
|
+
try {
|
|
77
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
78
|
+
const registryModel = registry.models?.[tier]?.primary;
|
|
79
|
+
if (registryModel) {
|
|
80
|
+
result[tier] = registryModel;
|
|
81
|
+
sources[tier] = 'registry';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
errors.push(`Failed to read registry: ${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Built-in default
|
|
90
|
+
result[tier] = BUILTIN_DEFAULTS[tier];
|
|
91
|
+
sources[tier] = 'default';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validation
|
|
95
|
+
const configured = Object.values(result).filter(Boolean).length;
|
|
96
|
+
|
|
97
|
+
if (strict && configured < 3) {
|
|
98
|
+
const missing = Object.entries(result)
|
|
99
|
+
.filter(([, v]) => !v)
|
|
100
|
+
.map(([k]) => k);
|
|
101
|
+
errors.push(`Missing configuration for tiers: ${missing.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
models: result,
|
|
106
|
+
sources,
|
|
107
|
+
configured,
|
|
108
|
+
complete: configured === 3,
|
|
109
|
+
errors: errors.length > 0 ? errors : null,
|
|
110
|
+
defaultsUsed: Object.values(sources).filter(s => s === 'default').length,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Quick access helpers
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export function getModelForTier(tier, options = {}) {
|
|
119
|
+
const resolved = resolveModelTiers(options);
|
|
120
|
+
return resolved.models[tier] || BUILTIN_DEFAULTS[tier];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getModelSource(tier, options = {}) {
|
|
124
|
+
const resolved = resolveModelTiers(options);
|
|
125
|
+
return resolved.sources[tier] || 'unknown';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Configuration helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export function validateModelTiers(options = {}) {
|
|
133
|
+
const resolved = resolveModelTiers({ ...options, strict: true });
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
valid: resolved.complete && !resolved.errors,
|
|
137
|
+
errors: resolved.errors,
|
|
138
|
+
warnings: resolved.defaultsUsed > 0
|
|
139
|
+
? [`Using ${resolved.defaultsUsed} built-in default(s). Consider configuring custom models.`]
|
|
140
|
+
: [],
|
|
141
|
+
resolution: resolved,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatModelStatus(options = {}) {
|
|
146
|
+
const resolved = resolveModelTiers(options);
|
|
147
|
+
|
|
148
|
+
let output = 'Model Configuration:\n\n';
|
|
149
|
+
|
|
150
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
151
|
+
const model = resolved.models[tier];
|
|
152
|
+
const source = resolved.sources[tier];
|
|
153
|
+
const icon = source === 'default' ? '⚠' : '✓';
|
|
154
|
+
|
|
155
|
+
output += `${icon} ${tier.padEnd(10)} ${model}\n`;
|
|
156
|
+
output += ` Source: ${source}\n\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (resolved.defaultsUsed > 0) {
|
|
160
|
+
output += '\nNote: Using built-in defaults. To customize:\n';
|
|
161
|
+
output += ' export CX_MODEL_REASONING=your-model\n';
|
|
162
|
+
output += ' export CX_MODEL_STANDARD=your-model\n';
|
|
163
|
+
output += ' export CX_MODEL_FAST=your-model\n';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return output;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Backwards compatibility
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Legacy wrapper for code that expects the old interface.
|
|
175
|
+
* New code should use resolveModelTiers() directly.
|
|
176
|
+
*/
|
|
177
|
+
export function readCurrentModels(envPath, registryModels = {}) {
|
|
178
|
+
// Maintains backwards compatibility with existing code
|
|
179
|
+
// while using the new resolution logic internally
|
|
180
|
+
|
|
181
|
+
const env = {};
|
|
182
|
+
|
|
183
|
+
// Load from env file if provided
|
|
184
|
+
if (envPath && fs.existsSync(envPath)) {
|
|
185
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
186
|
+
for (const line of content.split('\n')) {
|
|
187
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
188
|
+
if (match) {
|
|
189
|
+
env[match[1].trim()] = match[2].trim();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Merge with process.env
|
|
195
|
+
Object.assign(env, process.env);
|
|
196
|
+
|
|
197
|
+
const resolved = resolveModelTiers({ env });
|
|
198
|
+
|
|
199
|
+
// Return in legacy format
|
|
200
|
+
return {
|
|
201
|
+
reasoning: resolved.models.reasoning,
|
|
202
|
+
standard: resolved.models.standard,
|
|
203
|
+
fast: resolved.models.fast,
|
|
204
|
+
// Legacy code expects these fields
|
|
205
|
+
_resolved: resolved,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function setTierModel(tier, model, envPath) {
|
|
210
|
+
// Simplified interface for setting a model
|
|
211
|
+
// In practice, this should update the user config file
|
|
212
|
+
|
|
213
|
+
if (!['reasoning', 'standard', 'fast'].includes(tier)) {
|
|
214
|
+
throw new Error(`Invalid tier: ${tier}. Must be reasoning, standard, or fast.`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const envKey = `${ENV_PREFIX}_${tier.toUpperCase()}`;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
tier,
|
|
222
|
+
model,
|
|
223
|
+
envKey,
|
|
224
|
+
instruction: `Add to your shell profile: export ${envKey}="${model}"`,
|
|
225
|
+
};
|
|
226
|
+
}
|