coursecode 0.1.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 (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for CourseCode
5
+ *
6
+ * Standalone Model Context Protocol server with persistent headless browser.
7
+ * Runtime tools (state, navigate, interact, screenshot) execute directly in
8
+ * a headless Chrome via puppeteer. Authoring tools work on the filesystem.
9
+ *
10
+ * The MCP never starts its own preview server — it connects to one that's
11
+ * already running (started by the human or via `coursecode preview`).
12
+ *
13
+ * Tool definitions and instructions live in mcp-prompts.js.
14
+ *
15
+ * Usage: coursecode mcp [--port 4173]
16
+ */
17
+
18
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import {
21
+ CallToolRequestSchema,
22
+ ListToolsRequestSchema
23
+ } from '@modelcontextprotocol/sdk/types.js';
24
+ import {
25
+ getCssCatalog,
26
+ getPreviewStatus,
27
+ getComponentCatalog,
28
+ getInteractionCatalog,
29
+ getIconCatalog,
30
+ lintCourse,
31
+ buildCourse
32
+ } from './authoring-api.js';
33
+ import { getContentExport } from './export-content.js';
34
+ import headless from './headless-browser.js';
35
+ import { TOOLS, buildInstructions, getWorkflowStatusWithInstructions } from './mcp-prompts.js';
36
+
37
+ const DEFAULT_PORT = 4173;
38
+
39
+ /**
40
+ * Ensure headless browser is connected to preview server.
41
+ * Fails fast if preview is not running — humans own the preview lifecycle.
42
+ */
43
+ async function ensureHeadless(port) {
44
+ if (!headless.isRunning()) {
45
+ const status = await getPreviewStatus(port);
46
+ if (!status.running) {
47
+ throw new Error(
48
+ 'Preview server not running. Start it first:\n' +
49
+ ' • Human: run `coursecode preview` in a terminal\n' +
50
+ ' • AI agent: use run_command to execute `npm run preview`\n' +
51
+ 'Then retry this tool call.'
52
+ );
53
+ }
54
+ await headless.launch(port);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Create and run the MCP server
60
+ */
61
+ export async function startMcpServer(options = {}) {
62
+ const port = options.port || DEFAULT_PORT;
63
+
64
+ // Build dynamic instructions for current authoring stage
65
+ const instructions = await buildInstructions(port);
66
+
67
+ const server = new Server(
68
+ {
69
+ name: 'coursecode',
70
+ version: '2.0.0',
71
+ instructions
72
+ },
73
+ {
74
+ capabilities: {
75
+ tools: {}
76
+ }
77
+ }
78
+ );
79
+
80
+ // ========================================
81
+ // Tool Request Handlers
82
+ // ========================================
83
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
84
+ return { tools: TOOLS };
85
+ });
86
+
87
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
88
+ const { name, arguments: args } = request.params;
89
+
90
+ try {
91
+ let result;
92
+
93
+ switch (name) {
94
+ // === Runtime tools (headless browser) ===
95
+ case 'coursecode_state':
96
+ await ensureHeadless(port);
97
+ result = await headless.evaluate(() => {
98
+ const api = window.CourseCodeAutomation;
99
+ return {
100
+ slide: api.getCurrentSlide(),
101
+ toc: api.getToc(),
102
+ interactions: api.listInteractions(),
103
+ engagement: api.getEngagementState(),
104
+ frameworkLogs: api.getFrameworkLogs(),
105
+ lmsState: api.getLmsState()
106
+ };
107
+ });
108
+ // Read last 20 API log entries from parent window (stub player diagnostic)
109
+ result.apiLog = await headless.evaluateParent(() => {
110
+ return window._stubPlayerState?.apiLog?.slice(0, 20) || [];
111
+ });
112
+ // Read error log from parent window (stub player diagnostic)
113
+ result.errors = await headless.evaluateParent(() => {
114
+ return window._stubPlayerState?.errorLog || [];
115
+ });
116
+ // Append console errors/warnings captured from the page
117
+ result.consoleLogs = headless.getConsoleLogs();
118
+ break;
119
+
120
+ case 'coursecode_navigate':
121
+ if (!args?.slideId) throw new Error('slideId is required');
122
+ await ensureHeadless(port);
123
+ // Apply accessibility preferences before navigation
124
+ if (args.theme || args.highContrast !== undefined) {
125
+ await headless.evaluate(({ theme, highContrast }) => {
126
+ const api = window.CourseCodeAutomation;
127
+ if (theme) api.setAccessibilityPreference('theme', theme);
128
+ if (highContrast !== undefined) api.setAccessibilityPreference('highContrast', highContrast);
129
+ }, { theme: args.theme, highContrast: args.highContrast });
130
+ // Allow DOM to update after preference change
131
+ await new Promise(resolve => setTimeout(resolve, 50));
132
+ }
133
+ // Validate slide ID exists before navigating (avoids EventBus error cascades)
134
+ {
135
+ const validSlide = await headless.evaluate((slideId) => {
136
+ const toc = window.CourseCodeAutomation.getToc();
137
+ return toc.some(item => item.id === slideId);
138
+ }, args.slideId);
139
+ if (!validSlide) {
140
+ throw new Error(`Slide "${args.slideId}" not found. Use coursecode_state to get valid slide IDs.`);
141
+ }
142
+ }
143
+ await headless.evaluate((slideId) => {
144
+ window.CourseCodeAutomation.goToSlide(slideId);
145
+ }, args.slideId);
146
+ // State updates asynchronously after navigation
147
+ await new Promise(resolve => setTimeout(resolve, 50));
148
+ result = await headless.evaluate(() => {
149
+ const api = window.CourseCodeAutomation;
150
+ return {
151
+ slide: api.getCurrentSlide(),
152
+ interactions: api.listInteractions(),
153
+ engagement: api.getEngagementState(),
154
+ accessibility: api.getAccessibilityState()
155
+ };
156
+ });
157
+ break;
158
+
159
+ case 'coursecode_interact':
160
+ if (!args?.interactionId) throw new Error('interactionId is required');
161
+ if (args.response === undefined) throw new Error('response is required');
162
+ await ensureHeadless(port);
163
+ result = await headless.evaluate(({ interactionId, response }) => {
164
+ const api = window.CourseCodeAutomation;
165
+ api.setResponse(interactionId, response);
166
+ const checkResult = api.checkAnswer(interactionId);
167
+ return {
168
+ ...checkResult,
169
+ state: {
170
+ slide: api.getCurrentSlide(),
171
+ interactions: api.listInteractions()
172
+ }
173
+ };
174
+ }, { interactionId: args.interactionId, response: args.response });
175
+ break;
176
+
177
+ case 'coursecode_reset':
178
+ await ensureHeadless(port);
179
+ await headless.evaluate(() => {
180
+ localStorage.clear();
181
+ });
182
+ // Full browser restart to ensure clean state
183
+ await headless.shutdown();
184
+ await headless.launch(port);
185
+ result = { reset: true };
186
+ break;
187
+
188
+ case 'coursecode_screenshot':
189
+ await ensureHeadless(port);
190
+ // Validate slide ID if provided (same guard as navigate)
191
+ if (args?.slideId) {
192
+ const validSlide = await headless.evaluate((slideId) => {
193
+ const toc = window.CourseCodeAutomation.getToc();
194
+ return toc.some(item => item.id === slideId);
195
+ }, args.slideId);
196
+ if (!validSlide) {
197
+ throw new Error(`Slide "${args.slideId}" not found. Use coursecode_state to get valid slide IDs.`);
198
+ }
199
+ }
200
+ result = await headless.screenshot({
201
+ slideId: args?.slideId,
202
+ fullPage: args?.fullPage,
203
+ detailed: args?.detailed,
204
+ scrollY: args?.scrollY
205
+ });
206
+ return {
207
+ content: [{
208
+ type: 'image',
209
+ data: result.data,
210
+ mimeType: result.mimeType
211
+ }]
212
+ };
213
+
214
+ // === Workflow & build tools ===
215
+ case 'coursecode_workflow_status':
216
+ result = await getWorkflowStatusWithInstructions(port);
217
+ break;
218
+
219
+ case 'coursecode_build':
220
+ result = await buildCourse(args || {});
221
+ break;
222
+
223
+ // === Catalog & validation tools (filesystem, no preview needed) ===
224
+ case 'coursecode_css_catalog':
225
+ result = getCssCatalog(args?.category);
226
+ break;
227
+
228
+ case 'coursecode_component_catalog':
229
+ result = getComponentCatalog(args?.type);
230
+ break;
231
+ case 'coursecode_interaction_catalog':
232
+ result = getInteractionCatalog(args?.type);
233
+ break;
234
+ case 'coursecode_icon_catalog':
235
+ result = getIconCatalog(args?.name);
236
+ break;
237
+ case 'coursecode_lint':
238
+ result = await lintCourse();
239
+ break;
240
+
241
+ case 'coursecode_export_content':
242
+ result = await getContentExport(args || {});
243
+ if (result === null) {
244
+ throw new Error('Failed to export content. Ensure course-config.js exists in course/');
245
+ }
246
+ return {
247
+ content: [{
248
+ type: 'text',
249
+ text: result
250
+ }]
251
+ };
252
+
253
+ default:
254
+ throw new Error(`Unknown tool: ${name}`);
255
+ }
256
+
257
+ return {
258
+ content: [{
259
+ type: 'text',
260
+ text: JSON.stringify(result, null, 2)
261
+ }]
262
+ };
263
+ } catch (error) {
264
+ return {
265
+ content: [{
266
+ type: 'text',
267
+ text: `Error: ${error.message}`
268
+ }],
269
+ isError: true
270
+ };
271
+ }
272
+ });
273
+
274
+ // Graceful shutdown — clean up headless browser only.
275
+ // browser.close() tears down the entire Chrome process tree (parent + all
276
+ // child processes like GPU, renderer, utility). No manual PID hunting needed.
277
+ let cleaningUp = false;
278
+ const cleanup = async () => {
279
+ if (cleaningUp) return;
280
+ cleaningUp = true;
281
+ if (headless.isRunning()) {
282
+ await headless.shutdown();
283
+ }
284
+ };
285
+
286
+ process.on('SIGINT', async () => { await cleanup(); process.exit(0); });
287
+ process.on('SIGTERM', async () => { await cleanup(); process.exit(0); });
288
+
289
+ // Detect MCP host disconnect — when the IDE/editor drops the stdio pipe,
290
+ // stdin emits 'close'. This is the primary cleanup signal since SIGINT/SIGTERM
291
+ // are NOT sent when the host simply closes the transport.
292
+ process.stdin.on('close', async () => { await cleanup(); process.exit(0); });
293
+
294
+ process.on('uncaughtException', async (err) => {
295
+ process.stderr.write(`MCP uncaught exception: ${err.message}\n`);
296
+ await cleanup();
297
+ process.exit(1);
298
+ });
299
+ process.on('unhandledRejection', async (err) => {
300
+ process.stderr.write(`MCP unhandled rejection: ${err}\n`);
301
+ await cleanup();
302
+ process.exit(1);
303
+ });
304
+
305
+ // Start the server
306
+ const transport = new StdioServerTransport();
307
+ await server.connect(transport);
308
+ }
309
+
310
+ // CLI entry point
311
+ if (process.argv[1]?.endsWith('mcp-server.js')) {
312
+ const args = process.argv.slice(2);
313
+ const portArg = args.find(a => a.startsWith('--port='));
314
+ const port = portArg ? parseInt(portArg.split('=')[1], 10) : DEFAULT_PORT;
315
+ startMcpServer({ port });
316
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Narration command - generate audio narration from text
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import path from 'path';
7
+ import fs from 'fs';
8
+ import { validateProject } from './project-utils.js';
9
+
10
+
11
+ export async function narration(options = {}) {
12
+ validateProject();
13
+
14
+ const scriptPath = path.join(process.cwd(), 'framework', 'scripts', 'generate-narration.js');
15
+
16
+ if (!fs.existsSync(scriptPath)) {
17
+ console.error(`
18
+ ❌ Narration script not found at: framework/scripts/generate-narration.js
19
+
20
+ Make sure your framework is up to date:
21
+ scorm upgrade
22
+ `);
23
+ process.exit(1);
24
+ }
25
+
26
+ console.log(`
27
+ 🎙️ Generating audio narration...
28
+ `);
29
+
30
+ // Build args
31
+ const args = [scriptPath];
32
+ if (options.force) args.push('--force');
33
+ if (options.slide) args.push('--slide', options.slide);
34
+ if (options.dryRun) args.push('--dry-run');
35
+
36
+ return new Promise((resolve, reject) => {
37
+ const child = spawn('node', args, {
38
+ cwd: process.cwd(),
39
+ stdio: 'inherit',
40
+ shell: true
41
+ });
42
+
43
+ child.on('close', (code) => {
44
+ if (code === 0) {
45
+ resolve();
46
+ } else {
47
+ reject(new Error(`Narration generation failed with code ${code}`));
48
+ }
49
+ });
50
+
51
+ child.on('error', reject);
52
+ });
53
+ }
@@ -0,0 +1,142 @@
1
+ import PDFParser from 'pdf2json';
2
+
3
+
4
+ /**
5
+ * Custom PDF Structure Parser
6
+ * Uses pdf2json to extract text with layout information, then applies heuristics
7
+ * to identify headers and reconstruct document structure.
8
+ */
9
+ export function parsePdfStructure(filePath) {
10
+ const pdfParser = new PDFParser();
11
+
12
+ return new Promise((resolve, reject) => {
13
+ pdfParser.on('pdfParser_dataError', errData => reject(errData.parserError));
14
+ pdfParser.on('pdfParser_dataReady', pdfData => {
15
+ try {
16
+ const structure = processPdfData(pdfData);
17
+ resolve(structure);
18
+ } catch (err) {
19
+ reject(err);
20
+ }
21
+ });
22
+
23
+ pdfParser.loadPDF(filePath);
24
+ });
25
+ }
26
+
27
+ function processPdfData(pdfData) {
28
+ const pages = pdfData.Pages || [];
29
+ let allTextBlocks = [];
30
+
31
+ // 1. Flatten all text blocks across pages
32
+ pages.forEach((page, pageIndex) => {
33
+ const texts = page.Texts || [];
34
+ texts.forEach(text => {
35
+ // text.R is an array of runs (usually 1)
36
+ // text.x, text.y are position units (approx 1/25th of an inch?)
37
+
38
+ const content = text.R.map(run => decodeURIComponent(run.T)).join('');
39
+
40
+ // TS = [fontId, fontSize, isBold, isItalic]
41
+ // We need to parse TS from the first run
42
+ const run = text.R[0];
43
+ const fontSize = run?.TS?.[1] || 12;
44
+ const isBold = run?.TS?.[2] === 1;
45
+
46
+ if (content.trim()) {
47
+ allTextBlocks.push({
48
+ text: content,
49
+ x: text.x,
50
+ y: text.y + (pageIndex * 100), // Global Y including page offset (heuristic 100 units per page)
51
+ w: text.w,
52
+ fontSize,
53
+ isBold,
54
+ page: pageIndex + 1
55
+ });
56
+ }
57
+ });
58
+ });
59
+
60
+ // 2. Determine "Body Text" font size (most common size)
61
+ const sizeCounts = {};
62
+ allTextBlocks.forEach(block => {
63
+ const size = Math.round(block.fontSize * 10) / 10;
64
+ sizeCounts[size] = (sizeCounts[size] || 0) + 1;
65
+ });
66
+
67
+ let bodySize = 0;
68
+ let maxCount = 0;
69
+ for (const [size, count] of Object.entries(sizeCounts)) {
70
+ if (count > maxCount) {
71
+ maxCount = count;
72
+ bodySize = parseFloat(size);
73
+ }
74
+ }
75
+
76
+ // 3. Sort by Page -> Y -> X
77
+ allTextBlocks.sort((a, b) => {
78
+ if (a.page !== b.page) return a.page - b.page;
79
+ if (Math.abs(a.y - b.y) > 0.5) return a.y - b.y; // Roughly same line
80
+ return a.x - b.x;
81
+ });
82
+
83
+ // 4. Cluster into elements
84
+ const elements = [];
85
+ let currentElement = null;
86
+
87
+ allTextBlocks.forEach((block, index) => {
88
+ const isHeader = block.fontSize > bodySize * 1.1 || (block.isBold && block.fontSize >= bodySize);
89
+ const headingLevel = isHeader ? calculateHeaderLevel(block.fontSize, bodySize) : 0;
90
+ const type = isHeader ? `h${headingLevel}` : 'p';
91
+
92
+ // Check proximity to previous block
93
+ const prevBlock = allTextBlocks[index - 1];
94
+
95
+ // Vertical gap check (heuristic)
96
+ // If gap is small, it's likely the same paragraph
97
+ // If gap is large, it's a new paragraph
98
+ // If type changed (header <-> p), it's a new element
99
+
100
+ let isSameElement = false;
101
+ if (currentElement && currentElement.type === type && prevBlock) {
102
+ const verticalGap = block.y - prevBlock.y;
103
+ // Line height heuristic: adjacent lines are usually < 1.5 units apart in pdf2json coords
104
+ if (verticalGap < 2 && verticalGap > -0.5) {
105
+ isSameElement = true;
106
+ }
107
+ }
108
+
109
+ if (isSameElement) {
110
+ currentElement.text += ' ' + decode(block.text);
111
+ } else {
112
+ if (currentElement) elements.push(currentElement);
113
+ currentElement = {
114
+ type,
115
+ text: decode(block.text),
116
+ page: block.page,
117
+ metadata: {
118
+ fontSize: block.fontSize,
119
+ isBold: block.isBold
120
+ }
121
+ };
122
+ }
123
+ });
124
+
125
+ if (currentElement) elements.push(currentElement);
126
+
127
+ return {
128
+ metadata: pdfData.Meta,
129
+ elements
130
+ };
131
+ }
132
+
133
+ function calculateHeaderLevel(size, bodySize) {
134
+ const ratio = size / bodySize;
135
+ if (ratio > 1.5) return 1;
136
+ if (ratio > 1.2) return 2;
137
+ return 3;
138
+ }
139
+
140
+ function decode(str) {
141
+ return str.replace(/\s+/g, ' ').trim();
142
+ }