@mtgibbs/canvas-lms-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/esm/_dnt.shims.d.ts +6 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +61 -0
- package/esm/agent.d.ts +10 -0
- package/esm/agent.d.ts.map +1 -0
- package/esm/agent.js +17 -0
- package/esm/cli.js +6 -0
- package/esm/package.json +3 -0
- package/esm/src/api/assignments.d.ts +40 -0
- package/esm/src/api/assignments.d.ts.map +1 -0
- package/esm/src/api/assignments.js +133 -0
- package/esm/src/api/client.d.ts +61 -0
- package/esm/src/api/client.d.ts.map +1 -0
- package/esm/src/api/client.js +179 -0
- package/esm/src/api/courses.d.ts +32 -0
- package/esm/src/api/courses.d.ts.map +1 -0
- package/esm/src/api/courses.js +85 -0
- package/esm/src/api/stats.d.ts +15 -0
- package/esm/src/api/stats.d.ts.map +1 -0
- package/esm/src/api/stats.js +58 -0
- package/esm/src/api/submissions.d.ts +33 -0
- package/esm/src/api/submissions.d.ts.map +1 -0
- package/esm/src/api/submissions.js +103 -0
- package/esm/src/api/users.d.ts +87 -0
- package/esm/src/api/users.d.ts.map +1 -0
- package/esm/src/api/users.js +139 -0
- package/esm/src/mcp/prompts/course-analysis.d.ts +7 -0
- package/esm/src/mcp/prompts/course-analysis.d.ts.map +1 -0
- package/esm/src/mcp/prompts/course-analysis.js +36 -0
- package/esm/src/mcp/prompts/daily-checkin.d.ts +7 -0
- package/esm/src/mcp/prompts/daily-checkin.d.ts.map +1 -0
- package/esm/src/mcp/prompts/daily-checkin.js +31 -0
- package/esm/src/mcp/prompts/grade-recovery.d.ts +7 -0
- package/esm/src/mcp/prompts/grade-recovery.d.ts.map +1 -0
- package/esm/src/mcp/prompts/grade-recovery.js +35 -0
- package/esm/src/mcp/prompts/index.d.ts +15 -0
- package/esm/src/mcp/prompts/index.d.ts.map +1 -0
- package/esm/src/mcp/prompts/index.js +20 -0
- package/esm/src/mcp/prompts/missing-work-audit.d.ts +7 -0
- package/esm/src/mcp/prompts/missing-work-audit.d.ts.map +1 -0
- package/esm/src/mcp/prompts/missing-work-audit.js +36 -0
- package/esm/src/mcp/prompts/week-planning.d.ts +7 -0
- package/esm/src/mcp/prompts/week-planning.d.ts.map +1 -0
- package/esm/src/mcp/prompts/week-planning.js +34 -0
- package/esm/src/mcp/server.d.ts +15 -0
- package/esm/src/mcp/server.d.ts.map +1 -0
- package/esm/src/mcp/server.js +51 -0
- package/esm/src/mcp/tools/get-courses.d.ts +11 -0
- package/esm/src/mcp/tools/get-courses.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-courses.js +29 -0
- package/esm/src/mcp/tools/get-due-this-week.d.ts +13 -0
- package/esm/src/mcp/tools/get-due-this-week.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-due-this-week.js +75 -0
- package/esm/src/mcp/tools/get-missing-assignments.d.ts +12 -0
- package/esm/src/mcp/tools/get-missing-assignments.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-missing-assignments.js +33 -0
- package/esm/src/mcp/tools/get-stats.d.ts +12 -0
- package/esm/src/mcp/tools/get-stats.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-stats.js +28 -0
- package/esm/src/mcp/tools/get-todo.d.ts +13 -0
- package/esm/src/mcp/tools/get-todo.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-todo.js +55 -0
- package/esm/src/mcp/tools/get-unsubmitted-past-due.d.ts +12 -0
- package/esm/src/mcp/tools/get-unsubmitted-past-due.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-unsubmitted-past-due.js +64 -0
- package/esm/src/mcp/tools/get-upcoming-assignments.d.ts +12 -0
- package/esm/src/mcp/tools/get-upcoming-assignments.d.ts.map +1 -0
- package/esm/src/mcp/tools/get-upcoming-assignments.js +29 -0
- package/esm/src/mcp/tools/index.d.ts +18 -0
- package/esm/src/mcp/tools/index.d.ts.map +1 -0
- package/esm/src/mcp/tools/index.js +26 -0
- package/esm/src/mcp/tools/list-assignments.d.ts +13 -0
- package/esm/src/mcp/tools/list-assignments.d.ts.map +1 -0
- package/esm/src/mcp/tools/list-assignments.js +40 -0
- package/esm/src/mcp/types.d.ts +83 -0
- package/esm/src/mcp/types.d.ts.map +1 -0
- package/esm/src/mcp/types.js +20 -0
- package/esm/src/types/canvas.d.ts +288 -0
- package/esm/src/types/canvas.d.ts.map +1 -0
- package/esm/src/types/canvas.js +5 -0
- package/esm/src/utils/config.d.ts +19 -0
- package/esm/src/utils/config.d.ts.map +1 -0
- package/esm/src/utils/config.js +54 -0
- package/esm/src/utils/init.d.ts +9 -0
- package/esm/src/utils/init.d.ts.map +1 -0
- package/esm/src/utils/init.js +20 -0
- package/manifest.json +91 -0
- package/package.json +49 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Assignments API
|
|
3
|
+
*/
|
|
4
|
+
import type { Assignment, ListAssignmentsOptions } from "../types/canvas.js";
|
|
5
|
+
/**
|
|
6
|
+
* List assignments for a course
|
|
7
|
+
*/
|
|
8
|
+
export declare function listAssignments(options: ListAssignmentsOptions): Promise<Assignment[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Get a single assignment by ID
|
|
11
|
+
*/
|
|
12
|
+
export declare function getAssignment(courseId: number, assignmentId: number, include?: Array<"submission" | "assignment_visibility" | "all_dates" | "overrides">): Promise<Assignment>;
|
|
13
|
+
/**
|
|
14
|
+
* List assignments due within a date range
|
|
15
|
+
*/
|
|
16
|
+
export declare function listAssignmentsDueInRange(courseId: number, startDate: Date, endDate: Date): Promise<Assignment[]>;
|
|
17
|
+
/**
|
|
18
|
+
* List assignments due this week
|
|
19
|
+
*/
|
|
20
|
+
export declare function listAssignmentsDueThisWeek(courseId: number): Promise<Assignment[]>;
|
|
21
|
+
/**
|
|
22
|
+
* List upcoming assignments (next N days)
|
|
23
|
+
*/
|
|
24
|
+
export declare function listUpcomingAssignments(courseId: number, days?: number): Promise<Assignment[]>;
|
|
25
|
+
/**
|
|
26
|
+
* List overdue assignments
|
|
27
|
+
*/
|
|
28
|
+
export declare function listOverdueAssignments(courseId: number): Promise<Assignment[]>;
|
|
29
|
+
/**
|
|
30
|
+
* List unsubmitted assignments that are past their due date
|
|
31
|
+
* This catches items that Canvas hasn't flagged as "missing" yet
|
|
32
|
+
*/
|
|
33
|
+
export declare function listUnsubmittedPastDue(courseId: number): Promise<Assignment[]>;
|
|
34
|
+
/**
|
|
35
|
+
* List unsubmitted past-due assignments across multiple courses
|
|
36
|
+
*/
|
|
37
|
+
export declare function listUnsubmittedPastDueForCourses(courseIds: number[]): Promise<(Assignment & {
|
|
38
|
+
course_name?: string;
|
|
39
|
+
})[]>;
|
|
40
|
+
//# sourceMappingURL=assignments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../../../src/src/api/assignments.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE7E;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAoB5F;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,KAAK,CAAC,YAAY,GAAG,uBAAuB,GAAG,WAAW,GAAG,WAAW,CAAC,GAClF,OAAO,CAAC,UAAU,CAAC,CASrB;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,IAAI,EACf,OAAO,EAAE,IAAI,GACZ,OAAO,CAAC,UAAU,EAAE,CAAC,CAYvB;AAED;;GAEG;AACH,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAWxF;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,MAAU,GACf,OAAO,CAAC,UAAU,EAAE,CAAC,CAMvB;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAMpF;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAwBpF;AAED;;GAEG;AACH,wBAAsB,gCAAgC,CACpD,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,CAAC,UAAU,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,CAAC,CAiBpD"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Assignments API
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "./client.js";
|
|
5
|
+
/**
|
|
6
|
+
* List assignments for a course
|
|
7
|
+
*/
|
|
8
|
+
export async function listAssignments(options) {
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const { course_id, ...rest } = options;
|
|
11
|
+
const params = {};
|
|
12
|
+
if (rest.bucket) {
|
|
13
|
+
params.bucket = rest.bucket;
|
|
14
|
+
}
|
|
15
|
+
if (rest.order_by) {
|
|
16
|
+
params.order_by = rest.order_by;
|
|
17
|
+
}
|
|
18
|
+
if (rest.include) {
|
|
19
|
+
params.include = rest.include;
|
|
20
|
+
}
|
|
21
|
+
if (rest.search_term) {
|
|
22
|
+
params.search_term = rest.search_term;
|
|
23
|
+
}
|
|
24
|
+
return client.getAll(`/courses/${course_id}/assignments`, params);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get a single assignment by ID
|
|
28
|
+
*/
|
|
29
|
+
export async function getAssignment(courseId, assignmentId, include) {
|
|
30
|
+
const client = getClient();
|
|
31
|
+
const params = {};
|
|
32
|
+
if (include) {
|
|
33
|
+
params.include = include;
|
|
34
|
+
}
|
|
35
|
+
return client.get(`/courses/${courseId}/assignments/${assignmentId}`, params);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* List assignments due within a date range
|
|
39
|
+
*/
|
|
40
|
+
export async function listAssignmentsDueInRange(courseId, startDate, endDate) {
|
|
41
|
+
const assignments = await listAssignments({
|
|
42
|
+
course_id: courseId,
|
|
43
|
+
include: ["submission"],
|
|
44
|
+
order_by: "due_at",
|
|
45
|
+
});
|
|
46
|
+
return assignments.filter((assignment) => {
|
|
47
|
+
if (!assignment.due_at)
|
|
48
|
+
return false;
|
|
49
|
+
const dueDate = new Date(assignment.due_at);
|
|
50
|
+
return dueDate >= startDate && dueDate <= endDate;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* List assignments due this week
|
|
55
|
+
*/
|
|
56
|
+
export async function listAssignmentsDueThisWeek(courseId) {
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const startOfWeek = new Date(now);
|
|
59
|
+
startOfWeek.setDate(now.getDate() - now.getDay());
|
|
60
|
+
startOfWeek.setHours(0, 0, 0, 0);
|
|
61
|
+
const endOfWeek = new Date(startOfWeek);
|
|
62
|
+
endOfWeek.setDate(startOfWeek.getDate() + 7);
|
|
63
|
+
endOfWeek.setHours(23, 59, 59, 999);
|
|
64
|
+
return listAssignmentsDueInRange(courseId, startOfWeek, endOfWeek);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* List upcoming assignments (next N days)
|
|
68
|
+
*/
|
|
69
|
+
export async function listUpcomingAssignments(courseId, days = 7) {
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const endDate = new Date(now);
|
|
72
|
+
endDate.setDate(now.getDate() + days);
|
|
73
|
+
return listAssignmentsDueInRange(courseId, now, endDate);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* List overdue assignments
|
|
77
|
+
*/
|
|
78
|
+
export async function listOverdueAssignments(courseId) {
|
|
79
|
+
return listAssignments({
|
|
80
|
+
course_id: courseId,
|
|
81
|
+
bucket: "overdue",
|
|
82
|
+
include: ["submission"],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* List unsubmitted assignments that are past their due date
|
|
87
|
+
* This catches items that Canvas hasn't flagged as "missing" yet
|
|
88
|
+
*/
|
|
89
|
+
export async function listUnsubmittedPastDue(courseId) {
|
|
90
|
+
const assignments = await listAssignments({
|
|
91
|
+
course_id: courseId,
|
|
92
|
+
include: ["submission"],
|
|
93
|
+
order_by: "due_at",
|
|
94
|
+
});
|
|
95
|
+
const now = new Date();
|
|
96
|
+
return assignments.filter((assignment) => {
|
|
97
|
+
// Must have a due date
|
|
98
|
+
if (!assignment.due_at)
|
|
99
|
+
return false;
|
|
100
|
+
// Due date must be in the past
|
|
101
|
+
const dueDate = new Date(assignment.due_at);
|
|
102
|
+
if (dueDate >= now)
|
|
103
|
+
return false;
|
|
104
|
+
// Must not be submitted
|
|
105
|
+
const submission = assignment.submission;
|
|
106
|
+
if (!submission)
|
|
107
|
+
return true; // No submission record at all
|
|
108
|
+
if (!submission.submitted_at)
|
|
109
|
+
return true; // Has record but not submitted
|
|
110
|
+
return false;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* List unsubmitted past-due assignments across multiple courses
|
|
115
|
+
*/
|
|
116
|
+
export async function listUnsubmittedPastDueForCourses(courseIds) {
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const courseId of courseIds) {
|
|
119
|
+
try {
|
|
120
|
+
const assignments = await listUnsubmittedPastDue(courseId);
|
|
121
|
+
results.push(...assignments);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Skip courses we can't access
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Sort by due date (most recent first)
|
|
128
|
+
return results.sort((a, b) => {
|
|
129
|
+
if (!a.due_at || !b.due_at)
|
|
130
|
+
return 0;
|
|
131
|
+
return new Date(b.due_at).getTime() - new Date(a.due_at).getTime();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas API Client
|
|
3
|
+
* Handles authentication, pagination, and HTTP requests
|
|
4
|
+
*/
|
|
5
|
+
import type { PaginationLinks } from "../types/canvas.js";
|
|
6
|
+
export interface ClientOptions {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
apiToken: string;
|
|
9
|
+
/** Items per page for paginated requests (default: 100) */
|
|
10
|
+
perPage?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class CanvasClient {
|
|
13
|
+
private baseUrl;
|
|
14
|
+
private apiToken;
|
|
15
|
+
private perPage;
|
|
16
|
+
constructor(options: ClientOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Build the full URL for an API endpoint
|
|
19
|
+
*/
|
|
20
|
+
private buildUrl;
|
|
21
|
+
/**
|
|
22
|
+
* Parse Link header for pagination
|
|
23
|
+
*/
|
|
24
|
+
private parseLinkHeader;
|
|
25
|
+
/**
|
|
26
|
+
* Make an authenticated request to the Canvas API
|
|
27
|
+
*/
|
|
28
|
+
fetch<T>(path: string, options?: {
|
|
29
|
+
method?: string;
|
|
30
|
+
params?: Record<string, string | string[] | number | boolean | undefined>;
|
|
31
|
+
body?: unknown;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
data: T;
|
|
34
|
+
links: PaginationLinks;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Fetch all pages of a paginated endpoint
|
|
38
|
+
*/
|
|
39
|
+
fetchAll<T>(path: string, options?: {
|
|
40
|
+
params?: Record<string, string | string[] | number | boolean | undefined>;
|
|
41
|
+
/** Maximum number of items to fetch (default: unlimited) */
|
|
42
|
+
limit?: number;
|
|
43
|
+
}): Promise<T[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Convenience method for GET requests
|
|
46
|
+
*/
|
|
47
|
+
get<T>(path: string, params?: Record<string, string | string[] | number | boolean | undefined>): Promise<T>;
|
|
48
|
+
/**
|
|
49
|
+
* Convenience method for GET requests that return arrays (with pagination)
|
|
50
|
+
*/
|
|
51
|
+
getAll<T>(path: string, params?: Record<string, string | string[] | number | boolean | undefined>): Promise<T[]>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the singleton client (call once at startup)
|
|
55
|
+
*/
|
|
56
|
+
export declare function initClient(options: ClientOptions): CanvasClient;
|
|
57
|
+
/**
|
|
58
|
+
* Get the singleton client instance
|
|
59
|
+
*/
|
|
60
|
+
export declare function getClient(): CanvasClient;
|
|
61
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/src/api/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAY,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,aAAa;IAMlC;;OAEG;IACH,OAAO,CAAC,QAAQ;IAwBhB;;OAEG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;IACG,KAAK,CAAC,CAAC,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;QAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,GACA,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,CAAC;QAAC,KAAK,EAAE,eAAe,CAAA;KAAE,CAAC;IA4C/C;;OAEG;IACG,QAAQ,CAAC,CAAC,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;QAC1E,4DAA4D;QAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GACA,OAAO,CAAC,CAAC,EAAE,CAAC;IA6Cf;;OAEG;IACG,GAAG,CAAC,CAAC,EACT,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GACxE,OAAO,CAAC,CAAC,CAAC;IAKb;;OAEG;IACG,MAAM,CAAC,CAAC,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GACxE,OAAO,CAAC,CAAC,EAAE,CAAC;CAGhB;AAKD;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAG/D;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAKxC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas API Client
|
|
3
|
+
* Handles authentication, pagination, and HTTP requests
|
|
4
|
+
*/
|
|
5
|
+
export class CanvasClient {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
Object.defineProperty(this, "baseUrl", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: void 0
|
|
12
|
+
});
|
|
13
|
+
Object.defineProperty(this, "apiToken", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: void 0
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "perPage", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
26
|
+
this.apiToken = options.apiToken;
|
|
27
|
+
this.perPage = options.perPage || 100;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the full URL for an API endpoint
|
|
31
|
+
*/
|
|
32
|
+
buildUrl(path, params) {
|
|
33
|
+
const url = new URL(`${this.baseUrl}/api/v1${path}`);
|
|
34
|
+
// Add per_page by default for list endpoints
|
|
35
|
+
url.searchParams.set("per_page", String(this.perPage));
|
|
36
|
+
if (params) {
|
|
37
|
+
for (const [key, value] of Object.entries(params)) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
// Handle array params like include[]=foo&include[]=bar
|
|
42
|
+
for (const v of value) {
|
|
43
|
+
url.searchParams.append(`${key}[]`, v);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
url.searchParams.set(key, String(value));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return url.toString();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse Link header for pagination
|
|
55
|
+
*/
|
|
56
|
+
parseLinkHeader(linkHeader) {
|
|
57
|
+
if (!linkHeader)
|
|
58
|
+
return {};
|
|
59
|
+
const links = {};
|
|
60
|
+
const parts = linkHeader.split(",");
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
|
|
63
|
+
if (match) {
|
|
64
|
+
const [, url, rel] = match;
|
|
65
|
+
links[rel] = url;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return links;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Make an authenticated request to the Canvas API
|
|
72
|
+
*/
|
|
73
|
+
async fetch(path, options) {
|
|
74
|
+
const { method = "GET", params, body } = options || {};
|
|
75
|
+
const url = this.buildUrl(path, params);
|
|
76
|
+
const headers = {
|
|
77
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
78
|
+
Accept: "application/json",
|
|
79
|
+
};
|
|
80
|
+
const fetchOptions = {
|
|
81
|
+
method,
|
|
82
|
+
headers,
|
|
83
|
+
};
|
|
84
|
+
if (body) {
|
|
85
|
+
headers["Content-Type"] = "application/json";
|
|
86
|
+
fetchOptions.body = JSON.stringify(body);
|
|
87
|
+
}
|
|
88
|
+
const response = await fetch(url, fetchOptions);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
let errorMessage = `Canvas API error: ${response.status} ${response.statusText}`;
|
|
91
|
+
try {
|
|
92
|
+
const errorData = await response.json();
|
|
93
|
+
if (errorData.message) {
|
|
94
|
+
errorMessage = errorData.message;
|
|
95
|
+
}
|
|
96
|
+
else if (errorData.errors?.length) {
|
|
97
|
+
errorMessage = errorData.errors.map((e) => e.message).join(", ");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Couldn't parse error body, use default message
|
|
102
|
+
}
|
|
103
|
+
throw new Error(errorMessage);
|
|
104
|
+
}
|
|
105
|
+
const data = await response.json();
|
|
106
|
+
const links = this.parseLinkHeader(response.headers.get("Link"));
|
|
107
|
+
return { data, links };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Fetch all pages of a paginated endpoint
|
|
111
|
+
*/
|
|
112
|
+
async fetchAll(path, options) {
|
|
113
|
+
const { params, limit } = options || {};
|
|
114
|
+
const results = [];
|
|
115
|
+
let url = this.buildUrl(path, params);
|
|
116
|
+
while (url) {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
120
|
+
Accept: "application/json",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
let errorMessage = `Canvas API error: ${response.status} ${response.statusText}`;
|
|
125
|
+
try {
|
|
126
|
+
const errorData = await response.json();
|
|
127
|
+
if (errorData.message) {
|
|
128
|
+
errorMessage = errorData.message;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Couldn't parse error body
|
|
133
|
+
}
|
|
134
|
+
throw new Error(errorMessage);
|
|
135
|
+
}
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
results.push(...data);
|
|
138
|
+
// Check if we've hit the limit
|
|
139
|
+
if (limit && results.length >= limit) {
|
|
140
|
+
return results.slice(0, limit);
|
|
141
|
+
}
|
|
142
|
+
// Get next page URL from Link header
|
|
143
|
+
const links = this.parseLinkHeader(response.headers.get("Link"));
|
|
144
|
+
url = links.next;
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Convenience method for GET requests
|
|
150
|
+
*/
|
|
151
|
+
async get(path, params) {
|
|
152
|
+
const { data } = await this.fetch(path, { params });
|
|
153
|
+
return data;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Convenience method for GET requests that return arrays (with pagination)
|
|
157
|
+
*/
|
|
158
|
+
async getAll(path, params) {
|
|
159
|
+
return this.fetchAll(path, { params });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Singleton instance for CLI usage
|
|
163
|
+
let clientInstance = null;
|
|
164
|
+
/**
|
|
165
|
+
* Initialize the singleton client (call once at startup)
|
|
166
|
+
*/
|
|
167
|
+
export function initClient(options) {
|
|
168
|
+
clientInstance = new CanvasClient(options);
|
|
169
|
+
return clientInstance;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the singleton client instance
|
|
173
|
+
*/
|
|
174
|
+
export function getClient() {
|
|
175
|
+
if (!clientInstance) {
|
|
176
|
+
throw new Error("Canvas client not initialized. Call initClient() first.");
|
|
177
|
+
}
|
|
178
|
+
return clientInstance;
|
|
179
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Courses API
|
|
3
|
+
*/
|
|
4
|
+
import type { Course, Enrollment, ListCoursesOptions } from "../types/canvas.js";
|
|
5
|
+
/**
|
|
6
|
+
* List all courses for the authenticated user
|
|
7
|
+
*/
|
|
8
|
+
export declare function listCourses(options?: ListCoursesOptions): Promise<Course[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Get a single course by ID
|
|
11
|
+
*/
|
|
12
|
+
export declare function getCourse(courseId: number): Promise<Course>;
|
|
13
|
+
/**
|
|
14
|
+
* List enrollments for a course (includes grade info)
|
|
15
|
+
*/
|
|
16
|
+
export declare function listCourseEnrollments(courseId: number, options?: {
|
|
17
|
+
type?: string[];
|
|
18
|
+
state?: string[];
|
|
19
|
+
userId?: number | string;
|
|
20
|
+
}): Promise<Enrollment[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Get enrollment with grades for a specific user in a course
|
|
23
|
+
*/
|
|
24
|
+
export declare function getUserEnrollment(courseId: number, userId: number | string): Promise<Enrollment | null>;
|
|
25
|
+
/**
|
|
26
|
+
* List courses with grades for a user (observer or self)
|
|
27
|
+
* Enriches course data with enrollment/grade information
|
|
28
|
+
*/
|
|
29
|
+
export declare function listCoursesWithGrades(userId?: string | number): Promise<(Course & {
|
|
30
|
+
enrollment?: Enrollment;
|
|
31
|
+
})[]>;
|
|
32
|
+
//# sourceMappingURL=courses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/src/api/courses.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEjF;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBjF;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGjE;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,UAAU,EAAE,CAAC,CAgBvB;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,MAAM,GACtB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAO5B;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,GAAE,MAAM,GAAG,MAAe,GAC/B,OAAO,CAAC,CAAC,MAAM,GAAG;IAAE,UAAU,CAAC,EAAE,UAAU,CAAA;CAAE,CAAC,EAAE,CAAC,CAyBnD"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Courses API
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "./client.js";
|
|
5
|
+
/**
|
|
6
|
+
* List all courses for the authenticated user
|
|
7
|
+
*/
|
|
8
|
+
export async function listCourses(options) {
|
|
9
|
+
const client = getClient();
|
|
10
|
+
const params = {};
|
|
11
|
+
if (options?.enrollment_type) {
|
|
12
|
+
params.enrollment_type = options.enrollment_type;
|
|
13
|
+
}
|
|
14
|
+
if (options?.enrollment_state) {
|
|
15
|
+
params.enrollment_state = options.enrollment_state;
|
|
16
|
+
}
|
|
17
|
+
if (options?.include) {
|
|
18
|
+
params.include = options.include;
|
|
19
|
+
}
|
|
20
|
+
if (options?.state) {
|
|
21
|
+
params.state = options.state;
|
|
22
|
+
}
|
|
23
|
+
return client.getAll("/courses", params);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get a single course by ID
|
|
27
|
+
*/
|
|
28
|
+
export async function getCourse(courseId) {
|
|
29
|
+
const client = getClient();
|
|
30
|
+
return client.get(`/courses/${courseId}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* List enrollments for a course (includes grade info)
|
|
34
|
+
*/
|
|
35
|
+
export async function listCourseEnrollments(courseId, options) {
|
|
36
|
+
const client = getClient();
|
|
37
|
+
const params = {};
|
|
38
|
+
if (options?.type) {
|
|
39
|
+
params.type = options.type;
|
|
40
|
+
}
|
|
41
|
+
if (options?.state) {
|
|
42
|
+
params.state = options.state;
|
|
43
|
+
}
|
|
44
|
+
if (options?.userId) {
|
|
45
|
+
params.user_id = options.userId;
|
|
46
|
+
}
|
|
47
|
+
return client.getAll(`/courses/${courseId}/enrollments`, params);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get enrollment with grades for a specific user in a course
|
|
51
|
+
*/
|
|
52
|
+
export async function getUserEnrollment(courseId, userId) {
|
|
53
|
+
const enrollments = await listCourseEnrollments(courseId, {
|
|
54
|
+
userId,
|
|
55
|
+
type: ["StudentEnrollment"],
|
|
56
|
+
});
|
|
57
|
+
return enrollments[0] || null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* List courses with grades for a user (observer or self)
|
|
61
|
+
* Enriches course data with enrollment/grade information
|
|
62
|
+
*/
|
|
63
|
+
export async function listCoursesWithGrades(userId = "self") {
|
|
64
|
+
// Get all active courses with enrollments included
|
|
65
|
+
const courses = await listCourses({
|
|
66
|
+
enrollment_state: "active",
|
|
67
|
+
include: ["enrollments", "term"],
|
|
68
|
+
state: ["available"],
|
|
69
|
+
});
|
|
70
|
+
// Filter and enrich with the specific user's enrollment data
|
|
71
|
+
const coursesWithGrades = courses.map((course) => {
|
|
72
|
+
// Find the enrollment for this user (or observed user)
|
|
73
|
+
const enrollment = course.enrollments?.find((e) => {
|
|
74
|
+
if (userId === "self") {
|
|
75
|
+
return e.type === "StudentEnrollment" || e.type === "ObserverEnrollment";
|
|
76
|
+
}
|
|
77
|
+
return e.user_id === Number(userId) || e.observed_user?.id === Number(userId);
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
...course,
|
|
81
|
+
enrollment,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return coursesWithGrades;
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Student statistics API
|
|
3
|
+
* Calculates late/missing assignment statistics by course
|
|
4
|
+
*/
|
|
5
|
+
export interface CourseStats {
|
|
6
|
+
course_id: number;
|
|
7
|
+
course_name: string;
|
|
8
|
+
total: number;
|
|
9
|
+
late: number;
|
|
10
|
+
missing: number;
|
|
11
|
+
late_pct: number;
|
|
12
|
+
missing_pct: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function getStudentStats(studentId: string | number): Promise<CourseStats[]>;
|
|
15
|
+
//# sourceMappingURL=stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../../src/src/api/stats.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAyDxF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Student statistics API
|
|
3
|
+
* Calculates late/missing assignment statistics by course
|
|
4
|
+
*/
|
|
5
|
+
import { listCourses } from "./courses.js";
|
|
6
|
+
import { listSubmissions } from "./submissions.js";
|
|
7
|
+
import { getMissingSubmissions } from "./users.js";
|
|
8
|
+
export async function getStudentStats(studentId) {
|
|
9
|
+
const courses = await listCourses({
|
|
10
|
+
enrollment_state: "active",
|
|
11
|
+
state: ["available"],
|
|
12
|
+
});
|
|
13
|
+
// Get missing assignments from Canvas
|
|
14
|
+
const missing = await getMissingSubmissions({
|
|
15
|
+
studentId,
|
|
16
|
+
include: ["course"],
|
|
17
|
+
});
|
|
18
|
+
// Count missing by course
|
|
19
|
+
const missingByCourse = new Map();
|
|
20
|
+
for (const m of missing) {
|
|
21
|
+
missingByCourse.set(m.course_id, (missingByCourse.get(m.course_id) || 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
const stats = [];
|
|
24
|
+
for (const course of courses) {
|
|
25
|
+
try {
|
|
26
|
+
const subs = await listSubmissions({
|
|
27
|
+
course_id: course.id,
|
|
28
|
+
student_ids: [Number(studentId)],
|
|
29
|
+
include: ["assignment"],
|
|
30
|
+
});
|
|
31
|
+
let total = 0;
|
|
32
|
+
let late = 0;
|
|
33
|
+
for (const sub of subs) {
|
|
34
|
+
if (sub.assignment === null || sub.assignment === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
total++;
|
|
37
|
+
if (sub.late)
|
|
38
|
+
late++;
|
|
39
|
+
}
|
|
40
|
+
const missingCount = missingByCourse.get(course.id) || 0;
|
|
41
|
+
stats.push({
|
|
42
|
+
course_id: course.id,
|
|
43
|
+
course_name: course.name,
|
|
44
|
+
total,
|
|
45
|
+
late,
|
|
46
|
+
missing: missingCount,
|
|
47
|
+
late_pct: total > 0 ? Math.round((late / total) * 1000) / 10 : 0,
|
|
48
|
+
missing_pct: total > 0 ? Math.round((missingCount / total) * 1000) / 10 : 0,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Skip courses we can't access
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Sort by missing percentage descending (worst first)
|
|
56
|
+
stats.sort((a, b) => b.missing_pct - a.missing_pct || b.late_pct - a.late_pct);
|
|
57
|
+
return stats;
|
|
58
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Submissions API
|
|
3
|
+
*/
|
|
4
|
+
import type { Submission, ListSubmissionsOptions } from "../types/canvas.js";
|
|
5
|
+
/**
|
|
6
|
+
* List submissions for a course (all students or specific students)
|
|
7
|
+
*/
|
|
8
|
+
export declare function listSubmissions(options: ListSubmissionsOptions): Promise<Submission[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Get submission for a specific assignment and user
|
|
11
|
+
*/
|
|
12
|
+
export declare function getSubmission(courseId: number, assignmentId: number, userId: number | string, include?: Array<"submission_history" | "submission_comments" | "rubric_assessment">): Promise<Submission>;
|
|
13
|
+
/**
|
|
14
|
+
* List all submissions for a specific user in a course
|
|
15
|
+
*/
|
|
16
|
+
export declare function listUserSubmissions(courseId: number, userId: number | string, options?: {
|
|
17
|
+
include?: Array<"submission_history" | "submission_comments" | "assignment" | "user">;
|
|
18
|
+
workflowState?: "submitted" | "unsubmitted" | "graded" | "pending_review";
|
|
19
|
+
}): Promise<Submission[]>;
|
|
20
|
+
/**
|
|
21
|
+
* List graded submissions for a user
|
|
22
|
+
*/
|
|
23
|
+
export declare function listGradedSubmissions(courseId: number, userId: number | string): Promise<Submission[]>;
|
|
24
|
+
/**
|
|
25
|
+
* List submissions with grades below a threshold
|
|
26
|
+
*/
|
|
27
|
+
export declare function listSubmissionsBelowThreshold(courseId: number, userId: number | string, threshold: number): Promise<Submission[]>;
|
|
28
|
+
/**
|
|
29
|
+
* List unsubmitted past-due assignments for a student
|
|
30
|
+
* Uses the submissions endpoint to get the student's actual submission status
|
|
31
|
+
*/
|
|
32
|
+
export declare function listUnsubmittedPastDueForStudent(courseId: number, studentId: number | string): Promise<Submission[]>;
|
|
33
|
+
//# sourceMappingURL=submissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submissions.d.ts","sourceRoot":"","sources":["../../../src/src/api/submissions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE7E;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAuB5F;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,OAAO,CAAC,EAAE,KAAK,CAAC,oBAAoB,GAAG,qBAAqB,GAAG,mBAAmB,CAAC,GAClF,OAAO,CAAC,UAAU,CAAC,CAYrB;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,KAAK,CAAC,oBAAoB,GAAG,qBAAqB,GAAG,YAAY,GAAG,MAAM,CAAC,CAAC;IACtF,aAAa,CAAC,EAAE,WAAW,GAAG,aAAa,GAAG,QAAQ,GAAG,gBAAgB,CAAC;CAC3E,GACA,OAAO,CAAC,UAAU,EAAE,CAAC,CAOvB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,MAAM,GACtB,OAAO,CAAC,UAAU,EAAE,CAAC,CAQvB;AAED;;GAEG;AACH,wBAAsB,6BAA6B,CACjD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,MAAM,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CAWvB;AAED;;;GAGG;AACH,wBAAsB,gCAAgC,CACpD,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,OAAO,CAAC,UAAU,EAAE,CAAC,CAwBvB"}
|