@geraldmaron/construct 1.1.1 → 1.2.1
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 +30 -19
- package/bin/construct +487 -87
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +258 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +1 -1
- package/lib/auto-docs.mjs +90 -19
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +246 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +146 -15
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/intake-policy.mjs +209 -0
- package/lib/config/project-config.mjs +11 -2
- package/lib/config/schema.mjs +86 -6
- package/lib/config/source-targets.mjs +311 -0
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/document-export.mjs +384 -38
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +4 -3
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox.mjs +6 -10
- package/lib/embed/worker.mjs +4 -3
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/export-branding.mjs +32 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +18 -16
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +50 -119
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/intake-config.mjs +52 -105
- package/lib/intake/legacy-paths.mjs +5 -0
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +103 -10
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +373 -92
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/playwright-demo.mjs +254 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +259 -9
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +3 -2
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +28 -14
- package/personas/construct.md +2 -2
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +41 -28
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +7 -2
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +489 -0
- package/specialists/artifact-manifest.schema.json +83 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-reference.docx +0 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +8 -8
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
package/lib/model-router.mjs
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { spawnSync } from "child_process";
|
|
27
|
+
import { hasAnySecret } from "./providers/secret-resolver.mjs";
|
|
28
|
+
import { isOllamaModelInstalled, toOllamaNativeModelId } from "./ollama/installed-models.mjs";
|
|
29
|
+
import { readRawFromOpenCodeProvider } from "./providers/credential-sources.mjs";
|
|
27
30
|
import { findOpenCodeConfigPath } from "./opencode-config.mjs";
|
|
28
31
|
import { isLocalModel } from "./mcp/tool-budget.mjs";
|
|
29
32
|
import {
|
|
@@ -33,6 +36,11 @@ import {
|
|
|
33
36
|
selectForTier,
|
|
34
37
|
topForTier,
|
|
35
38
|
} from "./model-free-selector.mjs";
|
|
39
|
+
import {
|
|
40
|
+
applyModelVisibilityFilter,
|
|
41
|
+
loadModelsCatalogContext,
|
|
42
|
+
mergeLiveModelsIntoProviders,
|
|
43
|
+
} from "./models/catalog.mjs";
|
|
36
44
|
|
|
37
45
|
function uniqueStrings(values = []) {
|
|
38
46
|
return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
|
|
@@ -100,6 +108,10 @@ function inferSmallModelProfile(selectedModel) {
|
|
|
100
108
|
return false;
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
// Temporary compatibility fallback (construct-6zga.1.8): the ExecutionCapabilityProfile
|
|
112
|
+
// records this profile id as source 'compatibility_fallback'; specialist composition
|
|
113
|
+
// consumes the profile, not this function directly.
|
|
114
|
+
|
|
103
115
|
export function resolveModelOperatingProfile({
|
|
104
116
|
envValues = {},
|
|
105
117
|
selectedModel = null,
|
|
@@ -118,6 +130,10 @@ export function resolveModelOperatingProfile({
|
|
|
118
130
|
// comply with — floor (must-keep only), mid, or full. A COLLAPSED probe verdict forces
|
|
119
131
|
// floor; otherwise local size estimates the tier and any cloud model gets the full
|
|
120
132
|
// persona, so cloud configs are never slimmed.
|
|
133
|
+
//
|
|
134
|
+
// Temporary compatibility fallback (construct-6zga.1.8): this name/size heuristic is
|
|
135
|
+
// the fallback the ExecutionCapabilityProfile records as source 'compatibility_fallback'
|
|
136
|
+
// when no measured evidence exists. Consumers read the profile, not this directly.
|
|
121
137
|
|
|
122
138
|
export function resolveCapabilityTier({ model, verdict = null } = {}) {
|
|
123
139
|
if (!isLocalModel(model)) return 'full';
|
|
@@ -196,12 +212,12 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
196
212
|
resolve: ({ reasoning, standard, fast }) => ({
|
|
197
213
|
reasoning: reasoning ?? "openrouter/google/gemini-2.5-pro",
|
|
198
214
|
standard: standard ?? "openrouter/google/gemini-2.0-flash-001",
|
|
199
|
-
fast: fast ?? "openrouter/google/
|
|
215
|
+
fast: fast ?? "openrouter/google/gemini-2.0-flash-001",
|
|
200
216
|
}),
|
|
201
217
|
options: {
|
|
202
218
|
reasoning: ["openrouter/google/gemini-2.5-pro", "openrouter/google/gemini-2.5-flash"],
|
|
203
219
|
standard: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemini-2.5-flash"],
|
|
204
|
-
fast: ["openrouter/google/
|
|
220
|
+
fast: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemma-4-26b-a4b-it:free"],
|
|
205
221
|
},
|
|
206
222
|
},
|
|
207
223
|
{
|
|
@@ -239,14 +255,14 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
239
255
|
label: 'GitHub Copilot',
|
|
240
256
|
test: (modelId) => /^github-copilot\//.test(modelId),
|
|
241
257
|
resolve: ({ reasoning, standard, fast }) => ({
|
|
242
|
-
reasoning: reasoning ?? "github-copilot/gpt-5.
|
|
243
|
-
standard: standard ?? "github-copilot/gpt-5.
|
|
244
|
-
fast: fast ?? "github-copilot/gpt-5.
|
|
258
|
+
reasoning: reasoning ?? "github-copilot/gpt-5.5",
|
|
259
|
+
standard: standard ?? "github-copilot/gpt-5.4",
|
|
260
|
+
fast: fast ?? "github-copilot/gpt-5.4-mini",
|
|
245
261
|
}),
|
|
246
262
|
options: {
|
|
247
|
-
reasoning: ["github-copilot/gpt-5.
|
|
248
|
-
standard: ["github-copilot/gpt-5.
|
|
249
|
-
fast: ["github-copilot/gpt-5.
|
|
263
|
+
reasoning: ["github-copilot/gpt-5.5", "github-copilot/gpt-5.4", "github-copilot/claude-opus-4.8"],
|
|
264
|
+
standard: ["github-copilot/gpt-5.4", "github-copilot/gpt-4o", "github-copilot/claude-sonnet-4.6"],
|
|
265
|
+
fast: ["github-copilot/gpt-5.4-mini", "github-copilot/gpt-4o-mini"],
|
|
250
266
|
},
|
|
251
267
|
},
|
|
252
268
|
{
|
|
@@ -282,6 +298,21 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
282
298
|
fast: ["openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/meta-llama/llama-3.3-70b-instruct"],
|
|
283
299
|
},
|
|
284
300
|
},
|
|
301
|
+
{
|
|
302
|
+
id: 'openrouter',
|
|
303
|
+
label: 'OpenRouter (general)',
|
|
304
|
+
test: (modelId) => /^openrouter\//.test(modelId),
|
|
305
|
+
resolve: ({ reasoning, standard, fast }) => ({
|
|
306
|
+
reasoning: reasoning ?? "openrouter/qwen/qwen3-coder",
|
|
307
|
+
standard: standard ?? "openrouter/qwen/qwen3-coder:free",
|
|
308
|
+
fast: fast ?? "openrouter/qwen/qwen3-coder:free",
|
|
309
|
+
}),
|
|
310
|
+
options: {
|
|
311
|
+
reasoning: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder", "openrouter/deepseek/deepseek-r1"],
|
|
312
|
+
standard: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/google/gemini-2.0-flash-001"],
|
|
313
|
+
fast: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/meta-llama/llama-3.3-70b-instruct:free"],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
285
316
|
{
|
|
286
317
|
id: 'ollama',
|
|
287
318
|
label: 'Ollama (local)',
|
|
@@ -335,15 +366,46 @@ const PROVIDER_ENV_MAP = {
|
|
|
335
366
|
'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
336
367
|
'openai': ['OPENAI_API_KEY'],
|
|
337
368
|
'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
369
|
+
'openrouter': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
338
370
|
'ollama': ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
|
|
339
371
|
'local': ['LOCAL_LLM_BASE_URL'],
|
|
340
372
|
};
|
|
341
373
|
|
|
374
|
+
// A stored device-flow Copilot credential — Construct's own auth store or the
|
|
375
|
+
// standard ~/.config/github-copilot store other tools share — means Copilot is
|
|
376
|
+
// usable without a gh CLI session. Detection stays sync, file-only, and never
|
|
377
|
+
// performs the token exchange; copilot-auth.mjs owns read/write/refresh.
|
|
378
|
+
|
|
379
|
+
function hasCopilotCredential() {
|
|
380
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
381
|
+
if (!home) return false;
|
|
382
|
+
const candidates = [
|
|
383
|
+
path.join(home, '.construct', 'auth', 'github-copilot.json'),
|
|
384
|
+
path.join(home, '.config', 'github-copilot', 'apps.json'),
|
|
385
|
+
path.join(home, '.config', 'github-copilot', 'hosts.json'),
|
|
386
|
+
];
|
|
387
|
+
for (const file of candidates) {
|
|
388
|
+
try {
|
|
389
|
+
if (!fs.existsSync(file)) continue;
|
|
390
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
391
|
+
if (file.endsWith('github-copilot.json')) {
|
|
392
|
+
if (data && (data.oauth_token || data.token || data.refresh_token || data.refresh)) return true;
|
|
393
|
+
} else {
|
|
394
|
+
for (const entry of Object.values(data || {})) {
|
|
395
|
+
if (entry && (entry.oauth_token || entry.token)) return true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
} catch { /* unreadable or non-JSON is not a usable credential */ }
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
342
403
|
/**
|
|
343
404
|
* Check if a provider is configured by checking env vars and CLI tools.
|
|
344
|
-
* Uses the passed env,
|
|
405
|
+
* Uses the passed env, dotenv files, shell rc, then the Copilot credential store
|
|
406
|
+
* or gh CLI for Copilot. A stored op:// reference counts as configured.
|
|
345
407
|
*/
|
|
346
|
-
function isProviderConfigured(familyId, env) {
|
|
408
|
+
function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
|
|
347
409
|
const varNames = PROVIDER_ENV_MAP[familyId];
|
|
348
410
|
if (!varNames?.length) return false;
|
|
349
411
|
|
|
@@ -359,71 +421,31 @@ function isProviderConfigured(familyId, env) {
|
|
|
359
421
|
} catch { /* not available */ }
|
|
360
422
|
}
|
|
361
423
|
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
});
|
|
367
|
-
if (anyInEnv) return true;
|
|
424
|
+
// Any credential source counts — env, ~/.construct/config.env, ~/.env, the
|
|
425
|
+
// project .env, and shell rc exports — and a stored 1Password op:// reference
|
|
426
|
+
// counts as configured because the resolver reads it lazily at call time.
|
|
427
|
+
if (hasAnySecret(varNames, { env, allowAmbient })) return true;
|
|
368
428
|
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
372
|
-
const cfgPath = path.join(homeDir, '.construct', 'config.env');
|
|
373
|
-
if (fs.existsSync(cfgPath)) {
|
|
374
|
-
const content = fs.readFileSync(cfgPath, 'utf8');
|
|
375
|
-
const anyInConfig = varNames.some(name => {
|
|
376
|
-
const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
|
|
377
|
-
const m = content.match(re);
|
|
378
|
-
return m && m[1] && m[1].length > 0;
|
|
379
|
-
});
|
|
380
|
-
if (anyInConfig) return true;
|
|
381
|
-
}
|
|
382
|
-
} catch { /* skip */ }
|
|
383
|
-
|
|
384
|
-
// GitHub Copilot: detect via gh CLI auth status
|
|
429
|
+
// GitHub Copilot authenticates via OAuth rather than an API key: a stored
|
|
430
|
+
// device-flow credential, or a gh CLI session as a fallback, both count.
|
|
385
431
|
if (familyId === 'github-copilot') {
|
|
432
|
+
if (hasCopilotCredential()) return true;
|
|
386
433
|
try {
|
|
387
434
|
const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
|
|
388
435
|
return r.status === 0;
|
|
389
436
|
} catch { return false; }
|
|
390
437
|
}
|
|
391
438
|
|
|
392
|
-
// Check ~/.env
|
|
393
|
-
try {
|
|
394
|
-
const homeEnv = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.env');
|
|
395
|
-
if (fs.existsSync(homeEnv)) {
|
|
396
|
-
const content = fs.readFileSync(homeEnv, 'utf8');
|
|
397
|
-
const anyInHomeEnv = varNames.some(name => {
|
|
398
|
-
const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
|
|
399
|
-
const m = content.match(re);
|
|
400
|
-
return m && m[1] && m[1].length > 0;
|
|
401
|
-
});
|
|
402
|
-
if (anyInHomeEnv) return true;
|
|
403
|
-
}
|
|
404
|
-
} catch { /* skip */ }
|
|
405
|
-
|
|
406
|
-
// Check shell rc files for op:// refs (1Password CLI integration)
|
|
407
|
-
try {
|
|
408
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
409
|
-
const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map(f => path.join(homeDir, f));
|
|
410
|
-
for (const rcPath of shellFiles) {
|
|
411
|
-
if (!fs.existsSync(rcPath)) continue;
|
|
412
|
-
const content = fs.readFileSync(rcPath, 'utf8');
|
|
413
|
-
const anyInRc = varNames.some(name => {
|
|
414
|
-
// Direct export (with or without command substitution)
|
|
415
|
-
const re = new RegExp(`^\\s*export\\s+${name}=`, 'm');
|
|
416
|
-
return re.test(content);
|
|
417
|
-
});
|
|
418
|
-
if (anyInRc) return true;
|
|
419
|
-
}
|
|
420
|
-
} catch { /* skip */ }
|
|
421
|
-
|
|
422
439
|
return false;
|
|
423
440
|
}
|
|
424
441
|
|
|
425
|
-
export function getProviderModelCatalog({
|
|
426
|
-
|
|
442
|
+
export function getProviderModelCatalog({
|
|
443
|
+
env = process.env,
|
|
444
|
+
cwd = process.cwd(),
|
|
445
|
+
activeModelId = null,
|
|
446
|
+
registryModels: registryModelsOverride = null,
|
|
447
|
+
} = {}) {
|
|
448
|
+
const baseProviders = PROVIDER_FAMILY_TIERS.map((family) => {
|
|
427
449
|
const tiers = family.resolve({});
|
|
428
450
|
const options = {
|
|
429
451
|
reasoning: uniqueStrings([...(family.options?.reasoning ?? []), tiers.reasoning]),
|
|
@@ -442,13 +464,195 @@ export function getProviderModelCatalog({ env = process.env } = {}) {
|
|
|
442
464
|
};
|
|
443
465
|
});
|
|
444
466
|
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
467
|
+
const { modelsConfig, registryModels, liveModels } = loadModelsCatalogContext({ cwd, env });
|
|
468
|
+
const mergedProviders = mergeLiveModelsIntoProviders(
|
|
469
|
+
baseProviders,
|
|
470
|
+
liveModels,
|
|
471
|
+
{ maxLiveFree: modelsConfig.catalog.maxLiveFree },
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const tierOptionsRaw = {
|
|
475
|
+
reasoning: uniqueStrings(mergedProviders.flatMap((provider) => provider.options.reasoning)),
|
|
476
|
+
standard: uniqueStrings(mergedProviders.flatMap((provider) => provider.options.standard)),
|
|
477
|
+
fast: uniqueStrings(mergedProviders.flatMap((provider) => provider.options.fast)),
|
|
449
478
|
};
|
|
450
479
|
|
|
451
|
-
return
|
|
480
|
+
return applyModelVisibilityFilter(
|
|
481
|
+
{ providers: mergedProviders, tierOptions: tierOptionsRaw },
|
|
482
|
+
{
|
|
483
|
+
visibility: modelsConfig.visibility,
|
|
484
|
+
registryModels: registryModelsOverride ?? registryModels,
|
|
485
|
+
activeModelId,
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Provider families with open-ended model catalogs: credential presence is the
|
|
491
|
+
// gate; static option lists are hints, not exhaustive allowlists.
|
|
492
|
+
|
|
493
|
+
const LENIENT_MODEL_FAMILIES = new Set([
|
|
494
|
+
'openrouter-anthropic',
|
|
495
|
+
'openrouter-google',
|
|
496
|
+
'openrouter-deepseek',
|
|
497
|
+
'openrouter-qwen',
|
|
498
|
+
'openrouter-llama',
|
|
499
|
+
'openrouter',
|
|
500
|
+
'openai',
|
|
501
|
+
'anthropic',
|
|
502
|
+
'ollama',
|
|
503
|
+
'local',
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Check whether a model id can be used on this machine right now: the provider
|
|
508
|
+
* family must match, credentials must be present (including op:// and Copilot
|
|
509
|
+
* auth-store), and for fixed-catalog families (e.g. github-copilot) the id must
|
|
510
|
+
* appear in the shipped option list or tier defaults — not a stale CX_MODEL pin.
|
|
511
|
+
*/
|
|
512
|
+
export function isChatModelAvailable(modelId, { env = process.env, excludeFamilies = [] } = {}) {
|
|
513
|
+
if (!modelId || typeof modelId !== 'string') {
|
|
514
|
+
return { ok: false, reason: 'missing', modelId: modelId || null };
|
|
515
|
+
}
|
|
516
|
+
const family = matchProviderFamily(modelId);
|
|
517
|
+
if (!family) {
|
|
518
|
+
return { ok: false, reason: 'unknown_family', modelId };
|
|
519
|
+
}
|
|
520
|
+
if (excludeFamilies.includes(family.id)) {
|
|
521
|
+
return { ok: false, reason: 'excluded', modelId, provider: family.id };
|
|
522
|
+
}
|
|
523
|
+
if (!isProviderConfigured(family.id, env)) {
|
|
524
|
+
return { ok: false, reason: 'provider_not_configured', modelId, provider: family.id };
|
|
525
|
+
}
|
|
526
|
+
if (family.id === 'ollama') {
|
|
527
|
+
const nativeModel = toOllamaNativeModelId(modelId);
|
|
528
|
+
const installed = isOllamaModelInstalled(modelId, { env });
|
|
529
|
+
if (installed === true) {
|
|
530
|
+
return { ok: true, modelId, provider: family.id };
|
|
531
|
+
}
|
|
532
|
+
if (installed === false) {
|
|
533
|
+
return {
|
|
534
|
+
ok: false,
|
|
535
|
+
reason: 'model_not_pulled',
|
|
536
|
+
modelId,
|
|
537
|
+
provider: family.id,
|
|
538
|
+
nativeModel,
|
|
539
|
+
pullCommand: `ollama pull ${nativeModel}`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return { ok: true, modelId, provider: family.id };
|
|
543
|
+
}
|
|
544
|
+
if (LENIENT_MODEL_FAMILIES.has(family.id)) {
|
|
545
|
+
return { ok: true, modelId, provider: family.id };
|
|
546
|
+
}
|
|
547
|
+
const { providers } = getProviderModelCatalog({ env });
|
|
548
|
+
const provider = providers.find((p) => p.id === family.id);
|
|
549
|
+
if (!provider) {
|
|
550
|
+
return { ok: false, reason: 'unknown_family', modelId };
|
|
551
|
+
}
|
|
552
|
+
const known = uniqueStrings([
|
|
553
|
+
...(provider.options?.reasoning ?? []),
|
|
554
|
+
...(provider.options?.standard ?? []),
|
|
555
|
+
...(provider.options?.fast ?? []),
|
|
556
|
+
provider.tiers?.reasoning,
|
|
557
|
+
provider.tiers?.standard,
|
|
558
|
+
provider.tiers?.fast,
|
|
559
|
+
]);
|
|
560
|
+
if (known.includes(modelId)) {
|
|
561
|
+
return { ok: true, modelId, provider: family.id };
|
|
562
|
+
}
|
|
563
|
+
return { ok: false, reason: 'model_not_available', modelId, provider: family.id };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function availabilityNotice(rejected) {
|
|
567
|
+
if (!rejected?.modelId) return null;
|
|
568
|
+
const label = rejected.modelId;
|
|
569
|
+
if (rejected.reason === 'provider_not_configured') {
|
|
570
|
+
const family = rejected.provider || '';
|
|
571
|
+
if (family.startsWith('openrouter')) {
|
|
572
|
+
return `Saved ${label} — OpenRouter not configured. Set OPENROUTER_API_KEY in ~/.construct/config.env or run \`construct creds set openrouter\`.`;
|
|
573
|
+
}
|
|
574
|
+
return `Saved ${label} — provider not configured.`;
|
|
575
|
+
}
|
|
576
|
+
if (rejected.reason === 'model_not_available') {
|
|
577
|
+
return `Pinned ${label} — not available on your account.`;
|
|
578
|
+
}
|
|
579
|
+
if (rejected.reason === 'model_not_pulled') {
|
|
580
|
+
const native = rejected.nativeModel || toOllamaNativeModelId(label);
|
|
581
|
+
return `Pinned ${label} — not installed locally. Run \`ollama pull ${native}\` or \`construct ollama pull ${native}\`.`;
|
|
582
|
+
}
|
|
583
|
+
if (rejected.reason === 'unknown_family') {
|
|
584
|
+
return `Pinned ${label} — unrecognized model id.`;
|
|
585
|
+
}
|
|
586
|
+
return `Pinned ${label} — unavailable.`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function recommendTierModel(tier, { env = process.env, excludeFamilies = [] } = {}) {
|
|
590
|
+
const { providers } = getProviderModelCatalog({ env });
|
|
591
|
+
for (const provider of providers) {
|
|
592
|
+
if (!provider.configured || excludeFamilies.includes(provider.id)) continue;
|
|
593
|
+
const candidates = uniqueStrings([
|
|
594
|
+
provider.tiers?.[tier],
|
|
595
|
+
...(provider.options?.[tier] ?? []),
|
|
596
|
+
]);
|
|
597
|
+
for (const id of candidates) {
|
|
598
|
+
const check = isChatModelAvailable(id, { env, excludeFamilies });
|
|
599
|
+
if (check.ok) return { id, provider: provider.label, tier };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Resolve the chat model with validation: explicit request and CX_MODEL pins
|
|
607
|
+
* are preferences, not blind truth. Stale or unconfigured pins fall through to
|
|
608
|
+
* the next configured provider for the tier, with a human-readable notice.
|
|
609
|
+
*/
|
|
610
|
+
export function resolveValidatedChatModel({ env = process.env, requested = null, excludeFamilies = [] } = {}) {
|
|
611
|
+
const rejections = [];
|
|
612
|
+
|
|
613
|
+
if (requested) {
|
|
614
|
+
const check = isChatModelAvailable(requested, { env, excludeFamilies });
|
|
615
|
+
if (check.ok) {
|
|
616
|
+
return { id: requested, source: 'explicit', notice: null, rejected: [] };
|
|
617
|
+
}
|
|
618
|
+
rejections.push(check);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const pinOrder = [
|
|
622
|
+
['standard', env.CX_MODEL_STANDARD],
|
|
623
|
+
['reasoning', env.CX_MODEL_REASONING],
|
|
624
|
+
['fast', env.CX_MODEL_FAST],
|
|
625
|
+
];
|
|
626
|
+
for (const [tier, pin] of pinOrder) {
|
|
627
|
+
if (!pin) continue;
|
|
628
|
+
const check = isChatModelAvailable(pin, { env, excludeFamilies });
|
|
629
|
+
if (check.ok) {
|
|
630
|
+
const notice = rejections.length
|
|
631
|
+
? `${availabilityNotice(rejections[0])} Using ${pin}.`
|
|
632
|
+
: null;
|
|
633
|
+
return { id: pin, source: 'pin', tier, notice, rejected: rejections };
|
|
634
|
+
}
|
|
635
|
+
rejections.push(check);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const standard = recommendTierModel('standard', { env, excludeFamilies });
|
|
639
|
+
if (standard) {
|
|
640
|
+
const notice = rejections.length
|
|
641
|
+
? `${availabilityNotice(rejections[0])} Using ${standard.id} (${standard.provider}).`
|
|
642
|
+
: null;
|
|
643
|
+
return { id: standard.id, source: 'recommended', tier: 'standard', notice, rejected: rejections };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const anyTier = recommendTierModel('reasoning', { env, excludeFamilies }) || recommendTierModel('fast', { env, excludeFamilies });
|
|
647
|
+
if (anyTier) {
|
|
648
|
+
const notice = rejections.length
|
|
649
|
+
? `${availabilityNotice(rejections[0])} Using ${anyTier.id} (${anyTier.provider}).`
|
|
650
|
+
: null;
|
|
651
|
+
return { id: anyTier.id, source: 'recommended', tier: anyTier.tier, notice, rejected: rejections };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const notice = rejections.length ? availabilityNotice(rejections[0]) : null;
|
|
655
|
+
return { id: null, source: null, notice, rejected: rejections };
|
|
452
656
|
}
|
|
453
657
|
|
|
454
658
|
/**
|
|
@@ -473,13 +677,10 @@ export function resolveTiersForPrimary(primaryModelId) {
|
|
|
473
677
|
// callers (embedded model resolution, capability discovery) stay synchronous
|
|
474
678
|
// and deterministic, and never expose a key value, only its presence.
|
|
475
679
|
|
|
476
|
-
function familyDescriptor(family, env) {
|
|
680
|
+
function familyDescriptor(family, env, { allowAmbient = true } = {}) {
|
|
477
681
|
const requiresEnv = PROVIDER_ENV_MAP[family.id] || (Array.isArray(family.requiresEnv) ? family.requiresEnv : []);
|
|
478
682
|
const local = family.local === true;
|
|
479
|
-
const configured =
|
|
480
|
-
const value = env?.[key];
|
|
481
|
-
return typeof value === "string" && value.length > 0;
|
|
482
|
-
});
|
|
683
|
+
const configured = isProviderConfigured(family.id, env, { allowAmbient });
|
|
483
684
|
return {
|
|
484
685
|
id: family.id,
|
|
485
686
|
label: family.label,
|
|
@@ -495,18 +696,18 @@ function familyDescriptor(family, env) {
|
|
|
495
696
|
* confirm credentials and whether they are present. Returns null when no family
|
|
496
697
|
* matches.
|
|
497
698
|
*/
|
|
498
|
-
export function describeModelFamily(modelId, { env = process.env } = {}) {
|
|
699
|
+
export function describeModelFamily(modelId, { env = process.env, allowAmbient = true } = {}) {
|
|
499
700
|
const family = matchProviderFamily(modelId);
|
|
500
701
|
if (!family) return null;
|
|
501
|
-
return familyDescriptor(family, env);
|
|
702
|
+
return familyDescriptor(family, env, { allowAmbient });
|
|
502
703
|
}
|
|
503
704
|
|
|
504
705
|
/**
|
|
505
706
|
* Describe every provider family. Resolves a host-supplied provider id and
|
|
506
707
|
* enumerates model availability for the embedded contract layer.
|
|
507
708
|
*/
|
|
508
|
-
export function listModelFamilies({ env = process.env } = {}) {
|
|
509
|
-
return PROVIDER_FAMILY_TIERS.map((family) => familyDescriptor(family, env));
|
|
709
|
+
export function listModelFamilies({ env = process.env, allowAmbient = true } = {}) {
|
|
710
|
+
return PROVIDER_FAMILY_TIERS.map((family) => familyDescriptor(family, env, { allowAmbient }));
|
|
510
711
|
}
|
|
511
712
|
|
|
512
713
|
/**
|
|
@@ -515,17 +716,7 @@ export function listModelFamilies({ env = process.env } = {}) {
|
|
|
515
716
|
export function readOpenRouterApiKeyFromOpenCodeConfig(
|
|
516
717
|
configPath = findOpenCodeConfigPath(),
|
|
517
718
|
) {
|
|
518
|
-
|
|
519
|
-
if (!fs.existsSync(configPath)) return "";
|
|
520
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
521
|
-
const auth = config?.provider?.openrouter?.options?.headers?.Authorization;
|
|
522
|
-
if (typeof auth !== "string") return "";
|
|
523
|
-
const value = auth.replace(/^Bearer\s+/i, "").trim();
|
|
524
|
-
if (!value || value.includes("__OPENROUTER_API_KEY__")) return "";
|
|
525
|
-
return value;
|
|
526
|
-
} catch {
|
|
527
|
-
return "";
|
|
528
|
-
}
|
|
719
|
+
return readRawFromOpenCodeProvider('openrouter', configPath) || "";
|
|
529
720
|
}
|
|
530
721
|
|
|
531
722
|
// --- Internal helpers ---
|
|
@@ -571,9 +762,18 @@ function getRegistryDefaults(registryModels = {}) {
|
|
|
571
762
|
|
|
572
763
|
function normalizeEnvAssignments(envValues = {}) {
|
|
573
764
|
return {
|
|
574
|
-
reasoning: envValues.reasoning
|
|
575
|
-
|
|
576
|
-
|
|
765
|
+
reasoning: envValues.reasoning
|
|
766
|
+
?? envValues.CX_MODEL_REASONING
|
|
767
|
+
?? envValues.CONSTRUCT_MODEL_REASONING
|
|
768
|
+
?? null,
|
|
769
|
+
standard: envValues.standard
|
|
770
|
+
?? envValues.CX_MODEL_STANDARD
|
|
771
|
+
?? envValues.CONSTRUCT_MODEL_STANDARD
|
|
772
|
+
?? null,
|
|
773
|
+
fast: envValues.fast
|
|
774
|
+
?? envValues.CX_MODEL_FAST
|
|
775
|
+
?? envValues.CONSTRUCT_MODEL_FAST
|
|
776
|
+
?? null,
|
|
577
777
|
};
|
|
578
778
|
}
|
|
579
779
|
|
|
@@ -914,6 +1114,87 @@ export function setModelWithTierInference(envPath, tier, modelId, registryModels
|
|
|
914
1114
|
return resolved;
|
|
915
1115
|
}
|
|
916
1116
|
|
|
1117
|
+
export function resolveModelTiers(options = {}) {
|
|
1118
|
+
const {
|
|
1119
|
+
env = process.env,
|
|
1120
|
+
registryPath = null,
|
|
1121
|
+
strict = false,
|
|
1122
|
+
} = options;
|
|
1123
|
+
|
|
1124
|
+
let registryModels = {};
|
|
1125
|
+
if (registryPath && fs.existsSync(registryPath)) {
|
|
1126
|
+
try {
|
|
1127
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
1128
|
+
registryModels = registry.models ?? {};
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
return {
|
|
1131
|
+
models: { reasoning: null, standard: null, fast: null },
|
|
1132
|
+
sources: { reasoning: 'not configured', standard: 'not configured', fast: 'not configured' },
|
|
1133
|
+
configured: 0,
|
|
1134
|
+
complete: false,
|
|
1135
|
+
errors: [`Failed to read registry: ${err.message}`],
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const tiers = resolveTierAssignments(normalizeEnvAssignments(env), registryModels);
|
|
1141
|
+
const models = {};
|
|
1142
|
+
const sources = {};
|
|
1143
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
1144
|
+
models[tier] = tiers[tier].model;
|
|
1145
|
+
sources[tier] = tiers[tier].source;
|
|
1146
|
+
}
|
|
1147
|
+
const configured = Object.values(models).filter(Boolean).length;
|
|
1148
|
+
const errors = strict && configured < 3
|
|
1149
|
+
? [`Missing configuration for tiers: ${['reasoning', 'standard', 'fast'].filter((t) => !models[t]).join(', ')}`]
|
|
1150
|
+
: null;
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
models,
|
|
1154
|
+
sources,
|
|
1155
|
+
configured,
|
|
1156
|
+
complete: configured === 3,
|
|
1157
|
+
errors,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export function getModelForTier(tier, options = {}) {
|
|
1162
|
+
return resolveModelTiers(options).models[tier];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
export function getModelSource(tier, options = {}) {
|
|
1166
|
+
return resolveModelTiers(options).sources[tier] || 'unknown';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export function validateModelTiers(options = {}) {
|
|
1170
|
+
const resolved = resolveModelTiers({ ...options, strict: true });
|
|
1171
|
+
const unconfigured = Object.entries(resolved.sources)
|
|
1172
|
+
.filter(([, source]) => source === 'not configured')
|
|
1173
|
+
.map(([tier]) => tier);
|
|
1174
|
+
|
|
1175
|
+
return {
|
|
1176
|
+
valid: resolved.complete && !resolved.errors,
|
|
1177
|
+
errors: resolved.errors,
|
|
1178
|
+
warnings: unconfigured.length > 0
|
|
1179
|
+
? [`Unconfigured tier${unconfigured.length === 1 ? '' : 's'}: ${unconfigured.join(', ')}. Run 'construct models --apply' or set CX_MODEL_<TIER>.`]
|
|
1180
|
+
: [],
|
|
1181
|
+
resolution: resolved,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export function formatModelStatus(options = {}) {
|
|
1186
|
+
const resolved = resolveModelTiers(options);
|
|
1187
|
+
let output = 'Model Configuration:\n\n';
|
|
1188
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
1189
|
+
const model = resolved.models[tier];
|
|
1190
|
+
const source = resolved.sources[tier];
|
|
1191
|
+
const icon = source === 'not configured' ? '!' : 'ok';
|
|
1192
|
+
output += `${icon} ${tier.padEnd(10)} ${model ?? '(not configured)'}\n`;
|
|
1193
|
+
output += ` Source: ${source}\n\n`;
|
|
1194
|
+
}
|
|
1195
|
+
return output;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
917
1198
|
export function readCurrentModels(envPath, registryModels = {}) {
|
|
918
1199
|
const envValues = arguments.length > 2 ? arguments[2] : {};
|
|
919
1200
|
const fileAssignments = readEnvAssignments(envPath);
|