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,413 @@
1
+ /**
2
+ * Persistent Headless Browser for MCP Runtime Tools
3
+ *
4
+ * Manages a long-lived headless Chrome instance that loads the preview URL.
5
+ * All runtime tools (state, navigate, interact, reset, screenshot) execute
6
+ * directly in the browser via page.evaluate(), replacing the old WebSocket relay.
7
+ *
8
+ * Uses puppeteer-core with the system-installed Chrome — no bundled browser.
9
+ */
10
+
11
+ import puppeteer from 'puppeteer-core';
12
+ import http from 'http';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+
17
+ /**
18
+ * Find the Chrome executable on the current platform.
19
+ * @returns {string|null} Path to Chrome or null if not found
20
+ */
21
+ function findChrome() {
22
+ // Allow explicit override
23
+ if (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH)) {
24
+ return process.env.CHROME_PATH;
25
+ }
26
+
27
+ const platform = os.platform();
28
+ const candidates = [];
29
+
30
+ if (platform === 'darwin') {
31
+ candidates.push(
32
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
33
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
34
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
35
+ path.join(os.homedir(), 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
36
+ );
37
+ } else if (platform === 'linux') {
38
+ candidates.push(
39
+ '/usr/bin/google-chrome',
40
+ '/usr/bin/google-chrome-stable',
41
+ '/usr/bin/chromium-browser',
42
+ '/usr/bin/chromium',
43
+ '/snap/bin/chromium'
44
+ );
45
+ } else if (platform === 'win32') {
46
+ const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files';
47
+ const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
48
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
49
+ candidates.push(
50
+ path.join(programFiles, 'Google/Chrome/Application/chrome.exe'),
51
+ path.join(programFilesX86, 'Google/Chrome/Application/chrome.exe'),
52
+ path.join(localAppData, 'Google/Chrome/Application/chrome.exe')
53
+ );
54
+ }
55
+
56
+ return candidates.find(p => fs.existsSync(p)) || null;
57
+ }
58
+
59
+ /**
60
+ * Persistent headless browser manager.
61
+ * Singleton — one browser per MCP server session.
62
+ */
63
+ class HeadlessBrowser {
64
+ constructor() {
65
+ this.browser = null;
66
+ this.page = null;
67
+ this.courseFrame = null;
68
+ this.port = null;
69
+ this.chromePath = null;
70
+ this._sseReloadListener = null;
71
+ this._reconnectTimer = null;
72
+ this._stopped = false;
73
+ this._consoleLogs = [];
74
+ }
75
+
76
+ /**
77
+ * Launch headless Chrome and navigate to the preview URL.
78
+ * @param {number} port - Preview server port
79
+ * @returns {Promise<void>}
80
+ */
81
+ async launch(port) {
82
+ this.port = port;
83
+ this.chromePath = findChrome();
84
+
85
+ if (!this.chromePath) {
86
+ throw new Error(
87
+ 'Chrome not found. Install Google Chrome or set CHROME_PATH environment variable.\n' +
88
+ ' macOS: Install from https://www.google.com/chrome/\n' +
89
+ ' Linux: apt install google-chrome-stable\n' +
90
+ ' Windows: Install from https://www.google.com/chrome/'
91
+ );
92
+ }
93
+
94
+ this._stopped = false;
95
+ this.browser = await puppeteer.launch({
96
+ executablePath: this.chromePath,
97
+ headless: true,
98
+ args: [
99
+ '--no-sandbox',
100
+ '--disable-gpu',
101
+ '--disable-dev-shm-usage',
102
+ '--disable-extensions',
103
+ '--no-first-run',
104
+ '--mute-audio'
105
+ ]
106
+ });
107
+
108
+ this.page = await this.browser.newPage();
109
+ await this.page.setViewport({ width: 1280, height: 720 });
110
+
111
+ // Capture console warnings and errors
112
+ this._consoleLogs = [];
113
+ this.page.on('console', msg => {
114
+ const type = msg.type();
115
+ if (type === 'warning' || type === 'error') {
116
+ this._consoleLogs.push({
117
+ type,
118
+ text: msg.text(),
119
+ time: new Date().toISOString()
120
+ });
121
+ // Cap buffer
122
+ if (this._consoleLogs.length > 50) this._consoleLogs.shift();
123
+ }
124
+ });
125
+
126
+ // Navigate to preview and wait for course to load
127
+ await this._navigateToPreview();
128
+
129
+ // Listen for SSE reload events to auto-refresh on rebuild
130
+ this._startReloadListener();
131
+ }
132
+
133
+ /**
134
+ * Navigate to the preview URL and locate the course iframe.
135
+ * @private
136
+ */
137
+ async _navigateToPreview() {
138
+ const url = `http://localhost:${this.port}?headless`;
139
+ await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 3000 });
140
+
141
+ // The course runs inside an iframe — find it
142
+ await this._locateCourseFrame();
143
+ }
144
+
145
+ /**
146
+ * Find and cache the course iframe reference.
147
+ * The stub player loads the course at /course/index.html in an iframe.
148
+ * @private
149
+ */
150
+ async _locateCourseFrame() {
151
+ // Poll for the course frame — the iframe DOM element exists immediately
152
+ // but its content (/course/index.html) loads asynchronously.
153
+ const startTime = Date.now();
154
+ const timeout = 3000;
155
+
156
+ while (Date.now() - startTime < timeout) {
157
+ const frames = this.page.frames();
158
+ this.courseFrame = frames.find(f => f.url().includes('/course/'));
159
+ if (this.courseFrame) break;
160
+ await new Promise(r => setTimeout(r, 200));
161
+ }
162
+
163
+ if (!this.courseFrame) {
164
+ throw new Error('Could not locate course iframe in preview page');
165
+ }
166
+
167
+ // Wait for the framework to fully initialize (NavigationActions.init() etc.)
168
+ // CourseCodeAutomation exists early but .ready is set after full boot
169
+ await this.courseFrame.waitForFunction(
170
+ () => window.CourseCodeAutomation?.ready === true,
171
+ { timeout: 10000 }
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Listen for SSE reload events from the preview server.
177
+ * When Vite rebuilds, refresh the headless browser.
178
+ * @private
179
+ */
180
+ _startReloadListener() {
181
+ const reloadUrl = `http://localhost:${this.port}/__reload`;
182
+
183
+ const connect = () => {
184
+ if (this._stopped) return;
185
+
186
+ const req = http.get(reloadUrl, (res) => {
187
+ res.on('data', async (chunk) => {
188
+ if (this._stopped) return;
189
+ const data = chunk.toString();
190
+ if (data.includes('data: reload')) {
191
+ try {
192
+ await this._navigateToPreview();
193
+ } catch (_e) {
194
+ // Preview may be mid-rebuild, retry will happen on next SSE
195
+ }
196
+ }
197
+ });
198
+
199
+ res.on('end', () => {
200
+ if (this._stopped) return;
201
+ this._reconnectTimer = setTimeout(connect, 1000);
202
+ });
203
+ });
204
+
205
+ req.on('error', () => {
206
+ if (this._stopped) return;
207
+ this._reconnectTimer = setTimeout(connect, 2000);
208
+ });
209
+
210
+ this._sseReloadListener = req;
211
+ };
212
+
213
+ connect();
214
+ }
215
+
216
+ /**
217
+ * Execute a function in the course iframe's context.
218
+ * This is how runtime tools call CourseCodeAutomation methods.
219
+ *
220
+ * @param {Function} fn - Function to evaluate (receives window.CourseCodeAutomation)
221
+ * @param {...*} args - Arguments to pass to the function
222
+ * @returns {Promise<*>} Result of the function
223
+ */
224
+ async evaluate(fn, ...args) {
225
+ this._ensureRunning();
226
+ await this._ensureCourseFrame();
227
+ return this.courseFrame.evaluate(fn, ...args);
228
+ }
229
+
230
+ /**
231
+ * Execute a function in the main page context (stub player, NOT the course iframe).
232
+ * Use this to access stub player state like cmiData, apiLog, errorLog.
233
+ *
234
+ * @param {Function} fn - Function to evaluate in the parent page
235
+ * @param {...*} args - Arguments to pass to the function
236
+ * @returns {Promise<*>} Result of the function
237
+ */
238
+ async evaluateParent(fn, ...args) {
239
+ this._ensureRunning();
240
+ return this.page.evaluate(fn, ...args);
241
+ }
242
+
243
+ /**
244
+ * Get and clear buffered console warnings/errors.
245
+ * @returns {Array<{type: string, text: string, time: string}>}
246
+ */
247
+ getConsoleLogs() {
248
+ const logs = this._consoleLogs.slice();
249
+ this._consoleLogs = [];
250
+ return logs;
251
+ }
252
+
253
+ /**
254
+ * Take a screenshot of the current page.
255
+ *
256
+ * Two quality modes optimize for token efficiency:
257
+ * - normal (default): 800×450 JPEG@70 (~30-60KB) — quick layout checks
258
+ * - detailed: 1280×720 JPEG@90 (~100-200KB) — close text/element inspection
259
+ *
260
+ * fullPage captures the entire scrollable course content by screenshotting
261
+ * the course iframe element directly (bypasses the stub player chrome).
262
+ *
263
+ * @param {object} options
264
+ * @param {string} [options.slideId] - Navigate to this slide before screenshotting
265
+ * @param {boolean} [options.fullPage=false] - Capture full scrollable content
266
+ * @param {boolean} [options.detailed=false] - Use high-res detailed mode
267
+ * @returns {Promise<{data: string, mimeType: string}>} Base64-encoded JPEG
268
+ */
269
+ async screenshot(options = {}) {
270
+ this._ensureRunning();
271
+
272
+ const { slideId, fullPage = false, detailed = false, scrollY } = options;
273
+
274
+ // Screenshot quality presets
275
+ const preset = detailed
276
+ ? { width: 1280, height: 720, quality: 90 }
277
+ : { width: 800, height: 450, quality: 70 };
278
+
279
+ // Navigate to specific slide if requested
280
+ if (slideId) {
281
+ await this.evaluate((id) => {
282
+ window.CourseCodeAutomation.goToSlide(id);
283
+ }, slideId);
284
+ // Wait for slide transition
285
+ await new Promise(resolve => setTimeout(resolve, 500));
286
+ }
287
+
288
+ // Scroll within the course iframe if requested
289
+ if (scrollY !== undefined && scrollY > 0) {
290
+ await this._ensureCourseFrame();
291
+ await this.courseFrame.evaluate((y) => {
292
+ const container = document.querySelector('.slide-content')
293
+ || document.querySelector('[class*="slide"]')
294
+ || document.documentElement;
295
+ container.scrollTop = y;
296
+ }, scrollY);
297
+ await new Promise(resolve => setTimeout(resolve, 100));
298
+ }
299
+
300
+ // Resize viewport for the chosen mode
301
+ await this.page.setViewport({ width: preset.width, height: preset.height });
302
+ // Brief settle after resize
303
+ await new Promise(resolve => setTimeout(resolve, 100));
304
+
305
+ let screenshotBuffer;
306
+
307
+ if (fullPage) {
308
+ // For fullPage, measure the iframe content's full scrollable height,
309
+ // temporarily expand the viewport so nothing is clipped, then screenshot.
310
+ // ElementHandle.screenshot() only captures the element's bounding box,
311
+ // so we need to make the viewport tall enough to show everything.
312
+ await this._ensureCourseFrame();
313
+ const contentHeight = await this.courseFrame.evaluate(() => {
314
+ return Math.max(
315
+ document.body.scrollHeight,
316
+ document.documentElement.scrollHeight
317
+ );
318
+ });
319
+
320
+ const fullHeight = Math.max(contentHeight, preset.height);
321
+ await this.page.setViewport({ width: preset.width, height: fullHeight });
322
+ await new Promise(resolve => setTimeout(resolve, 200));
323
+
324
+ const iframeElement = await this.page.$('iframe');
325
+ if (iframeElement) {
326
+ screenshotBuffer = await iframeElement.screenshot({
327
+ type: 'jpeg',
328
+ quality: preset.quality
329
+ });
330
+ } else {
331
+ screenshotBuffer = await this.page.screenshot({
332
+ type: 'jpeg',
333
+ quality: preset.quality,
334
+ fullPage: true
335
+ });
336
+ }
337
+ } else {
338
+ screenshotBuffer = await this.page.screenshot({
339
+ type: 'jpeg',
340
+ quality: preset.quality
341
+ });
342
+ }
343
+
344
+ // Restore default viewport
345
+ await this.page.setViewport({ width: 1280, height: 720 });
346
+
347
+ return {
348
+ data: screenshotBuffer.toString('base64'),
349
+ mimeType: 'image/jpeg'
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Ensure the browser is running, throw if not.
355
+ * @private
356
+ */
357
+ _ensureRunning() {
358
+ if (!this.browser || !this.page) {
359
+ throw new Error('Headless browser not running. Call launch() first.');
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Ensure the course frame reference is still valid (survives page reloads).
365
+ * @private
366
+ */
367
+ async _ensureCourseFrame() {
368
+ try {
369
+ // Quick check that the frame is still attached
370
+ await this.courseFrame.evaluate(() => true);
371
+ } catch (_e) {
372
+ // Frame detached (page reloaded), re-locate it
373
+ await this._locateCourseFrame();
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Check if the browser is running.
379
+ * @returns {boolean}
380
+ */
381
+ isRunning() {
382
+ return this.browser !== null && this.browser.connected;
383
+ }
384
+
385
+ /**
386
+ * Cleanly shut down the browser.
387
+ */
388
+ async shutdown() {
389
+ this._stopped = true;
390
+
391
+ if (this._reconnectTimer) {
392
+ clearTimeout(this._reconnectTimer);
393
+ this._reconnectTimer = null;
394
+ }
395
+
396
+ if (this._sseReloadListener) {
397
+ this._sseReloadListener.destroy();
398
+ this._sseReloadListener = null;
399
+ }
400
+
401
+ if (this.browser) {
402
+ await this.browser.close();
403
+ this.browser = null;
404
+ this.page = null;
405
+ this.courseFrame = null;
406
+ }
407
+ }
408
+ }
409
+
410
+ // Singleton instance
411
+ const headless = new HeadlessBrowser();
412
+ export default headless;
413
+ export { findChrome };