# Project export: College Copilot

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: A multi-agent supported website for UC Berkeley students to assemble a conflict-free schedule and find research opportunities worth a warm outreach email.
- Devpost: https://devpost.com/software/college-copilot
- GitHub: https://github.com/vinaypalta10/College-Copilot.git
- Video: https://www.youtube.com/embed/jOi6n9Lqmv4?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — AngelaWuRX (12 commits), BRQ7 (8 commits), Claude Opus 4.8 (7 commits), vinaypalta10 (1 commits)

## Devpost submission (written by the team)

### Inspiration

Getting into UC Berkeley is hard, but then navigating through it can be even harder. After earning a place at a top public university, students face scattered course catalogs, outdated opportunity pages, professor directories, and job boards. They may know what they want to become without knowing which classes to take, whom to work with, or which opportunities are actually relevant. College Copilot turns that information overload into a personalized path from coursework to research to industry. Clone the repository, sign in, describe your goals and constraints, and explore Berkeley resources from one local workspace. A guided demo video is published! College Copilot began as three ideas from three Berkeley students. Two freshmen proposed course planning and professor/research discovery; a junior proposed an industry-opportunity assistant. We realized these were not separate issues, but rather they are three stages of the same student journey. Our iterations moved the project away from a single all-purpose chatbot. We separated each workflow into narrow agents, moved scoring and schedule conflicts into testable deterministic code, added compressed context and execution traces, introduced keyless fallbacks, and required human approval for outreach. The architecture reflects the decisions and constraints we encountered, not just an interface placed around an LLM.

### What it does

Plan courses: Rank Berkeley classes against remaining requirements, interests, professor ratings, workload, time constraints, and open seats. Find research: Discover professors, labs, and undergraduate research opportunities, then prepare outreach for the student to review. Explore industry: Normalize job postings, digest qualifications, generate resume-tailoring prompts, and identify possible networking leads. Explain every result: Show fit scores, evidence, warnings, and an agent trace instead of returning an unexplained answer.

### How we built it

We built College Copilot as three connected agent systems: course planning, research discovery, and industry opportunities. TypeScript and Express power the backend, SQLite stores user data, and Redis supports semantic vector retrieval and short-term agent memory. Claude interprets student goals and explains recommendations, while deterministic code handles ranking, requirement matching, workload evaluation, and schedule conflicts. Additionally, the UI has Deepgram for all text fields to improve accessibility for users. The frontend presents evidence, warnings, and agent traces so students can understand each recommendation.

### Challenges we ran into

University information is fragmented across course catalogs, faculty directories, research pages, and job boards, often using inconsistent or outdated formats. We also had to preserve recommendation quality without sending enormous datasets to an LLM. Coordinating specialized agents, isolating user data, handling unavailable external services, and preventing automated outreach required careful architectural boundaries. We addressed these challenges with normalized schemas, compressed context packets, deterministic fallbacks, and explicit human approval for consequential actions.

### Accomplishments we're proud of

We are proud that College Copilot connects three stages of the student journey instead of solving only one isolated task. The system produces explainable course rankings, searches research and industry opportunities, prepares outreach without sending it, and reports measurable context-compression statistics. Redis is used beyond caching for semantic course retrieval and per-user agent memory. Most importantly, the project transforms our three original ideas into one coherent tool focused on educational access and economic opportunity.

### What we learned

We learned that effective AI systems should not ask an LLM to do everything. Deterministic tools are better for filtering, scoring, conflict detection, and enforcing constraints; language models are most useful for understanding intent and communicating results. We also learned that access to information is not the same as access to opportunity. Institutional knowledge must be organized, personalized, and actionable before it can genuinely help students.

### What's next

Our next ambitious feature is a Living Opportunity Graph connecting student goals, courses, prerequisites, skills, professors, labs, research programs, internships, and jobs. We hope to collaborate with CalCentral and official UC Berkeley departments to integrate verified academic requirements, enrollment information, deadlines, and advising resources. This would allow College Copilot to provide current, source-backed guidance while complementing, rather than replacing, official advisers. College Copilot could grow into a real product for incoming freshmen, beginning at Berkeley and expanding through university-specific data partnerships. Investment would help us build secure institutional integrations, production infrastructure, adviser-reviewed evaluations, and a scalable university adapter system. Its long-term potential is trusted advising infrastructure that gives every student access to the guidance and opportunities currently available mainly through strong personal networks.

## README (from the GitHub repository)

# College Copilot

Getting into UC Berkeley is hard. Navigating it can be even harder. After earning a place at a top public university, students face scattered course catalogs, outdated opportunity pages, professor directories, and job boards. They may know what they want to become without knowing which classes to take, whom to work with, or which opportunities are actually relevant.

**College Copilot turns that information overload into a personalized path from coursework to research to industry.** Clone the repository, sign in, describe your goals and constraints, and explore Berkeley resources from one local workspace. A guided demo is coming soon.

## What It Does

- **Plan courses:** Rank Berkeley classes against remaining requirements, interests, professor ratings, workload, time constraints, and open seats.
- **Find research:** Discover professors, labs, and undergraduate research opportunities, then prepare outreach for the student to review.
- **Explore industry:** Normalize job postings, digest qualifications, generate resume-tailoring prompts, and identify possible networking leads.
- **Explain every result:** Show fit scores, evidence, warnings, and an agent trace instead of returning an unexplained answer.

## Why These Tracks

### Anthropic: Educational and Economic Opportunity

Built with Claude Code and powered by Claude when an API key is available, College Copilot helps students translate open-ended goals into concrete academic and career decisions. Claude understands natural-language intent and explains recommendations, while specialized agents and deterministic tools handle retrieval, policy interpretation, ranking, and conflict detection. This division makes AI useful where judgment and communication matter without asking it to invent facts that code can verify.

### The Token Company: Context Compression

An LLM should not need the entire course catalog to recommend eight classes. College Copilot retrieves and scores candidates first, then compresses course records, student constraints, requirement coverage, and policy warnings into a small context packet. The final explanation is generated only from that packet, and the API reports estimated tokens before and after compression, the compression ratio, and percentage saved. This reduces noise and token use while preserving the evidence needed for a high-quality recommendation.

### Redis: Agent Memory and Vector Retrieval

College Copilot uses Redis beyond conventional response caching. It stores a semantic vector index of Berkeley courses for context retrieval and records per-user research and job agent-memory events with bounded lifetimes, allowing independent agents to retain useful workflow context without passing entire histories to an LLM. The architecture combines deterministic embeddings and ranking, isolated key namespaces, health metrics, read-through retrieval, and a resilient SQLite fallback so Redis accelerates a scalable student workflow without becoming a single point of failure. Together, vector search and agent memory turn fragmented university data into a fast, personalized discovery experience for a real human problem.

### Ddoski's World: Educational Access

University resources may be public, but access to them is not equal. Students with experienced mentors and established networks know which courses matter, how to find research, and where to look for career opportunities; first-generation and under-networked students often must reconstruct that knowledge alone. College Copilot makes this hidden institutional knowledge searchable, personalized, and actionable, helping more students turn education into research experience and economic opportunity while keeping every consequential decision under their control.

## Ethical Considerations

College Copilot follows a **draft, explain, and confirm** model. Agents can surface public contact information and prepare outreach, but they never send emails, LinkedIn messages, applications, or connection requests automatically. Recommendations expose their evidence, label heuristic judgments, and do not claim to replace official academic advising or degree audits.

User profiles, plans, and results are isolated behind authenticated sessions. Production deployments can restrict Google sign-in to verified `@berkeley.edu` accounts with `OAUTH_HOSTED_DOMAIN=berkeley.edu`; local development intentionally provides a clearly separated keyless login. We collect only the information needed to personalize results and keep consequential decisions with the student.

## Brainstorming and Process

College Copilot began as three ideas from three Berkeley students. Two freshmen proposed course planning and professor/research discovery; a junior proposed an industry-opportunity assistant. We realized these were not separate problems but three stages of the same student journey.

Our iterations moved the project away from a single all-purpose chatbot. We separated each workflow into narrow agents, moved scoring and schedule conflicts into testable deterministic code, added compressed context and execution traces, introduced keyless fallbacks, and required human approval for outreach. The architecture reflects the decisions and constraints we encountered, not just an interface placed around an LLM.

## The Ambitious Next Step: An Opportunity Graph

Our next step is a living graph connecting:

```text
student goals
  -> courses and prerequisites
  -> skills
  -> professors and labs
  -> research opportunities
  -> internships and jobs
```

A student could ask, *"I want to work in climate AI next summer. What should I take, whom should I learn from, and what should I apply to?"* College Copilot would produce an evidence-backed, semester-by-semester action plan, identify missing skills, monitor deadlines and stale listings, and prepare user-approved outreach. Each agent would receive only a compressed student context capsule, joining personalized guidance, token efficiency, and practical automation in one system.

## Quick Start

```bash
cp .env.example .env
npm install
npm run import:courses
npm run import:professors
npm run dev
```

Open [http://localhost:4174](http://localhost:4174). API keys are optional: without Google OAuth the local app enables development login, and without an Anthropic key the advisor uses deterministic parsing and scoring fallbacks.

```bash
npm test
```

## Stack

TypeScript, Node.js, Express, Claude, SQLite, Redis, Zod, Google OAuth, and a vanilla JavaScript frontend. See [`src/agents`](src/agents/README.md) for the agent architecture and [`ROADMAP.md`](ROADMAP.md) for current implementation status.


## Detected evidence (automated analysis)

Indexed codebase: 89 recognized source files, 1047 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Redis (technology) — detected in the code
- SQL (language) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (94 of 94)

```
.env.example
.gitattributes
.gitignore
list.html
package.json
public/index.html
public/js/app.js
public/js/berkeley-majors.js
public/js/voice.js
public/styles.css
README.md
scripts/redis-wsl.sh
src/agents/course-planner/advising-orchestrator.ts
src/agents/course-planner/index.ts
src/agents/course-planner/planning-evaluator-agent.ts
src/agents/course-planner/README.md
src/agents/course-planner/school-policy-agent.ts
src/agents/course-planner/student-query-agent.ts
src/agents/index.ts
src/agents/industry-jobs/index.ts
src/agents/industry-jobs/jd-digest-agent.ts
src/agents/industry-jobs/job-normalizer-agent.ts
src/agents/industry-jobs/job-search-agent.ts
src/agents/industry-jobs/jobs-orchestrator.ts
src/agents/industry-jobs/networking-agent.ts
src/agents/industry-jobs/README.md
src/agents/industry-jobs/resume-prompt-agent.ts
src/agents/industry-jobs/types.ts
src/agents/opportunity-orchestrator.ts
src/agents/README.md
src/agents/registry.ts
src/agents/research-opportunities/lab-opportunities.ts
src/agents/research-opportunities/README.md
src/agents/shared/web.ts
src/agents/types.ts
src/api/advisor.ts
src/api/auth.ts
src/api/courses.ts
src/api/jobs.ts
src/api/opportunities.ts
src/api/plans.ts
src/api/professors.ts
src/api/profile.ts
src/api/schedule.ts
src/api/voice.ts
src/auth/google.ts
src/auth/session.ts
src/db/client.ts
src/db/courseCache.ts
src/db/redis.ts
src/db/repo.ts
src/db/schema.sql
src/db/vectorStore.ts
src/ingest/berkeleyFaculty.ts
src/ingest/berkeleytime.ts
src/ingest/ratemyprofessors.ts
src/lib/embed.ts
src/lib/instructors.ts
src/lib/loadEnv.ts
src/lib/log.ts
src/lib/professors.ts
src/lib/rateLimit.ts
src/lib/validate.ts
src/memory/agentMemory.ts
src/providers/anthropic.ts
src/providers/berkeleyProfessors.ts
src/providers/deepgram.ts
src/providers/index.ts
src/providers/types.ts
src/scorer/candidates.ts
src/scorer/courseScore.ts
src/scorer/majorSubjects.ts
src/scorer/opportunityScore.ts
src/scorer/requirements.ts
src/scorer/scheduleBuilder.ts
src/scripts/clean-legacy-data.ts
src/scripts/import-courses.ts
src/scripts/import-professors.ts
src/server.ts
src/skills/index.ts
src/skills/professor-rating.ts
src/skills/registry.ts
tests/advisor.test.ts
tests/agents.test.ts
tests/berkeleyProfessors.test.ts
tests/courseCache.test.ts
tests/courses.test.ts
tests/db.test.ts
tests/deepgram.test.ts
tests/embed.test.ts
tests/jobs.test.ts
tests/researchLabs.test.ts
tests/security.test.ts
tsconfig.json
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.39.0, @types/better-sqlite3@^7.6.12, @types/express@^5.0.0, @types/node@^22.10.5, better-sqlite3@^11.5.0, express@^4.21.2, redis@^4.7.1, tsx@^4.19.2, typescript@^5.7.2, zod@^3.24.1

### Recent commits (newest first)

- Delete SPONSORS.md
- Delete ROADMAP.md
- Merge remote-tracking branch 'origin/main' into codex/small-fixes-front-page-logo
- Small fixes and front-page logo update
- Add industry-jobs agent system and research-lab opportunities
- Expand professor database
- Merge remote-tracking branch 'origin/main' into codex/faculty-import-and-smart-course-search
- Add Berkeley faculty import and smarter course search
- Merge pull request #2 from vinaypalta10/feat/three-agent-systems
- Merge remote-tracking branch 'origin/main' into feat/three-agent-systems
- course-planner agent
- Merge remote-tracking branch 'origin/main' into feat/three-agent-systems
- Preserve Berkeley UI before professor sync
- WIP: in-progress refactor before merging groupmates' features
- Added professors data
- Cleaned up UI for professors
- Added submenus for research of professors
- Fix research/jobs result quality: junk links, dupes, undefined score
- Load .env at startup so local API keys + DB path take effect
- Build three multi-agent systems (course-planner, research, industry-jobs)

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "college-copilot",
  "version": "0.3.0",
  "private": true,
  "description": "College Copilot — a multi-user agent that finds UC Berkeley classes (by requirement, professor rating, workload, time) and research opportunities.",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "start": "tsx src/server.ts",
    "clean:legacy-data": "tsx src/scripts/clean-legacy-data.ts",
    "import:courses": "tsx src/scripts/import-courses.ts",
    "import:professors": "tsx src/scripts/import-professors.ts",
    "test": "node --test --import tsx tests/*.test.ts"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.39.0",
    "better-sqlite3": "^11.5.0",
    "express": "^4.21.2",
    "redis": "^4.7.1",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.12",
    "@types/express": "^5.0.0",
    "@types/node": "^22.10.5",
    "tsx": "^4.19.2",
    "typescript": "^5.7.2"
  }
}

```

### src/server.ts

```typescript
import "./lib/loadEnv.ts";
import express from "express";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { closeDb, getDb } from "./db/client.ts";
import { Repo } from "./db/repo.ts";
import { authRouter } from "./api/auth.ts";
import { profileRouter } from "./api/profile.ts";
import { coursesRouter } from "./api/courses.ts";
import { advisorRouter } from "./api/advisor.ts";
import { plansRouter } from "./api/plans.ts";
import { scheduleRouter } from "./api/schedule.ts";
import { opportunitiesRouter } from "./api/opportunities.ts";
import { jobsRouter } from "./api/jobs.ts";
import { professorsRouter } from "./api/professors.ts";
import { voiceRouter } from "./api/voice.ts";
import { attachUser } from "./auth/session.ts";
import "./skills/index.ts";
import "./agents/index.ts";
import { rateLimit } from "./lib/rateLimit.ts";
import { log } from "./lib/log.ts";
import { getRedis, redisConfigured, redisHealthy, closeRedis } from "./db/redis.ts";
import { cacheStats } from "./db/courseCache.ts";
import { vectorStats } from "./db/vectorStore.ts";

const here = dirname(fileURLToPath(import.meta.url));
const publicDir = join(here, "..", "public");

const port = Number(process.env.PORT || 4174);

const db = getDb();
const repo = new Repo(db);

const app = express();
app.disable("x-powered-by");
app.use(express.json({ limit: "200kb" }));

const writeLimit = rateLimit({ capacity: 10, refillPerSec: 0.2 });

// Gate POSTs to LLM-touching routers without throttling the UI's GET polling.
const limitPosts = (limiter: express.RequestHandler) =>
  (req: express.Request, res: express.Response, next: express.NextFunction) =>
    req.method === "POST" ? limiter(req, res, next) : next();

// Resolve the session cookie -> req.user for every request (no-op when absent).
app.use(attachUser(repo));

app.use("/api/auth", authRouter(db));
app.use("/api/profile", profileRouter(db));
app.use("/api/courses", coursesRouter(db));
app.use("/api/advisor", limitPosts(writeLimit), advisorRouter(db));
app.use("/api/plans", plansRouter(db));
app.use("/api/schedule", scheduleRouter(db));
app.use("/api/opportunities", opportunitiesRouter(db));
app.use("/api/jobs", limitPosts(writeLimit), jobsRouter(db));
app.use("/api/professors", limitPosts(writeLimit), professorsRouter(db));
app.use("/api/voice", limitPosts(writeLimit), voiceRouter());

app.get("/api/healthz", (_req, res) => {
  res.json({
    ok: true,
    courses: repo.countCourses(),
    professors: repo.countProfessors(),
    redis: {
      configured: redisConfigured(),
      connected: redisHealthy(),
      catalogCache: { ...cacheStats },
      vectorIndex: { ...vectorStats },
    },
  });
});

app.use(express.static(publicDir, { extensions: ["html"], index: "index.html" }));

app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
  log.error("unhandled error", { error: err.message });
  res.status(500).json({ error: process.env.NODE_ENV === "production" ? "internal server error" : err.message });
});

const server = app.listen(port, () => {
  log.info("server listening", { port, courses: repo.countCourses() });
  console.log(`College Copilot: http://localhost:${port}`);
  // Warm the Redis connection (and surface its status) without blocking startup.
  if (redisConfigured()) {
    getRedis().then(r => {
      if (r) console.log("Redis: connected (course catalog cache active)");
      else console.log("Redis: configured but unreachable — using SQLite fallback");
    });
  } else {
    console.log("Redis: not configured (set REDIS_URL to enable the catalog cache)");
  }
});

let shuttingDown = false;
async function shutdown(signal: string): Promise<void> {
  if (shuttingDown) return;
  shuttingDown = true;
  log.info("shutdown", { signal });
  server.close(async () => {
    await closeRedis();
    closeDb();
    process.exit(0);
  });
}

process.on("SIGINT", () => { void shutdown("SIGINT"); });
process.on("SIGTERM", () => { void shutdown("SIGTERM"); });

```

### src/agents/index.ts

```typescript
/** Side-effect entry point for the currently active course-planner agents. */
import "./course-planner/index.ts";
import "./opportunity-orchestrator.ts";
import "./industry-jobs/index.ts";

export { listAgents, getAgent, runAgent, registerAgent } from "./registry.ts";
export type { Agent, AgentContext, AgentStatus } from "./types.ts";

```

### src/skills/index.ts

```typescript
/**
 * Skill registry entry point.
 *
 * Skills are atomic tools that agents call during a single run. Importing this
 * file side-effect-registers every skill below.
 */

import "./professor-rating.ts";

export { listSkills, getSkill, runSkill, registerSkill, SkillNotImplementedError } from "./registry.ts";
export type { Skill, SkillContext, SkillStatus } from "./registry.ts";

```

### src/providers/index.ts

```typescript
/**
 * Provider factory.
 *
 * College Copilot uses the Claude API (Anthropic) for writer/scorer/critic and
 * the course-advisor's NL parsing. The factory is kept as a thin seam so a
 * second provider could be added later, but Anthropic is the only backend today.
 */

import { anthropicProvider } from "./anthropic.ts";
import type { Provider } from "./types.ts";
import { log } from "../lib/log.ts";

let active: Provider | null = null;

export function getProvider(): Provider {
  if (active) return active;
  active = anthropicProvider;
  if (!active.available()) {
    log.warn("ANTHROPIC_API_KEY not set — LLM calls fall back to local heuristics", { provider: active.name });
  } else {
    log.info("llm provider active", { provider: active.name });
  }
  return active;
}

/** Test-only — force a reset so the provider can be re-picked on the next call. */
export function _resetProvider(): void {
  active = null;
}

export type { Provider, ChatRequest, ChatResponse, SystemBlock } from "./types.ts";

```

### src/agents/course-planner/index.ts

```typescript
import "./student-query-agent.ts";
import "./school-policy-agent.ts";
import "./planning-evaluator-agent.ts";
import "./advising-orchestrator.ts";

export { advise } from "./advising-orchestrator.ts";
export type { AdviseInput, AdviseOutput, AdviceStep } from "./advising-orchestrator.ts";
export { manageStudentQuery, parseQuery, heuristicParse, mergePrefs } from "./student-query-agent.ts";
export type { QueryConstraints } from "./student-query-agent.ts";

```

### src/agents/industry-jobs/index.ts

```typescript
/** Side-effect registration + public surface for the industry-jobs system. */
import "./job-search-agent.ts";
import "./jd-digest-agent.ts";
import "./job-normalizer-agent.ts";
import "./resume-prompt-agent.ts";
import "./networking-agent.ts";
import "./jobs-orchestrator.ts";

