@geraldmaron/construct 1.1.1 → 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 +29 -19
- package/bin/construct +311 -81
- 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 +1 -1
- 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 +129 -15
- 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 +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/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 +360 -36
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- 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/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 +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 +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 +252 -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 +228 -7
- 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 +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 +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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/command-suggest.mjs — slash-command autocomplete for construct chat.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the advertised commands and their one-line actions,
|
|
5
|
+
* shared by the web composer dropdown, the Ink footer (ghost completion + Tab),
|
|
6
|
+
* and tests. No I/O. `/models` is intentionally absent here — it stays a hidden
|
|
7
|
+
* alias of `/model` in the command handlers so only one model command is shown.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const SLASH_COMMAND_INFO = Object.freeze([
|
|
11
|
+
{ cmd: '/help', desc: 'list every command and what it does' },
|
|
12
|
+
{ cmd: '/model', desc: 'show or pick the model (opens a searchable picker)' },
|
|
13
|
+
{ cmd: '/free', desc: 'switch to OpenRouter free-router mode' },
|
|
14
|
+
{ cmd: '/set', desc: 'change a setting — /set <key> <on|off|value>' },
|
|
15
|
+
{ cmd: '/settings', desc: 'show the current settings' },
|
|
16
|
+
{ cmd: '/layers', desc: 'show transparency layers (Ctrl+1–5 to toggle)' },
|
|
17
|
+
{ cmd: '/usage', desc: 'session token and cost breakdown' },
|
|
18
|
+
{ cmd: '/oracle', desc: 'Oracle overseer verdict and pending approvals' },
|
|
19
|
+
{ cmd: '/host', desc: 'show the active engine / host' },
|
|
20
|
+
{ cmd: '/inspect', desc: 'toggle the inspector panel' },
|
|
21
|
+
{ cmd: '/export', desc: 'save the answer to markdown — /export [last|session]' },
|
|
22
|
+
{ cmd: '/clear', desc: 'clear the conversation' },
|
|
23
|
+
{ cmd: '/exit', desc: 'quit (terminal only)' },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export const SLASH_COMMANDS = Object.freeze(SLASH_COMMAND_INFO.map((c) => c.cmd));
|
|
27
|
+
|
|
28
|
+
export const SETTING_KEY_INFO = Object.freeze([
|
|
29
|
+
{ key: 'thinking', desc: 'show model reasoning inline' },
|
|
30
|
+
{ key: 'path', desc: 'show the routing path' },
|
|
31
|
+
{ key: 'specialists', desc: 'show the specialist chain' },
|
|
32
|
+
{ key: 'tools', desc: 'show tool calls' },
|
|
33
|
+
{ key: 'observability', desc: 'show usage + telemetry' },
|
|
34
|
+
{ key: 'permission', desc: 'ask | allow_once | allow_always | reject' },
|
|
35
|
+
{ key: 'sandbox', desc: 'read-only | workspace-write | danger-full-access' },
|
|
36
|
+
{ key: 'model', desc: 'set the model id' },
|
|
37
|
+
{ key: 'ascii', desc: 'ASCII-only glyphs (on|off)' },
|
|
38
|
+
{ key: 'inspector', desc: 'inspector panel (on|off|auto)' },
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export const SETTING_KEYS = Object.freeze(SETTING_KEY_INFO.map((s) => s.key));
|
|
42
|
+
|
|
43
|
+
const DESC_BY_CMD = new Map(SLASH_COMMAND_INFO.map((c) => [c.cmd, c.desc]));
|
|
44
|
+
const DESC_BY_KEY = new Map(SETTING_KEY_INFO.map((s) => [s.key, s.desc]));
|
|
45
|
+
|
|
46
|
+
export function slashCommandMatches(input) {
|
|
47
|
+
const trimmed = String(input || '').trimStart();
|
|
48
|
+
if (!trimmed.startsWith('/')) return [];
|
|
49
|
+
const token = trimmed.split(/\s+/)[0].toLowerCase();
|
|
50
|
+
if (!token || token === '/') return [...SLASH_COMMANDS];
|
|
51
|
+
return SLASH_COMMANDS.filter((cmd) => cmd.startsWith(token));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function slashCommandGhost(input) {
|
|
55
|
+
const matches = slashCommandMatches(input);
|
|
56
|
+
if (!matches.length) return '';
|
|
57
|
+
const trimmed = String(input || '').trimStart();
|
|
58
|
+
const token = trimmed.split(/\s+/)[0];
|
|
59
|
+
const best = matches.find((cmd) => cmd.startsWith(token.toLowerCase())) || matches[0];
|
|
60
|
+
if (best.length <= token.length) return '';
|
|
61
|
+
return best.slice(token.length);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function completeSlashCommand(input) {
|
|
65
|
+
const trimmed = String(input || '').trimStart();
|
|
66
|
+
if (!trimmed.startsWith('/')) return input;
|
|
67
|
+
const parts = trimmed.split(/\s+/);
|
|
68
|
+
const token = parts[0];
|
|
69
|
+
const matches = slashCommandMatches(token);
|
|
70
|
+
if (!matches.length) return input;
|
|
71
|
+
const lower = token.toLowerCase();
|
|
72
|
+
const exact = matches.find((cmd) => cmd === lower);
|
|
73
|
+
const best = exact || matches.find((cmd) => cmd.startsWith(lower)) || matches[0];
|
|
74
|
+
const rest = parts.slice(1).join(' ');
|
|
75
|
+
const completed = rest ? `${best} ${rest}` : `${best} `;
|
|
76
|
+
const prefix = input.startsWith(' ') ? input.slice(0, input.indexOf('/')) : '';
|
|
77
|
+
return `${prefix}${completed}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function cycleSlashCommand(input, direction = 1) {
|
|
81
|
+
const matches = slashCommandMatches(input);
|
|
82
|
+
if (!matches.length) return input;
|
|
83
|
+
const trimmed = String(input || '').trimStart();
|
|
84
|
+
const token = trimmed.split(/\s+/)[0].toLowerCase();
|
|
85
|
+
const idx = Math.max(0, matches.findIndex((cmd) => cmd.startsWith(token)));
|
|
86
|
+
const next = matches[(idx + direction + matches.length) % matches.length];
|
|
87
|
+
const prefix = input.startsWith(' ') ? input.slice(0, input.indexOf('/')) : '';
|
|
88
|
+
return `${prefix}${next} `;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function setKeyMatches(input) {
|
|
92
|
+
const trimmed = String(input || '').trimStart();
|
|
93
|
+
const m = trimmed.match(/^\/set\s+(\S*)$/i);
|
|
94
|
+
if (!m) return [];
|
|
95
|
+
const partial = (m[1] || '').toLowerCase();
|
|
96
|
+
if (!partial) return [...SETTING_KEYS];
|
|
97
|
+
return SETTING_KEYS.filter((k) => k.startsWith(partial));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function completeSetKey(input) {
|
|
101
|
+
const trimmed = String(input || '').trimStart();
|
|
102
|
+
const m = trimmed.match(/^\/set\s+(\S*)$/i);
|
|
103
|
+
if (!m) return input;
|
|
104
|
+
const partial = m[1] || '';
|
|
105
|
+
const matches = setKeyMatches(input);
|
|
106
|
+
if (!matches.length) return input;
|
|
107
|
+
const best = matches.find((k) => k.startsWith(partial.toLowerCase())) || matches[0];
|
|
108
|
+
return `/set ${best} `;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Rich suggestions for the composer dropdown: each entry carries the token to
|
|
112
|
+
// complete plus its one-line action. Returns `/set` keys while the user is
|
|
113
|
+
// typing a setting, otherwise the matching commands. Empty once an argument is
|
|
114
|
+
// being typed for a non-/set command, so the dropdown gets out of the way.
|
|
115
|
+
|
|
116
|
+
export function slashCommandSuggestions(input, { limit = 8 } = {}) {
|
|
117
|
+
const raw = String(input || '');
|
|
118
|
+
const trimmed = raw.trimStart();
|
|
119
|
+
if (!trimmed.startsWith('/')) return { kind: 'none', items: [] };
|
|
120
|
+
|
|
121
|
+
if (/^\/set(\s|$)/i.test(trimmed)) {
|
|
122
|
+
const keys = setKeyMatches(trimmed);
|
|
123
|
+
if (keys.length) {
|
|
124
|
+
return {
|
|
125
|
+
kind: 'set',
|
|
126
|
+
items: keys.slice(0, limit).map((key) => ({ value: key, desc: DESC_BY_KEY.get(key) || '' })),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { kind: 'none', items: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (trimmed.includes(' ')) return { kind: 'none', items: [] };
|
|
133
|
+
|
|
134
|
+
const matches = slashCommandMatches(trimmed);
|
|
135
|
+
return {
|
|
136
|
+
kind: 'command',
|
|
137
|
+
items: matches.slice(0, limit).map((cmd) => ({ value: cmd, desc: DESC_BY_CMD.get(cmd) || '' })),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function commandSuggestHint(input) {
|
|
142
|
+
if (!String(input || '').trimStart().startsWith('/')) return '';
|
|
143
|
+
const cmdMatches = slashCommandMatches(input);
|
|
144
|
+
if (cmdMatches.length && !input.trim().includes(' ')) {
|
|
145
|
+
return cmdMatches.slice(0, 6).join(' ');
|
|
146
|
+
}
|
|
147
|
+
const setMatches = setKeyMatches(input);
|
|
148
|
+
if (setMatches.length) return setMatches.slice(0, 8).join(' ');
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function applyTabCompletion(input) {
|
|
153
|
+
const setDone = completeSetKey(input);
|
|
154
|
+
if (setDone !== input) return setDone;
|
|
155
|
+
return completeSlashCommand(input);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isSlashOnlyInput(input) {
|
|
159
|
+
const trimmed = String(input || '').trimStart();
|
|
160
|
+
return trimmed.startsWith('/') && !trimmed.includes(' ');
|
|
161
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/commands.mjs — in-session slash commands for `construct chat`.
|
|
3
|
+
*
|
|
4
|
+
* Owns the interactive control surface: changing the model, toggling transparency
|
|
5
|
+
* layers, editing and persisting settings, and inspecting token usage — without
|
|
6
|
+
* leaving the chat. Each command mutates the shared `session` object (which the
|
|
7
|
+
* renderer reads per turn) and, for settings, writes through to the project config
|
|
8
|
+
* via lib/chat/config.mjs. handle() returns false only for /exit so the REPL loop
|
|
9
|
+
* stays the single owner of the readline lifecycle. Ink passes `onClear` instead of
|
|
10
|
+
* ANSI clear-screen codes; both surfaces share `createCollectWriter` for capture.
|
|
11
|
+
*
|
|
12
|
+
* Model enumeration is delegated to the driver's optional listModels(); hosts that
|
|
13
|
+
* cannot enumerate models degrade to a clear notice rather than failing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { saveChatConfig, LAYER_KEYS, PERMISSION_MODES, SANDBOX_LEVELS } from './config.mjs';
|
|
17
|
+
import { exportTurns } from './export.mjs';
|
|
18
|
+
import { formatUsagePanel } from './tui/usage.mjs';
|
|
19
|
+
import { applySessionSetting } from './session-settings.mjs';
|
|
20
|
+
import { commitPickerModel, promptModelPickerTerminal } from './model-picker.mjs';
|
|
21
|
+
import { handleDemoCommand, registerDemoCommands } from './demo-guide.mjs';
|
|
22
|
+
|
|
23
|
+
export const HELP = [
|
|
24
|
+
['/help', 'show this help'],
|
|
25
|
+
['/model [id]', 'show or set the model (no id opens a searchable picker)'],
|
|
26
|
+
['/free', 'set OpenRouter free-router mode (--free equivalent)'],
|
|
27
|
+
['/export [last|session]', 'write plain markdown answer to .cx/chat-sessions/exports/'],
|
|
28
|
+
['/set <key> <on|off|value>', 'change a setting (thinking, tools, path, specialists, observability, permission, sandbox, model)'],
|
|
29
|
+
['/settings', 'show current settings'],
|
|
30
|
+
['/layers', 'show transparency layers'],
|
|
31
|
+
['/usage', 'show session token and cost breakdown'],
|
|
32
|
+
['/oracle', 'show Oracle overseer verdict and pending approvals'],
|
|
33
|
+
['/host', 'show the active host'],
|
|
34
|
+
['/clear', 'clear the screen'],
|
|
35
|
+
['/inspect', 'toggle turn inspector panel (on/off/auto)'],
|
|
36
|
+
['/exit', 'quit'],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function ask(rl, question) {
|
|
40
|
+
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createCommands({ driver, host, hostId = host, version, cwd = process.cwd(), env = process.env, turnBlocksRef = null, demoGuide = null } = {}) {
|
|
44
|
+
async function handle(input, ctx) {
|
|
45
|
+
const { output, colors, layers, session, rl, onClear } = ctx;
|
|
46
|
+
const runtimeEnv = ctx.env || env;
|
|
47
|
+
const [cmd, ...rest] = input.trim().split(/\s+/);
|
|
48
|
+
const arg = rest.join(' ').trim();
|
|
49
|
+
const activeGuide = demoGuide || session?.demoGuide || null;
|
|
50
|
+
|
|
51
|
+
switch (cmd) {
|
|
52
|
+
case '/exit':
|
|
53
|
+
case '/quit':
|
|
54
|
+
return false;
|
|
55
|
+
|
|
56
|
+
case '/help':
|
|
57
|
+
output.write(`${colors.bold}commands${colors.reset}\n`);
|
|
58
|
+
for (const [name, desc] of registerDemoCommands(HELP, activeGuide)) {
|
|
59
|
+
output.write(` ${colors.cyan}${name.padEnd(28)}${colors.reset}${colors.dim}${desc}${colors.reset}\n`);
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case '/demo':
|
|
64
|
+
handleDemoCommand(arg, { demoGuide: activeGuide, output, colors });
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case '/host': {
|
|
68
|
+
const mode = session.modelMode === 'free-router' ? 'free-router' : 'pinned';
|
|
69
|
+
output.write(`${colors.dim}engine:${colors.reset} ${host} (owned loop)${version ? ` (${version})` : ''} ${colors.dim}model:${colors.reset} ${session.model || '(default)'} ${colors.dim}mode:${colors.reset} ${mode}\n`);
|
|
70
|
+
output.write(`${colors.dim}construct runs the loop itself; switch models with /model${colors.reset}\n`);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case '/models':
|
|
75
|
+
case '/model':
|
|
76
|
+
if (arg) {
|
|
77
|
+
commitPickerModel(session, { mode: 'pinned', modelId: arg }, { cwd, hostId, layers: session.layers });
|
|
78
|
+
output.write(`${colors.green}model set:${colors.reset} ${arg} ${colors.dim}(pinned, saved)${colors.reset}\n`);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
await promptModelPickerTerminal({
|
|
82
|
+
output,
|
|
83
|
+
colors,
|
|
84
|
+
session,
|
|
85
|
+
rl,
|
|
86
|
+
askFn: ctx.ask,
|
|
87
|
+
env: runtimeEnv,
|
|
88
|
+
cwd,
|
|
89
|
+
hostId,
|
|
90
|
+
layers: session.layers,
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case '/free': {
|
|
95
|
+
const { resolveFreeOpenRouterModel } = await import('../../apps/chat/engine/models.mjs');
|
|
96
|
+
const freeId = await resolveFreeOpenRouterModel({ env: runtimeEnv, tier: 'standard' });
|
|
97
|
+
if (!freeId) {
|
|
98
|
+
output.write(`${colors.red}OpenRouter free router needs OPENROUTER_API_KEY${colors.reset}\n`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
commitPickerModel(session, { mode: 'free-router', modelId: freeId }, { cwd, hostId, layers: session.layers });
|
|
102
|
+
output.write(`${colors.green}model mode:${colors.reset} free-router → ${freeId} ${colors.dim}(saved)${colors.reset}\n`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case '/export': {
|
|
107
|
+
const scope = arg === 'session' ? 'session' : 'last';
|
|
108
|
+
const blocks = turnBlocksRef?.() || [];
|
|
109
|
+
const result = exportTurns(blocks, { scope, cwd });
|
|
110
|
+
if (!result.ok) output.write(`${colors.red}${result.error}${colors.reset}\n`);
|
|
111
|
+
else output.write(`${colors.green}exported${colors.reset} ${result.count} turn(s) to ${result.path}\n`);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case '/set':
|
|
116
|
+
applySetting(output, colors, session, layers, rest);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case '/settings':
|
|
120
|
+
showSettings(output, colors, session, layers);
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case '/layers':
|
|
124
|
+
output.write(`${colors.dim}layers:${colors.reset} ${LAYER_KEYS.map((k) => `${k}=${layers[k] ? 'on' : 'off'}`).join(' ')}\n`);
|
|
125
|
+
output.write(`${colors.dim}toggle with: /set <layer> on|off${colors.reset}\n`);
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case '/usage':
|
|
129
|
+
output.write(formatUsagePanel(session.usage, colors) + '\n');
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case '/oracle': {
|
|
133
|
+
const { readOracleDockState, formatOracleDockDetail } = await import('../intake/session-prelude.mjs');
|
|
134
|
+
const state = readOracleDockState({ cwd, env: runtimeEnv });
|
|
135
|
+
output.write(`${colors.bold}oracle${colors.reset}\n`);
|
|
136
|
+
output.write(`${formatOracleDockDetail(state)}\n`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case '/inspect': {
|
|
141
|
+
session.inspectorForced = session.inspectorForced === true ? false : session.inspectorForced === false ? null : true;
|
|
142
|
+
const label = session.inspectorForced === true ? 'on' : session.inspectorForced === false ? 'off' : 'auto';
|
|
143
|
+
output.write(`${colors.green}inspector:${colors.reset} ${label}\n`);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case '/clear':
|
|
148
|
+
if (typeof onClear === 'function') onClear();
|
|
149
|
+
else output.write('\u001b[2J\u001b[H');
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
output.write(`${colors.dim}unknown command: ${cmd} \u2014 try /help${colors.reset}\n`);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function applySetting(output, colors, session, layers, parts) {
|
|
159
|
+
if (parts.length < 2) {
|
|
160
|
+
output.write(`${colors.dim}usage: /set <key> <value> (keys: ${[...LAYER_KEYS, 'thinking', 'permission', 'sandbox', 'model', 'ascii', 'inspector'].join(', ')})${colors.reset}\n`);
|
|
161
|
+
output.write(`${colors.dim}or run /set alone in the Ink UI for a searchable picker${colors.reset}\n`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const [key, ...valueParts] = parts;
|
|
165
|
+
const value = valueParts.join(' ');
|
|
166
|
+
if (key === 'host') {
|
|
167
|
+
output.write(`${colors.dim}host can only be changed by relaunching: construct chat --host ${value}${colors.reset}\n`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const result = applySessionSetting(session, layers, key, value, { cwd, hostId });
|
|
171
|
+
if (!result.ok) { output.write(`${colors.red}${result.error}${colors.reset}\n`); return; }
|
|
172
|
+
output.write(`${colors.green}set:${colors.reset} ${result.key} = ${result.value} ${colors.dim}(saved)${colors.reset}\n`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function showSettings(output, colors, session, layers) {
|
|
176
|
+
output.write(`${colors.bold}settings${colors.reset}\n`);
|
|
177
|
+
output.write(` ${colors.cyan}host${colors.reset} ${host}\n`);
|
|
178
|
+
output.write(` ${colors.cyan}model${colors.reset} ${session.model || '(host default)'} ${colors.dim}(${session.modelMode || 'pinned'})${colors.reset}\n`);
|
|
179
|
+
output.write(` ${colors.cyan}thinking${colors.reset} ${layers.thinking ? 'on' : 'off'}\n`);
|
|
180
|
+
output.write(` ${colors.cyan}layers${colors.reset} ${LAYER_KEYS.map((k) => `${k}=${layers[k] ? 'on' : 'off'}`).join(' ')}\n`);
|
|
181
|
+
const perm = session.permissionMode || 'allow_once';
|
|
182
|
+
output.write(` ${colors.cyan}permission${colors.reset} ${perm} ${colors.dim}(${PERMISSION_MODES.join('/')})${colors.reset}\n`);
|
|
183
|
+
output.write(` ${colors.cyan}ascii${colors.reset} ${session.ui?.ascii ? 'on' : 'off'} ${colors.dim}(glyph fallback for limited terminals)${colors.reset}\n`);
|
|
184
|
+
output.write(` ${colors.cyan}inspector${colors.reset} ${session.ui?.inspector || 'auto'} ${colors.dim}(off/auto/on — per-turn detail panel)${colors.reset}\n`);
|
|
185
|
+
output.write(` ${colors.cyan}sandbox${colors.reset} ${session.sandbox || '(host default)'} ${colors.dim}(${SANDBOX_LEVELS.join('/')})${colors.reset}\n`);
|
|
186
|
+
output.write(` ${colors.dim}chat sandbox gates tools in this session; isolated project copies use \`construct sandbox create\`${colors.reset}\n`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function persist(session, layers = session.layers) {
|
|
190
|
+
try {
|
|
191
|
+
saveChatConfig({
|
|
192
|
+
host: hostId,
|
|
193
|
+
model: session.modelMode === 'pinned' ? session.model : null,
|
|
194
|
+
modelMode: session.modelMode || 'pinned',
|
|
195
|
+
layers,
|
|
196
|
+
thinking: layers?.thinking,
|
|
197
|
+
permissionMode: session.permissionMode,
|
|
198
|
+
sandbox: session.sandbox,
|
|
199
|
+
ui: session.ui,
|
|
200
|
+
}, { cwd });
|
|
201
|
+
} catch { /* settings persistence is best-effort */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { handle };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createCollectWriter() {
|
|
208
|
+
const parts = [];
|
|
209
|
+
return {
|
|
210
|
+
stream: { write(chunk) { parts.push(String(chunk)); } },
|
|
211
|
+
text() { return parts.join(''); },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const PLAIN_COLORS = Object.freeze({ bold: '', dim: '', reset: '', cyan: '', green: '', red: '', yellow: '' });
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/config.mjs — persisted settings for `construct chat`.
|
|
3
|
+
*
|
|
4
|
+
* Settings are layered: built-in DEFAULTS < global (~/.cx/chat-config.json) <
|
|
5
|
+
* project (<project>/.cx/chat-config.json). Reads merge all three; writes always go
|
|
6
|
+
* to the project file (resolveProjectScopedPath falls back to ~/.cx when there is no
|
|
7
|
+
* project root), so a user's global preferences seed every project while per-project
|
|
8
|
+
* overrides stay local. Only the known, validated keys are persisted.
|
|
9
|
+
*
|
|
10
|
+
* Exposed settings: host, model, transparency layers, thinking, permissionMode,
|
|
11
|
+
* sandbox, ui (ascii, inspector). Validation lives here so the command layer and the launcher
|
|
12
|
+
* share one source of truth for allowed values.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
19
|
+
|
|
20
|
+
export const LAYER_KEYS = ['thinking', 'path', 'specialists', 'tools', 'observability'];
|
|
21
|
+
export const PERMISSION_MODES = ['ask', 'allow_once', 'allow_always', 'reject'];
|
|
22
|
+
export const SANDBOX_LEVELS = ['read-only', 'workspace-write', 'danger-full-access'];
|
|
23
|
+
|
|
24
|
+
export const INSPECTOR_MODES = ['off', 'auto', 'on'];
|
|
25
|
+
export const THEME_MODES = ['auto', 'light', 'dark'];
|
|
26
|
+
export const MODEL_MODES = ['pinned', 'free-router'];
|
|
27
|
+
|
|
28
|
+
export const DEFAULTS = Object.freeze({
|
|
29
|
+
host: null,
|
|
30
|
+
model: null,
|
|
31
|
+
modelMode: 'pinned',
|
|
32
|
+
layers: Object.freeze(Object.fromEntries(LAYER_KEYS.map((k) => [k, true]))),
|
|
33
|
+
thinking: true,
|
|
34
|
+
permissionMode: 'allow_once',
|
|
35
|
+
sandbox: null,
|
|
36
|
+
ui: Object.freeze({ ascii: false, inspector: 'off', theme: 'auto' }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const CONFIG_BASENAME = 'chat-config.json';
|
|
40
|
+
|
|
41
|
+
function readJson(file) {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(file)) return null;
|
|
44
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mergeConfig(base, override) {
|
|
51
|
+
if (!override || typeof override !== 'object') return base;
|
|
52
|
+
const out = { ...base };
|
|
53
|
+
for (const key of Object.keys(DEFAULTS)) {
|
|
54
|
+
if (override[key] == null) continue;
|
|
55
|
+
if (key === 'layers' && typeof override.layers === 'object') {
|
|
56
|
+
out.layers = { ...base.layers, ...override.layers };
|
|
57
|
+
} else if (key === 'ui' && typeof override.ui === 'object') {
|
|
58
|
+
out.ui = { ...base.ui, ...override.ui };
|
|
59
|
+
} else {
|
|
60
|
+
out[key] = override[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function globalConfigPath() {
|
|
67
|
+
return path.join(os.homedir(), '.cx', CONFIG_BASENAME);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function projectConfigPath({ cwd = process.cwd() } = {}) {
|
|
71
|
+
return resolveProjectScopedPath(CONFIG_BASENAME, { cwd, ensureDir: false });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function loadChatConfig({ cwd = process.cwd() } = {}) {
|
|
75
|
+
let config = { ...DEFAULTS, layers: { ...DEFAULTS.layers } };
|
|
76
|
+
config = mergeConfig(config, readJson(globalConfigPath()));
|
|
77
|
+
const projectPath = projectConfigPath({ cwd });
|
|
78
|
+
config = mergeConfig(config, readJson(projectPath));
|
|
79
|
+
return { config, path: projectPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function saveChatConfig(config, { cwd = process.cwd() } = {}) {
|
|
83
|
+
const target = resolveProjectScopedPath(CONFIG_BASENAME, { cwd, ensureDir: true });
|
|
84
|
+
const persisted = {};
|
|
85
|
+
for (const key of Object.keys(DEFAULTS)) {
|
|
86
|
+
if (config[key] == null) continue;
|
|
87
|
+
persisted[key] = key === 'layers' ? { ...config.layers } : key === 'ui' ? { ...config.ui } : config[key];
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(target, `${JSON.stringify(persisted, null, 2)}\n`);
|
|
90
|
+
return target;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Validates and coerces a single `key value` setting change. Returns
|
|
94
|
+
// { ok, value, error } so the command layer can report precisely.
|
|
95
|
+
|
|
96
|
+
export function validateSetting(key, rawValue) {
|
|
97
|
+
switch (key) {
|
|
98
|
+
case 'host':
|
|
99
|
+
return { ok: true, value: String(rawValue) };
|
|
100
|
+
case 'model':
|
|
101
|
+
return { ok: true, value: String(rawValue) };
|
|
102
|
+
case 'thinking': {
|
|
103
|
+
const v = parseBool(rawValue);
|
|
104
|
+
return v == null ? { ok: false, error: 'thinking must be on/off' } : { ok: true, value: v };
|
|
105
|
+
}
|
|
106
|
+
case 'permissionMode':
|
|
107
|
+
case 'permission':
|
|
108
|
+
return PERMISSION_MODES.includes(rawValue)
|
|
109
|
+
? { ok: true, key: 'permissionMode', value: rawValue }
|
|
110
|
+
: { ok: false, error: `permission must be one of: ${PERMISSION_MODES.join(', ')}` };
|
|
111
|
+
case 'sandbox':
|
|
112
|
+
return SANDBOX_LEVELS.includes(rawValue)
|
|
113
|
+
? { ok: true, value: rawValue }
|
|
114
|
+
: { ok: false, error: `sandbox must be one of: ${SANDBOX_LEVELS.join(', ')}` };
|
|
115
|
+
case 'ascii': {
|
|
116
|
+
const v = parseBool(rawValue);
|
|
117
|
+
return v == null ? { ok: false, error: 'ascii must be on/off' } : { ok: true, key: 'ui.ascii', value: v };
|
|
118
|
+
}
|
|
119
|
+
case 'inspector':
|
|
120
|
+
return INSPECTOR_MODES.includes(String(rawValue).toLowerCase())
|
|
121
|
+
? { ok: true, key: 'ui.inspector', value: String(rawValue).toLowerCase() }
|
|
122
|
+
: { ok: false, error: `inspector must be one of: ${INSPECTOR_MODES.join(', ')}` };
|
|
123
|
+
case 'theme':
|
|
124
|
+
return THEME_MODES.includes(String(rawValue).toLowerCase())
|
|
125
|
+
? { ok: true, key: 'ui.theme', value: String(rawValue).toLowerCase() }
|
|
126
|
+
: { ok: false, error: `theme must be one of: ${THEME_MODES.join(', ')}` };
|
|
127
|
+
default:
|
|
128
|
+
if (LAYER_KEYS.includes(key)) {
|
|
129
|
+
const v = parseBool(rawValue);
|
|
130
|
+
return v == null ? { ok: false, error: `${key} must be on/off` } : { ok: true, key: `layers.${key}`, value: v };
|
|
131
|
+
}
|
|
132
|
+
return { ok: false, error: `unknown setting: ${key}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseBool(v) {
|
|
137
|
+
if (v === true || v === false) return v;
|
|
138
|
+
const s = String(v).toLowerCase();
|
|
139
|
+
if (['on', 'true', '1', 'yes', 'show'].includes(s)) return true;
|
|
140
|
+
if (['off', 'false', '0', 'no', 'hide'].includes(s)) return false;
|
|
141
|
+
return null;
|
|
142
|
+
}
|