@kainoa/5c-mcp 0.4.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/README.md +33 -0
- package/dist/chunk-3PCAZ5XS.js +258 -0
- package/dist/chunk-B3TGUWJT.js +253 -0
- package/dist/chunk-R2DX67JM.js +257 -0
- package/dist/hyperschedule-32D52IZE-P6VRQDQR.js +21 -0
- package/dist/hyperschedule-OT6BLROL-BRSP2NPX.js +21 -0
- package/dist/hyperschedule-VZ6NIDDP-NJRKLKJI.js +21 -0
- package/dist/server.js +4968 -0
- package/package.json +50 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../core/dist/chunk-D2NNBE6W.js
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var rawDateSchema = z.object({
|
|
6
|
+
year: z.number(),
|
|
7
|
+
month: z.number().min(1).max(12),
|
|
8
|
+
day: z.number().min(1).max(31)
|
|
9
|
+
});
|
|
10
|
+
var rawCourseCodeSchema = z.object({
|
|
11
|
+
department: z.string(),
|
|
12
|
+
courseNumber: z.number(),
|
|
13
|
+
suffix: z.string(),
|
|
14
|
+
affiliation: z.string()
|
|
15
|
+
});
|
|
16
|
+
var rawScheduleSchema = z.object({
|
|
17
|
+
startTime: z.number(),
|
|
18
|
+
// seconds since midnight
|
|
19
|
+
endTime: z.number(),
|
|
20
|
+
days: z.array(z.string()),
|
|
21
|
+
locations: z.array(z.string())
|
|
22
|
+
});
|
|
23
|
+
var rawSectionSchema = z.object({
|
|
24
|
+
courseAreas: z.array(z.string()),
|
|
25
|
+
credits: z.number(),
|
|
26
|
+
permCount: z.number().default(0),
|
|
27
|
+
seatsTotal: z.number(),
|
|
28
|
+
seatsFilled: z.number(),
|
|
29
|
+
status: z.string(),
|
|
30
|
+
// O | C | R | U (keep raw; map to labels downstream)
|
|
31
|
+
startDate: rawDateSchema,
|
|
32
|
+
endDate: rawDateSchema,
|
|
33
|
+
instructors: z.array(z.object({ name: z.string() })),
|
|
34
|
+
course: z.object({
|
|
35
|
+
code: rawCourseCodeSchema,
|
|
36
|
+
title: z.string(),
|
|
37
|
+
description: z.string(),
|
|
38
|
+
primaryAssociation: z.string(),
|
|
39
|
+
potentialError: z.boolean().default(false)
|
|
40
|
+
}),
|
|
41
|
+
schedules: z.array(rawScheduleSchema),
|
|
42
|
+
potentialError: z.boolean().default(false),
|
|
43
|
+
identifier: z.object({
|
|
44
|
+
department: z.string(),
|
|
45
|
+
courseNumber: z.number(),
|
|
46
|
+
suffix: z.string(),
|
|
47
|
+
affiliation: z.string(),
|
|
48
|
+
sectionNumber: z.number(),
|
|
49
|
+
year: z.number(),
|
|
50
|
+
term: z.string(),
|
|
51
|
+
// FA | SP | SU
|
|
52
|
+
half: z.nullable(z.object({ prefix: z.string(), number: z.number() }))
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
var rawSectionsSchema = z.array(rawSectionSchema);
|
|
56
|
+
var rawHistoryEntrySchema = z.object({
|
|
57
|
+
code: rawCourseCodeSchema,
|
|
58
|
+
terms: z.array(z.object({ year: z.number(), term: z.string() }))
|
|
59
|
+
});
|
|
60
|
+
var rawHistorySchema = z.array(rawHistoryEntrySchema);
|
|
61
|
+
var rawTermSchema = z.object({ year: z.number(), term: z.string() });
|
|
62
|
+
var rawCourseAreaSchema = z.object({
|
|
63
|
+
area: z.string(),
|
|
64
|
+
description: z.string()
|
|
65
|
+
});
|
|
66
|
+
var termSchema = z.string().regex(/^(FA|SP|SU)\d{4}$/, "Term must look like FA2026");
|
|
67
|
+
var daySchema = z.enum(["M", "T", "W", "R", "F", "S", "U"]);
|
|
68
|
+
var statusSchema = z.enum(["O", "C", "R", "U"]);
|
|
69
|
+
var meetingSchema = z.object({
|
|
70
|
+
days: z.array(daySchema),
|
|
71
|
+
startMin: z.number(),
|
|
72
|
+
// minutes since midnight (math)
|
|
73
|
+
endMin: z.number(),
|
|
74
|
+
startStr: z.string(),
|
|
75
|
+
// "13:15" (quote)
|
|
76
|
+
endStr: z.string(),
|
|
77
|
+
start12: z.string(),
|
|
78
|
+
// "1:15 PM" (display)
|
|
79
|
+
end12: z.string(),
|
|
80
|
+
locations: z.array(z.string())
|
|
81
|
+
});
|
|
82
|
+
var sectionSchema = z.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
// "CSCI 051 PO-01 FA2026" (canonical, frozen v1)
|
|
85
|
+
term: termSchema,
|
|
86
|
+
year: z.number(),
|
|
87
|
+
termCode: z.enum(["FA", "SP", "SU"]),
|
|
88
|
+
half: z.nullable(z.object({ prefix: z.string(), number: z.number() })),
|
|
89
|
+
dept: z.string(),
|
|
90
|
+
number: z.number(),
|
|
91
|
+
suffix: z.string(),
|
|
92
|
+
school: z.string(),
|
|
93
|
+
// affiliation, uppercase (PO/CM/HM/SC/PZ + KS/JP/…)
|
|
94
|
+
sectionNo: z.number(),
|
|
95
|
+
code: z.string(),
|
|
96
|
+
// "CSCI 051" (dept + zero-padded + suffix, no school)
|
|
97
|
+
title: z.string(),
|
|
98
|
+
description: z.string(),
|
|
99
|
+
areas: z.array(z.string()),
|
|
100
|
+
instructors: z.array(z.string()),
|
|
101
|
+
meetings: z.array(meetingSchema),
|
|
102
|
+
credits: z.number(),
|
|
103
|
+
status: statusSchema,
|
|
104
|
+
statusLabel: z.string(),
|
|
105
|
+
// Open | Closed | Reopened | Unknown
|
|
106
|
+
seats: z.object({
|
|
107
|
+
filled: z.number(),
|
|
108
|
+
total: z.number(),
|
|
109
|
+
available: z.number(),
|
|
110
|
+
permCount: z.number()
|
|
111
|
+
}),
|
|
112
|
+
startDate: z.string(),
|
|
113
|
+
// ISO
|
|
114
|
+
endDate: z.string(),
|
|
115
|
+
potentialError: z.boolean(),
|
|
116
|
+
primaryAssociation: z.string(),
|
|
117
|
+
affiliation: z.string()
|
|
118
|
+
});
|
|
119
|
+
var cacheMetaSchema = z.object({
|
|
120
|
+
term: z.string(),
|
|
121
|
+
fetched_at: z.string(),
|
|
122
|
+
etag: z.string().nullable(),
|
|
123
|
+
hash: z.string(),
|
|
124
|
+
count: z.number(),
|
|
125
|
+
source_url: z.string(),
|
|
126
|
+
ttl_kind: z.string(),
|
|
127
|
+
stale: z.boolean(),
|
|
128
|
+
source: z.enum(["live", "stale-cache", "offline-file"]),
|
|
129
|
+
quarantined: z.number().int().nonnegative().default(0)
|
|
130
|
+
});
|
|
131
|
+
var BANANA_BASE = "https://banana.hyperschedule.io/v4";
|
|
132
|
+
var USER_AGENT = "5C-Index/0.1 (+research/edu; cache 60s)";
|
|
133
|
+
async function sleep(ms) {
|
|
134
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
135
|
+
}
|
|
136
|
+
async function fetchJson(url, validate, opts = {}, fetchFn = globalThis.fetch) {
|
|
137
|
+
const { etag, timeoutMs = 15e3, retries = 3 } = opts;
|
|
138
|
+
let lastErr = null;
|
|
139
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
140
|
+
const ctrl = new AbortController();
|
|
141
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetchFn(url, {
|
|
144
|
+
signal: ctrl.signal,
|
|
145
|
+
headers: {
|
|
146
|
+
Accept: "application/json",
|
|
147
|
+
"User-Agent": USER_AGENT,
|
|
148
|
+
...etag ? { "If-None-Match": etag } : {}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
if (res.status === 304) {
|
|
153
|
+
return { data: void 0, etag: etag ?? null, notModified: true };
|
|
154
|
+
}
|
|
155
|
+
if (res.status === 404) {
|
|
156
|
+
const err = new Error(`Not found: ${url}`);
|
|
157
|
+
err.code = "E_NOT_FOUND_UPSTREAM";
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
if (res.status === 429 || res.status >= 500) {
|
|
161
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
162
|
+
const wait = retryAfter ? Math.min(Number.parseInt(retryAfter, 10) * 1e3 || 2e3, 15e3) : Math.min(500 * 2 ** attempt + Math.random() * 250, 8e3);
|
|
163
|
+
lastErr = new Error(`Upstream ${res.status} for ${url}`);
|
|
164
|
+
if (attempt < retries) {
|
|
165
|
+
await sleep(wait);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
throw lastErr;
|
|
169
|
+
}
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const err = new Error(`Upstream ${res.status} for ${url}`);
|
|
172
|
+
err.code = "E_FETCH";
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
const json = await res.json();
|
|
176
|
+
return {
|
|
177
|
+
data: validate(json),
|
|
178
|
+
etag: res.headers.get("ETag"),
|
|
179
|
+
notModified: false
|
|
180
|
+
};
|
|
181
|
+
} catch (err) {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
if (err.code === "E_NOT_FOUND_UPSTREAM") throw err;
|
|
184
|
+
if (err.name === "AbortError") {
|
|
185
|
+
lastErr = new Error(`Timeout fetching ${url}`);
|
|
186
|
+
} else {
|
|
187
|
+
lastErr = err;
|
|
188
|
+
}
|
|
189
|
+
if (attempt < retries && (!err.code || err.code === "E_FETCH")) {
|
|
190
|
+
if (err.code === "E_FETCH") throw err;
|
|
191
|
+
await sleep(Math.min(500 * 2 ** attempt + Math.random() * 250, 8e3));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw lastErr;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw lastErr;
|
|
198
|
+
}
|
|
199
|
+
function fetchAllTerms(opts, fetchFn) {
|
|
200
|
+
return fetchJson(
|
|
201
|
+
`${BANANA_BASE}/term/all`,
|
|
202
|
+
(u) => rawTermSchema.array().parse(u),
|
|
203
|
+
opts,
|
|
204
|
+
fetchFn
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
function fetchCurrentTerm(opts, fetchFn) {
|
|
208
|
+
return fetchJson(
|
|
209
|
+
`${BANANA_BASE}/term/current`,
|
|
210
|
+
(u) => rawTermSchema.parse(u),
|
|
211
|
+
opts,
|
|
212
|
+
fetchFn
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function fetchSections(term, opts, fetchFn) {
|
|
216
|
+
return fetchJson(
|
|
217
|
+
`${BANANA_BASE}/sections/${term}`,
|
|
218
|
+
(u) => rawSectionsSchema.parse(u),
|
|
219
|
+
opts,
|
|
220
|
+
fetchFn
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function fetchOfferingHistory(term, opts, fetchFn) {
|
|
224
|
+
return fetchJson(
|
|
225
|
+
`${BANANA_BASE}/offering-history/${term}`,
|
|
226
|
+
(u) => rawHistorySchema.parse(u),
|
|
227
|
+
opts,
|
|
228
|
+
fetchFn
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
function fetchCourseAreas(opts, fetchFn) {
|
|
232
|
+
return fetchJson(
|
|
233
|
+
`${BANANA_BASE}/course-areas`,
|
|
234
|
+
(u) => u.map((x) => ({
|
|
235
|
+
area: String(x.area),
|
|
236
|
+
description: String(x.description)
|
|
237
|
+
})),
|
|
238
|
+
opts,
|
|
239
|
+
fetchFn
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export {
|
|
244
|
+
rawSectionSchema,
|
|
245
|
+
rawSectionsSchema,
|
|
246
|
+
rawHistorySchema,
|
|
247
|
+
sectionSchema,
|
|
248
|
+
cacheMetaSchema,
|
|
249
|
+
BANANA_BASE,
|
|
250
|
+
USER_AGENT,
|
|
251
|
+
fetchJson,
|
|
252
|
+
fetchAllTerms,
|
|
253
|
+
fetchCurrentTerm,
|
|
254
|
+
fetchSections,
|
|
255
|
+
fetchOfferingHistory,
|
|
256
|
+
fetchCourseAreas
|
|
257
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BANANA_BASE,
|
|
4
|
+
USER_AGENT,
|
|
5
|
+
fetchAllTerms,
|
|
6
|
+
fetchCourseAreas,
|
|
7
|
+
fetchCurrentTerm,
|
|
8
|
+
fetchJson,
|
|
9
|
+
fetchOfferingHistory,
|
|
10
|
+
fetchSections,
|
|
11
|
+
} from "./chunk-3PCAZ5XS.js";
|
|
12
|
+
export {
|
|
13
|
+
BANANA_BASE,
|
|
14
|
+
USER_AGENT,
|
|
15
|
+
fetchAllTerms,
|
|
16
|
+
fetchCourseAreas,
|
|
17
|
+
fetchCurrentTerm,
|
|
18
|
+
fetchJson,
|
|
19
|
+
fetchOfferingHistory,
|
|
20
|
+
fetchSections,
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BANANA_BASE,
|
|
4
|
+
USER_AGENT,
|
|
5
|
+
fetchAllTerms,
|
|
6
|
+
fetchCourseAreas,
|
|
7
|
+
fetchCurrentTerm,
|
|
8
|
+
fetchJson,
|
|
9
|
+
fetchOfferingHistory,
|
|
10
|
+
fetchSections
|
|
11
|
+
} from "./chunk-R2DX67JM.js";
|
|
12
|
+
export {
|
|
13
|
+
BANANA_BASE,
|
|
14
|
+
USER_AGENT,
|
|
15
|
+
fetchAllTerms,
|
|
16
|
+
fetchCourseAreas,
|
|
17
|
+
fetchCurrentTerm,
|
|
18
|
+
fetchJson,
|
|
19
|
+
fetchOfferingHistory,
|
|
20
|
+
fetchSections
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BANANA_BASE,
|
|
4
|
+
USER_AGENT,
|
|
5
|
+
fetchAllTerms,
|
|
6
|
+
fetchCourseAreas,
|
|
7
|
+
fetchCurrentTerm,
|
|
8
|
+
fetchJson,
|
|
9
|
+
fetchOfferingHistory,
|
|
10
|
+
fetchSections
|
|
11
|
+
} from "./chunk-B3TGUWJT.js";
|
|
12
|
+
export {
|
|
13
|
+
BANANA_BASE,
|
|
14
|
+
USER_AGENT,
|
|
15
|
+
fetchAllTerms,
|
|
16
|
+
fetchCourseAreas,
|
|
17
|
+
fetchCurrentTerm,
|
|
18
|
+
fetchJson,
|
|
19
|
+
fetchOfferingHistory,
|
|
20
|
+
fetchSections
|
|
21
|
+
};
|