coursecode 0.1.44 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -175,12 +175,27 @@ The preview server provides:
175
175
 
176
176
  When ready, deploy:
177
177
 
178
- **With [CourseCode Cloud](https://coursecodecloud.com)**: Push your course and get a live link. Cloud handles hosting, generates any LMS format on demand, and gives you sharable preview links with optional password protection. No ZIP files, no manual uploads.
178
+ **With [CourseCode Cloud](https://coursecodecloud.com)**: Push your course and get a live link. Cloud handles hosting, generates any LMS format on demand, and gives you a shareable preview link with optional password protection. No ZIP files, no manual uploads.
179
179
 
180
180
  ```bash
181
181
  coursecode deploy
182
182
  ```
183
183
 
184
+ For stakeholder review, deploy a preview-only version and password-protect the preview link:
185
+
186
+ ```bash
187
+ coursecode deploy --preview --password
188
+ ```
189
+
190
+ You can inspect recent deployments and move pointers without rebuilding:
191
+
192
+ ```bash
193
+ coursecode deployments
194
+ coursecode promote --preview
195
+ coursecode promote --production
196
+ coursecode preview-link --password
197
+ ```
198
+
184
199
  If the cloud course was deleted but the project still has the old local binding, redeploy with:
185
200
 
186
201
  ```bash
@@ -212,6 +227,9 @@ coursecode preview --export
212
227
  | `coursecode lint` | Validate course structure and content |
213
228
  | `coursecode build` | Build a package for LMS upload |
214
229
  | `coursecode deploy` | Build and deploy to CourseCode Cloud |
230
+ | `coursecode deployments` | List recent Cloud deployments |
231
+ | `coursecode promote` | Move the Production or Preview pointer |
232
+ | `coursecode preview-link` | Manage the Cloud preview link |
215
233
  | `coursecode narration` | Generate audio narration from text |
216
234
 
217
235
  For the full command list and deployment options, see the [User Guide](framework/docs/USER_GUIDE.md#sharing-and-deploying) or run `coursecode --help`.
package/bin/cli.js CHANGED
@@ -365,7 +365,7 @@ program
365
365
  .option('--promote', 'Force-promote: always move production pointer regardless of deploy_mode setting. Mutually exclusive with --stage.')
366
366
  .option('--stage', 'Force-stage: never move production pointer regardless of deploy_mode setting. Mutually exclusive with --promote.')
367
367
  .option('--repair-binding', 'Clear a stale local Cloud binding if the remote course was deleted, then continue')
368
- .option('--password', 'Password-protect preview (interactive prompt, requires --preview)')
368
+ .option('--password [password]', 'Password-protect preview. Prompts if no value is provided.')
369
369
  .option('-m, --message <message>', 'Deploy reason (e.g. "Fixed accessibility issues")')
370
370
  .option('--local', 'Use local Cloud instance (http://localhost:3000)')
371
371
  .option('--json', 'Emit machine-readable JSON result')
@@ -404,6 +404,17 @@ program
404
404
  await status({ json: options.json, repairBinding: options.repairBinding });
405
405
  });
406
406
 
407
+ program
408
+ .command('deployments')
409
+ .description('List recent Cloud deployments for current course')
410
+ .option('--local', 'Use local Cloud instance (http://localhost:3000)')
411
+ .option('--json', 'Output raw JSON')
412
+ .action(async (options) => {
413
+ const { deployments, setLocalMode } = await import('../lib/cloud.js');
414
+ if (options.local) setLocalMode();
415
+ await deployments({ json: options.json });
416
+ });
417
+
407
418
  program
408
419
  .command('preview-link')
409
420
  .description('Show or update the Cloud preview link for the current course')
@@ -679,7 +679,12 @@ Runs **in Node.js** during build (via `vite.framework-dev.config.js` `closeBundl
679
679
 
680
680
  ### MCP `coursecode_lint` — Build-Time Only
681
681
 
682
- The MCP `coursecode_lint` tool runs the build linter (config validation, CSS class verification, structure checks). It does NOT include runtime errors. For runtime errors and contrast warnings, use `coursecode_errors` (lightweight just errors and console logs) or `coursecode_state` (full state snapshot including errors).
682
+ The MCP `coursecode_lint` tool runs the static/build-time linter (config validation, CSS class verification, structure checks). It does **not** inspect the running preview and does **not** include live runtime, browser console, or Vite build-watch diagnostics.
683
+
684
+ Use:
685
+ - `coursecode_lint` for static preflight validation after source/config edits
686
+ - `coursecode_errors` for the live "what is broken right now?" preview rollup
687
+ - `coursecode_state` when you also need current slide, TOC, engagement, LMS state, and diagnostics
683
688
 
684
689
  ### Shared Rules (`lib/validation-rules.js`)
685
690
 
@@ -861,16 +866,18 @@ The MCP server runs a **persistent headless Chrome** internally via `puppeteer-c
861
866
 
862
867
  ### Preview Server Ownership
863
868
 
864
- The MCP does **not** start or manage the preview server. The preview must be running before using runtime tools:
869
+ The MCP does **not** start or manage the preview server. Runtime tools connect to an already-running preview server.
865
870
 
866
- - **Human**: run `coursecode preview` (or `npm run preview`) in a terminal
867
- - **AI agent**: use your terminal/command execution tool to run `npm run preview`
871
+ - If preview is already running for the current project, use it. Do **not** start a second preview server.
872
+ - If preview is not running, start it in a terminal with `coursecode preview`.
873
+ - For framework development from this repo, use `npm run preview`.
874
+ - AI agents may start preview only via their terminal/command execution tool, and only after confirming preview is not already running or after a runtime MCP tool reports that preview is not running.
868
875
 
869
876
  If the preview is not running, runtime tools fail fast with a clear error message.
870
877
 
871
878
  ### Setup
872
879
 
873
- 1. Start the preview server externally (see above)
880
+ 1. Make sure the preview server is running externally (see above)
874
881
  2. Add to IDE MCP config:
875
882
 
876
883
  ```json
@@ -885,10 +892,12 @@ If the preview is not running, runtime tools fail fast with a clear error messag
885
892
 
886
893
  ### Runtime Tools (require preview server)
887
894
 
895
+ Tool results include machine-readable `structuredContent` plus text content for compatibility. Tool failures use structured error payloads with stable `code`, `message`, `hint`, and optional `details` fields so AI clients can recover without parsing prose.
896
+
888
897
  | Tool | Purpose | Returns |
889
898
  |------|---------|--------|
890
- | `coursecode_state` | Full course snapshot | `{slide, toc, interactions, engagement, lmsState, apiLog, errors, frameworkLogs, consoleLogs}` |
891
- | `coursecode_errors` | Errors + console logs only | `{errors, consoleLogs, count, clean}` — same error sources as `coursecode_state`, without the state payload |
899
+ | `coursecode_state` | Full course snapshot + live diagnostics | `{slide, toc, interactions, engagement, lmsState, apiLog, diagnostics, issues, errors, frameworkLogs, consoleLogs}` |
900
+ | `coursecode_errors` | Live diagnostic rollup only | `{build, runtime, framework, console, issues, errors, count, clean}` — same diagnostic sources as `coursecode_state`, without the state payload |
892
901
  | `coursecode_navigate` | Go to slide by ID | `{slide, interactions, engagement, accessibility}` |
893
902
  | `coursecode_interact` | Set response + evaluate | `{interactionId, response}` → `{correct, score, feedback}` |
894
903
  | `coursecode_screenshot` | Visual capture (JPEG) | Optional `slideId` to navigate first, `fullPage` for scroll capture |
@@ -933,7 +942,7 @@ MCP Server (IDE) ──puppeteer──▶ Headless Chrome ──HTTP──▶ Pr
933
942
  └── Course iframe (CourseCodeAutomation API)
934
943
  ```
935
944
 
936
- - **Preview not running?** → Tools return clear error: "Start preview server first"
945
+ - **Preview not running?** → Tools return a clear error. Start preview externally in a terminal, then retry.
937
946
  - **Chrome not found?** → Install Google Chrome or set `CHROME_PATH` env var
938
947
 
939
948
  ### Pre-Release Responsive Checks (Framework)
@@ -297,7 +297,8 @@ Once connected, your AI assistant gains these capabilities:
297
297
  | `coursecode_screenshot` | Take a screenshot of any slide |
298
298
  | `coursecode_interact` | Answer an interaction and check if it's correct |
299
299
  | `coursecode_reset` | Clear progress and restart the course |
300
- | `coursecode_lint` | Check for errors (bad CSS classes, missing components, config issues) |
300
+ | `coursecode_errors` | Check live preview diagnostics (build, runtime, framework, and console issues) |
301
+ | `coursecode_lint` | Run static preflight checks (bad CSS classes, missing components, config issues) |
301
302
  | `coursecode_component_catalog` | Browse available UI components (tabs, accordion, cards, etc.) |
302
303
  | `coursecode_interaction_catalog` | Browse available interaction types (multiple choice, drag-drop, etc.) |
303
304
  | `coursecode_css_catalog` | Browse available CSS classes by category |
@@ -306,7 +307,7 @@ Once connected, your AI assistant gains these capabilities:
306
307
  | `coursecode_workflow_status` | Get guidance on what to do next based on your project's current state |
307
308
  | `coursecode_build` | Build the course for LMS deployment |
308
309
 
309
- > **Note:** The preview server must be running before using runtime tools like `coursecode_state`, `coursecode_screenshot`, or `coursecode_navigate`. Start it with `coursecode preview` in a terminal.
310
+ > **Note:** The preview server must be running before using runtime tools like `coursecode_state`, `coursecode_errors`, `coursecode_screenshot`, or `coursecode_navigate`. If preview is not already running for this project, start it with `coursecode preview` in a terminal. Do not start a second preview server if one is already running.
310
311
 
311
312
  ### How the Workflow Changes
312
313
 
@@ -740,18 +741,42 @@ Open the URL in any browser, log in with your CourseCode account, and enter the
740
741
  - **Preview pointer** — the version served on the cloud preview link (for stakeholder review).
741
742
  - **deploy_mode** — a per-course or org setting in the Cloud dashboard. Default is auto-promote (new uploads immediately go live). Can be set to staged (new uploads require a manual promote step).
742
743
  - `--promote` and `--stage` are mutually exclusive.
744
+ - `--password` can be combined with `--preview` to create or update the main preview link password. If you omit the password value in an interactive terminal, the CLI prompts for it. In `--json` mode you must pass the value explicitly.
743
745
  - **GitHub-linked courses:** If your course is connected to a GitHub repo in the Cloud dashboard, production deploys happen via `git push` — the CLI blocks direct production uploads. Use `coursecode deploy --preview` to push a preview build for stakeholder review.
744
746
  - If a cloud deployment was deleted outside the CLI and this project still has the old local binding, rerun with `coursecode deploy --repair-binding`. To clear the stale binding without deploying yet, run `coursecode status --repair-binding`.
745
747
 
748
+ **Managing previews and pointers after deploy:**
749
+
750
+ Use these commands when you want to change Cloud state without rebuilding the course:
751
+
752
+ ```bash
753
+ coursecode status
754
+ coursecode deployments
755
+ coursecode promote --preview
756
+ coursecode promote --production
757
+ coursecode preview-link --enable
758
+ coursecode preview-link --password
759
+ coursecode preview-link --remove-password
760
+ coursecode preview-link --expires-in-days 7
761
+ coursecode preview-link --disable
762
+ ```
763
+
764
+ - `coursecode deployments` lists recent immutable deployments and marks the current Production and Preview pointers.
765
+ - `coursecode promote --preview` moves the Preview pointer to an existing deployment. If you do not pass `--deployment <id>`, the CLI prompts you to pick from recent deployments.
766
+ - `coursecode promote --production` moves the Production pointer. Preview-only deployments cannot be promoted to Production.
767
+ - `coursecode preview-link` manages the main preview link. That link follows the Preview pointer, so the URL can stay the same while you choose which deployment reviewers see.
768
+ - Cloud can also create additional pinned preview links for specific deployments in the web app. The main CLI preview link is the pointer-following link.
769
+
746
770
  **Typical Cloud workflow:**
747
771
  1. Run `coursecode login` once, open the URL shown, and enter the code.
748
- 2. Run `coursecode deploy` from your project folder.
772
+ 2. Run `coursecode deploy` from your project folder, or `coursecode deploy --preview --password` for a password-protected review build.
749
773
  3. Open the CourseCode Cloud dashboard link shown after deploy.
750
- 4. Use Cloud preview links for review.
751
- 5. Download the LMS format you need from Cloud when you're ready to deliver.
774
+ 4. Use the main preview link for review.
775
+ 5. Move the Preview or Production pointer when needed.
776
+ 6. Download the LMS format you need from Cloud when you're ready to deliver.
752
777
 
753
778
  **Prefer a GUI instead of the terminal?**
754
- - Use **CourseCode Desktop** for the same project workflow with buttons for Preview / Export / Deploy.
779
+ - Use **CourseCode Desktop** for the same project workflow with buttons for Preview / Export / Deploy, plus a focused Cloud Deployments panel for preview-link password/expiry management, recent deployments, and Production/Preview pointer changes.
755
780
  - Desktop docs: `coursecode-desktop/USER_GUIDE.md`
756
781
 
757
782
  **When to use Cloud vs local export:**
@@ -761,7 +786,7 @@ Open the URL in any browser, log in with your CourseCode account, and enter the
761
786
  **Benefits:**
762
787
  - **No format decisions** — download the right ZIP for any LMS directly from the cloud
763
788
  - **Instant updates** — redeploy and all future launches get the new version
764
- - **Preview sharing** — cloud provides a shareable preview link automatically
789
+ - **Preview sharing** — cloud provides a shareable preview link that can be password-protected and pointed at the review deployment you choose
765
790
 
766
791
  ### Exporting Content for Review
767
792
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import interactionRegistry from '../managers/interaction-registry.js';
8
8
  import { courseConfig } from '../../../course/course-config.js';
9
- import { recordInteractionResult } from '../components/interactions/interaction-base.js';
9
+ import { getInteractionState, recordInteractionResult } from '../components/interactions/interaction-base.js';
10
10
  import { logger } from '../utilities/logger.js';
11
11
 
12
12
  /**
@@ -18,11 +18,26 @@ export function createInteractionMethods(logTrace) {
18
18
  return {
19
19
  listInteractions() {
20
20
  const interactions = interactionRegistry.getAll();
21
- const simplifiedList = interactions.map(i => ({
22
- id: i.id,
23
- type: i.type,
24
- description: i.description
25
- }));
21
+ const simplifiedList = interactions.map(i => {
22
+ const savedState = getInteractionState(i.id);
23
+ let response = savedState?.response;
24
+
25
+ if (response === undefined && typeof i.instance?.getResponse === 'function') {
26
+ try {
27
+ response = i.instance.getResponse();
28
+ } catch {
29
+ response = undefined;
30
+ }
31
+ }
32
+
33
+ return {
34
+ id: i.id,
35
+ type: i.type,
36
+ description: i.description,
37
+ hasResponse: response !== null && response !== undefined && response !== '',
38
+ isChecked: savedState?.submitted === true
39
+ };
40
+ });
26
41
  logTrace('listInteractions', { count: simplifiedList.length });
27
42
  return simplifiedList;
28
43
  },
@@ -33,10 +48,23 @@ export function createInteractionMethods(logTrace) {
33
48
  throw new Error(`CourseCodeAutomation: Interaction "${interactionId}" not found on the current slide`);
34
49
  }
35
50
  logTrace('getInteractionMetadata', { interactionId });
51
+ const savedState = getInteractionState(entry.id);
52
+ let response = savedState?.response;
53
+
54
+ if (response === undefined && typeof entry.instance?.getResponse === 'function') {
55
+ try {
56
+ response = entry.instance.getResponse();
57
+ } catch {
58
+ response = undefined;
59
+ }
60
+ }
61
+
36
62
  return {
37
63
  id: entry.id,
38
64
  type: entry.type,
39
- description: entry.description
65
+ description: entry.description,
66
+ hasResponse: response !== null && response !== undefined && response !== '',
67
+ isChecked: savedState?.submitted === true
40
68
  };
41
69
  },
42
70
 
package/lib/cloud.js CHANGED
@@ -1036,6 +1036,28 @@ export async function deploy(options = {}) {
1036
1036
  const promoteMode = options.promote ? 'promote' : options.stage ? 'stage' : 'auto';
1037
1037
  const previewForce = !!options.preview;
1038
1038
  const previewOnly = previewForce && promoteMode === 'auto';
1039
+ let previewPassword;
1040
+ if (options.password !== undefined) {
1041
+ if (!previewForce) {
1042
+ logErr('\n❌ --password requires --preview.\n');
1043
+ if (options.json) process.stdout.write(JSON.stringify({ success: false, error: '--password requires --preview' }) + '\n');
1044
+ process.exit(1);
1045
+ }
1046
+ previewPassword = options.password;
1047
+ if (previewPassword === true || previewPassword === '') {
1048
+ if (options.json) {
1049
+ logErr('\n❌ --password requires a value when using --json.\n');
1050
+ process.stdout.write(JSON.stringify({ success: false, error: '--password requires a value when using --json' }) + '\n');
1051
+ process.exit(1);
1052
+ }
1053
+ previewPassword = await prompt(' Preview password: ');
1054
+ }
1055
+ if (!previewPassword) {
1056
+ logErr('\n❌ Preview password cannot be empty.\n');
1057
+ if (options.json) process.stdout.write(JSON.stringify({ success: false, error: 'Preview password cannot be empty' }) + '\n');
1058
+ process.exit(1);
1059
+ }
1060
+ }
1039
1061
 
1040
1062
  log('\n📦 Building...\n');
1041
1063
  emitProgress('building');
@@ -1196,6 +1218,7 @@ export async function deploy(options = {}) {
1196
1218
  previewForce,
1197
1219
  previewOnly,
1198
1220
  };
1221
+ if (previewPassword) finalizeBody.previewPassword = previewPassword;
1199
1222
  if (options.message) finalizeBody.message = options.message;
1200
1223
 
1201
1224
  const finalizeRes = await cloudFetch(
@@ -1544,6 +1567,51 @@ export async function status(options = {}) {
1544
1567
  console.log('');
1545
1568
  }
1546
1569
 
1570
+ /**
1571
+ * coursecode deployments — list recent deployments for the current course
1572
+ */
1573
+ export async function deployments(options = {}) {
1574
+ await ensureAuthenticated();
1575
+ const slug = resolveSlug();
1576
+ const rcConfig = readRcConfig();
1577
+ const orgQuery = rcConfig?.orgId ? `?orgId=${rcConfig.orgId}` : '';
1578
+
1579
+ const makeRequest = async (_isRetry = false) => {
1580
+ const token = readCredentials()?.token;
1581
+ const res = await cloudFetch(
1582
+ `/api/cli/courses/${encodeURIComponent(slug)}/versions${orgQuery}`,
1583
+ {},
1584
+ token
1585
+ );
1586
+ return handleResponse(res, { retryFn: makeRequest, _isRetry });
1587
+ };
1588
+
1589
+ const data = await makeRequest();
1590
+
1591
+ if (options.json) {
1592
+ process.stdout.write(JSON.stringify(data) + '\n');
1593
+ return;
1594
+ }
1595
+
1596
+ const rows = data.deployments ?? [];
1597
+ if (rows.length === 0) {
1598
+ console.log('\nNo deployments found for this course.\n');
1599
+ return;
1600
+ }
1601
+
1602
+ console.log(`\n${slug} — Recent Deployments\n`);
1603
+ rows.forEach((deployment, index) => {
1604
+ const marker = deployment.id === data.production_deployment_id
1605
+ ? ' [production]'
1606
+ : deployment.id === data.preview_deployment_id
1607
+ ? ' [preview]'
1608
+ : '';
1609
+ const flags = deployment.preview_only ? 'preview-only' : deployment.source;
1610
+ console.log(` ${index + 1}. ${formatDate(deployment.created_at)} — ${deployment.file_count} files, ${formatBytes(deployment.total_size)} — ${flags}${marker}`);
1611
+ });
1612
+ console.log('');
1613
+ }
1614
+
1547
1615
  /**
1548
1616
  * coursecode preview-link — show or update the current preview link
1549
1617
  */
@@ -1794,4 +1862,3 @@ export async function deleteCourse(options = {}) {
1794
1862
  }
1795
1863
  console.log('');
1796
1864
  }
1797
-
@@ -358,8 +358,8 @@ class HeadlessBrowser {
358
358
 
359
359
  // Navigate to specific slide if requested
360
360
  if (slideId) {
361
- await this.evaluate((id) => {
362
- window.CourseCodeAutomation.goToSlide(id);
361
+ await this.evaluate(async (id) => {
362
+ await window.CourseCodeAutomation.goToSlide(id);
363
363
  }, slideId);
364
364
  // Wait for slide transition
365
365
  await new Promise(resolve => setTimeout(resolve, 500));
@@ -20,7 +20,7 @@ export const TOOLS = [
20
20
  // --- Runtime Tools (require headless browser) ---
21
21
  {
22
22
  name: 'coursecode_state',
23
- description: `Get course state, runtime errors, and warnings in one call. This is the primary tool for checking errors.
23
+ description: `Get course state and live preview diagnostics in one call.
24
24
 
25
25
  Returns:
26
26
  - slide: current slide ID (string)
@@ -29,12 +29,15 @@ Returns:
29
29
  - engagement: slide engagement {complete, percentage, requirements}
30
30
  - lmsState: LMS data {score, completion, success, bookmark, format, objectives, state}
31
31
  - apiLog: last 20 LMS API calls [{timestamp, method, args, result}]
32
- - errors: runtime errors/warnings [{type, message, hint, isWarning}]
32
+ - diagnostics: live issue rollup with build, runtime, framework, console, issues, count, clean
33
+ - issues/errors: flat live issue list [{source, severity, isWarning, type, message, hint?}]
34
+ - runtimeErrors: runtime/debug-panel errors and warnings only
35
+ - buildErrors/buildWarnings: backward-compatible build-watch aliases
33
36
  - frameworkLogs: structured framework log events [{level, domain, operation, message, stack?, timestamp}]
34
37
  - consoleLogs: browser console warnings/errors [{type, text, time}]
35
38
 
36
39
  Use this first to understand the course state before taking actions.
37
- For error checking only (after file edits), prefer coursecode_errors — same error sources, smaller payload.
40
+ For checking what is broken after file edits, prefer coursecode_errors — same live diagnostic sources, smaller payload.
38
41
  Requires preview server to be running.`,
39
42
  inputSchema: {
40
43
  type: 'object',
@@ -48,15 +51,23 @@ Requires preview server to be running.`,
48
51
  },
49
52
  {
50
53
  name: 'coursecode_errors',
51
- description: `Get runtime errors and warnings from the live preview. Uses the same error sources as coursecode_state (preview server errors + browser console) but without the heavyweight state payload.
54
+ description: `Get all current live preview diagnostics without the heavyweight course state payload.
55
+
56
+ This is the primary "what is broken right now?" tool after edits. It aggregates:
57
+ - build: Vite/build-watch errors and warnings from the running preview server
58
+ - runtime: stub LMS/debug-panel errors and warnings
59
+ - framework: CourseCode logger warnings/errors
60
+ - console: browser console warnings/errors
61
+
62
+ This is different from coursecode_lint, which is a static/build-time linter and does not inspect the running preview.
52
63
 
53
64
  Returns:
54
- - errors: [{type, message, hint?, isWarning?}] — preview server errors and warnings
55
- - consoleLogs: [{type, text, time}] browser console warnings/errors
56
- - count: total number of errors + console logs
57
- - clean: true if no errors, warnings, or console issues
65
+ - build/runtime/framework/console: grouped diagnostics by source
66
+ - issues/errors: flat list [{source, severity, isWarning, type, message, hint?}] for agents
67
+ - count: total issue count
68
+ - clean: true if no live issues were found
69
+ - runtimeErrors/frameworkLogs/consoleLogs: source-specific aliases
58
70
 
59
- Use after making file changes to check for breakage without the overhead of coursecode_state.
60
71
  Requires preview server to be running.`,
61
72
  inputSchema: {
62
73
  type: 'object',
@@ -378,16 +389,16 @@ Use to discover available interactions before creating assessments.`,
378
389
  },
379
390
  {
380
391
  name: 'coursecode_lint',
381
- description: `Run the course linter and get structured results.
392
+ description: `Run the static course linter and get structured results.
382
393
 
383
- Always runs build-time lint (config, CSS classes, structure). Does NOT include runtime errors use coursecode_state for runtime errors and contrast warnings when the preview server is running.
394
+ This is a preflight/static validation tool for config, source structure, CSS class names, and schema rules. It does NOT inspect the running preview and does NOT include live runtime, browser console, or Vite build-watch diagnostics. Use coursecode_errors for the live "what is broken right now?" view when preview is running.
384
395
 
385
396
  Returns:
386
397
  - errors: [{slideId?, rule, message, severity, hint?}]
387
398
  - warnings: [{slideId?, rule, message, severity, hint?}]
388
399
  - passed: boolean
389
400
 
390
- Build-time rules (always checked):
401
+ Static rules (always checked):
391
402
  - undefined-css-class: hallucinated or stale class names (with fix suggestions)
392
403
  - unknown-component: unregistered data-component types
393
404
  - requirement-missing-component: engagement requirement without matching component
@@ -396,7 +407,7 @@ Build-time rules (always checked):
396
407
  - assessment-id-mismatch: config ID doesn't match assessment ID
397
408
  - invalid-gating: bad gating condition configuration
398
409
 
399
- Use AFTER making changes to validate the course.`,
410
+ Use after making source/config changes as a fast static check. Then use coursecode_errors against the running preview for live diagnostics.`,
400
411
  inputSchema: {
401
412
  type: 'object',
402
413
  properties: {},
@@ -764,12 +775,16 @@ All runtime tools (state, navigate, interact, screenshot, viewport, reset) execu
764
775
 
765
776
  ### Preview Server Ownership
766
777
  - The MCP does NOT start or manage the preview server
767
- - The preview must be started externally: run \`coursecode preview\` in a terminal (human) or via a terminal/command execution tool (AI agent)
768
- - If preview is not running, runtime tools will fail with a clear error message
778
+ - Before using runtime tools, check whether preview is already running for this project
779
+ - If preview is already running, use it; do not start a second preview server
780
+ - If preview is not running, start it externally in a terminal: run \`coursecode preview\` (or \`npm run preview\` for framework development)
781
+ - AI agents may start preview only via a terminal/command execution tool, and only after a runtime tool reports that preview is not running
769
782
  - The headless browser auto-reconnects when Vite rebuilds (file changes)
770
783
 
771
784
  ### Navigation API
772
- - coursecode_state → get slim TOC with slide IDs, current slide, interactions, engagement, lmsState, apiLog, errors, frameworkLogs, consoleLogs
785
+ - coursecode_state → get slim TOC with slide IDs, current slide, interactions, engagement, lmsState, apiLog, diagnostics, frameworkLogs, consoleLogs
786
+ - coursecode_errors → get live diagnostics only (build/runtime/framework/console); use this after edits
787
+ - coursecode_lint → run static preflight lint only; it is not a live preview diagnostic
773
788
  - coursecode_navigate(slideId) → go to any slide instantly by ID
774
789
  - coursecode_viewport(breakpoint or {width,height}) → set viewport for responsive testing (persists until changed)
775
790
  - coursecode_screenshot(slideId) → navigate + capture in one call (quality modes only, never changes viewport)
package/lib/mcp-server.js CHANGED
@@ -37,6 +37,178 @@ import { TOOLS, buildInstructions, getWorkflowStatusWithInstructions } from './m
37
37
 
38
38
  const DEFAULT_PORT = 4173;
39
39
 
40
+ class McpToolError extends Error {
41
+ constructor(code, message, options = {}) {
42
+ super(message);
43
+ this.name = 'McpToolError';
44
+ this.code = code;
45
+ this.hint = options.hint;
46
+ this.details = options.details;
47
+ }
48
+ }
49
+
50
+ function normalizePort(port) {
51
+ const parsed = parseInt(port ?? DEFAULT_PORT, 10);
52
+ return Number.isFinite(parsed) ? parsed : DEFAULT_PORT;
53
+ }
54
+
55
+ function makeToolResult(result) {
56
+ return {
57
+ content: [{
58
+ type: 'text',
59
+ text: JSON.stringify(result, null, 2)
60
+ }],
61
+ structuredContent: result
62
+ };
63
+ }
64
+
65
+ function normalizeToolError(error) {
66
+ if (error instanceof McpToolError) {
67
+ return {
68
+ code: error.code,
69
+ message: error.message,
70
+ ...(error.hint ? { hint: error.hint } : {}),
71
+ ...(error.details ? { details: error.details } : {})
72
+ };
73
+ }
74
+
75
+ const message = error?.message || String(error);
76
+ if (/Preview server not running/i.test(message)) {
77
+ return {
78
+ code: 'preview_not_running',
79
+ message,
80
+ hint: 'Start the CourseCode preview server for this project, then retry the MCP tool call.'
81
+ };
82
+ }
83
+
84
+ if (/Slide ".*" not found/i.test(message)) {
85
+ return {
86
+ code: 'invalid_slide_id',
87
+ message,
88
+ hint: 'Call coursecode_state to get valid slide IDs, then retry with one of those IDs.'
89
+ };
90
+ }
91
+
92
+ if (/is required|Provide either/i.test(message)) {
93
+ return {
94
+ code: 'invalid_arguments',
95
+ message,
96
+ hint: 'Check the tool input schema and retry with the required arguments.'
97
+ };
98
+ }
99
+
100
+ if (/Unknown tool:/i.test(message)) {
101
+ return {
102
+ code: 'unknown_tool',
103
+ message,
104
+ hint: 'Call tools/list and retry with one of the advertised CourseCode tool names.'
105
+ };
106
+ }
107
+
108
+ return {
109
+ code: 'tool_failed',
110
+ message,
111
+ hint: 'Inspect the message and retry after correcting the underlying issue.'
112
+ };
113
+ }
114
+
115
+ function makeToolErrorResult(error) {
116
+ const normalized = normalizeToolError(error);
117
+ const result = {
118
+ success: false,
119
+ error: normalized,
120
+ code: normalized.code,
121
+ message: normalized.message,
122
+ ...(normalized.hint ? { hint: normalized.hint } : {}),
123
+ ...(normalized.details ? { details: normalized.details } : {})
124
+ };
125
+
126
+ return {
127
+ content: [{
128
+ type: 'text',
129
+ text: JSON.stringify(result, null, 2)
130
+ }],
131
+ structuredContent: result,
132
+ isError: true
133
+ };
134
+ }
135
+
136
+ function normalizeIssue(source, severity, issue) {
137
+ const isWarning = severity === 'warning' || issue?.isWarning === true || issue?.level === 'warn';
138
+ return {
139
+ source,
140
+ severity: isWarning ? 'warning' : 'error',
141
+ isWarning,
142
+ type: issue?.type || issue?.domain || source,
143
+ message: issue?.message || issue?.text || String(issue),
144
+ ...(issue?.hint ? { hint: issue.hint } : {}),
145
+ ...(issue?.operation ? { operation: issue.operation } : {}),
146
+ ...(issue?.time ? { time: issue.time } : {}),
147
+ ...(issue?.timestamp ? { timestamp: issue.timestamp } : {})
148
+ };
149
+ }
150
+
151
+ async function getLiveDiagnostics(port, frameworkLogs = []) {
152
+ const diagnostics = {
153
+ build: { errors: [], warnings: [] },
154
+ runtime: { errors: [], warnings: [] },
155
+ framework: { errors: [], warnings: [] },
156
+ console: { errors: [], warnings: [] },
157
+ issues: []
158
+ };
159
+
160
+ try {
161
+ const buildResp = await fetch(`http://localhost:${port}/__mcp/errors`);
162
+ if (buildResp.ok) {
163
+ const buildData = await buildResp.json();
164
+ diagnostics.build.errors = buildData.errors || [];
165
+ diagnostics.build.warnings = buildData.warnings || [];
166
+ }
167
+ } catch {
168
+ // Preview build diagnostics unavailable — leave empty.
169
+ }
170
+
171
+ try {
172
+ const errResp = await fetch(`http://localhost:${port}/__lms/errors`);
173
+ if (errResp.ok) {
174
+ const errData = await errResp.json();
175
+ diagnostics.runtime.errors = errData.errors || [];
176
+ diagnostics.runtime.warnings = errData.warnings || [];
177
+ }
178
+ } catch {
179
+ // Runtime diagnostics unavailable — leave empty.
180
+ }
181
+
182
+ for (const log of frameworkLogs || []) {
183
+ if (log.level === 'warn') diagnostics.framework.warnings.push(log);
184
+ else if (log.level === 'error' || log.level === 'fatal') diagnostics.framework.errors.push(log);
185
+ }
186
+
187
+ const consoleLogs = headless.getConsoleLogs();
188
+ for (const log of consoleLogs) {
189
+ // logger.warn/error entries are already represented as structured
190
+ // framework diagnostics, so do not double-count their console echo.
191
+ if (/^\[(WARN|ERROR|FATAL)\]/.test(log.text || '')) continue;
192
+ if (log.type === 'warning') diagnostics.console.warnings.push(log);
193
+ else diagnostics.console.errors.push(log);
194
+ }
195
+
196
+ diagnostics.issues = [
197
+ ...diagnostics.build.errors.map(issue => normalizeIssue('build', 'error', issue)),
198
+ ...diagnostics.build.warnings.map(issue => normalizeIssue('build', 'warning', issue)),
199
+ ...diagnostics.runtime.errors.map(issue => normalizeIssue('runtime', 'error', issue)),
200
+ ...diagnostics.runtime.warnings.map(issue => normalizeIssue('runtime', 'warning', issue)),
201
+ ...diagnostics.framework.errors.map(issue => normalizeIssue('framework', 'error', issue)),
202
+ ...diagnostics.framework.warnings.map(issue => normalizeIssue('framework', 'warning', issue)),
203
+ ...diagnostics.console.errors.map(issue => normalizeIssue('console', 'error', issue)),
204
+ ...diagnostics.console.warnings.map(issue => normalizeIssue('console', 'warning', issue))
205
+ ];
206
+
207
+ diagnostics.count = diagnostics.issues.length;
208
+ diagnostics.clean = diagnostics.count === 0;
209
+ return diagnostics;
210
+ }
211
+
40
212
  /**
41
213
  * Ensure headless browser is connected to preview server.
42
214
  * Fails fast if preview is not running — humans own the preview lifecycle.
@@ -60,7 +232,7 @@ async function ensureHeadless(port) {
60
232
  * Create and run the MCP server
61
233
  */
62
234
  export async function startMcpServer(options = {}) {
63
- const port = options.port || DEFAULT_PORT;
235
+ const port = normalizePort(options.port);
64
236
 
65
237
  // Build dynamic instructions for current authoring stage
66
238
  const instructions = await buildInstructions(port);
@@ -68,10 +240,10 @@ export async function startMcpServer(options = {}) {
68
240
  const server = new Server(
69
241
  {
70
242
  name: 'coursecode',
71
- version: '2.0.0',
72
- instructions
243
+ version: '2.0.0'
73
244
  },
74
245
  {
246
+ instructions,
75
247
  capabilities: {
76
248
  tools: {}
77
249
  }
@@ -106,29 +278,32 @@ export async function startMcpServer(options = {}) {
106
278
  lmsState: api.getLmsState()
107
279
  };
108
280
  });
109
- // Read API log and error log from preview server (same data user sees in debug panel)
281
+ // Read API log plus a unified diagnostic rollup from the preview server/headless page.
110
282
  try {
111
- const [logResp, errResp] = await Promise.all([
112
- fetch(`http://localhost:${port}/__lms/log`),
113
- fetch(`http://localhost:${port}/__lms/errors`)
114
- ]);
283
+ const logResp = await fetch(`http://localhost:${port}/__lms/log`);
115
284
  result.apiLog = logResp.ok ? (await logResp.json()).entries?.slice(0, 20) || [] : [];
116
- if (errResp.ok) {
117
- const errData = await errResp.json();
118
- result.errors = [...(errData.errors || []), ...(errData.warnings || [])];
119
- } else {
120
- result.errors = [];
121
- }
122
285
  } catch {
123
286
  result.apiLog = [];
124
- result.errors = [];
125
287
  }
126
- // Append console errors/warnings captured from the page
127
- result.consoleLogs = headless.getConsoleLogs();
288
+ result.diagnostics = await getLiveDiagnostics(port, result.frameworkLogs);
289
+ result.issues = result.diagnostics.issues;
290
+ result.errors = result.diagnostics.issues;
291
+ result.runtimeErrors = [
292
+ ...result.diagnostics.runtime.errors,
293
+ ...result.diagnostics.runtime.warnings
294
+ ];
295
+ result.buildErrors = result.diagnostics.build.errors;
296
+ result.buildWarnings = result.diagnostics.build.warnings;
297
+ result.consoleLogs = [
298
+ ...result.diagnostics.console.errors,
299
+ ...result.diagnostics.console.warnings
300
+ ];
128
301
  break;
129
302
 
130
303
  case 'coursecode_navigate':
131
- if (!args?.slideId) throw new Error('slideId is required');
304
+ if (!args?.slideId) throw new McpToolError('missing_required_argument', 'slideId is required', {
305
+ hint: 'Call coursecode_state to get valid slide IDs, then pass one as slideId.'
306
+ });
132
307
  await ensureHeadless(port);
133
308
  // Apply accessibility preferences before navigation
134
309
  if (args.theme || args.highContrast !== undefined) {
@@ -147,11 +322,14 @@ export async function startMcpServer(options = {}) {
147
322
  return toc.some(item => item.id === slideId);
148
323
  }, args.slideId);
149
324
  if (!validSlide) {
150
- throw new Error(`Slide "${args.slideId}" not found. Use coursecode_state to get valid slide IDs.`);
325
+ throw new McpToolError('invalid_slide_id', `Slide "${args.slideId}" not found.`, {
326
+ hint: 'Call coursecode_state to get valid slide IDs, then retry with one of those IDs.',
327
+ details: { slideId: args.slideId }
328
+ });
151
329
  }
152
330
  }
153
- await headless.evaluate((slideId) => {
154
- window.CourseCodeAutomation.goToSlide(slideId);
331
+ await headless.evaluate(async (slideId) => {
332
+ await window.CourseCodeAutomation.goToSlide(slideId);
155
333
  }, args.slideId);
156
334
  // State updates asynchronously after navigation
157
335
  await new Promise(resolve => setTimeout(resolve, 50));
@@ -167,8 +345,12 @@ export async function startMcpServer(options = {}) {
167
345
  break;
168
346
 
169
347
  case 'coursecode_interact':
170
- if (!args?.interactionId) throw new Error('interactionId is required');
171
- if (args.response === undefined) throw new Error('response is required');
348
+ if (!args?.interactionId) throw new McpToolError('missing_required_argument', 'interactionId is required', {
349
+ hint: 'Call coursecode_state on the target slide to list valid interaction IDs.'
350
+ });
351
+ if (args.response === undefined) throw new McpToolError('missing_required_argument', 'response is required', {
352
+ hint: 'Pass a response value matching the interaction type.'
353
+ });
172
354
  await ensureHeadless(port);
173
355
  result = await headless.evaluate(({ interactionId, response }) => {
174
356
  const api = window.CourseCodeAutomation;
@@ -204,7 +386,10 @@ export async function startMcpServer(options = {}) {
204
386
  return toc.some(item => item.id === slideId);
205
387
  }, args.slideId);
206
388
  if (!validSlide) {
207
- throw new Error(`Slide "${args.slideId}" not found. Use coursecode_state to get valid slide IDs.`);
389
+ throw new McpToolError('invalid_slide_id', `Slide "${args.slideId}" not found.`, {
390
+ hint: 'Call coursecode_state to get valid slide IDs, then retry with one of those IDs.',
391
+ details: { slideId: args.slideId }
392
+ });
208
393
  }
209
394
  }
210
395
  result = await headless.screenshot({
@@ -228,30 +413,33 @@ export async function startMcpServer(options = {}) {
228
413
  } else if (args?.width && args?.height) {
229
414
  result = await headless.setViewport({ width: args.width, height: args.height });
230
415
  } else {
231
- throw new Error('Provide either a breakpoint name or both width and height.');
416
+ throw new McpToolError('missing_required_argument', 'Provide either a breakpoint name or both width and height.', {
417
+ hint: 'Use breakpoint: "desktop" or pass explicit width and height numbers.'
418
+ });
232
419
  }
233
420
  break;
234
421
 
235
422
  case 'coursecode_errors': {
236
- // Same error-gathering mechanism as coursecode_state,
237
- // but without the heavyweight state payload (TOC, interactions, etc.)
423
+ // Live diagnostic rollup without the heavyweight state payload (TOC, interactions, etc.).
238
424
  await ensureHeadless(port);
239
- let errors = [];
240
- try {
241
- const errResp = await fetch(`http://localhost:${port}/__lms/errors`);
242
- if (errResp.ok) {
243
- const errData = await errResp.json();
244
- errors = [...(errData.errors || []), ...(errData.warnings || [])];
245
- }
246
- } catch {
247
- // Preview server unreachable — errors array stays empty
248
- }
249
- const consoleLogs = headless.getConsoleLogs();
425
+ const frameworkLogs = await headless.evaluate(() => {
426
+ return window.CourseCodeAutomation.getFrameworkLogs();
427
+ });
428
+ const diagnostics = await getLiveDiagnostics(port, frameworkLogs);
250
429
  result = {
251
- errors,
252
- consoleLogs,
253
- count: errors.length + consoleLogs.length,
254
- clean: errors.length === 0 && consoleLogs.length === 0
430
+ ...diagnostics,
431
+ // Convenience aliases for agents that expect a flat list.
432
+ issues: diagnostics.issues,
433
+ errors: diagnostics.issues,
434
+ runtimeErrors: [
435
+ ...diagnostics.runtime.errors,
436
+ ...diagnostics.runtime.warnings
437
+ ],
438
+ frameworkLogs,
439
+ consoleLogs: [
440
+ ...diagnostics.console.errors,
441
+ ...diagnostics.console.warnings
442
+ ]
255
443
  };
256
444
  break;
257
445
  }
@@ -295,23 +483,15 @@ export async function startMcpServer(options = {}) {
295
483
 
296
484
 
297
485
  default:
298
- throw new Error(`Unknown tool: ${name}`);
486
+ throw new McpToolError('unknown_tool', `Unknown tool: ${name}`, {
487
+ hint: 'Call tools/list and retry with one of the advertised CourseCode tool names.',
488
+ details: { name }
489
+ });
299
490
  }
300
-
301
- return {
302
- content: [{
303
- type: 'text',
304
- text: JSON.stringify(result, null, 2)
305
- }]
306
- };
491
+
492
+ return makeToolResult(result);
307
493
  } catch (error) {
308
- return {
309
- content: [{
310
- type: 'text',
311
- text: `Error: ${error.message}`
312
- }],
313
- isError: true
314
- };
494
+ return makeToolErrorResult(error);
315
495
  }
316
496
  });
317
497
 
@@ -355,6 +535,7 @@ export async function startMcpServer(options = {}) {
355
535
  if (process.argv[1]?.endsWith('mcp-server.js')) {
356
536
  const args = process.argv.slice(2);
357
537
  const portArg = args.find(a => a.startsWith('--port='));
358
- const port = portArg ? parseInt(portArg.split('=')[1], 10) : DEFAULT_PORT;
538
+ const portValue = portArg ? portArg.split('=')[1] : args[args.indexOf('--port') + 1];
539
+ const port = normalizePort(portValue);
359
540
  startMcpServer({ port });
360
541
  }
@@ -269,6 +269,11 @@ export async function previewServer(options = {}) {
269
269
  viteProcess.stdout.on('data', (data) => {
270
270
  const output = data.toString();
271
271
  process.stdout.write(output);
272
+ if (output.includes('Building...')) {
273
+ buildState.errors = [];
274
+ buildState.warnings = [];
275
+ buildState.lastBuildSuccess = false;
276
+ }
272
277
  if (output.includes('Build complete')) {
273
278
  buildState.lastBuildTime = new Date().toISOString();
274
279
  buildState.lastBuildSuccess = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.44",
3
+ "version": "0.1.47",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {