@geraldmaron/construct 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -18
- package/bin/construct +392 -91
- 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 +112 -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/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +77 -10
- package/lib/auto-docs.mjs +87 -18
- 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 +245 -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 +232 -11
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- 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 +703 -0
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +302 -0
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +4 -2
- package/lib/doctor/project-adapters.mjs +44 -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/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +88 -0
- package/lib/document-ingest.mjs +32 -18
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/semantic.mjs +5 -2
- package/lib/embed/worker.mjs +2 -2
- 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/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/handoffs/cleanup.mjs +1 -1
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/audit-trail.mjs +14 -28
- 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 +4 -5
- 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 +27 -97
- 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/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- 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 +84 -9
- 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/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 +425 -93
- 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/persona-sections.mjs +66 -0
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +9 -4
- 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 +229 -8
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +61 -9
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +102 -2
- 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/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- 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 +29 -13
- package/personas/construct.md +13 -13
- 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 +187 -60
- 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 +8 -3
- 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 +480 -0
- package/specialists/artifact-manifest.schema.json +53 -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-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 +10 -10
- 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/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
package/lib/model-router.mjs
CHANGED
|
@@ -24,7 +24,11 @@
|
|
|
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";
|
|
31
|
+
import { isLocalModel } from "./mcp/tool-budget.mjs";
|
|
28
32
|
import {
|
|
29
33
|
isFreeModel,
|
|
30
34
|
pollFreeModels,
|
|
@@ -32,6 +36,11 @@ import {
|
|
|
32
36
|
selectForTier,
|
|
33
37
|
topForTier,
|
|
34
38
|
} from "./model-free-selector.mjs";
|
|
39
|
+
import {
|
|
40
|
+
applyModelVisibilityFilter,
|
|
41
|
+
loadModelsCatalogContext,
|
|
42
|
+
mergeLiveModelsIntoProviders,
|
|
43
|
+
} from "./models/catalog.mjs";
|
|
35
44
|
|
|
36
45
|
function uniqueStrings(values = []) {
|
|
37
46
|
return [...new Set(values.filter((value) => typeof value === "string" && value.trim()))];
|
|
@@ -79,15 +88,30 @@ function normalizeModelOperatingProfile(value) {
|
|
|
79
88
|
return MODEL_OPERATING_PROFILES[normalized] ? normalized : null;
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
function parseModelSizeB(model) {
|
|
92
|
+
const size = String(model || "").toLowerCase().match(/(?:[:/-])(\d+(?:\.\d+)?)b\b/);
|
|
93
|
+
return size ? parseFloat(size[1]) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
function inferSmallModelProfile(selectedModel) {
|
|
83
97
|
const model = String(selectedModel || '').toLowerCase();
|
|
84
98
|
if (!model) return false;
|
|
85
|
-
|
|
99
|
+
// Match any parameter-count marker (7b, 24b, 30b, 32b, …) rather than a fixed list —
|
|
100
|
+
// the old list silently missed 24b/30b, so Devstral-24B and qwen3-coder-30B resolved
|
|
101
|
+
// to the balanced profile. Treat <=34B local models as small.
|
|
102
|
+
if (/^(ollama|local)\//.test(model)) {
|
|
103
|
+
const size = parseModelSizeB(model);
|
|
104
|
+
if (size !== null && size <= 34) return true;
|
|
105
|
+
}
|
|
86
106
|
if (/^(anthropic|openrouter\/anthropic)\/.*haiku/.test(model)) return true;
|
|
87
107
|
if (/gpt-5\.1-mini|gemma-3|gemma-4|phi3:mini/.test(model)) return true;
|
|
88
108
|
return false;
|
|
89
109
|
}
|
|
90
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
|
+
|
|
91
115
|
export function resolveModelOperatingProfile({
|
|
92
116
|
envValues = {},
|
|
93
117
|
selectedModel = null,
|
|
@@ -100,6 +124,49 @@ export function resolveModelOperatingProfile({
|
|
|
100
124
|
return MODEL_OPERATING_PROFILES.balanced;
|
|
101
125
|
}
|
|
102
126
|
|
|
127
|
+
// Small local models follow large multi-instruction prompts poorly; the binding
|
|
128
|
+
// constraint is instruction-following capacity, not the token window (which the cx32k
|
|
129
|
+
// Modelfile variants already widen). Map a model to the persona section tier it can
|
|
130
|
+
// comply with — floor (must-keep only), mid, or full. A COLLAPSED probe verdict forces
|
|
131
|
+
// floor; otherwise local size estimates the tier and any cloud model gets the full
|
|
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.
|
|
137
|
+
|
|
138
|
+
export function resolveCapabilityTier({ model, verdict = null } = {}) {
|
|
139
|
+
if (!isLocalModel(model)) return 'full';
|
|
140
|
+
if (verdict === 'COLLAPSED') return 'floor';
|
|
141
|
+
const size = parseModelSizeB(model);
|
|
142
|
+
if (size === null) return 'floor';
|
|
143
|
+
if (size >= 24) return 'mid';
|
|
144
|
+
return 'floor';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CODE_MODEL_RE = /coder|codellama|starcoder|deepseek-coder|devstral/i;
|
|
148
|
+
|
|
149
|
+
// Pick the model for the narrow local editor (construct-local) from the DECLARED local
|
|
150
|
+
// inventory rather than the generic fast-tier default (which, for an Ollama family,
|
|
151
|
+
// resolves to a non-code generalist like llama3.2:3b). The editor does bounded code
|
|
152
|
+
// edits, so prefer a code-specialized model in the reliable size band [7,34]B (smallest =
|
|
153
|
+
// cheapest competent editor), then the smallest code model, then the smallest local model
|
|
154
|
+
// of any kind. Candidates should already exclude probe-COLLAPSED models. Returns null only
|
|
155
|
+
// when there are no candidates, so the caller keeps its own fallback.
|
|
156
|
+
|
|
157
|
+
export function selectLocalEditorModel(candidates = []) {
|
|
158
|
+
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
159
|
+
const sized = candidates.map((m) => ({ m, size: parseModelSizeB(m) }));
|
|
160
|
+
const pick = (arr) => {
|
|
161
|
+
const band = arr.filter((x) => x.size !== null && x.size >= 7 && x.size <= 34).sort((a, b) => a.size - b.size);
|
|
162
|
+
if (band.length) return band[0].m;
|
|
163
|
+
const anySized = arr.filter((x) => x.size !== null).sort((a, b) => a.size - b.size);
|
|
164
|
+
return anySized.length ? anySized[0].m : arr[0].m;
|
|
165
|
+
};
|
|
166
|
+
const coders = sized.filter((x) => CODE_MODEL_RE.test(x.m));
|
|
167
|
+
return coders.length ? pick(coders) : pick(sized);
|
|
168
|
+
}
|
|
169
|
+
|
|
103
170
|
/**
|
|
104
171
|
* Provider-family definitions. Each entry contains:
|
|
105
172
|
* - `test`: RegExp that matches provider URLs.
|
|
@@ -145,12 +212,12 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
145
212
|
resolve: ({ reasoning, standard, fast }) => ({
|
|
146
213
|
reasoning: reasoning ?? "openrouter/google/gemini-2.5-pro",
|
|
147
214
|
standard: standard ?? "openrouter/google/gemini-2.0-flash-001",
|
|
148
|
-
fast: fast ?? "openrouter/google/
|
|
215
|
+
fast: fast ?? "openrouter/google/gemini-2.0-flash-001",
|
|
149
216
|
}),
|
|
150
217
|
options: {
|
|
151
218
|
reasoning: ["openrouter/google/gemini-2.5-pro", "openrouter/google/gemini-2.5-flash"],
|
|
152
219
|
standard: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemini-2.5-flash"],
|
|
153
|
-
fast: ["openrouter/google/
|
|
220
|
+
fast: ["openrouter/google/gemini-2.0-flash-001", "openrouter/google/gemma-4-26b-a4b-it:free"],
|
|
154
221
|
},
|
|
155
222
|
},
|
|
156
223
|
{
|
|
@@ -188,14 +255,14 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
188
255
|
label: 'GitHub Copilot',
|
|
189
256
|
test: (modelId) => /^github-copilot\//.test(modelId),
|
|
190
257
|
resolve: ({ reasoning, standard, fast }) => ({
|
|
191
|
-
reasoning: reasoning ?? "github-copilot/gpt-5.
|
|
192
|
-
standard: standard ?? "github-copilot/gpt-5.
|
|
193
|
-
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",
|
|
194
261
|
}),
|
|
195
262
|
options: {
|
|
196
|
-
reasoning: ["github-copilot/gpt-5.
|
|
197
|
-
standard: ["github-copilot/gpt-5.
|
|
198
|
-
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"],
|
|
199
266
|
},
|
|
200
267
|
},
|
|
201
268
|
{
|
|
@@ -231,6 +298,21 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
231
298
|
fast: ["openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/meta-llama/llama-3.3-70b-instruct"],
|
|
232
299
|
},
|
|
233
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
|
+
},
|
|
234
316
|
{
|
|
235
317
|
id: 'ollama',
|
|
236
318
|
label: 'Ollama (local)',
|
|
@@ -284,15 +366,46 @@ const PROVIDER_ENV_MAP = {
|
|
|
284
366
|
'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
285
367
|
'openai': ['OPENAI_API_KEY'],
|
|
286
368
|
'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
369
|
+
'openrouter': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
287
370
|
'ollama': ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
|
|
288
371
|
'local': ['LOCAL_LLM_BASE_URL'],
|
|
289
372
|
};
|
|
290
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
|
+
|
|
291
403
|
/**
|
|
292
404
|
* Check if a provider is configured by checking env vars and CLI tools.
|
|
293
|
-
* 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.
|
|
294
407
|
*/
|
|
295
|
-
function isProviderConfigured(familyId, env) {
|
|
408
|
+
function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
|
|
296
409
|
const varNames = PROVIDER_ENV_MAP[familyId];
|
|
297
410
|
if (!varNames?.length) return false;
|
|
298
411
|
|
|
@@ -308,71 +421,31 @@ function isProviderConfigured(familyId, env) {
|
|
|
308
421
|
} catch { /* not available */ }
|
|
309
422
|
}
|
|
310
423
|
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
});
|
|
316
|
-
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;
|
|
317
428
|
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
321
|
-
const cfgPath = path.join(homeDir, '.construct', 'config.env');
|
|
322
|
-
if (fs.existsSync(cfgPath)) {
|
|
323
|
-
const content = fs.readFileSync(cfgPath, 'utf8');
|
|
324
|
-
const anyInConfig = varNames.some(name => {
|
|
325
|
-
const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
|
|
326
|
-
const m = content.match(re);
|
|
327
|
-
return m && m[1] && m[1].length > 0;
|
|
328
|
-
});
|
|
329
|
-
if (anyInConfig) return true;
|
|
330
|
-
}
|
|
331
|
-
} catch { /* skip */ }
|
|
332
|
-
|
|
333
|
-
// 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.
|
|
334
431
|
if (familyId === 'github-copilot') {
|
|
432
|
+
if (hasCopilotCredential()) return true;
|
|
335
433
|
try {
|
|
336
434
|
const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
|
|
337
435
|
return r.status === 0;
|
|
338
436
|
} catch { return false; }
|
|
339
437
|
}
|
|
340
438
|
|
|
341
|
-
// Check ~/.env
|
|
342
|
-
try {
|
|
343
|
-
const homeEnv = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.env');
|
|
344
|
-
if (fs.existsSync(homeEnv)) {
|
|
345
|
-
const content = fs.readFileSync(homeEnv, 'utf8');
|
|
346
|
-
const anyInHomeEnv = varNames.some(name => {
|
|
347
|
-
const re = new RegExp(`^${name}=["']?(.+?)["']?$`, 'm');
|
|
348
|
-
const m = content.match(re);
|
|
349
|
-
return m && m[1] && m[1].length > 0;
|
|
350
|
-
});
|
|
351
|
-
if (anyInHomeEnv) return true;
|
|
352
|
-
}
|
|
353
|
-
} catch { /* skip */ }
|
|
354
|
-
|
|
355
|
-
// Check shell rc files for op:// refs (1Password CLI integration)
|
|
356
|
-
try {
|
|
357
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
358
|
-
const shellFiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map(f => path.join(homeDir, f));
|
|
359
|
-
for (const rcPath of shellFiles) {
|
|
360
|
-
if (!fs.existsSync(rcPath)) continue;
|
|
361
|
-
const content = fs.readFileSync(rcPath, 'utf8');
|
|
362
|
-
const anyInRc = varNames.some(name => {
|
|
363
|
-
// Direct export (with or without command substitution)
|
|
364
|
-
const re = new RegExp(`^\\s*export\\s+${name}=`, 'm');
|
|
365
|
-
return re.test(content);
|
|
366
|
-
});
|
|
367
|
-
if (anyInRc) return true;
|
|
368
|
-
}
|
|
369
|
-
} catch { /* skip */ }
|
|
370
|
-
|
|
371
439
|
return false;
|
|
372
440
|
}
|
|
373
441
|
|
|
374
|
-
export function getProviderModelCatalog({
|
|
375
|
-
|
|
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) => {
|
|
376
449
|
const tiers = family.resolve({});
|
|
377
450
|
const options = {
|
|
378
451
|
reasoning: uniqueStrings([...(family.options?.reasoning ?? []), tiers.reasoning]),
|
|
@@ -391,13 +464,195 @@ export function getProviderModelCatalog({ env = process.env } = {}) {
|
|
|
391
464
|
};
|
|
392
465
|
});
|
|
393
466
|
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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)),
|
|
398
478
|
};
|
|
399
479
|
|
|
400
|
-
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 };
|
|
401
656
|
}
|
|
402
657
|
|
|
403
658
|
/**
|
|
@@ -422,13 +677,10 @@ export function resolveTiersForPrimary(primaryModelId) {
|
|
|
422
677
|
// callers (embedded model resolution, capability discovery) stay synchronous
|
|
423
678
|
// and deterministic, and never expose a key value, only its presence.
|
|
424
679
|
|
|
425
|
-
function familyDescriptor(family, env) {
|
|
680
|
+
function familyDescriptor(family, env, { allowAmbient = true } = {}) {
|
|
426
681
|
const requiresEnv = PROVIDER_ENV_MAP[family.id] || (Array.isArray(family.requiresEnv) ? family.requiresEnv : []);
|
|
427
682
|
const local = family.local === true;
|
|
428
|
-
const configured =
|
|
429
|
-
const value = env?.[key];
|
|
430
|
-
return typeof value === "string" && value.length > 0;
|
|
431
|
-
});
|
|
683
|
+
const configured = isProviderConfigured(family.id, env, { allowAmbient });
|
|
432
684
|
return {
|
|
433
685
|
id: family.id,
|
|
434
686
|
label: family.label,
|
|
@@ -444,18 +696,18 @@ function familyDescriptor(family, env) {
|
|
|
444
696
|
* confirm credentials and whether they are present. Returns null when no family
|
|
445
697
|
* matches.
|
|
446
698
|
*/
|
|
447
|
-
export function describeModelFamily(modelId, { env = process.env } = {}) {
|
|
699
|
+
export function describeModelFamily(modelId, { env = process.env, allowAmbient = true } = {}) {
|
|
448
700
|
const family = matchProviderFamily(modelId);
|
|
449
701
|
if (!family) return null;
|
|
450
|
-
return familyDescriptor(family, env);
|
|
702
|
+
return familyDescriptor(family, env, { allowAmbient });
|
|
451
703
|
}
|
|
452
704
|
|
|
453
705
|
/**
|
|
454
706
|
* Describe every provider family. Resolves a host-supplied provider id and
|
|
455
707
|
* enumerates model availability for the embedded contract layer.
|
|
456
708
|
*/
|
|
457
|
-
export function listModelFamilies({ env = process.env } = {}) {
|
|
458
|
-
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 }));
|
|
459
711
|
}
|
|
460
712
|
|
|
461
713
|
/**
|
|
@@ -464,17 +716,7 @@ export function listModelFamilies({ env = process.env } = {}) {
|
|
|
464
716
|
export function readOpenRouterApiKeyFromOpenCodeConfig(
|
|
465
717
|
configPath = findOpenCodeConfigPath(),
|
|
466
718
|
) {
|
|
467
|
-
|
|
468
|
-
if (!fs.existsSync(configPath)) return "";
|
|
469
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
470
|
-
const auth = config?.provider?.openrouter?.options?.headers?.Authorization;
|
|
471
|
-
if (typeof auth !== "string") return "";
|
|
472
|
-
const value = auth.replace(/^Bearer\s+/i, "").trim();
|
|
473
|
-
if (!value || value.includes("__OPENROUTER_API_KEY__")) return "";
|
|
474
|
-
return value;
|
|
475
|
-
} catch {
|
|
476
|
-
return "";
|
|
477
|
-
}
|
|
719
|
+
return readRawFromOpenCodeProvider('openrouter', configPath) || "";
|
|
478
720
|
}
|
|
479
721
|
|
|
480
722
|
// --- Internal helpers ---
|
|
@@ -520,9 +762,18 @@ function getRegistryDefaults(registryModels = {}) {
|
|
|
520
762
|
|
|
521
763
|
function normalizeEnvAssignments(envValues = {}) {
|
|
522
764
|
return {
|
|
523
|
-
reasoning: envValues.reasoning
|
|
524
|
-
|
|
525
|
-
|
|
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,
|
|
526
777
|
};
|
|
527
778
|
}
|
|
528
779
|
|
|
@@ -863,6 +1114,87 @@ export function setModelWithTierInference(envPath, tier, modelId, registryModels
|
|
|
863
1114
|
return resolved;
|
|
864
1115
|
}
|
|
865
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
|
+
|
|
866
1198
|
export function readCurrentModels(envPath, registryModels = {}) {
|
|
867
1199
|
const envValues = arguments.length > 2 ? arguments[2] : {};
|
|
868
1200
|
const fileAssignments = readEnvAssignments(envPath);
|