@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.
Files changed (408) hide show
  1. package/README.md +29 -19
  2. package/bin/construct +311 -81
  3. package/bin/construct-postinstall.mjs +30 -2
  4. package/examples/distribution/README.md +42 -0
  5. package/examples/distribution/manifest.json +53 -0
  6. package/examples/distribution/sources/adr.md +84 -0
  7. package/examples/distribution/sources/deck-one-pager.md +65 -0
  8. package/examples/distribution/sources/prd-platform.md +161 -0
  9. package/examples/distribution/sources/research-brief.md +80 -0
  10. package/examples/distribution/sources/rfc-platform.md +82 -0
  11. package/examples/distribution/sources/runbook.md +103 -0
  12. package/examples/distribution/sources/strategy.md +88 -0
  13. package/lib/adapters-sync.mjs +67 -0
  14. package/lib/artifact-gate-notice.mjs +38 -0
  15. package/lib/artifact-manifest.mjs +112 -0
  16. package/lib/artifact-release-gate.mjs +171 -0
  17. package/lib/artifact-reviewers.mjs +68 -0
  18. package/lib/artifact-type-from-path.mjs +79 -0
  19. package/lib/audit-skills.mjs +4 -0
  20. package/lib/audit-specialists.mjs +285 -0
  21. package/lib/audit-trail.mjs +1 -1
  22. package/lib/auto-docs.mjs +87 -18
  23. package/lib/beads/drift.mjs +27 -6
  24. package/lib/beads-client.mjs +49 -121
  25. package/lib/beads-optimistic.mjs +5 -12
  26. package/lib/brand-fonts.mjs +93 -0
  27. package/lib/brand-prose.mjs +214 -0
  28. package/lib/brand-tokens.mjs +92 -0
  29. package/lib/bridges/copilot-proxy.mjs +13 -26
  30. package/lib/capability-ledger.mjs +156 -0
  31. package/lib/certification/artifact-fixtures.mjs +132 -0
  32. package/lib/certification/artifact-gates.mjs +63 -0
  33. package/lib/certification/artifact-provenance.mjs +97 -0
  34. package/lib/certification/canonical-scenarios.mjs +78 -0
  35. package/lib/certification/cli.mjs +191 -0
  36. package/lib/certification/dashboard-api.mjs +71 -0
  37. package/lib/certification/demo-parity.mjs +116 -0
  38. package/lib/certification/document-io-fixtures.mjs +246 -0
  39. package/lib/certification/document-workflow.mjs +97 -0
  40. package/lib/certification/eval-bridge.mjs +77 -0
  41. package/lib/certification/model-routing.mjs +149 -0
  42. package/lib/certification/prompt-budget.mjs +119 -0
  43. package/lib/certification/rc-gate.mjs +206 -0
  44. package/lib/certification/real-llm-scenarios.mjs +303 -0
  45. package/lib/certification/role-cards.mjs +113 -0
  46. package/lib/certification/role-overlays.mjs +117 -0
  47. package/lib/certification/run.mjs +122 -0
  48. package/lib/certification/runner.mjs +323 -0
  49. package/lib/certification/scenarios.mjs +51 -0
  50. package/lib/certification/skill-inventory.mjs +289 -0
  51. package/lib/certification/skill-scenarios.mjs +147 -0
  52. package/lib/certification/specialist-contracts.mjs +85 -0
  53. package/lib/certification/specialist-scenarios.mjs +175 -0
  54. package/lib/certification/stale-impact.mjs +146 -0
  55. package/lib/certification/status.mjs +252 -0
  56. package/lib/certification/store.mjs +77 -0
  57. package/lib/chat/cli.mjs +333 -0
  58. package/lib/chat/command-suggest.mjs +161 -0
  59. package/lib/chat/commands.mjs +215 -0
  60. package/lib/chat/config.mjs +142 -0
  61. package/lib/chat/context-compactor.mjs +250 -0
  62. package/lib/chat/context-continuation.mjs +253 -0
  63. package/lib/chat/continuation-source.mjs +58 -0
  64. package/lib/chat/demo-guide.mjs +61 -0
  65. package/lib/chat/design-tokens.mjs +91 -0
  66. package/lib/chat/desktop-binary.mjs +79 -0
  67. package/lib/chat/desktop-build.mjs +130 -0
  68. package/lib/chat/desktop-launcher.mjs +133 -0
  69. package/lib/chat/evidence.mjs +145 -0
  70. package/lib/chat/export.mjs +74 -0
  71. package/lib/chat/harness/driver.mjs +91 -0
  72. package/lib/chat/list-picker.mjs +112 -0
  73. package/lib/chat/model-picker.mjs +356 -0
  74. package/lib/chat/openrouter-fallback.mjs +151 -0
  75. package/lib/chat/permission-prompt.mjs +33 -0
  76. package/lib/chat/picker-catalog.mjs +45 -0
  77. package/lib/chat/policy-telemetry.mjs +34 -0
  78. package/lib/chat/present.mjs +245 -0
  79. package/lib/chat/session-context.mjs +39 -0
  80. package/lib/chat/session-persist.mjs +73 -0
  81. package/lib/chat/session-restore.mjs +71 -0
  82. package/lib/chat/session-settings.mjs +53 -0
  83. package/lib/chat/system-prompt.mjs +52 -0
  84. package/lib/chat/transparency.mjs +93 -0
  85. package/lib/chat/tui/color-scheme.mjs +42 -0
  86. package/lib/chat/tui/markdown.mjs +123 -0
  87. package/lib/chat/tui/presentation.mjs +100 -0
  88. package/lib/chat/tui/render.mjs +500 -0
  89. package/lib/chat/tui/turn-block.mjs +284 -0
  90. package/lib/chat/tui/turn-present.mjs +18 -0
  91. package/lib/chat/tui/turn-state.mjs +88 -0
  92. package/lib/chat/tui/usage.mjs +122 -0
  93. package/lib/chat/web-commands.mjs +146 -0
  94. package/lib/chat/web-launcher.mjs +63 -0
  95. package/lib/chat/web-picker-keys.mjs +46 -0
  96. package/lib/chat/web-session.mjs +159 -0
  97. package/lib/cli-commands.mjs +129 -15
  98. package/lib/comment-lint.mjs +6 -4
  99. package/lib/config/schema.mjs +36 -5
  100. package/lib/contract-schemas/decision.json +50 -0
  101. package/lib/contract-schemas/implementation.json +51 -0
  102. package/lib/contract-schemas/review-report.json +32 -0
  103. package/lib/contract-schemas/test-report.json +43 -0
  104. package/lib/contracts/construct-handoff.mjs +60 -0
  105. package/lib/contracts/validate.mjs +32 -3
  106. package/lib/contracts/violation-log.mjs +58 -5
  107. package/lib/dashboard-demo.mjs +71 -0
  108. package/lib/dashboard-static.mjs +19 -5
  109. package/lib/decisions/registry.mjs +5 -3
  110. package/lib/deck-export-pptx.mjs +1152 -0
  111. package/lib/demo-recording.mjs +142 -0
  112. package/lib/demo-script.mjs +114 -0
  113. package/lib/demo-surface.mjs +249 -0
  114. package/lib/demo.mjs +598 -140
  115. package/lib/diagram-export.mjs +192 -0
  116. package/lib/diagram.mjs +13 -11
  117. package/lib/docs-verify.mjs +25 -7
  118. package/lib/doctor/index.mjs +3 -1
  119. package/lib/doctor/project-adapters.mjs +44 -0
  120. package/lib/doctor/watchers/cx-budget.mjs +98 -0
  121. package/lib/doctor/watchers/graph-staleness.mjs +52 -0
  122. package/lib/doctor/watchers/process-pressure.mjs +4 -3
  123. package/lib/document-export.mjs +360 -36
  124. package/lib/document-extract.mjs +44 -4
  125. package/lib/document-ingest.mjs +29 -20
  126. package/lib/embed/cli.mjs +2 -2
  127. package/lib/embed/daemon.mjs +18 -46
  128. package/lib/embed/worker.mjs +2 -2
  129. package/lib/embedded-contract/model-resolve.mjs +10 -9
  130. package/lib/env-config.mjs +26 -0
  131. package/lib/evals/dataset.mjs +137 -0
  132. package/lib/evals/gates.mjs +175 -0
  133. package/lib/graph/build-co-change.mjs +54 -0
  134. package/lib/graph/build-from-registry.mjs +136 -0
  135. package/lib/graph/build-import-graph.mjs +153 -0
  136. package/lib/graph/cli.mjs +110 -0
  137. package/lib/graph/impact-cli.mjs +89 -0
  138. package/lib/graph/impact.mjs +108 -0
  139. package/lib/graph/staleness.mjs +44 -0
  140. package/lib/graph/store.mjs +172 -0
  141. package/lib/health-check.mjs +90 -76
  142. package/lib/hooks/artifact-release-gate.mjs +43 -0
  143. package/lib/hooks/brand-prose-lint.mjs +38 -0
  144. package/lib/hooks/graph-impact-advisory.mjs +62 -0
  145. package/lib/hooks/session-optimize.mjs +84 -207
  146. package/lib/hooks/session-start.mjs +4 -5
  147. package/lib/host-disposition.mjs +7 -0
  148. package/lib/improvement/cli.mjs +189 -0
  149. package/lib/improvement/controller.mjs +137 -0
  150. package/lib/improvement/proposal.mjs +120 -0
  151. package/lib/improvement/specialist-loop.mjs +192 -0
  152. package/lib/improvement/store.mjs +89 -0
  153. package/lib/improvement/surface.mjs +219 -0
  154. package/lib/ingest-tooling.mjs +97 -0
  155. package/lib/init/doc-lanes.mjs +165 -0
  156. package/lib/init-docs.mjs +31 -178
  157. package/lib/init-unified.mjs +27 -97
  158. package/lib/init-update-guide.mjs +102 -0
  159. package/lib/init-update.mjs +32 -1
  160. package/lib/init.mjs +8 -0
  161. package/lib/install/desktop-binary-download.mjs +85 -0
  162. package/lib/install/legacy-global-cleanup.mjs +189 -0
  163. package/lib/intake/daemon.mjs +2 -0
  164. package/lib/intake/git-queue.mjs +9 -8
  165. package/lib/intake/queue.mjs +2 -1
  166. package/lib/intake/session-prelude.mjs +90 -1
  167. package/lib/libreoffice-export.mjs +97 -0
  168. package/lib/logging/rotate.mjs +9 -0
  169. package/lib/maintenance/docker-reclaim.mjs +206 -0
  170. package/lib/mcp/external-schema-cost.mjs +74 -0
  171. package/lib/mcp/server.mjs +84 -9
  172. package/lib/mcp/stdio-mcp-probe.mjs +188 -0
  173. package/lib/mcp/tool-budget.mjs +55 -4
  174. package/lib/mcp/tools/document.mjs +18 -4
  175. package/lib/mcp/tools/skills.mjs +32 -3
  176. package/lib/mcp/tools/workflow.mjs +13 -1
  177. package/lib/model-free-selector.mjs +18 -0
  178. package/lib/model-registry.mjs +13 -229
  179. package/lib/model-router.mjs +373 -92
  180. package/lib/models/behavior-matrix.mjs +289 -0
  181. package/lib/models/catalog.mjs +209 -0
  182. package/lib/models/execution-capability-profile.mjs +196 -0
  183. package/lib/models/execution-policy.mjs +307 -0
  184. package/lib/models/provider-poll.mjs +383 -0
  185. package/lib/npm-spawn-env.mjs +17 -0
  186. package/lib/ollama/installed-models.mjs +129 -0
  187. package/lib/oracle/actions.mjs +187 -0
  188. package/lib/oracle/artifact-gate.mjs +99 -0
  189. package/lib/oracle/cli.mjs +204 -0
  190. package/lib/oracle/daemon-entry.mjs +14 -0
  191. package/lib/oracle/dispatch.mjs +81 -0
  192. package/lib/oracle/execute.mjs +143 -0
  193. package/lib/oracle/gaps.mjs +76 -0
  194. package/lib/oracle/index.mjs +84 -0
  195. package/lib/oracle/issues.mjs +164 -0
  196. package/lib/oracle/org-graph.mjs +170 -0
  197. package/lib/oracle/policy.mjs +80 -0
  198. package/lib/oracle/read-model.mjs +398 -0
  199. package/lib/oracle/reconcile.mjs +191 -0
  200. package/lib/oracle/routing.mjs +89 -0
  201. package/lib/oracle/synthesize.mjs +384 -0
  202. package/lib/oracle/verdicts.mjs +51 -0
  203. package/lib/orchestration/worker.mjs +88 -27
  204. package/lib/orchestration-policy.mjs +50 -0
  205. package/lib/parity.mjs +96 -2
  206. package/lib/playwright-demo.mjs +252 -0
  207. package/lib/project-init-shared.mjs +4 -1
  208. package/lib/prompt-composer.js +7 -3
  209. package/lib/prompt-validation-contract.mjs +24 -0
  210. package/lib/provider-capabilities.js +57 -11
  211. package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
  212. package/lib/providers/contract/adapters/git/index.mjs +115 -0
  213. package/lib/providers/contract/adapters/github/index.mjs +166 -0
  214. package/lib/providers/contract/adapters/jira/index.mjs +187 -0
  215. package/lib/providers/contract/adapters/slack/index.mjs +175 -0
  216. package/lib/providers/contract/contract-tests.mjs +57 -0
  217. package/lib/providers/contract/errors.mjs +48 -0
  218. package/lib/providers/contract/interface.mjs +50 -0
  219. package/lib/providers/contract/registry.mjs +102 -0
  220. package/lib/providers/copilot-auth.mjs +297 -0
  221. package/lib/providers/credential-bootstrap.mjs +178 -0
  222. package/lib/providers/credential-catalog.mjs +46 -0
  223. package/lib/providers/credential-sources.mjs +63 -0
  224. package/lib/providers/creds.mjs +5 -2
  225. package/lib/providers/op-run.mjs +59 -0
  226. package/lib/providers/secret-resolver.mjs +159 -0
  227. package/lib/publish-template.mjs +163 -0
  228. package/lib/publish-tooling.mjs +119 -0
  229. package/lib/publish.mjs +305 -0
  230. package/lib/registry/cli.mjs +82 -0
  231. package/lib/registry/consolidation.mjs +147 -0
  232. package/lib/registry/generate-docs.mjs +75 -0
  233. package/lib/registry/skill-verification.mjs +57 -0
  234. package/lib/registry/surface-map.mjs +76 -0
  235. package/lib/registry/validate.mjs +135 -0
  236. package/lib/resources/budget.mjs +82 -0
  237. package/lib/resources/process-budget.mjs +45 -0
  238. package/lib/rules-delivery.mjs +9 -2
  239. package/lib/rules-read.mjs +26 -0
  240. package/lib/runtime-env.mjs +23 -0
  241. package/lib/runtime-pressure.mjs +51 -7
  242. package/lib/schema-infer.mjs +13 -2
  243. package/lib/server/chat-loop.mjs +622 -0
  244. package/lib/server/demo-preview.mjs +63 -0
  245. package/lib/server/index.mjs +228 -7
  246. package/lib/service-manager.mjs +60 -8
  247. package/lib/setup.mjs +70 -7
  248. package/lib/specialists/prompt-schema.mjs +19 -10
  249. package/lib/specialists/roster.mjs +43 -0
  250. package/lib/specialists/scaffold.mjs +56 -0
  251. package/lib/storage/backend.mjs +6 -6
  252. package/lib/storage/hybrid-query.mjs +7 -4
  253. package/lib/storage/state-source.mjs +5 -5
  254. package/lib/storage/sync.mjs +2 -3
  255. package/lib/telemetry/rule-calls.mjs +52 -0
  256. package/lib/template-registry.mjs +2 -2
  257. package/lib/templates/visual-requirements.mjs +27 -51
  258. package/lib/test-corpus-inventory.mjs +313 -0
  259. package/lib/uninstall/uninstall.mjs +19 -7
  260. package/lib/update.mjs +12 -0
  261. package/lib/upgrade.mjs +14 -0
  262. package/lib/wireframe.mjs +20 -14
  263. package/lib/worker/run.mjs +17 -4
  264. package/lib/worker/trace.mjs +11 -3
  265. package/package.json +28 -14
  266. package/personas/construct.md +2 -2
  267. package/platforms/claude/settings.template.json +36 -0
  268. package/rules/common/patterns.md +1 -1
  269. package/rules/common/release-gates.md +4 -3
  270. package/scripts/sync-specialists.mjs +41 -28
  271. package/skills/devops/data-engineering.md +1 -1
  272. package/skills/docs/adr-workflow.md +1 -0
  273. package/skills/docs/backlog-proposal-workflow.md +1 -0
  274. package/skills/docs/codebase-research-workflow.md +40 -0
  275. package/skills/docs/customer-profile-workflow.md +1 -0
  276. package/skills/docs/document-ingest-workflow.md +1 -0
  277. package/skills/docs/evidence-ingest-workflow.md +1 -0
  278. package/skills/docs/init-docs.md +2 -2
  279. package/skills/docs/init-project.md +7 -2
  280. package/skills/docs/prd-workflow.md +24 -1
  281. package/skills/docs/prfaq-workflow.md +1 -0
  282. package/skills/docs/product-intelligence-workflow.md +1 -0
  283. package/skills/docs/product-signal-workflow.md +1 -0
  284. package/skills/docs/research-workflow.md +54 -37
  285. package/skills/docs/runbook-workflow.md +1 -0
  286. package/skills/docs/strategy-workflow.md +1 -0
  287. package/skills/docs/user-research-workflow.md +40 -0
  288. package/skills/operating/orchestration-reference.md +1 -1
  289. package/skills/roles/architect.md +5 -0
  290. package/skills/roles/operator.docs.md +4 -0
  291. package/skills/routing.md +4 -2
  292. package/specialists/artifact-manifest.json +480 -0
  293. package/specialists/artifact-manifest.schema.json +53 -0
  294. package/specialists/audit-enrichments.json +454 -0
  295. package/specialists/contracts.json +37 -25
  296. package/specialists/prompts/_shared/validation-contract.md +26 -0
  297. package/specialists/prompts/cx-accessibility.md +21 -2
  298. package/specialists/prompts/cx-ai-engineer.md +24 -1
  299. package/specialists/prompts/cx-architect.md +4 -1
  300. package/specialists/prompts/cx-business-strategist.md +23 -2
  301. package/specialists/prompts/cx-data-analyst.md +22 -1
  302. package/specialists/prompts/cx-data-engineer.md +23 -2
  303. package/specialists/prompts/cx-debugger.md +21 -2
  304. package/specialists/prompts/cx-designer.md +26 -3
  305. package/specialists/prompts/cx-devil-advocate.md +19 -2
  306. package/specialists/prompts/cx-docs-keeper.md +28 -1
  307. package/specialists/prompts/cx-engineer.md +22 -1
  308. package/specialists/prompts/cx-evaluator.md +18 -1
  309. package/specialists/prompts/cx-explorer.md +21 -2
  310. package/specialists/prompts/cx-legal-compliance.md +21 -2
  311. package/specialists/prompts/cx-operations.md +21 -2
  312. package/specialists/prompts/cx-oracle.md +94 -0
  313. package/specialists/prompts/cx-orchestrator.md +20 -1
  314. package/specialists/prompts/cx-platform-engineer.md +25 -2
  315. package/specialists/prompts/cx-product-manager.md +32 -1
  316. package/specialists/prompts/cx-qa.md +24 -2
  317. package/specialists/prompts/cx-rd-lead.md +23 -2
  318. package/specialists/prompts/cx-release-manager.md +23 -2
  319. package/specialists/prompts/cx-researcher.md +22 -2
  320. package/specialists/prompts/cx-reviewer.md +18 -1
  321. package/specialists/prompts/cx-security.md +22 -2
  322. package/specialists/prompts/cx-sre.md +30 -3
  323. package/specialists/prompts/cx-test-automation.md +7 -1
  324. package/specialists/prompts/cx-trace-reviewer.md +22 -3
  325. package/specialists/prompts/cx-ux-researcher.md +21 -2
  326. package/specialists/registry.json +52 -173
  327. package/specialists/tone-profiles.json +42 -0
  328. package/templates/demos/playwright/demo-recording.config.mjs +47 -0
  329. package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
  330. package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
  331. package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
  332. package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
  333. package/templates/demos/tapes/resource-guard-rails.tape +49 -0
  334. package/templates/demos/vhs/construct-cockpit.json +24 -0
  335. package/templates/distribution/construct-brand.typ +446 -0
  336. package/templates/distribution/construct-decision.typ +38 -0
  337. package/templates/distribution/construct-deck.html +95 -0
  338. package/templates/distribution/construct-pdf.typ +38 -0
  339. package/templates/distribution/construct-prd.typ +38 -0
  340. package/templates/distribution/construct-research.typ +38 -0
  341. package/templates/distribution/construct-web.html +92 -0
  342. package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
  343. package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
  344. package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
  345. package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
  346. package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
  347. package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
  348. package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
  349. package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
  350. package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
  351. package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
  352. package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
  353. package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
  354. package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
  355. package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
  356. package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
  357. package/templates/distribution/fonts/README.md +36 -0
  358. package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
  359. package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
  360. package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
  361. package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
  362. package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
  363. package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
  364. package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
  365. package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
  366. package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
  367. package/templates/distribution/icons/activity.svg +15 -0
  368. package/templates/distribution/icons/alert-triangle.svg +17 -0
  369. package/templates/distribution/icons/book-open.svg +16 -0
  370. package/templates/distribution/icons/bot.svg +20 -0
  371. package/templates/distribution/icons/brain.svg +22 -0
  372. package/templates/distribution/icons/circle-check.svg +16 -0
  373. package/templates/distribution/icons/clipboard-check.svg +17 -0
  374. package/templates/distribution/icons/cpu.svg +28 -0
  375. package/templates/distribution/icons/database.svg +17 -0
  376. package/templates/distribution/icons/eye.svg +16 -0
  377. package/templates/distribution/icons/file-text.svg +19 -0
  378. package/templates/distribution/icons/gauge.svg +16 -0
  379. package/templates/distribution/icons/git-branch.svg +17 -0
  380. package/templates/distribution/icons/key.svg +17 -0
  381. package/templates/distribution/icons/layers.svg +17 -0
  382. package/templates/distribution/icons/list-checks.svg +19 -0
  383. package/templates/distribution/icons/lock.svg +16 -0
  384. package/templates/distribution/icons/message-square.svg +15 -0
  385. package/templates/distribution/icons/network.svg +19 -0
  386. package/templates/distribution/icons/route.svg +17 -0
  387. package/templates/distribution/icons/scale.svg +19 -0
  388. package/templates/distribution/icons/search.svg +16 -0
  389. package/templates/distribution/icons/send.svg +16 -0
  390. package/templates/distribution/icons/server.svg +18 -0
  391. package/templates/distribution/icons/shield-check.svg +16 -0
  392. package/templates/distribution/icons/users.svg +18 -0
  393. package/templates/distribution/icons/webhook.svg +17 -0
  394. package/templates/distribution/icons/workflow.svg +17 -0
  395. package/templates/distribution/icons/wrench.svg +15 -0
  396. package/templates/distribution/run.mjs +18 -1
  397. package/templates/docs/adr.md +7 -0
  398. package/templates/docs/construct_guide.md +8 -8
  399. package/templates/docs/customer-profile.md +4 -0
  400. package/templates/docs/one-pager.md +4 -0
  401. package/templates/docs/postmortem.md +31 -0
  402. package/templates/docs/prd-platform.md +19 -0
  403. package/templates/docs/prd.md +15 -0
  404. package/templates/docs/prfaq.md +7 -0
  405. package/templates/docs/qa-strategy.md +103 -0
  406. package/templates/docs/research-brief.md +8 -0
  407. package/templates/docs/rfc-platform.md +12 -0
  408. 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
+ }
@@ -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-config.mjs';
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 = loadConstructEnv({ warn: false });
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. High-fidelity backend (docling Python sidecar via uv, whisper.cpp for audio/video) is wired into `construct ingest`; this MCP path returns the legacy regex extraction for backwards compatibility. Supports PDF, DOCX, XLSX, PPTX, HTML, plain text, email, and transcripts.',
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. "security/security-arch", "web/design-quality").',
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, or HTML document via Pandoc (DOCX/HTML) and Typst (PDF engine), per ADR-0024. Both engines are optional system binaries 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.',
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 = exportMarkdown({ inputPath: args.input_path, outputPath: args.output_path, format: args.format });
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
+ }