@epic-web/workshop-mcp 5.18.2 → 5.19.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.
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +38 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/prompts.d.ts +13 -0
- package/dist/esm/prompts.d.ts.map +1 -0
- package/dist/esm/prompts.js +53 -0
- package/dist/esm/prompts.js.map +1 -0
- package/dist/esm/resources.d.ts +73 -0
- package/dist/esm/resources.d.ts.map +1 -0
- package/dist/esm/resources.js +375 -0
- package/dist/esm/resources.js.map +1 -0
- package/dist/esm/tools.d.ts +5 -0
- package/dist/esm/tools.d.ts.map +1 -0
- package/dist/esm/tools.js +218 -0
- package/dist/esm/tools.js.map +1 -0
- package/dist/esm/utils.d.ts +9 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +19 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +8 -8
- package/dist/esm/cli.d.ts +0 -3
- package/dist/esm/cli.d.ts.map +0 -1
- package/dist/esm/cli.js +0 -400
- package/dist/esm/cli.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { initPrompts } from './prompts.js';
|
|
5
|
+
import { initResources } from './resources.js';
|
|
6
|
+
import { initPromptTools, initResourceTools, initTools } from './tools.js';
|
|
7
|
+
// Create server instance
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'epicshop',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
capabilities: {
|
|
12
|
+
tools: {},
|
|
13
|
+
},
|
|
14
|
+
}, {
|
|
15
|
+
instructions: `
|
|
16
|
+
This is intended to be used within a workshop using the Epic Workshop App
|
|
17
|
+
(@epic-web/workshop-app) to help learners in the process of completing the
|
|
18
|
+
workshop exercises and understanding the learning outcomes.
|
|
19
|
+
|
|
20
|
+
The user's work in progress is in the \`playground\` directory. Any changes they
|
|
21
|
+
ask you to make should be in this directory.
|
|
22
|
+
`.trim(),
|
|
23
|
+
});
|
|
24
|
+
initTools(server);
|
|
25
|
+
initResourceTools(server);
|
|
26
|
+
initResources(server);
|
|
27
|
+
initPrompts(server);
|
|
28
|
+
initPromptTools(server);
|
|
29
|
+
async function main() {
|
|
30
|
+
const transport = new StdioServerTransport();
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
console.error('epicshop MCP Server running on stdio');
|
|
33
|
+
}
|
|
34
|
+
main().catch((error) => {
|
|
35
|
+
console.error('Fatal error in main():', error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE1E,yBAAyB;AACzB,MAAM,MAAM,GAAG,IAAI,SAAS,CAC3B;IACC,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,OAAO;IAChB,YAAY,EAAE;QACb,KAAK,EAAE,EAAE;KACT;CACD,EACD;IACC,YAAY,EAAE;;;;;;;GAOb,CAAC,IAAI,EAAE;CACR,CACD,CAAA;AAED,SAAS,CAAC,MAAM,CAAC,CAAA;AACjB,iBAAiB,CAAC,MAAM,CAAC,CAAA;AACzB,aAAa,CAAC,MAAM,CAAC,CAAA;AACrB,WAAW,CAAC,MAAM,CAAC,CAAA;AACnB,eAAe,CAAC,MAAM,CAAC,CAAA;AAEvB,KAAK,UAAU,IAAI;IAClB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAA;IAC5C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC/B,OAAO,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAA;AACtD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACtB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAA;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAChB,CAAC,CAAC,CAAA","sourcesContent":["#!/usr/bin/env node\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { initPrompts } from './prompts.js'\nimport { initResources } from './resources.js'\nimport { initPromptTools, initResourceTools, initTools } from './tools.js'\n\n// Create server instance\nconst server = new McpServer(\n\t{\n\t\tname: 'epicshop',\n\t\tversion: '1.0.0',\n\t\tcapabilities: {\n\t\t\ttools: {},\n\t\t},\n\t},\n\t{\n\t\tinstructions: `\nThis is intended to be used within a workshop using the Epic Workshop App\n(@epic-web/workshop-app) to help learners in the process of completing the\nworkshop exercises and understanding the learning outcomes.\n\nThe user's work in progress is in the \\`playground\\` directory. Any changes they\nask you to make should be in this directory.\n\t\t`.trim(),\n\t},\n)\n\ninitTools(server)\ninitResourceTools(server)\ninitResources(server)\ninitPrompts(server)\ninitPromptTools(server)\n\nasync function main() {\n\tconst transport = new StdioServerTransport()\n\tawait server.connect(transport)\n\tconsole.error('epicshop MCP Server running on stdio')\n}\n\nmain().catch((error) => {\n\tconsole.error('Fatal error in main():', error)\n\tprocess.exit(1)\n})\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type GetPromptResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export declare const quizMeInputSchema: {
|
|
5
|
+
workshopDirectory: z.ZodString;
|
|
6
|
+
exerciseNumber: z.ZodOptional<z.ZodString>;
|
|
7
|
+
};
|
|
8
|
+
export declare function quizMe({ workshopDirectory, exerciseNumber: providedExerciseNumber, }: {
|
|
9
|
+
workshopDirectory: string;
|
|
10
|
+
exerciseNumber?: string;
|
|
11
|
+
}): Promise<GetPromptResult>;
|
|
12
|
+
export declare function initPrompts(server: McpServer): void;
|
|
13
|
+
//# sourceMappingURL=prompts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/prompts.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,eAAO,MAAM,iBAAiB;;;CAQ7B,CAAA;AAED,wBAAsB,MAAM,CAAC,EAC5B,iBAAiB,EACjB,cAAc,EAAE,sBAAsB,GACtC,EAAE;IACF,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqC3B;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,QAO5C"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getExercises } from '@epic-web/workshop-utils/apps.server';
|
|
2
|
+
import { getWorkshopConfig } from '@epic-web/workshop-utils/config.server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { exerciseContextResource } from './resources.js';
|
|
5
|
+
import { handleWorkshopDirectory } from './utils.js';
|
|
6
|
+
export const quizMeInputSchema = {
|
|
7
|
+
workshopDirectory: z.string().describe('The workshop directory'),
|
|
8
|
+
exerciseNumber: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('The exercise number to get quizzed on (leave blank for a random exercise).'),
|
|
12
|
+
};
|
|
13
|
+
export async function quizMe({ workshopDirectory, exerciseNumber: providedExerciseNumber, }) {
|
|
14
|
+
const workshopRoot = await handleWorkshopDirectory(workshopDirectory);
|
|
15
|
+
const config = getWorkshopConfig();
|
|
16
|
+
let exerciseNumber = Number(providedExerciseNumber);
|
|
17
|
+
if (!providedExerciseNumber) {
|
|
18
|
+
const exercises = await getExercises();
|
|
19
|
+
const randomExercise = exercises[Math.floor(Math.random() * exercises.length)];
|
|
20
|
+
exerciseNumber = randomExercise?.exerciseNumber ?? 0;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
messages: [
|
|
24
|
+
{
|
|
25
|
+
role: 'user',
|
|
26
|
+
content: {
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: `
|
|
29
|
+
You are an expert teacher.
|
|
30
|
+
|
|
31
|
+
Below is context about exercise ${exerciseNumber} in the workshop titled "${config.title}" (subtitled "${config.subtitle}") found at ${workshopRoot}.
|
|
32
|
+
|
|
33
|
+
Please use this context to provide quiz questions, one at a time, to me to help me solidify my understanding of this material. Ask me the question, I will provide a response, you will either congratulate my correct understanding and move on or guide me to the correct answer with follow-up questions and hints until I either ask you to tell me the answer or ask you to continue to the next question.
|
|
34
|
+
`.trim(),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
role: 'user',
|
|
39
|
+
content: {
|
|
40
|
+
type: 'resource',
|
|
41
|
+
resource: await exerciseContextResource.getResource({
|
|
42
|
+
workshopDirectory,
|
|
43
|
+
exerciseNumber,
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function initPrompts(server) {
|
|
51
|
+
server.prompt('quiz_me', 'Have the LLM quiz you on topics from the workshop exercises', quizMeInputSchema, quizMe);
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAG1E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAEpD,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wBAAwB,CAAC;IAChE,cAAc,EAAE,CAAC;SACf,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACR,4EAA4E,CAC5E;CACF,CAAA;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAC5B,iBAAiB,EACjB,cAAc,EAAE,sBAAsB,GAItC;IACA,MAAM,YAAY,GAAG,MAAM,uBAAuB,CAAC,iBAAiB,CAAC,CAAA;IACrE,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,IAAI,cAAc,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAA;IACnD,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,MAAM,YAAY,EAAE,CAAA;QACtC,MAAM,cAAc,GACnB,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;QACxD,cAAc,GAAG,cAAc,EAAE,cAAc,IAAI,CAAC,CAAA;IACrD,CAAC;IACD,OAAO;QACN,QAAQ,EAAE;YACT;gBACC,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACR,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE;;;kCAGuB,cAAc,4BAA4B,MAAM,CAAC,KAAK,iBAAiB,MAAM,CAAC,QAAQ,eAAe,YAAY;;;QAG3I,CAAC,IAAI,EAAE;iBACV;aACD;YACD;gBACC,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACR,IAAI,EAAE,UAAU;oBAChB,QAAQ,EAAE,MAAM,uBAAuB,CAAC,WAAW,CAAC;wBACnD,iBAAiB;wBACjB,cAAc;qBACd,CAAC;iBACF;aACD;SACD;KACD,CAAA;AACF,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAiB;IAC5C,MAAM,CAAC,MAAM,CACZ,SAAS,EACT,6DAA6D,EAC7D,iBAAiB,EACjB,MAAM,CACN,CAAA;AACF,CAAC","sourcesContent":["import { getExercises } from '@epic-web/workshop-utils/apps.server'\nimport { getWorkshopConfig } from '@epic-web/workshop-utils/config.server'\nimport { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { type GetPromptResult } from '@modelcontextprotocol/sdk/types.js'\nimport { z } from 'zod'\nimport { exerciseContextResource } from './resources.js'\nimport { handleWorkshopDirectory } from './utils.js'\n\nexport const quizMeInputSchema = {\n\tworkshopDirectory: z.string().describe('The workshop directory'),\n\texerciseNumber: z\n\t\t.string()\n\t\t.optional()\n\t\t.describe(\n\t\t\t'The exercise number to get quizzed on (leave blank for a random exercise).',\n\t\t),\n}\n\nexport async function quizMe({\n\tworkshopDirectory,\n\texerciseNumber: providedExerciseNumber,\n}: {\n\tworkshopDirectory: string\n\texerciseNumber?: string\n}): Promise<GetPromptResult> {\n\tconst workshopRoot = await handleWorkshopDirectory(workshopDirectory)\n\tconst config = getWorkshopConfig()\n\tlet exerciseNumber = Number(providedExerciseNumber)\n\tif (!providedExerciseNumber) {\n\t\tconst exercises = await getExercises()\n\t\tconst randomExercise =\n\t\t\texercises[Math.floor(Math.random() * exercises.length)]\n\t\texerciseNumber = randomExercise?.exerciseNumber ?? 0\n\t}\n\treturn {\n\t\tmessages: [\n\t\t\t{\n\t\t\t\trole: 'user',\n\t\t\t\tcontent: {\n\t\t\t\t\ttype: 'text',\n\t\t\t\t\ttext: `\nYou are an expert teacher.\n\nBelow is context about exercise ${exerciseNumber} in the workshop titled \"${config.title}\" (subtitled \"${config.subtitle}\") found at ${workshopRoot}.\n\nPlease use this context to provide quiz questions, one at a time, to me to help me solidify my understanding of this material. Ask me the question, I will provide a response, you will either congratulate my correct understanding and move on or guide me to the correct answer with follow-up questions and hints until I either ask you to tell me the answer or ask you to continue to the next question.\n\t\t\t\t\t\t\t`.trim(),\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\trole: 'user',\n\t\t\t\tcontent: {\n\t\t\t\t\ttype: 'resource',\n\t\t\t\t\tresource: await exerciseContextResource.getResource({\n\t\t\t\t\t\tworkshopDirectory,\n\t\t\t\t\t\texerciseNumber,\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t}\n}\n\nexport function initPrompts(server: McpServer) {\n\tserver.prompt(\n\t\t'quiz_me',\n\t\t'Have the LLM quiz you on topics from the workshop exercises',\n\t\tquizMeInputSchema,\n\t\tquizMe,\n\t)\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { type InputSchemaType } from './utils.js';
|
|
5
|
+
export declare const getWorkshopContextInputSchema: {
|
|
6
|
+
workshopDirectory: z.ZodString;
|
|
7
|
+
};
|
|
8
|
+
export declare function getWorkshopContext({ workshopDirectory, }: InputSchemaType<typeof getWorkshopContextInputSchema>): Promise<{
|
|
9
|
+
meta: {
|
|
10
|
+
'README.md': string | null;
|
|
11
|
+
config: any;
|
|
12
|
+
instructions: string | null;
|
|
13
|
+
finishedInstructions: string | null;
|
|
14
|
+
};
|
|
15
|
+
exercises: Array<any>;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function getWorkshopContextResource({ workshopDirectory, }: InputSchemaType<typeof getWorkshopContextInputSchema>): Promise<ReadResourceResult['contents'][number]>;
|
|
18
|
+
export declare const workshopContextResource: {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
uriTemplate: ResourceTemplate;
|
|
22
|
+
getResource: typeof getWorkshopContextResource;
|
|
23
|
+
inputSchema: {
|
|
24
|
+
workshopDirectory: z.ZodString;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
declare function getExerciseContextResource({ workshopDirectory, exerciseNumber, }: {
|
|
28
|
+
workshopDirectory: string;
|
|
29
|
+
exerciseNumber?: number;
|
|
30
|
+
}): Promise<ReadResourceResult['contents'][number]>;
|
|
31
|
+
export declare const exerciseContextResource: {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
uriTemplate: ResourceTemplate;
|
|
35
|
+
getResource: typeof getExerciseContextResource;
|
|
36
|
+
inputSchema: {
|
|
37
|
+
workshopDirectory: z.ZodString;
|
|
38
|
+
exerciseNumber: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
declare const diffBetweenAppsInputSchema: {
|
|
42
|
+
workshopDirectory: z.ZodString;
|
|
43
|
+
app1: z.ZodString;
|
|
44
|
+
app2: z.ZodString;
|
|
45
|
+
};
|
|
46
|
+
declare function getDiffBetweenAppsResource({ workshopDirectory, app1, app2, }: InputSchemaType<typeof diffBetweenAppsInputSchema>): Promise<ReadResourceResult['contents'][number]>;
|
|
47
|
+
export declare const diffBetweenAppsResource: {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
uriTemplate: ResourceTemplate;
|
|
51
|
+
getResource: typeof getDiffBetweenAppsResource;
|
|
52
|
+
inputSchema: {
|
|
53
|
+
workshopDirectory: z.ZodString;
|
|
54
|
+
app1: z.ZodString;
|
|
55
|
+
app2: z.ZodString;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
declare const getExerciseStepProgressDiffInputSchema: {
|
|
59
|
+
workshopDirectory: z.ZodString;
|
|
60
|
+
};
|
|
61
|
+
declare function getExerciseStepProgressDiffResource({ workshopDirectory, }: InputSchemaType<typeof getExerciseStepProgressDiffInputSchema>): Promise<ReadResourceResult['contents'][number]>;
|
|
62
|
+
export declare const exerciseStepProgressDiffResource: {
|
|
63
|
+
name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
uriTemplate: ResourceTemplate;
|
|
66
|
+
getResource: typeof getExerciseStepProgressDiffResource;
|
|
67
|
+
inputSchema: {
|
|
68
|
+
workshopDirectory: z.ZodString;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
export declare function initResources(server: McpServer): void;
|
|
72
|
+
export {};
|
|
73
|
+
//# sourceMappingURL=resources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../src/resources.ts"],"names":[],"mappings":"AAkBA,OAAO,EACN,KAAK,SAAS,EACd,gBAAgB,EAChB,MAAM,yCAAyC,CAAA;AAChD,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,oCAAoC,CAAA;AAC5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAEN,KAAK,eAAe,EAEpB,MAAM,YAAY,CAAA;AAEnB,eAAO,MAAM,6BAA6B;;CAMzC,CAAA;AAED,wBAAsB,kBAAkB,CAAC,EACxC,iBAAiB,GACjB,EAAE,eAAe,CAAC,OAAO,6BAA6B,CAAC;;;;;;;eAcrC,KAAK,CAAC,GAAG,CAAC;GAwC5B;AAOD,wBAAsB,0BAA0B,CAAC,EAChD,iBAAiB,GACjB,EAAE,eAAe,CAAC,OAAO,6BAA6B,CAAC,GAAG,OAAO,CACjE,kBAAkB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CACtC,CAQA;AAED,eAAO,MAAM,uBAAuB;;;;;;;;CAMnC,CAAA;AAiLD,iBAAe,0BAA0B,CAAC,EACzC,iBAAiB,EACjB,cAAc,GACd,EAAE;IACF,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,OAAO,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAalD;AAED,eAAO,MAAM,uBAAuB;;;;;;;;;CAMnC,CAAA;AAED,QAAA,MAAM,0BAA0B;;;;CAI/B,CAAA;AA4CD,iBAAe,0BAA0B,CAAC,EACzC,iBAAiB,EACjB,IAAI,EACJ,IAAI,GACJ,EAAE,eAAe,CAAC,OAAO,0BAA0B,CAAC,GAAG,OAAO,CAC9D,kBAAkB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CACtC,CAYA;AAOD,eAAO,MAAM,uBAAuB;;;;;;;;;;CAMnC,CAAA;AAED,QAAA,MAAM,sCAAsC;;CAE3C,CAAA;AA+BD,iBAAe,mCAAmC,CAAC,EAClD,iBAAiB,GACjB,EAAE,eAAe,CAAC,OAAO,sCAAsC,CAAC,GAAG,OAAO,CAC1E,kBAAkB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CACtC,CAUA;AAOD,eAAO,MAAM,gCAAgC;;;;;;;;CAM5C,CAAA;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,QA2F9C"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { invariant } from '@epic-web/invariant';
|
|
3
|
+
import { extractNumbersAndTypeFromAppNameOrPath, findSolutionDir, getApps, getExercise, getExercises, getFullPathFromAppName, getPlaygroundApp, isExerciseStepApp, isPlaygroundApp, } from '@epic-web/workshop-utils/apps.server';
|
|
4
|
+
import { getAuthInfo } from '@epic-web/workshop-utils/db.server';
|
|
5
|
+
import { getEpicVideoInfos, userHasAccessToWorkshop, } from '@epic-web/workshop-utils/epic-api.server';
|
|
6
|
+
import { ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { handleWorkshopDirectory, safeReadFile, } from './utils.js';
|
|
9
|
+
export const getWorkshopContextInputSchema = {
|
|
10
|
+
workshopDirectory: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The workshop directory (the root directory of the workshop repo.).'),
|
|
13
|
+
};
|
|
14
|
+
export async function getWorkshopContext({ workshopDirectory, }) {
|
|
15
|
+
const workshopRoot = await handleWorkshopDirectory(workshopDirectory);
|
|
16
|
+
const inWorkshop = (...d) => path.join(workshopRoot, ...d);
|
|
17
|
+
const readInWorkshop = (...d) => safeReadFile(inWorkshop(...d));
|
|
18
|
+
const output = {
|
|
19
|
+
meta: {
|
|
20
|
+
'README.md': await readInWorkshop('README.md'),
|
|
21
|
+
config: JSON.parse((await readInWorkshop('package.json')) || '{}').epicshop,
|
|
22
|
+
instructions: await readInWorkshop('exercise', 'README.mdx'),
|
|
23
|
+
finishedInstructions: await readInWorkshop('exercise', 'FINISHED.mdx'),
|
|
24
|
+
},
|
|
25
|
+
exercises: [],
|
|
26
|
+
};
|
|
27
|
+
const exercises = await getExercises();
|
|
28
|
+
for (const exercise of exercises) {
|
|
29
|
+
const exerciseInfo = {
|
|
30
|
+
fullPath: exercise.fullPath,
|
|
31
|
+
exerciseNumber: exercise.exerciseNumber,
|
|
32
|
+
title: exercise.title,
|
|
33
|
+
instructions: await safeReadFile(path.join(exercise.fullPath, 'README.mdx')),
|
|
34
|
+
finishedInstructions: await safeReadFile(path.join(exercise.fullPath, 'FINISHED.mdx')),
|
|
35
|
+
steps: exercise.steps.map((step) => {
|
|
36
|
+
return {
|
|
37
|
+
stepNumber: step.stepNumber,
|
|
38
|
+
title: step.problem?.title ?? step.solution?.title ?? null,
|
|
39
|
+
problem: step.problem
|
|
40
|
+
? {
|
|
41
|
+
fullPath: step.problem.fullPath,
|
|
42
|
+
testConfig: step.problem.test,
|
|
43
|
+
devConfig: step.problem.dev,
|
|
44
|
+
}
|
|
45
|
+
: 'No problem app',
|
|
46
|
+
solution: step.solution
|
|
47
|
+
? {
|
|
48
|
+
fullPath: step.solution.fullPath,
|
|
49
|
+
testConfig: step.solution.test,
|
|
50
|
+
devConfig: step.solution.dev,
|
|
51
|
+
}
|
|
52
|
+
: 'No solution app',
|
|
53
|
+
};
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
output.exercises.push(exerciseInfo);
|
|
57
|
+
}
|
|
58
|
+
return output;
|
|
59
|
+
}
|
|
60
|
+
const workshopContextUriTemplate = new ResourceTemplate('epicshop://{workshopDirectory}/workshop-context', { list: undefined });
|
|
61
|
+
export async function getWorkshopContextResource({ workshopDirectory, }) {
|
|
62
|
+
return {
|
|
63
|
+
uri: workshopContextUriTemplate.uriTemplate.expand({
|
|
64
|
+
workshopDirectory,
|
|
65
|
+
}),
|
|
66
|
+
mimeType: 'application/json',
|
|
67
|
+
text: JSON.stringify(await getWorkshopContext({ workshopDirectory })),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export const workshopContextResource = {
|
|
71
|
+
name: 'workshop_context',
|
|
72
|
+
description: 'The context of the workshop',
|
|
73
|
+
uriTemplate: workshopContextUriTemplate,
|
|
74
|
+
getResource: getWorkshopContextResource,
|
|
75
|
+
inputSchema: getWorkshopContextInputSchema,
|
|
76
|
+
};
|
|
77
|
+
const getExerciseContextInputSchema = {
|
|
78
|
+
workshopDirectory: z
|
|
79
|
+
.string()
|
|
80
|
+
.describe('The workshop directory (the root directory of the workshop repo.).'),
|
|
81
|
+
exerciseNumber: z.coerce
|
|
82
|
+
.number()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe(`The exercise number to get the context for (defaults to the exercise number the playground is currently set to)`),
|
|
85
|
+
};
|
|
86
|
+
async function getExerciseContext({ workshopDirectory, exerciseNumber, }) {
|
|
87
|
+
await handleWorkshopDirectory(workshopDirectory);
|
|
88
|
+
const userHasAccess = await userHasAccessToWorkshop();
|
|
89
|
+
const authInfo = await getAuthInfo();
|
|
90
|
+
let stepNumber = 1;
|
|
91
|
+
const playgroundApp = await getPlaygroundApp();
|
|
92
|
+
invariant(playgroundApp, 'No playground app found');
|
|
93
|
+
const numbers = extractNumbersAndTypeFromAppNameOrPath(playgroundApp.appName);
|
|
94
|
+
const isCurrentExercise = exerciseNumber === undefined ||
|
|
95
|
+
exerciseNumber === Number(numbers?.exerciseNumber);
|
|
96
|
+
if (exerciseNumber === undefined) {
|
|
97
|
+
invariant(numbers, 'No numbers found in playground app name');
|
|
98
|
+
exerciseNumber = Number(numbers.exerciseNumber);
|
|
99
|
+
stepNumber = Number(numbers.stepNumber);
|
|
100
|
+
}
|
|
101
|
+
const exercise = await getExercise(exerciseNumber);
|
|
102
|
+
invariant(exercise, `No exercise found for exercise number ${exerciseNumber}`);
|
|
103
|
+
const videoInfos = await getEpicVideoInfos([
|
|
104
|
+
...(exercise.instructionsEpicVideoEmbeds ?? []),
|
|
105
|
+
...exercise.steps.flatMap((s) => s.problem?.epicVideoEmbeds ?? []),
|
|
106
|
+
...exercise.steps.flatMap((s) => s.solution?.epicVideoEmbeds ?? []),
|
|
107
|
+
...(exercise.finishedEpicVideoEmbeds ?? []),
|
|
108
|
+
]);
|
|
109
|
+
function getTranscripts(embeds) {
|
|
110
|
+
if (!embeds)
|
|
111
|
+
return [];
|
|
112
|
+
if (!userHasAccess && embeds.length) {
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
message: `User must upgrade before they can get access to ${embeds.length} transcript${embeds.length === 1 ? '' : 's'}.`,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
return embeds.map((embed) => {
|
|
120
|
+
const info = videoInfos[embed];
|
|
121
|
+
if (info) {
|
|
122
|
+
if (info.status === 'error') {
|
|
123
|
+
if (info.type === 'region-restricted') {
|
|
124
|
+
return {
|
|
125
|
+
embed,
|
|
126
|
+
status: 'error',
|
|
127
|
+
type: info.type,
|
|
128
|
+
requestedCountry: info.requestCountry,
|
|
129
|
+
restrictedCountry: info.restrictedCountry,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
return {
|
|
134
|
+
embed,
|
|
135
|
+
status: 'error',
|
|
136
|
+
type: info.type,
|
|
137
|
+
statusCode: info.statusCode,
|
|
138
|
+
statusText: info.statusText,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
return {
|
|
144
|
+
embed,
|
|
145
|
+
status: 'success',
|
|
146
|
+
transcript: info.transcript,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
return {
|
|
152
|
+
embed,
|
|
153
|
+
status: 'error',
|
|
154
|
+
type: 'not-found',
|
|
155
|
+
message: 'No transcript found',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function getFileContent(filePath) {
|
|
161
|
+
return {
|
|
162
|
+
path: filePath,
|
|
163
|
+
content: (await safeReadFile(filePath)) ?? 'None found',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const context = {
|
|
167
|
+
currentContext: {
|
|
168
|
+
user: {
|
|
169
|
+
hasAccess: userHasAccess,
|
|
170
|
+
isAuthenticated: Boolean(authInfo),
|
|
171
|
+
email: authInfo?.email,
|
|
172
|
+
},
|
|
173
|
+
playground: isCurrentExercise
|
|
174
|
+
? {
|
|
175
|
+
exerciseNumber,
|
|
176
|
+
stepNumber,
|
|
177
|
+
}
|
|
178
|
+
: 'currently set to a different exercise',
|
|
179
|
+
},
|
|
180
|
+
exerciseBackground: {
|
|
181
|
+
number: exerciseNumber,
|
|
182
|
+
intro: {
|
|
183
|
+
content: await getFileContent(path.join(exercise.fullPath, 'README.mdx')),
|
|
184
|
+
transcripts: getTranscripts(exercise.instructionsEpicVideoEmbeds),
|
|
185
|
+
},
|
|
186
|
+
outro: {
|
|
187
|
+
content: await getFileContent(path.join(exercise.fullPath, 'FINISHED.mdx')),
|
|
188
|
+
transcripts: getTranscripts(exercise.finishedEpicVideoEmbeds),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
steps: exercise.steps
|
|
192
|
+
? await Promise.all(exercise.steps.map(async (app) => ({
|
|
193
|
+
number: app.stepNumber,
|
|
194
|
+
isCurrent: isCurrentExercise && app.stepNumber === stepNumber,
|
|
195
|
+
problem: {
|
|
196
|
+
content: app.problem
|
|
197
|
+
? await getFileContent(path.join(app.problem.fullPath, 'README.mdx'))
|
|
198
|
+
: 'No problem found',
|
|
199
|
+
transcripts: getTranscripts(app.problem?.epicVideoEmbeds),
|
|
200
|
+
},
|
|
201
|
+
solution: {
|
|
202
|
+
content: app.solution
|
|
203
|
+
? await getFileContent(path.join(app.solution.fullPath, 'README.mdx'))
|
|
204
|
+
: 'No solution found',
|
|
205
|
+
transcripts: getTranscripts(app.solution?.epicVideoEmbeds),
|
|
206
|
+
},
|
|
207
|
+
})))
|
|
208
|
+
: [],
|
|
209
|
+
notes: [],
|
|
210
|
+
};
|
|
211
|
+
if (exercise.steps) {
|
|
212
|
+
if (isCurrentExercise) {
|
|
213
|
+
context.notes.push(`Reminder, the current step is ${stepNumber} of ${exercise.steps.length + 1}. The most relevant information will be in the context above within the current step.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
context.notes.push('Unusually, this exercise has no steps.');
|
|
218
|
+
}
|
|
219
|
+
return context;
|
|
220
|
+
}
|
|
221
|
+
const exerciseContextUriTemplate = new ResourceTemplate('epicshop://{workshopDirectory}/exercise/{exerciseNumber}', { list: undefined });
|
|
222
|
+
async function getExerciseContextResource({ workshopDirectory, exerciseNumber, }) {
|
|
223
|
+
const context = await getExerciseContext({
|
|
224
|
+
workshopDirectory,
|
|
225
|
+
exerciseNumber,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
uri: exerciseContextUriTemplate.uriTemplate.expand({
|
|
229
|
+
workshopDirectory,
|
|
230
|
+
exerciseNumber: String(context.exerciseBackground.number),
|
|
231
|
+
}),
|
|
232
|
+
mimeType: 'application/json',
|
|
233
|
+
text: JSON.stringify(context),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export const exerciseContextResource = {
|
|
237
|
+
name: 'exercise_context',
|
|
238
|
+
description: 'The context of the exercise',
|
|
239
|
+
uriTemplate: exerciseContextUriTemplate,
|
|
240
|
+
getResource: getExerciseContextResource,
|
|
241
|
+
inputSchema: getExerciseContextInputSchema,
|
|
242
|
+
};
|
|
243
|
+
const diffBetweenAppsInputSchema = {
|
|
244
|
+
workshopDirectory: z.string().describe('The workshop directory'),
|
|
245
|
+
app1: z.string().describe('The ID of the first app'),
|
|
246
|
+
app2: z.string().describe('The ID of the second app'),
|
|
247
|
+
};
|
|
248
|
+
async function getDiffBetweenApps({ workshopDirectory, app1, app2, }) {
|
|
249
|
+
await handleWorkshopDirectory(workshopDirectory);
|
|
250
|
+
const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
|
|
251
|
+
const app1Name = extractNumbersAndTypeFromAppNameOrPath(app1);
|
|
252
|
+
const app2Name = extractNumbersAndTypeFromAppNameOrPath(app2);
|
|
253
|
+
const apps = await getApps();
|
|
254
|
+
const app1App = apps
|
|
255
|
+
.filter(isExerciseStepApp)
|
|
256
|
+
.find((a) => a.exerciseNumber === Number(app1Name?.exerciseNumber) &&
|
|
257
|
+
a.stepNumber === Number(app1Name?.stepNumber) &&
|
|
258
|
+
a.type === app1Name?.type);
|
|
259
|
+
const app2App = apps
|
|
260
|
+
.filter(isExerciseStepApp)
|
|
261
|
+
.find((a) => a.exerciseNumber === Number(app2Name?.exerciseNumber) &&
|
|
262
|
+
a.stepNumber === Number(app2Name?.stepNumber) &&
|
|
263
|
+
a.type === app2Name?.type);
|
|
264
|
+
invariant(app1App, `No app found for ${app1}`);
|
|
265
|
+
invariant(app2App, `No app found for ${app2}`);
|
|
266
|
+
const diffCode = await getDiffOutputWithRelativePaths(app1App, app2App);
|
|
267
|
+
if (!diffCode)
|
|
268
|
+
return 'No changes';
|
|
269
|
+
return diffCode;
|
|
270
|
+
}
|
|
271
|
+
async function getDiffBetweenAppsResource({ workshopDirectory, app1, app2, }) {
|
|
272
|
+
return {
|
|
273
|
+
uri: diffBetweenAppsUriTemplate.uriTemplate.expand({
|
|
274
|
+
workshopDirectory,
|
|
275
|
+
app1,
|
|
276
|
+
app2,
|
|
277
|
+
}),
|
|
278
|
+
mimeType: 'application/json',
|
|
279
|
+
text: JSON.stringify(await getDiffBetweenApps({ workshopDirectory, app1, app2 })),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const diffBetweenAppsUriTemplate = new ResourceTemplate('epicshop://{workshopDirectory}/diff-between-apps/{app1}__vs___{app2}', { list: undefined });
|
|
283
|
+
export const diffBetweenAppsResource = {
|
|
284
|
+
name: 'diff_between_apps',
|
|
285
|
+
description: 'The diff between two apps',
|
|
286
|
+
uriTemplate: diffBetweenAppsUriTemplate,
|
|
287
|
+
getResource: getDiffBetweenAppsResource,
|
|
288
|
+
inputSchema: diffBetweenAppsInputSchema,
|
|
289
|
+
};
|
|
290
|
+
const getExerciseStepProgressDiffInputSchema = {
|
|
291
|
+
workshopDirectory: z.string().describe('The workshop directory'),
|
|
292
|
+
};
|
|
293
|
+
async function getExerciseStepProgressDiff({ workshopDirectory, }) {
|
|
294
|
+
await handleWorkshopDirectory(workshopDirectory);
|
|
295
|
+
const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
|
|
296
|
+
const apps = await getApps();
|
|
297
|
+
const playgroundApp = apps.find(isPlaygroundApp);
|
|
298
|
+
invariant(playgroundApp, 'No playground app found');
|
|
299
|
+
const baseApp = playgroundApp;
|
|
300
|
+
const solutionDir = await findSolutionDir({
|
|
301
|
+
fullPath: await getFullPathFromAppName(playgroundApp.appName),
|
|
302
|
+
});
|
|
303
|
+
const headApp = apps.find((a) => a.fullPath === solutionDir);
|
|
304
|
+
invariant(headApp, 'No playground solution app found');
|
|
305
|
+
const diffCode = await getDiffOutputWithRelativePaths(baseApp, headApp);
|
|
306
|
+
if (!diffCode)
|
|
307
|
+
return 'No changes';
|
|
308
|
+
return diffCode;
|
|
309
|
+
}
|
|
310
|
+
async function getExerciseStepProgressDiffResource({ workshopDirectory, }) {
|
|
311
|
+
return {
|
|
312
|
+
uri: exerciseStepProgressDiffUriTemplate.uriTemplate.expand({
|
|
313
|
+
workshopDirectory,
|
|
314
|
+
}),
|
|
315
|
+
mimeType: 'application/json',
|
|
316
|
+
text: JSON.stringify(await getExerciseStepProgressDiff({ workshopDirectory })),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const exerciseStepProgressDiffUriTemplate = new ResourceTemplate('epicshop://{workshopDirectory}/exercise-step-progress-diff', { list: undefined });
|
|
320
|
+
export const exerciseStepProgressDiffResource = {
|
|
321
|
+
name: 'exercise_step_progress_diff',
|
|
322
|
+
description: 'The diff between the current exercise step and the solution',
|
|
323
|
+
uriTemplate: exerciseStepProgressDiffUriTemplate,
|
|
324
|
+
getResource: getExerciseStepProgressDiffResource,
|
|
325
|
+
inputSchema: getExerciseStepProgressDiffInputSchema,
|
|
326
|
+
};
|
|
327
|
+
export function initResources(server) {
|
|
328
|
+
server.resource(workshopContextResource.name, workshopContextResource.uriTemplate, { description: workshopContextResource.description }, async (_uri, { workshopDirectory }) => {
|
|
329
|
+
invariant(typeof workshopDirectory === 'string', 'A single workshopDirectory is required');
|
|
330
|
+
const resource = await workshopContextResource.getResource({
|
|
331
|
+
workshopDirectory,
|
|
332
|
+
});
|
|
333
|
+
return { contents: [resource] };
|
|
334
|
+
});
|
|
335
|
+
server.resource(exerciseContextResource.name, exerciseContextResource.uriTemplate, { description: exerciseContextResource.description }, async (_uri, { workshopDirectory, exerciseNumber: providedExerciseNumber }) => {
|
|
336
|
+
invariant(typeof workshopDirectory === 'string', 'A single workshopDirectory is required');
|
|
337
|
+
invariant(typeof providedExerciseNumber === 'string', 'A single exerciseNumber is required');
|
|
338
|
+
const exerciseNumber = Number(providedExerciseNumber);
|
|
339
|
+
invariant(!isNaN(exerciseNumber), 'exerciseNumber must be a number');
|
|
340
|
+
invariant(exerciseNumber >= 0, 'exerciseNumber must be greater than or equal to 0');
|
|
341
|
+
return {
|
|
342
|
+
contents: [
|
|
343
|
+
await exerciseContextResource.getResource({
|
|
344
|
+
workshopDirectory,
|
|
345
|
+
exerciseNumber,
|
|
346
|
+
}),
|
|
347
|
+
],
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
server.resource(diffBetweenAppsResource.name, diffBetweenAppsResource.uriTemplate, { description: diffBetweenAppsResource.description }, async (_uri, { workshopDirectory, app1, app2 }) => {
|
|
351
|
+
invariant(typeof workshopDirectory === 'string', 'A single workshopDirectory is required');
|
|
352
|
+
invariant(typeof app1 === 'string', 'A single app1 is required');
|
|
353
|
+
invariant(typeof app2 === 'string', 'A single app2 is required');
|
|
354
|
+
return {
|
|
355
|
+
contents: [
|
|
356
|
+
await diffBetweenAppsResource.getResource({
|
|
357
|
+
workshopDirectory,
|
|
358
|
+
app1,
|
|
359
|
+
app2,
|
|
360
|
+
}),
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
server.resource(exerciseStepProgressDiffResource.name, exerciseStepProgressDiffResource.uriTemplate, { description: exerciseStepProgressDiffResource.description }, async (_uri, { workshopDirectory }) => {
|
|
365
|
+
invariant(typeof workshopDirectory === 'string', 'A single workshopDirectory is required');
|
|
366
|
+
return {
|
|
367
|
+
contents: [
|
|
368
|
+
await exerciseStepProgressDiffResource.getResource({
|
|
369
|
+
workshopDirectory,
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=resources.js.map
|