Tutorial
CS357: Foundations of Artificial Intelligence - Setup Basics
Terminal basics, Python and pip, tokens, and the optional tools for the course setup.
Contents
Terminal basics, Python and pip, tokens, and the optional tools for your course setup.
The Overview assignment links here at each point you need one of these. Words like “Step 4” and “Part 1.5” refer to that assignment, and A1 through A6 are in the Course Container tutorial.
Opening a terminal, moving around, and saving a file
Every setup step happens at a terminal, and several ask you to save a file. Here is how, on every system this course supports. Come back to this section whenever a step says “open a terminal,” “cd,” or “save it.”
1. Open a terminal.
| System | How to open it | What the prompt looks like |
|---|---|---|
| macOS | Press Cmd+Space, type Terminal, press Enter (Terminal also lives in Applications > Utilities) |
you@laptop ~ % |
| Windows, PowerShell | Open the Start menu, type PowerShell, press Enter (not “Command Prompt”). On Windows 11, right-clicking the Start button and choosing Terminal opens PowerShell too |
PS C:\Users\you> |
| Windows, WSL2 Ubuntu | Open the Start menu, type Ubuntu, press Enter (A1 of the optional Route A setup installs it) |
you@laptop:~$ |
| Linux | Press Ctrl+Alt+T, or open Terminal from the applications menu | you@laptop:~$ |
| VS Code, on any system | Press Ctrl+` (backtick), or View > Terminal. It opens in the folder you have open | one of the above |
2. Find where you are, and move. The terminal always has a current folder, and every relative path is measured from it. These commands work the same in every shell above, including PowerShell:
pwd # print the folder you are in
ls # list what is here (ls -la also shows hidden files; in PowerShell, plain ls)
cd ~ # go to your home folder
mkdir -p ~/cs357 # make a folder for this course (in PowerShell: mkdir ~/cs357)
cd ~/cs357 # go into it
cd .. # go up one level
What
~means on each system.~is your home folder:
/Users/youon macOSC:\Users\youin PowerShell/home/youin WSL2 Ubuntu; from WSL2, your Windows files are under/mnt/c/Users/youDo all of this course’s work in
~/cs357, or in thecs357-workclone from A2 on Route A, so that everycd ~/cs357in the setup lands in the same place. Press Tab to complete a name you have started typing, and the up arrow to recall the previous command.
3. Save a file. When a step says “save this as ollama_check.py,” first cd into the folder the file belongs in, then use one of these:
| Editor | Where it works | How to save ollama_check.py |
|---|---|---|
| nano | macOS, Linux, WSL2, and inside the course container | Run nano ollama_check.py, paste the contents (Cmd+V on macOS; right-click or Ctrl+Shift+V in Ubuntu), press Ctrl+O then Enter to write the file, then Ctrl+X to exit |
| vim | Every Unix system | Run vim ollama_check.py, press i to enter insert mode, paste, press Esc, then type :wq and press Enter to write and quit. If you get stuck, press Esc, type :q!, and press Enter to leave without saving |
| VS Code | Any system | From the folder, run code . to open it (or File > Open Folder), then File > New File, paste, and press Ctrl+S (Cmd+S on macOS) to save under the name the step gives. Type the name with its extension, ollama_check.py, and check that the editor did not add .txt |
| Notepad | PowerShell without nano | Run notepad ollama_check.py, click Yes to create the file, paste, save, and close Notepad |
For a one-line file in PowerShell, Set-Content notes.txt "model: llama3.2" writes it directly.
4. Confirm it landed. Run ls and see the file’s name; run cat ollama_check.py and see its contents. If ls does not show it, you saved into a different folder than the one you are in, and pwd tells you which one that is.
Where am I typing?
Most setup failures come from running a command in the wrong place. Before every command, read your prompt.
| Your prompt looks like | You are on | What localhost means there |
|---|---|---|
you@laptop ~ %, you@laptop:~$, or PS C:\Users\you> |
Your host: macOS, Linux, WSL2 Ubuntu, or PowerShell | Your laptop, where Ollama listens |
student@a1b2c3d4e5f6:/workspace$ |
Inside the course container | The container itself, where nothing listens |
The address rule. Every command on this page that names Ollama’s address is written as
localhost:11434. On your host, leave it as written. Inside the container, replacelocalhostwithhost.docker.internal, becauselocalhostinside a container means the container. That is the only substitution on this page, and it applies only inside the container. Running Ollama itself as a Docker container does not change the rule: with its port published (-p 11434:11434),localhost:11434on your host still reaches it.
Windows. Use PowerShell (Windows 10 and 11 include it; open it from the Start menu) or WSL2 Ubuntu, not the old Command Prompt. Every command in the setup and in the labs is written for PowerShell or a Unix shell. In PowerShell,
python3is spelledpython. When a transcript from inside the container is what you have, include the container prompt in your copy-paste so it is visible where each command ran.
Installing Python and pip
Step 4 runs a Python script, Part 1.5 installs uv, and every lab assumes a working Python somewhere. Whether you need to install one on your host depends on your route:
| Route A | Route B | |
|---|---|---|
| Do you need Python on your host? | Not for this page: the container carries Python 3.11 and every course library, and uv in Part 1.5, Step 3 brings its own Python. Install one anyway if you want to run scripts outside the container, or if you land on the python -m venv fallback there |
Yes, before Step 4: Python 3 and its package installer, pip |
| Which version | Whatever the table below gives you | 3.11 or later, to match the container’s Python; an older Python 3 runs this page’s script, but the labs’ libraries are tested against 3.11 |
Do. First check what you already have. Open a terminal (Opening a terminal above) and run both commands:
python3 --version
python3 -m pip --version
In PowerShell, spell them python --version and python -m pip --version. If both print a version, and the first is 3.11 or later, you are done with this section. Otherwise, install Python for your system from the row below; every row brings pip with it.
| System | How to install Python (pip comes with it) | Then |
|---|---|---|
| macOS, with Homebrew | brew install python@3.12. If you do not have Homebrew, install it first with the one-line command on its home page; it is also the easiest way to install uv in Part 1.5 |
Open a new terminal window |
| macOS, without Homebrew | Download the macOS installer from python.org/downloads and run it | Open a new terminal window |
| Windows, PowerShell | Download the Windows installer from python.org/downloads, run it, and on its first screen check the box that adds Python to PATH before you click Install Now. Or, in PowerShell, winget install --id Python.Python.3.12 -e |
Open a new PowerShell window, so the updated PATH loads |
| WSL2 Ubuntu, or Linux | sudo apt update && sudo apt install python3 python3-pip python3-venv. Ubuntu ships Python 3 but not pip or venv, which is why all three packages are named |
Nothing; the commands work in the same window |
What pip is, and why every pip command on this page starts with
python3 -m.pip is Python’s package installer, the tool that fetches libraries such as
requestsfrom the Python Package Index and puts them whereimportcan find them. It has shipped inside Python since version 3.4, so installing Python installs pip.A machine can hold several Pythons, though. Apple’s developer tools add one, Homebrew adds another, and each
uvenvironment has its own. A barepip installmay therefore target a different Python than thepython3you run scripts with. Writingpython3 -m pip install requestsruns pip from inside the Python you name, so the library lands where that Python will look for it.That one detail is the whole cause of the
ModuleNotFoundErrorin Step 4’s troubleshooting, and the reason this page writes every pip command the long way.Natively, pip also belongs inside a virtual environment, a per-project copy of the library folder. Route B: installing
requestsin Step 4 shows how to make and activate one, and every nativepip installon this page assumes you have.
What you should see. In a new terminal, python3 --version prints Python 3.12.x (or whatever you installed), and python3 -m pip --version prints a pip version followed by the path of the Python it belongs to. Check that the path points at the Python you just installed.
Paste. On Route B, both version lines, next to your operating system name in Stage 0.
Troubleshooting.
python3: command not found, or'python' is not recognized, right after installing means the terminal predates the install. Open a new one.- On Windows, typing
pythonopens the Microsoft Store when Python is not installed yet. Close the Store and run the installer above. If the Store keeps opening afterwards, open Manage app execution aliases from the Start menu and turn off thepython.exeandpython3.exeentries.No module named pipmeans pip was skipped.python3 -m ensurepip --upgraderestores it on macOS and Windows;sudo apt install python3-pipdoes on Ubuntu.- On a fresh Mac, typing
python3may offer to install Apple’s Command Line Tools instead. That copy of Python 3 runs Step 4, but it is several versions old, so install a current one from the table anyway.- If Homebrew says your macOS version is unsupported or a pre-release, run
brew updatefirst and retry.error: externally-managed-environmenton Ubuntu or Debian is handled under Step 4.See the Stage 0 rows in the Overview assignment’s Troubleshooting section.
Note.
uv, which Part 1.5 installs, can also fetch a Python of its own (uv python install 3.12) and run scripts with it (uv run python ollama_check.py). Install a system Python first anyway, so thatpython3works in every terminal and every editor, and so that thepython -m venvfallback has something to run.
Making a token, and putting it in your environment
A6 needs a GitHub token, and so does every tool you point at GitHub afterwards: gh inside the container, the GitHub tool server in the MCP session, and any agent you let file an issue on your behalf. None of them should ever read that token out of a file you commit. This section is where the token comes from and where it lives; come back whenever a page says “set GH_TOKEN” or writes something like {env:GITHUB_PAT} in a configuration file.
Do, the token. Go to github.com/settings/personal-access-tokens/new, which is the direct link for the menu path A6 spells out, and make a fine-grained token:
| Field | What to choose | Why |
|---|---|---|
| Repository access | Only select repositories, then the one repository you mean | A token that reaches one repository can damage one repository |
| Repository permissions | The narrowest set that does the job. Contents: Read and write pushes code; add Issues and Pull requests, read and write, only if an agent will open them for you | Permissions you did not grant are failures you cannot cause |
| Expiration | At or beyond the end of the semester | An expired token in week ten is a lab you cannot finish |
Watch out! The token is displayed once. Copy it before you leave that page, because the only repair is to delete it and make another.
Do, the variable. A configuration file should name a variable; your shell should hold the value. Set it for this terminal, and then set it for good so a new window still has it:
| System | For this terminal | To make it stick |
|---|---|---|
| macOS (zsh), Linux, WSL2 Ubuntu | export GITHUB_PAT=github_pat_yourtokenhere |
Add that same line to ~/.zshrc on macOS, or ~/.bashrc on Linux and WSL2, then open a new terminal |
| Windows, PowerShell | $env:GITHUB_PAT = "github_pat_yourtokenhere" |
[Environment]::SetEnvironmentVariable("GITHUB_PAT", "github_pat_yourtokenhere", "User"), then open a new PowerShell window so it loads |
| The course container, compose route | Nothing extra: set it on your host | The environment: block in docker-compose.yml passes it through by name on every docker compose run |
| The course container, VS Code route | export GITHUB_PAT=github_pat_yourtokenhere at the container prompt |
VS Code does not read the compose file, so this one lasts the session unless you add a remoteEnv entry to devcontainer.json |
What you should see. Nothing, which is the point. Check that the variable is set without printing what is in it:
[ -n "$GITHUB_PAT" ] && echo set
In PowerShell, if ($env:GITHUB_PAT) { "set" }.
Why not just
echoit? Because a token you print is a token in your scrollback, your shell history, and any screenshot you paste into the course channel. This is the same instinct as Part 1.5, where you printid_ed25519.puband neverid_ed25519.
And never paste one into an LLM. Not into a chat window, not into an agent prompt, not into a file you then ask an agent to read. Text you send to a hosted model rests in that provider’s logs, and it may be retained, reviewed, or trained on; deleting the conversation does not reach any of that. Scrollback you can clear. This you cannot.
This course makes the mistake easy, which is the reason to say it here rather than later. You will have a terminal with a live token in its environment and an agent in the next pane, and the natural move when something breaks is to paste the config and ask why. Paste the variable name, never its value, and redact before you send.
If it happens anyway, it is a two-minute problem and not a crisis: revoke the token at github.com/settings/personal-access-tokens, issue a new one, update the variable. Treat “I pasted it” as “it is compromised,” because from the outside those are the same thing. MCP, REST, and OAuth 2.0 Together shows how a token sitting in a context window gets extracted by an injected instruction, and the Coding Agents session builds a hook that rejects such a message before the model ever sees it.
Which name to use. Different tools read different variables, and that is worth one table rather than one guess:
| What reads it | Variable |
|---|---|
gh, the GitHub CLI |
GH_TOKEN, or GITHUB_TOKEN |
An opencode.json or .mcp.json configuration |
Whatever name you wrote inside the {env:...} reference |
| A server you write yourself | Whatever name your own os.environ call asks for |
Give a token the name of the thing it can reach, so GITHUB_SCRATCH_PAT for one scoped to a throwaway scratchpad repository and GITHUB_PAT for the one that reaches cs357-work. Two tokens under two names is the correct end state, not a mess to tidy up: share one name between them and the wider token will quietly satisfy the narrower use, which is exactly the blast-radius argument A6 makes about the container credential, arriving one layer down as a naming convention.
If an environment variable is awkward. opencode can also read the value out of a file, which survives a new terminal and a reboot without editing a shell profile:
"headers": { "Authorization": "Bearer {file:~/.secrets/github-scratch-pat}" }
Make it with mkdir -p ~/.secrets, put the token in ~/.secrets/github-scratch-pat, and run chmod 600 ~/.secrets/github-scratch-pat so only you can read it. The configuration still holds a reference rather than a value, so the rule survives intact.
If you would rather do neither. Pasting the token straight into opencode.json works, and it is the right call at midnight when nothing else does. Two conditions. Put the file in .gitignore before the token goes in, not after, because a commit that contains a token has leaked it whether or not the next commit removes it. And expect GitHub’s push protection to reject the push anyway, which is that lesson arriving from the other direction. For work you hand in, use the variable or the file.
Where the configuration file goes. opencode reads opencode.json from the project directory as well as from ~/.config/opencode/, and the choice is about scope rather than convenience. A configuration that belongs to one repository belongs in that repository, where its tools load when you are working there and nowhere else; a configuration you want everywhere goes in the home-directory copy. Step 5b comes back to this, and the MCP session leans on it hard.
Optional: Install OpenWebUI, the chat interface over Ollama
OpenWebUI is a self-hosted web front end that sits in front of your Ollama server: a private chat window over the model you pulled in Step 1, with tools, knowledge bases, and an API of its own. Nothing in this assignment needs it. The Local Agent lab’s no-code path and several later tutorials assume it is installed, so doing it now, while the model is fresh, saves you the first fifteen minutes of that lab.
| Optional OpenWebUI at a glance | |
|---|---|
| Where you type | Your host terminal, on both routes. On Route A, OpenWebUI is its own container beside the course container, not inside it |
| What you install | OpenWebUI by one of two routes: Docker (recommended if Docker Desktop is already running) or pip (no Docker required) |
| You paste | Nothing is required. A screenshot of the model dropdown showing llama3.2 is welcome |
Do. Confirm Ollama is running (ollama list shows llama3.2), then install by one route.
Route 1, Docker. Each flag does one job: -p 3000:8080 publishes the container’s port 8080 on your host’s port 3000; --add-host=host.docker.internal:host-gateway lets the container reach Ollama on your host (required on Linux, harmless elsewhere); -v open-webui:/app/backend/data keeps your account, models, and chat history in a named volume that survives restarts.
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway \
-v open-webui:/app/backend/data --name open-webui --restart always \
ghcr.io/open-webui/open-webui:main
Route 2, pip. The install downloads a large dependency set; expect several minutes. The last command starts the server in your terminal; leave that terminal open. When the startup banner shows Uvicorn running on http://0.0.0.0:8080, the server is up.
python3 -m venv owui
source owui/bin/activate # PowerShell: .\owui\Scripts\Activate.ps1
python3 -m pip install open-webui
open-webui serve
If
pipsaysexternally-managed-environment. Your operating system manages its own Python (common on Ubuntu, Debian, WSL, and Homebrew), so it refuses packages installed directly into it, because they could break the tools that depend on it. Nothing is wrong with your machine, and the virtual environment is the fix: run the first two lines, and then install again. Do not use--break-system-packages.
If
python3 -m venvsaysensurepip is not available. On Ubuntu, Debian, and WSL, virtual environments come in a separate package. Runsudo apt install python3-venv, delete the partial folder withrm -rf owui, and then start again from thevenvline. If that package name is not found, use the version-specific name the error message gives you, such aspython3.14-venv.
Then, in the browser (http://localhost:3000 for Docker, http://localhost:8080 for pip):
- Create an admin account (name, email, password). It exists only in your local OpenWebUI database; the email is a local username.
- In the model selector at the top left, confirm
llama3.2:latestappears. OpenWebUI finds a local Ollama server on its own. - Send one message (“Say hello in one sentence.”) and read the reply.
- Create an API key: click your initials (bottom left), then Settings, Account, API Keys, Create new key. Keep it somewhere you will find again. It authenticates you to a server on your own machine; it is not a payment credential. The Local AI session and the Local Agent lab both use it.
Troubleshooting.
- An empty model dropdown means OpenWebUI cannot reach Ollama. On the Docker route, confirm the
--add-hostflag was present, then open Admin Panel, Settings, Connections and set the Ollama URL tohttp://host.docker.internal:11434. Notlocalhost, which inside the container means the container itself; see The address rule above. On the pip route the URL ishttp://localhost:11434, andollama servemust be running.- If
http://localhost:3000refuses to connect on the Docker route, rundocker ps. When the container is missing,docker logs open-webuisays why. A port conflict means something else owns 3000, so re-run with-p 3001:8080and browse to 3001.- If
pip install open-webuifails with a resolver or build error, it is almost always a Python version issue. Deleteowui, create a 3.11 environment withpython3.11 -m venv owui && source owui/bin/activate, and install inside it.
Optional 5d: connect a hosted endpoint, if you are given one
This is not graded and is not on the Overview checklist. It is here for when you are given access to a hosted, OpenAI-compatible endpoint (an address ending in something like
/api/v1, plus an API key). Your local Ollama provider stays exactly as 5b left it; this adds a second provider beside it.
- Install the auto-models plugin by asking the agent. In
opencode, with any model selected (the free default is enough), type the promptinstall this opencode plugin https://github.com/BillJr99/opencode-auto-models. Read the change it proposes toopencode.jsonbefore you approve it. Then type/quitand startopencodeagain, because plugins load only at startup. The plugin asks each provider which models it serves, so you never type model names by hand. -
Name the endpoint in
opencode.json. Add a second entry to theprovidermap you wrote in 5b, holding only the address: no key, and nomodelsmap, since the plugin fills that in.myendpointis a placeholder ID; you type the same word again in the next step."myendpoint": { "npm": "@ai-sdk/openai-compatible", "name": "My Endpoint", "options": { "baseURL": "https://YOUR-ENDPOINT/api/v1" } }Check that the file still parses with
python3 -m json.tool opencode.json, then/quitand restart. - Store the key with
/connect. Type/connect, scroll to Other, enter the same provider ID (myendpoint), and paste your API key at the key prompt./connectasks only for the ID and the key, never the address, which is why step 2 comes first. - Pick a model. Type
/modelsand choose from the list under your endpoint’s name.
Watch out. The
/connectkey prompt is the one place to paste the key. Never paste it into an ordinary prompt, where it is sent to the model. If/modelsshows your endpoint with no models, the ID you typed in/connectdoes not match the key in theproviderblock, or you have not restarted since installing the plugin. The End-to-End OpenCode Setup tutorial walks the same route in more detail.
Optional: install herdr while you are here
This is not graded and is not on the Overview checklist. It is here because installing it now costs one command, and a later lab assumes it is on your bench.
herdr is an agent-aware terminal multiplexer. It keeps agents running after you close your laptop or drop an SSH connection, and it shows you which agent is blocked, working, or done, so you are not cycling through terminal panes to find the one waiting on you.
| Where | How to install |
|---|---|
| Route A | Already in the course image, alongside opencode. Confirm with herdr --version |
| Route B: macOS, Linux, WSL | curl -fsSL https://herdr.dev/install.sh \| sh |
| Route B: macOS with Homebrew | brew install herdr |
| Route B: native Windows | powershell -ExecutionPolicy Bypass -c "irm https://herdr.dev/install.ps1 \| iex" |
The installer downloads one binary, checks its SHA-256, and puts it in ~/.local/bin. If you include herdr --version in your transcript, I will read it, but its absence costs you nothing.
Alternative: do it by hand, with ssh-keygen
gh auth login automates a three-step exchange that is worth seeing once, and if gh will not install on your machine you need it anyway. Nothing below is required if 2b succeeded.
Check for a key you already have.
ls -al ~/.ssh
If id_ed25519.pub (or id_rsa.pub) is listed and you know its passphrase, skip to registering it.
Create one. Use the email address tied to your GitHub account:
ssh-keygen -t ed25519 -C "you@example.com"
Press Enter to accept the default location, and set a passphrase rather than leaving it empty. Then load it into the agent so you type that passphrase once per session rather than once per push:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Register the public half with GitHub. Print the .pub file, which is the public one:
cat ~/.ssh/id_ed25519.pub
Watch out! Never paste or send the file without the extension, which is the private key. Only the
.publine goes to GitHub.
On GitHub, go to Settings -> SSH and GPG keys -> New SSH key (the direct link is github.com/settings/keys). Title it after the machine it lives on, so you can revoke exactly one laptop later; leave the key type as Authentication Key; paste the whole line, and save. Then test with ssh -T git@github.com and continue from 2d, using git clone git@github.com:<your-username>/<your-repo>.git in place of gh repo clone.
Other alternatives for 2d and 2e (choose at most one)
| Tool | Replaces | Best for | Verify with |
|---|---|---|---|
| VS Code | The clone in 2d | Anyone already working in VS Code | git log --oneline in the integrated terminal |
| GitHub Desktop | 2d and 2e | Anyone who would rather not type git commands yet | git log --oneline from Repository > Open in terminal |
VS Code, for the clone in 2d. It drives the same git underneath, so the result is identical.
With no folder open, the Source Control view (Ctrl+Shift+G, or Cmd+Shift+G on macOS) offers a Clone Repository button. From anywhere, the Command Palette (Ctrl+Shift+P, or Cmd+Shift+P) runs Git: Clone. Either one asks for the repository address, where you paste the same SSH URL, then asks which local folder to put it in, ~/cs357 here, and offers to open the clone when it finishes.
Say yes. The integrated terminal (Ctrl+`) then opens already inside the repository, which is where you run git log --oneline for your transcript.
The palette also offers Clone from GitHub, which lets you pick from a list of your repositories instead of pasting a URL. Note that it signs you in to GitHub inside VS Code and authenticates as that account rather than with your key.
GitHub Desktop, for 2d and 2e. If you would rather not type git commands yet, GitHub Desktop is a supported option and handles authentication for you: install it, sign in, use File > New repository (or Add local repository) on your cs357 folder, commit from the Changes tab, and Publish repository to push. Then paste the output of git log --oneline from Repository > Open in terminal, which is the same transcript the command-line route produces.
Watch out! GitHub Desktop is not a substitute for 2a. It bundles Git for Windows, not
gh, and signing in to Desktop does not sign ingh. Do 2a and 2b anyway, because the Coding Agents session and every agent you run afterwards drive GitHub from a terminal.
The two GitHub downloads are easy to confuse on native Windows without WSL2:
GitHub CLI (gh) |
GitHub Desktop | |
|---|---|---|
| What it is | The command-line tool, a separate install | The graphical client |
| What it bundles | Nothing else | Git for Windows, which is where ssh-keygen and Git Bash come from |
| How it authenticates | gh auth login, over SSH or HTTPS |
Its own sign-in, handled for you |
| Does it give you the other one? | No, and it does not need to | No. Desktop cannot run gh issue create |
| Installing both | Common, and they coexist happily |
The Ubuntu or WSL2 route gives you the standard Unix tooling instead, and every command on this page then works as written.
Route A and credentials inside the container. The credential you just made lives on your host, which is where it belongs. Inside the course container, A6 recommends a fine-grained personal access token scoped to
cs357-workinstead, because that container will soon be running agent code, and a credential placed inside it is a credential that code can use. That is why A2 clonescs357-workover HTTPS: the token authenticates HTTPS pushes. Step 6 of the Development Environment activity goes deeper, and A6 shows the read-only~/.sshmount if you would rather use your key there.
Command-Line Survival: reference (use as needed, not required reading cover-to-cover)
| Resource | What it is for |
|---|---|
| The Shell, in Full | The course’s own shell tutorial, from the first prompt through pipes, PATH, and processes |
| tldr pages | Plain-language, example-first cheat sheets for any command (tldr tar) |
| explainshell | Paste any command line and see each flag explained |
| ShellCheck | Catches bugs in shell scripts before they bite |
curl and HTTPie plus jq |
You will hit JSON APIs (Ollama, MCP) all semester; curl ... \| jq is your friend |