@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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/maintenance/docker-reclaim.mjs — reclaim abandoned per-home Postgres
|
|
3
|
+
* containers and data volumes that outlived the HOME that created them.
|
|
4
|
+
*
|
|
5
|
+
* Construct derives a Postgres container/volume namespace from a sha256 slice
|
|
6
|
+
* of the resolved home directory (lib/home-namespace.mjs) so isolated HOMEs —
|
|
7
|
+
* test sandboxes, ephemeral /tmp footprint runs, deleted accounts — never
|
|
8
|
+
* collide on one machine. Each container carries a `restart: unless-stopped`
|
|
9
|
+
* policy. When the owning HOME or its compose project directory is deleted,
|
|
10
|
+
* Docker keeps the container alive with nothing left to reap it: an orphan that
|
|
11
|
+
* pins memory and leaks disk through its named volume.
|
|
12
|
+
*
|
|
13
|
+
* The reclaim runs from the upgrade flows (lib/update.mjs, lib/upgrade.mjs)
|
|
14
|
+
* once per upgrade and from `construct cleanup`, never from `construct sync`,
|
|
15
|
+
* which fires constantly and must not mutate machine-global Docker state.
|
|
16
|
+
*
|
|
17
|
+
* An orphan is removed only when every signal agrees it is abandoned:
|
|
18
|
+
* - the name carries a Construct per-home suffix (construct-postgres-<8 hex>),
|
|
19
|
+
* so hand-named and third-party containers stay untouched;
|
|
20
|
+
* - the suffix differs from the current HOME's suffix, sparing the live install;
|
|
21
|
+
* - the name differs from an operator-pinned CONSTRUCT_PG_CONTAINER;
|
|
22
|
+
* - a container's compose working_dir label points at a path that is gone;
|
|
23
|
+
* - a volume has no surviving container for its suffix.
|
|
24
|
+
* The legacy singular `construct-postgres` (no suffix) is out of scope.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import os from 'node:os';
|
|
30
|
+
|
|
31
|
+
import { homeNamespaceSuffix } from '../home-namespace.mjs';
|
|
32
|
+
|
|
33
|
+
const CONTAINER_RE = /^construct-postgres-([0-9a-f]{8})$/;
|
|
34
|
+
const VOLUME_RE = /construct-postgres-([0-9a-f]{8})/;
|
|
35
|
+
const CONTAINER_FORMAT = '{{.Names}}\t{{.Label "com.docker.compose.project.working_dir"}}';
|
|
36
|
+
const DEFAULT_MAX = 25;
|
|
37
|
+
|
|
38
|
+
export function defaultRunDocker(args) {
|
|
39
|
+
const r = spawnSync('docker', args, { encoding: 'utf8' });
|
|
40
|
+
return {
|
|
41
|
+
status: r.status ?? 1,
|
|
42
|
+
stdout: r.stdout || '',
|
|
43
|
+
stderr: r.stderr || '',
|
|
44
|
+
error: r.error || null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// `docker ps -a` lists every container with its compose working_dir label on a
|
|
49
|
+
// tab. Anything that does not match the per-home naming scheme is dropped, so
|
|
50
|
+
// the parser only surfaces Construct-managed candidates.
|
|
51
|
+
|
|
52
|
+
export function parseContainers(stdout) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const raw of String(stdout || '').split('\n')) {
|
|
55
|
+
const line = raw.replace(/\s+$/, '');
|
|
56
|
+
if (!line) continue;
|
|
57
|
+
const tab = line.indexOf('\t');
|
|
58
|
+
const name = (tab === -1 ? line : line.slice(0, tab)).trim();
|
|
59
|
+
const workingDir = tab === -1 ? '' : line.slice(tab + 1).trim();
|
|
60
|
+
const match = CONTAINER_RE.exec(name);
|
|
61
|
+
if (!match) continue;
|
|
62
|
+
out.push({ name, suffix: match[1], workingDir });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseVolumes(stdout) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const raw of String(stdout || '').split('\n')) {
|
|
70
|
+
const name = raw.trim();
|
|
71
|
+
if (!name) continue;
|
|
72
|
+
const match = VOLUME_RE.exec(name);
|
|
73
|
+
if (!match) continue;
|
|
74
|
+
out.push({ name, suffix: match[1] });
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A container is an orphan when it belongs to a foreign home namespace, is not
|
|
80
|
+
// the operator-pinned container, and its compose project directory is gone. A
|
|
81
|
+
// volume is an orphan when its suffix is foreign and no surviving container —
|
|
82
|
+
// including containers spared this pass — still claims it.
|
|
83
|
+
|
|
84
|
+
export function selectOrphans({
|
|
85
|
+
containers,
|
|
86
|
+
volumes,
|
|
87
|
+
currentSuffix,
|
|
88
|
+
pinnedName = null,
|
|
89
|
+
pathExists = (p) => fs.existsSync(p),
|
|
90
|
+
}) {
|
|
91
|
+
const orphanContainers = containers.filter((c) =>
|
|
92
|
+
c.suffix !== currentSuffix &&
|
|
93
|
+
c.name !== pinnedName &&
|
|
94
|
+
c.workingDir &&
|
|
95
|
+
!pathExists(c.workingDir));
|
|
96
|
+
|
|
97
|
+
const orphanSuffixes = new Set(orphanContainers.map((c) => c.suffix));
|
|
98
|
+
const liveSuffixes = new Set(
|
|
99
|
+
containers.filter((c) => !orphanSuffixes.has(c.suffix)).map((c) => c.suffix),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const orphanVolumes = volumes.filter((v) =>
|
|
103
|
+
v.suffix &&
|
|
104
|
+
v.suffix !== currentSuffix &&
|
|
105
|
+
!liveSuffixes.has(v.suffix));
|
|
106
|
+
|
|
107
|
+
return { orphanContainers, orphanVolumes };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function reclaimOrphanedDockerResources({
|
|
111
|
+
runDocker = defaultRunDocker,
|
|
112
|
+
homeDir = os.homedir(),
|
|
113
|
+
env = process.env,
|
|
114
|
+
dryRun = false,
|
|
115
|
+
max = DEFAULT_MAX,
|
|
116
|
+
pathExists = (p) => fs.existsSync(p),
|
|
117
|
+
} = {}) {
|
|
118
|
+
const summary = {
|
|
119
|
+
available: false,
|
|
120
|
+
reason: null,
|
|
121
|
+
scanned: { containers: 0, volumes: 0 },
|
|
122
|
+
removedContainers: [],
|
|
123
|
+
removedVolumes: [],
|
|
124
|
+
skipped: [],
|
|
125
|
+
errors: [],
|
|
126
|
+
dryRun,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// A non-zero `docker ps` means the daemon is down or Docker is absent; the
|
|
130
|
+
// machine then holds no reclaimable state and the pass is a clean no-op.
|
|
131
|
+
|
|
132
|
+
const psResult = runDocker(['ps', '-a', '--format', CONTAINER_FORMAT]);
|
|
133
|
+
if (!psResult || psResult.status !== 0) {
|
|
134
|
+
summary.reason = 'docker-unavailable';
|
|
135
|
+
return summary;
|
|
136
|
+
}
|
|
137
|
+
summary.available = true;
|
|
138
|
+
|
|
139
|
+
const containers = parseContainers(psResult.stdout);
|
|
140
|
+
summary.scanned.containers = containers.length;
|
|
141
|
+
|
|
142
|
+
const volResult = runDocker(['volume', 'ls', '--format', '{{.Name}}']);
|
|
143
|
+
const volumes = volResult && volResult.status === 0 ? parseVolumes(volResult.stdout) : [];
|
|
144
|
+
summary.scanned.volumes = volumes.length;
|
|
145
|
+
|
|
146
|
+
const currentSuffix = homeNamespaceSuffix(homeDir);
|
|
147
|
+
const pinnedName = (env.CONSTRUCT_PG_CONTAINER || '').trim() || null;
|
|
148
|
+
|
|
149
|
+
const { orphanContainers, orphanVolumes } = selectOrphans({
|
|
150
|
+
containers,
|
|
151
|
+
volumes,
|
|
152
|
+
currentSuffix,
|
|
153
|
+
pinnedName,
|
|
154
|
+
pathExists,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let budget = Number.isFinite(max) && max > 0 ? max : DEFAULT_MAX;
|
|
158
|
+
|
|
159
|
+
for (const c of orphanContainers) {
|
|
160
|
+
if (budget <= 0) {
|
|
161
|
+
summary.skipped.push({ name: c.name, reason: 'reclaim-budget-exhausted' });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
budget -= 1;
|
|
165
|
+
if (dryRun) {
|
|
166
|
+
summary.removedContainers.push(c.name);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const r = runDocker(['rm', '-f', '-v', c.name]);
|
|
170
|
+
if (r && r.status === 0) summary.removedContainers.push(c.name);
|
|
171
|
+
else summary.errors.push({ name: c.name, error: (r?.stderr || '').trim() || `docker rm exited ${r?.status}` });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A named data volume only drops once nothing references it. `docker volume
|
|
175
|
+
// rm` exits non-zero for an in-use or already-gone volume, so a failure here
|
|
176
|
+
// is recorded as skipped rather than a hard error.
|
|
177
|
+
|
|
178
|
+
for (const v of orphanVolumes) {
|
|
179
|
+
if (budget <= 0) {
|
|
180
|
+
summary.skipped.push({ name: v.name, reason: 'reclaim-budget-exhausted' });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
budget -= 1;
|
|
184
|
+
if (dryRun) {
|
|
185
|
+
summary.removedVolumes.push(v.name);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const r = runDocker(['volume', 'rm', v.name]);
|
|
189
|
+
if (r && r.status === 0) summary.removedVolumes.push(v.name);
|
|
190
|
+
else summary.skipped.push({ name: v.name, reason: 'in-use-or-missing' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return summary;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatReclaim(summary) {
|
|
197
|
+
if (!summary || !summary.available) return null;
|
|
198
|
+
const containers = summary.removedContainers.length;
|
|
199
|
+
const volumes = summary.removedVolumes.length;
|
|
200
|
+
if (!containers && !volumes) return null;
|
|
201
|
+
const parts = [];
|
|
202
|
+
if (containers) parts.push(`${containers} orphaned Postgres container${containers === 1 ? '' : 's'}`);
|
|
203
|
+
if (volumes) parts.push(`${volumes} abandoned data volume${volumes === 1 ? '' : 's'}`);
|
|
204
|
+
const verb = summary.dryRun ? 'Would reclaim' : 'Reclaimed';
|
|
205
|
+
return `${verb} ${parts.join(' and ')} from deleted home namespaces.`;
|
|
206
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/external-schema-cost.mjs — measured token cost for heavy external MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Committed fixtures under tests/fixtures/mcp-tool-schemas/ hold captured tools/list
|
|
5
|
+
* payloads per server. Regenerate with scripts/measure-external-mcp-schemas.mjs.
|
|
6
|
+
* Sums schemaTokens via estimateToolTokens (JSON length × 0.25) for sync-time trim
|
|
7
|
+
* comments and ADR-0032 footnotes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { HEAVY_EXTERNAL_MCP_IDS, estimateToolTokens } from './tool-budget.mjs';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const REPO_ROOT = join(__dirname, '..', '..');
|
|
17
|
+
export const EXTERNAL_MCP_SCHEMA_FIXTURE_DIR = join(REPO_ROOT, 'tests', 'fixtures', 'mcp-tool-schemas');
|
|
18
|
+
|
|
19
|
+
function readJson(path) {
|
|
20
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Load one server's committed fixture; missing files throw so CI fails loudly.
|
|
24
|
+
|
|
25
|
+
export function loadExternalMcpSchemaFixture(serverId) {
|
|
26
|
+
const path = join(EXTERNAL_MCP_SCHEMA_FIXTURE_DIR, `${serverId}.json`);
|
|
27
|
+
if (!existsSync(path)) {
|
|
28
|
+
throw new Error(`Missing external MCP schema fixture: ${path}`);
|
|
29
|
+
}
|
|
30
|
+
const doc = readJson(path);
|
|
31
|
+
const tools = Array.isArray(doc.tools) ? doc.tools : [];
|
|
32
|
+
const schemaTokens = Number.isFinite(doc.schemaTokens)
|
|
33
|
+
? doc.schemaTokens
|
|
34
|
+
: estimateToolTokens(tools);
|
|
35
|
+
return {
|
|
36
|
+
serverId,
|
|
37
|
+
capturedAt: doc.capturedAt || null,
|
|
38
|
+
toolCount: tools.length,
|
|
39
|
+
schemaTokens,
|
|
40
|
+
tools,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Per-server breakdown plus total for HEAVY_EXTERNAL_MCP_IDS.
|
|
45
|
+
|
|
46
|
+
export function measuredHeavyExternalMcpCosts() {
|
|
47
|
+
const servers = {};
|
|
48
|
+
let totalSchemaTokens = 0;
|
|
49
|
+
for (const id of HEAVY_EXTERNAL_MCP_IDS) {
|
|
50
|
+
const row = loadExternalMcpSchemaFixture(id);
|
|
51
|
+
servers[id] = {
|
|
52
|
+
toolCount: row.toolCount,
|
|
53
|
+
schemaTokens: row.schemaTokens,
|
|
54
|
+
capturedAt: row.capturedAt,
|
|
55
|
+
};
|
|
56
|
+
totalSchemaTokens += row.schemaTokens;
|
|
57
|
+
}
|
|
58
|
+
return { servers, totalSchemaTokens };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function measuredHeavyExternalMcpTokenCost() {
|
|
62
|
+
return measuredHeavyExternalMcpCosts().totalSchemaTokens;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Lazy constant — fixtures may be absent while scripts/measure-external-mcp-schemas.mjs runs.
|
|
66
|
+
|
|
67
|
+
let cachedMeasuredTotal = null;
|
|
68
|
+
|
|
69
|
+
export function getMeasuredHeavyExternalMcpTokenCost() {
|
|
70
|
+
if (cachedMeasuredTotal === null) {
|
|
71
|
+
cachedMeasuredTotal = measuredHeavyExternalMcpTokenCost();
|
|
72
|
+
}
|
|
73
|
+
return cachedMeasuredTotal;
|
|
74
|
+
}
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { resolve, dirname } from 'node:path';
|
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { realpathSync } from 'node:fs';
|
|
15
15
|
import { loadToolkitEnv } from '../toolkit-env.mjs';
|
|
16
|
-
import { loadConstructEnv } from '../env
|
|
16
|
+
import { loadConstructEnv, prepareConstructEnv } from '../runtime-env.mjs';
|
|
17
17
|
import { getInstalledVersion } from '../version.mjs';
|
|
18
18
|
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
19
19
|
import { withGenAiSpan, GenAiAttrs, extractTraceContext, injectTraceContext } from '../telemetry/otel-tracer.mjs';
|
|
@@ -21,7 +21,7 @@ import { withGenAiSpan, GenAiAttrs, extractTraceContext, injectTraceContext } fr
|
|
|
21
21
|
// Apply config.env values to process.env, letting config.env win over shell env
|
|
22
22
|
// so telemetry/OpenRouter credentials are always correct regardless of host env.
|
|
23
23
|
{
|
|
24
|
-
const confEnv =
|
|
24
|
+
const confEnv = prepareConstructEnv({ warn: false });
|
|
25
25
|
for (const [k, v] of Object.entries(confEnv)) {
|
|
26
26
|
process.env[k] = v;
|
|
27
27
|
}
|
|
@@ -170,7 +170,7 @@ const ALL_TOOL_DEFS = [
|
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
name: 'extract_document_text',
|
|
173
|
-
description: 'Extracts readable text from a local document path.
|
|
173
|
+
description: 'Extracts readable text from a local document path. Uses node-native extractors (unpdf/mammoth) first; escalates to the docling Python sidecar or whisper.cpp when needed (same pipeline as `construct ingest`). Supports PDF, DOCX, XLSX, PPTX, HTML, plain text, email, and transcripts.',
|
|
174
174
|
inputSchema: {
|
|
175
175
|
type: 'object',
|
|
176
176
|
properties: {
|
|
@@ -375,7 +375,7 @@ const ALL_TOOL_DEFS = [
|
|
|
375
375
|
},
|
|
376
376
|
{
|
|
377
377
|
name: 'get_skill',
|
|
378
|
-
description: 'Reads a specific skill playbook from the Construct knowledge base (e.g. "
|
|
378
|
+
description: 'Reads a specific skill playbook from the Construct knowledge base (e.g. "docs/adr-workflow", "roles/engineer", "architecture/security-arch").',
|
|
379
379
|
inputSchema: {
|
|
380
380
|
type: 'object',
|
|
381
381
|
properties: {
|
|
@@ -932,15 +932,50 @@ const ALL_TOOL_DEFS = [
|
|
|
932
932
|
},
|
|
933
933
|
{
|
|
934
934
|
name: 'document_export',
|
|
935
|
-
description: 'Convert a markdown file into a distributable PDF, DOCX,
|
|
935
|
+
description: 'Convert a markdown file into a distributable document — PDF, DOCX, legacy DOC (LibreOffice), deck HTML, PPTX, HTML, RTF, ODT, EPUB, LaTeX, plain text, or Markdown — via Pandoc and Typst (PDF engine) plus optional pptxgenjs/LibreOffice, per ADR-0024. Engines are optional dependencies discovered at runtime — the tool returns a structured "install <binary>" error when tooling is absent, never crashes. Use `detect_only=true` to check availability without running an export.',
|
|
936
936
|
inputSchema: {
|
|
937
937
|
type: 'object',
|
|
938
938
|
required: ['format'],
|
|
939
939
|
properties: {
|
|
940
940
|
input_path: { type: 'string', description: 'Absolute path to the markdown source file (required unless detect_only=true).' },
|
|
941
941
|
output_path: { type: 'string', description: 'Optional absolute output path; defaults to <input>.<format> next to the source.' },
|
|
942
|
-
format: { type: 'string', enum: ['pdf', 'docx', 'html'], description: 'Target format.' },
|
|
942
|
+
format: { type: 'string', enum: ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'], description: 'Target format.' },
|
|
943
943
|
detect_only: { type: 'boolean', description: 'When true, report binary availability without exporting. Default false.' },
|
|
944
|
+
figures: { type: 'boolean', description: 'When true, render d2/mermaid via pandoc-ext/diagram filter. Default false.' },
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
name: 'publish_detect',
|
|
950
|
+
description: 'Detect availability of publish pipeline tooling: Pandoc/Typst export, pandoc-ext/diagram figures, VHS terminal demos, and Playwright dashboard demos.',
|
|
951
|
+
inputSchema: {
|
|
952
|
+
type: 'object',
|
|
953
|
+
properties: {
|
|
954
|
+
format: { type: 'string', enum: ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'], description: 'Export format to probe (default pdf).' },
|
|
955
|
+
figures: { type: 'boolean', description: 'Include figure binaries (default true).' },
|
|
956
|
+
demo: { type: 'string', description: 'When set, require VHS/asciinema for terminal demo.' },
|
|
957
|
+
dashboard_demo: { type: 'string', description: 'When set, require Playwright workspace for dashboard demo.' },
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
name: 'publish_run',
|
|
963
|
+
description: 'Run the publish pipeline: export markdown with optional figure filter and optional demo recordings. Use dry_run=true to probe tooling only.',
|
|
964
|
+
inputSchema: {
|
|
965
|
+
type: 'object',
|
|
966
|
+
required: ['input_path'],
|
|
967
|
+
properties: {
|
|
968
|
+
input_path: { type: 'string', description: 'Absolute path to markdown research brief.' },
|
|
969
|
+
output_path: { type: 'string', description: 'Optional output path for export.' },
|
|
970
|
+
format: { type: 'string', enum: ['pdf', 'docx', 'doc', 'deck', 'pptx', 'html', 'rtf', 'odt', 'epub', 'tex', 'txt', 'md', 'mdx'], description: 'Target format (default pdf).' },
|
|
971
|
+
demo: { type: 'string', description: 'Terminal tape name to record.' },
|
|
972
|
+
dashboard_demo: { type: 'string', description: 'Dashboard Playwright demo spec basename.' },
|
|
973
|
+
figures: { type: 'boolean', description: 'Render diagrams (default true).' },
|
|
974
|
+
strict: { type: 'boolean', description: 'Fail when toolchain or release gate fails (default true).' },
|
|
975
|
+
source_only: { type: 'boolean', description: 'Write sources only (default false).' },
|
|
976
|
+
no_gate: { type: 'boolean', description: 'Skip artifact release gate (default false).' },
|
|
977
|
+
artifact_type: { type: 'string', description: 'Manifest doc type for release gate when path inference is ambiguous.' },
|
|
978
|
+
dry_run: { type: 'boolean', description: 'Detect tooling only (default false).' },
|
|
944
979
|
},
|
|
945
980
|
},
|
|
946
981
|
},
|
|
@@ -1270,7 +1305,7 @@ export async function dispatchToolByName(name, args = {}) {
|
|
|
1270
1305
|
if (name === 'agent_health') result = agentHealth(args);
|
|
1271
1306
|
else if (name === 'summarize_diff') result = summarizeDiff(args);
|
|
1272
1307
|
else if (name === 'scan_file') result = scanFile(args);
|
|
1273
|
-
else if (name === 'extract_document_text') result = extractDocumentText(args);
|
|
1308
|
+
else if (name === 'extract_document_text') result = await extractDocumentText(args);
|
|
1274
1309
|
else if (name === 'ingest_document') result = await ingestDocument(args);
|
|
1275
1310
|
else if (name === 'infer_document_schema') result = await inferDocumentSchemaTool(args);
|
|
1276
1311
|
else if (name === 'list_schema_artifacts') result = listSchemaArtifactsTool(args);
|
|
@@ -1324,9 +1359,49 @@ export async function dispatchToolByName(name, args = {}) {
|
|
|
1324
1359
|
else if (name === 'document_export') {
|
|
1325
1360
|
const { detect, exportMarkdown } = await import('../document-export.mjs');
|
|
1326
1361
|
if (args.detect_only) {
|
|
1327
|
-
result = detect(args.format);
|
|
1362
|
+
result = detect(args.format, process.env, { figures: Boolean(args.figures) });
|
|
1363
|
+
} else {
|
|
1364
|
+
result = exportMarkdown({
|
|
1365
|
+
inputPath: args.input_path,
|
|
1366
|
+
outputPath: args.output_path,
|
|
1367
|
+
format: args.format,
|
|
1368
|
+
figures: Boolean(args.figures),
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
else if (name === 'publish_detect') {
|
|
1373
|
+
const { detectPublishPipeline } = await import('../publish-tooling.mjs');
|
|
1374
|
+
result = detectPublishPipeline({
|
|
1375
|
+
format: args.format || 'pdf',
|
|
1376
|
+
includeFigures: args.figures !== false,
|
|
1377
|
+
includeTerminalDemo: Boolean(args.demo),
|
|
1378
|
+
includeDashboardDemo: Boolean(args.dashboard_demo),
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
else if (name === 'publish_run') {
|
|
1382
|
+
const { runPublish } = await import('../publish.mjs');
|
|
1383
|
+
if (args.dry_run) {
|
|
1384
|
+
const { detectPublishPipeline } = await import('../publish-tooling.mjs');
|
|
1385
|
+
result = detectPublishPipeline({
|
|
1386
|
+
format: args.format || 'pdf',
|
|
1387
|
+
includeFigures: args.figures !== false,
|
|
1388
|
+
includeTerminalDemo: Boolean(args.demo),
|
|
1389
|
+
includeDashboardDemo: Boolean(args.dashboard_demo),
|
|
1390
|
+
});
|
|
1328
1391
|
} else {
|
|
1329
|
-
result =
|
|
1392
|
+
result = runPublish({
|
|
1393
|
+
inputPath: args.input_path,
|
|
1394
|
+
format: args.format || 'pdf',
|
|
1395
|
+
outputPath: args.output_path,
|
|
1396
|
+
demos: args.demo ? [args.demo] : [],
|
|
1397
|
+
dashboardDemos: args.dashboard_demo ? [args.dashboard_demo] : [],
|
|
1398
|
+
figures: args.figures !== false,
|
|
1399
|
+
strict: args.strict !== false,
|
|
1400
|
+
gate: args.no_gate !== true,
|
|
1401
|
+
artifactType: args.artifact_type || args.type || null,
|
|
1402
|
+
sourceOnly: Boolean(args.source_only),
|
|
1403
|
+
repoRoot: process.cwd(),
|
|
1404
|
+
});
|
|
1330
1405
|
}
|
|
1331
1406
|
}
|
|
1332
1407
|
else if (name === 'profile_show') result = profileShow(args);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/stdio-mcp-probe.mjs — newline-delimited JSON-RPC probe for stdio MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* Spawns a child MCP process, completes initialize + notifications/initialized, then
|
|
5
|
+
* issues tools/list to capture serialized tool schemas for token-budget fixtures.
|
|
6
|
+
* Stdout is the only protocol channel; stderr is ignored.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 25_000;
|
|
12
|
+
|
|
13
|
+
function parseFrames(chunk, state) {
|
|
14
|
+
state.buffer += chunk;
|
|
15
|
+
let idx;
|
|
16
|
+
while ((idx = state.buffer.indexOf('\n')) >= 0) {
|
|
17
|
+
const raw = state.buffer.slice(0, idx).trim();
|
|
18
|
+
state.buffer = state.buffer.slice(idx + 1);
|
|
19
|
+
if (!raw) continue;
|
|
20
|
+
try {
|
|
21
|
+
state.frames.push(JSON.parse(raw));
|
|
22
|
+
} catch {
|
|
23
|
+
/* non-JSON noise */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function send(child, frame) {
|
|
29
|
+
child.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function waitForFrame(frames, predicate, timeoutMs) {
|
|
33
|
+
const deadline = Date.now() + timeoutMs;
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const tick = () => {
|
|
36
|
+
const hit = frames.find(predicate);
|
|
37
|
+
if (hit) {
|
|
38
|
+
resolve(hit);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (Date.now() >= deadline) {
|
|
42
|
+
reject(new Error(`Timed out after ${timeoutMs}ms; frames=${frames.length}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setTimeout(tick, 50);
|
|
46
|
+
};
|
|
47
|
+
tick();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Probe a stdio MCP server and return the tools/list result array.
|
|
52
|
+
|
|
53
|
+
export async function probeStdioMcpTools(command, args, {
|
|
54
|
+
env = {},
|
|
55
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const child = spawn(command, args, {
|
|
58
|
+
env: { ...process.env, ...env },
|
|
59
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
|
+
});
|
|
61
|
+
const state = { buffer: '', frames: [] };
|
|
62
|
+
child.stdout.setEncoding('utf8');
|
|
63
|
+
child.stdout.on('data', (chunk) => parseFrames(chunk, state));
|
|
64
|
+
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
try {
|
|
67
|
+
child.kill('SIGTERM');
|
|
68
|
+
} catch {
|
|
69
|
+
/* already exited */
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
send(child, {
|
|
75
|
+
jsonrpc: '2.0',
|
|
76
|
+
id: 1,
|
|
77
|
+
method: 'initialize',
|
|
78
|
+
params: {
|
|
79
|
+
protocolVersion: '2024-11-05',
|
|
80
|
+
capabilities: {},
|
|
81
|
+
clientInfo: { name: 'construct-mcp-probe', version: '1' },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
await waitForFrame(state.frames, (f) => f.id === 1, timeoutMs);
|
|
85
|
+
send(child, { jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
86
|
+
send(child, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
|
|
87
|
+
const listFrame = await waitForFrame(state.frames, (f) => f.id === 2, timeoutMs);
|
|
88
|
+
if (listFrame.error) {
|
|
89
|
+
throw new Error(listFrame.error.message || JSON.stringify(listFrame.error));
|
|
90
|
+
}
|
|
91
|
+
return listFrame.result?.tools || [];
|
|
92
|
+
} finally {
|
|
93
|
+
cleanup();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Parse JSON or MCP-over-SSE (event: message / data: {...}) bodies from HTTP MCP hosts.
|
|
98
|
+
|
|
99
|
+
function parseMcpHttpFrame(text) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(text);
|
|
102
|
+
} catch {
|
|
103
|
+
for (const line of text.split('\n')) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (!trimmed.startsWith('data:')) continue;
|
|
106
|
+
const payload = trimmed.slice(5).trim();
|
|
107
|
+
if (!payload) continue;
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(payload);
|
|
110
|
+
} catch {
|
|
111
|
+
/* try next data line */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new Error('response was not JSON or SSE data');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Probe an HTTP MCP endpoint (GitHub Copilot MCP) with bearer auth.
|
|
119
|
+
|
|
120
|
+
export async function probeHttpMcpTools(url, headers = {}, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
121
|
+
const initBody = {
|
|
122
|
+
jsonrpc: '2.0',
|
|
123
|
+
id: 1,
|
|
124
|
+
method: 'initialize',
|
|
125
|
+
params: {
|
|
126
|
+
protocolVersion: '2024-11-05',
|
|
127
|
+
capabilities: {},
|
|
128
|
+
clientInfo: { name: 'construct-mcp-probe', version: '1' },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const listBody = { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} };
|
|
132
|
+
const commonHeaders = {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
Accept: 'application/json, text/event-stream',
|
|
135
|
+
...headers,
|
|
136
|
+
};
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
139
|
+
|
|
140
|
+
async function postRpc(body, extraHeaders = {}) {
|
|
141
|
+
const res = await fetch(url, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { ...commonHeaders, ...extraHeaders },
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
signal: controller.signal,
|
|
146
|
+
});
|
|
147
|
+
const text = await res.text();
|
|
148
|
+
return { res, text };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const { res: initRes, text: initText } = await postRpc(initBody);
|
|
153
|
+
if (!initRes.ok) {
|
|
154
|
+
throw new Error(`initialize HTTP ${initRes.status}: ${initText.slice(0, 160)}`);
|
|
155
|
+
}
|
|
156
|
+
parseMcpHttpFrame(initText);
|
|
157
|
+
|
|
158
|
+
const sessionId = initRes.headers.get('mcp-session-id')
|
|
159
|
+
|| initRes.headers.get('Mcp-Session-Id')
|
|
160
|
+
|| null;
|
|
161
|
+
const sessionHeaders = sessionId ? { 'mcp-session-id': sessionId } : {};
|
|
162
|
+
|
|
163
|
+
const { res: listRes, text: listText } = await postRpc(listBody, sessionHeaders);
|
|
164
|
+
if (!listRes.ok) {
|
|
165
|
+
throw new Error(`tools/list HTTP ${listRes.status}: ${listText.slice(0, 160)}`);
|
|
166
|
+
}
|
|
167
|
+
const frame = parseMcpHttpFrame(listText);
|
|
168
|
+
if (frame.error) {
|
|
169
|
+
throw new Error(frame.error.message || JSON.stringify(frame.error));
|
|
170
|
+
}
|
|
171
|
+
return frame.result?.tools || [];
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Probe the Construct memory stdio bridge, which forwards tools/list to cm.
|
|
178
|
+
|
|
179
|
+
export async function probeMemoryBridgeTools({
|
|
180
|
+
bridgePath,
|
|
181
|
+
backendUrl = 'http://127.0.0.1:8765/',
|
|
182
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
183
|
+
} = {}) {
|
|
184
|
+
return probeStdioMcpTools(process.execPath, [bridgePath], {
|
|
185
|
+
env: { CONSTRUCT_MEMORY_BRIDGE_URL: backendUrl },
|
|
186
|
+
timeoutMs,
|
|
187
|
+
});
|
|
188
|
+
}
|