export { searchJobs, scoreJob, rowToScoredJob } from "./jobs-orchestrator.ts";
export { digestJob, heuristicDigest } from "./jd-digest-agent.ts";
export { normalizeJob, classifyEmployment, extractLocation, extractDeadline } from "./job-normalizer-agent.ts";
export { buildResumePrompt } from "./resume-prompt-agent.ts";
export { buildNetworkingLeads } from "./networking-agent.ts";
export { findJobs, planJobSources } from "./job-search-agent.ts";
export type {
  NormalizedJob,
  ScoredJob,
  JobSearchInput,
  JobSearchOutput,
  NetworkingResult,
  NetworkingLead,
} from "./types.ts";

```

### public/js/app.js

```javascript
// College Copilot — single-page controller (vanilla ES modules, no build step).

import { BERKELEY_COLLEGES } from "./berkeley-majors.js";
import { initVoice } from "./voice.js";

const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, ch => ({
  "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;",
}[ch]));
function safeExternalUrl(value) {
  try {
    const url = new URL(String(value));
    return ["http:", "https:"].includes(url.protocol) ? url.toString() : "";
  } catch { return ""; }
}

async function api(path, opts = {}) {
  const res = await fetch(`/api${path}`, {
    headers: { "Content-Type": "application/json" },
    ...opts,
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  if (!res.ok) {
    let msg = res.statusText;
    try { msg = (await res.json()).error || msg; } catch {}
    throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg));
  }
  return res.status === 204 ? null : res.json();
}

function toast(msg) {
  let el = $(".toast");
  if (!el) { el = document.createElement("div"); el.className = "toast"; document.body.appendChild(el); }
  el.textContent = msg;
  el.classList.add("show");
  setTimeout(() => el.classList.remove("show"), 2400);
}

const minToHHMM = (m) => m == null ? "" : `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
const csv = (s) => (s || "").split(",").map(x => x.trim()).filter(Boolean);
const esc = (s = "") => String(s).replace(/[&<>"']/g, ch => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[ch]));
const BERKELEY_TERM_IDS = { "fall-2026": "8588" };
const CURRENT_TERM = "fall-2026";

function berkeleyCourseUrl(course) {
  const params = new URLSearchParams({ search: `${course.subject} ${course.number}` });
  const termId = BERKELEY_TERM_IDS[CURRENT_TERM];
  if (termId) params.append("f[0]", `term:${termId}`);
  return `https://classes.berkeley.edu/search/class?${params}`;
}

const COURSE_PAGE_SIZE = 60;
const state = { me: null, courses: [], courseOffset: 0, courseCount: 0, cart: loadCart(), savedPlans: [] };

function loadCart() { try { return JSON.parse(localStorage.getItem("cc_cart") || "[]"); } catch { return []; } }
function saveCart() { localStorage.setItem("cc_cart", JSON.stringify(state.cart)); }

// ───────── Auth ─────────
async function boot() {
  initTheme();
  state.me = await api("/auth/me");
  if (!state.me.user) return showLogin();
  showApp();
}

function showLogin() {
  $("#appView").hidden = true;
  $("#loginView").hidden = false;
  $("#googleBtn").hidden = !state.me.googleEnabled;
  $("#devLoginForm").hidden = !state.me.devLogin;
}

async function showApp() {
  $("#loginView").hidden = true;
  $("#appView").hidden = false;
  $("#userChip").textContent = state.me.user.email;
  await loadProfileIntoForm();
  if (!state.me.hasProfile) { switchTab("profile"); toast("Set your preferences to get tailored rankings."); }
  else { switchTab("discover"); }
  await loadSubjects();
  await refreshCourses();
  renderCart();
  loadSavedPlans();
  initVoice({ toast });
}

$("#devLoginForm").addEventListener("submit", async (e) => {
  e.preventDefault();
  try {
    await api("/auth/dev-login", { method: "POST", body: { email: $("#devEmail").value } });
    state.me = await api("/auth/me");
    showApp();
  } catch (err) { toast(err.message); }
});

$("#logoutBtn").addEventListener("click", async () => {
  await api("/auth/logout", { method: "POST" });
  location.reload();
});

