@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.
@@ -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
+ };