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 +19 -1
- package/bin/cli.js +12 -1
- package/framework/docs/FRAMEWORK_GUIDE.md +17 -8
- package/framework/docs/USER_GUIDE.md +32 -7
- package/framework/js/automation/api-interactions.js +35 -7
- package/lib/cloud.js +68 -1
- package/lib/headless-browser.js +2 -2
- package/lib/mcp-prompts.js +31 -16
- package/lib/mcp-server.js +239 -58
- package/lib/preview-server.js +5 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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.
|
|
869
|
+
The MCP does **not** start or manage the preview server. Runtime tools connect to an already-running preview server.
|
|
865
870
|
|
|
866
|
-
-
|
|
867
|
-
-
|
|
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.
|
|
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` |
|
|
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
|
|
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
|
-
| `
|
|
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`.
|
|
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
|
|
751
|
-
5.
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
package/lib/headless-browser.js
CHANGED
|
@@ -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));
|
package/lib/mcp-prompts.js
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
55
|
-
-
|
|
56
|
-
- count: total
|
|
57
|
-
- clean: true if no
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
768
|
-
- If preview is
|
|
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,
|
|
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
|
|
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
|
|
281
|
+
// Read API log plus a unified diagnostic rollup from the preview server/headless page.
|
|
110
282
|
try {
|
|
111
|
-
const
|
|
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
|
-
|
|
127
|
-
result.
|
|
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
|
|
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
|
|
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
|
|
171
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
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
|
|
538
|
+
const portValue = portArg ? portArg.split('=')[1] : args[args.indexOf('--port') + 1];
|
|
539
|
+
const port = normalizePort(portValue);
|
|
359
540
|
startMcpServer({ port });
|
|
360
541
|
}
|
package/lib/preview-server.js
CHANGED
|
@@ -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;
|