$("#findLabsBtn")?.addEventListener("click", searchLabs);
$("#labsQuery")?.addEventListener("keydown", (e) => { if (e.key === "Enter") searchLabs(); });
$("#findJobsBtn")?.addEventListener("click", () => searchJobs($("#findJobsBtn")));
$("#jobsQuery")?.addEventListener("keydown", (e) => { if (e.key === "Enter") searchJobs($("#findJobsBtn")); });
$("#findProfessorsBtn")?.addEventListener("click", searchProfessors);
$("#professorQuery")?.addEventListener("keydown", (e) => { if (e.key === "Enter") searchProfessors(); });

// ───────── Tabs ─────────
function switchTab(name) {
  $$(".tab").forEach(t => t.classList.toggle("active", t.dataset.tab === name));
  $("#opportunitiesMenuBtn").classList.toggle("active", name === "professors" || name === "programs" || name === "jobs");
  $(".tab-menu")?.classList.remove("open");
  $("#opportunitiesMenuBtn").setAttribute("aria-expanded", "false");
  $(".tab-submenu.open")?.classList.remove("open");
  $(".submenu-trigger")?.setAttribute("aria-expanded", "false");
  $$(".panel-view").forEach(v => v.hidden = v.dataset.view !== name);
  if (name === "schedule") renderCalendar();
  if (name === "professors") initProfessorPanel();
  if (name === "programs") initLabsPanel();
  if (name === "jobs") loadJobs();
}
$("#tabs").addEventListener("click", (e) => {
  const tab = e.target.closest(".tab");
  if (tab?.dataset.tab) switchTab(tab.dataset.tab);
});
$("#opportunitiesMenuBtn").addEventListener("click", () => {
  const menu = $(".tab-menu");
  const open = menu.classList.toggle("open");
  $("#opportunitiesMenuBtn").setAttribute("aria-expanded", String(open));
});
$(".submenu-trigger")?.addEventListener("click", (e) => {
  e.stopPropagation();
  const submenu = e.target.closest(".tab-submenu");
  if (!submenu) return;
  const open = submenu.classList.toggle("open");
  e.target.setAttribute("aria-expanded", String(open));
});
document.addEventListener("click", (e) => {
  if (e.target.closest(".tab-menu")) return;
  $(".tab-menu")?.classList.remove("open");
  $("#opportunitiesMenuBtn").setAttribute("aria-expanded", "false");
  $(".tab-submenu.open")?.classList.remove("open");
  $(".submenu-trigger")?.setAttribute("aria-expanded", "false");
});

// ───────── Theme ─────────
function initTheme() {
  const saved = localStorage.getItem("cc_theme");
  if (saved) document.documentElement.dataset.theme = 
[truncated — 30582 more characters]
```

### tests/researchLabs.test.ts

```typescript
import test from "node:test";
import assert from "node:assert/strict";
import { RESEARCH_LABS, searchResearchLabs } from "../src/agents/research-opportunities/lab-opportunities.ts";

test("lab search returns the directory for an empty query", () => {
  assert.equal(searchResearchLabs("").length, RESEARCH_LABS.length);
});

test("lab search filters by research topic", () => {
  const labs = searchResearchLabs("natural language processing");
  assert.equal(labs[0]?.name, "Berkeley NLP");
  assert.ok(labs.every((lab) => /language|nlp/i.test(`${lab.name} ${lab.description} ${lab.topics.join(" ")}`)));
});

test("lab search returns no unrelated matches", () => {
  assert.deepEqual(searchResearchLabs("quantum chemistry"), []);
});

```

### tests/deepgram.test.ts

```typescript
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseDeepgramTranscript } from "../src/providers/deepgram.ts";

test("parseDeepgramTranscript extracts the top alternative transcript", () => {
  const json = {
    results: { channels: [{ alternatives: [{ transcript: "  find me machine learning classes  ", confidence: 0.99 }] }] },
  };
  assert.equal(parseDeepgramTranscript(json), "find me machine learning classes");
});

test("parseDeepgramTranscript returns empty string on missing/garbage shapes", () => {
  assert.equal(parseDeepgramTranscript({}), "");
  assert.equal(parseDeepgramTranscript(null), "");
  assert.equal(parseDeepgramTranscript({ results: { channels: [] } }), "");
  assert.equal(parseDeepgramTranscript({ results: { channels: [{ alternatives: [{}] }] } }), "");
});

```

[74 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]