Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/commands/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Run the full test suite with `npm test` and report the results.

- Show a clear pass/fail summary (total tests, passed, failed).
- If any tests fail, quote the failing test name(s) and the assertion error.
- If all tests pass, confirm the count and that everything is green.
23 changes: 23 additions & 0 deletions .claude/skills/route-convention-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: route-convention-check
description: Verify that a newly added or modified route follows project conventions. Use when a route handler is added or changed in routes/.
---
# Route convention check

For every route touched in the current change, verify:

1. **Auth** — is the route mounted before or after `requireToken` in `server.js`?
If it mutates data (POST/PUT/DELETE) and is after the gate, confirm it also
has the correct inline middleware (`requireToken`, `requireAdmin`, or both).

2. **Input validation** — does the route validate `:id` params with `parseId`
from `lib/parseId.js`? Does it use `req.body ?? {}` before destructuring?

3. **Error shape** — do all error responses follow `{ "error": "message" }`
with the right status (400 bad input, 404 missing record)?

4. **Store access** — does the route read/write state only through `db/store.js`,
never holding state directly?

Report each violation as a short bullet. If everything looks correct, say so in
one line and move on.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ npm-debug.log*

# Personal, machine-local Claude settings (project-scoped settings ARE committed)
.claude/settings.local.json
.claude/memory/
20 changes: 20 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"mcpServers": {
"memory": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-memory"
],
"env": {
"MEMORY_FILE_PATH": "/Users/ton/Code/claude-wire-into-your-stack/.claude/memory/mcp_memory.json"
}
},
"fetch": {
"type": "stdio",
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ A small Express API used as the working project throughout the Claude Code cours
- All data access goes through `db/store.js` — routes never hold state directly
- Validate input in the route and return `400` on bad input, `404` when a record is missing
- Error responses are JSON in the shape `{ "error": "message" }`

## GitHub CLI

Use `/opt/homebrew/bin/gh` (never bare `gh`) for all GitHub CLI operations.

- Create PR: `/opt/homebrew/bin/gh pr create --title "…" --body "…"`
- List PRs: `/opt/homebrew/bin/gh pr list`
- View PR: `/opt/homebrew/bin/gh pr view [<number>]`
- CI status: `/opt/homebrew/bin/gh pr checks [<number>]`
- Merge PR: `/opt/homebrew/bin/gh pr merge [<number>] --squash --delete-branch`

Never push directly to `main` — always create a branch and open a PR.

## Memory

Use the `memory` MCP server to persist knowledge across conversations.

- **Always store** important project context, decisions, user preferences, and architectural notes in the memory graph via `mcp__memory__create_entities` or `mcp__memory__add_observations`.
- A `UserPromptSubmit` hook injects a memory-read reminder each turn — respond to it by calling `mcp__memory__read_graph` when you haven't already done so this session.
- Entity types to use: `Project`, `Decision`, `Convention`, `Person`, `Bug`, `Feature`.
- Keep observations factual and concise; one observation per distinct fact.
35 changes: 35 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Project 3 Notes

## 1. Server (MCP)

I connected two servers in `.mcp.json`: **`fetch`** (via `uvx mcp-server-fetch`) and **`memory`** (via `npx @modelcontextprotocol/server-memory`).

The `fetch` server is useful here because it lets Claude pull live API references and docs during development — for example, fetching Express or Node.js documentation without leaving the session. No credentials required. The permission rule in `.claude/settings.local.json` explicitly allows `mcp__fetch__fetch` (and `mcp__memory__read_graph`) rather than blanket-allowing every tool the servers expose, keeping the surface area small.

The `memory` server persists project context across sessions — architectural decisions, conventions, and who did what — so Claude arrives with background knowledge rather than starting cold every conversation.

## 2. Skill

The skill I encoded is **`route-convention-check`**, defined in `.claude/skills/route-convention-check/SKILL.md`.

This project has a clear, repeated pattern every time a route is added or modified: check that auth middleware is applied correctly, that `:id` params are parsed through `lib/parseId.js`, that error responses follow `{ "error": "message" }` with the right status codes, and that data access goes only through `db/store.js`. Without a skill these checks get forgotten under time pressure.

The description reads: *"Verify that a newly added or modified route follows project conventions. Use when a route handler is added or changed in routes/."* The phrase "route handler is added or changed in routes/" is specific enough to fire on route work and nothing else — it won't trigger on, say, a test refactor or a database change.

## 3. Command

I added a **`/test`** command in `.claude/commands/test.md`.

It runs `npm test` and returns a structured summary: total tests, pass/fail count, and — on failure — the exact test name and assertion error. Running tests is the most frequent development action on this repo (every change needs a green suite before committing), and having a consistent report format means the output is always scannable rather than raw runner noise.

## 4. Hook

The hook is set in `.claude/settings.local.json` on the **`UserPromptSubmit`** event.

It **reacts** (does not prevent) — it fires after the user submits a prompt but before Claude responds, echoing a reminder to call `mcp__memory__read_graph` if it hasn't been called yet this session. This ensures Claude always loads project context from the memory graph before answering, rather than starting from a blank slate. The event choice is deliberate: `UserPromptSubmit` is the earliest point where context injection matters; using `PostToolUse` would be too late in the turn.

## 5. Headless run

I ran the memory initialization task headless: seeding the knowledge graph with initial project entities (architecture, conventions, commands) so the memory MCP has baseline context from day one.

The `--allowedTools` was locked down to just `mcp__memory__create_entities` and `mcp__memory__read_graph` — the minimum needed to read the existing graph and write new entities. No file editing, no bash, no fetch. This mirrors the principle from the course: pre-approve only what the task provably needs, so an autonomous run can't drift into unintended side effects.
47 changes: 45 additions & 2 deletions db/store.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
// In-memory data store. Every route reads and writes through these helpers,
// so swapping in a real database later only touches this one file.

const crypto = require('crypto');

let users = [];
let nextId = 1;

let tokens = [];
let nextTokenId = 1;

function seed() {
users = [
{ id: 1, name: 'Ada Lovelace', email: 'ada@example.com' },
{ id: 2, name: 'Alan Turing', email: 'alan@example.com' },
{ id: 3, name: 'Grace Hopper', email: 'grace@example.com' },
{ id: 4, name: 'Linus Torvalds', email: 'linus@example.com' },
];
nextId = 3;
nextId = 5;

tokens = [];
nextTokenId = 1;
}
seed();

Expand All @@ -36,9 +46,42 @@ function updateUser(id, fields) {
return user;
}

function deleteUser(id) {
const index = users.findIndex((u) => u.id === id);
if (index !== -1) users.splice(index, 1);
}

// --- Token helpers ---

function createToken({ name, role = 'client', value } = {}) {
const token = {
id: nextTokenId,
name: name ?? null,
role,
token: value ?? crypto.randomBytes(32).toString('hex'),
createdAt: new Date().toISOString(),
};
nextTokenId += 1;
tokens.push(token);
return token;
}

function getTokenByValue(value) {
return tokens.find((t) => t.token === value);
}

function revokeToken(id) {
const index = tokens.findIndex((t) => t.id === id);
if (index !== -1) tokens.splice(index, 1);
}

// Reset to the seed data. Used by the tests so each one starts clean.
function reset() {
seed();
}

module.exports = { listUsers, getUser, createUser, updateUser, reset };
module.exports = {
listUsers, getUser, createUser, updateUser, deleteUser,
createToken, getTokenByValue, revokeToken,
reset,
};
9 changes: 9 additions & 0 deletions lib/parseId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Parse a route :id param string into a positive integer.
// Returns null for anything that is not a whole number ≥ 1
// (NaN, floats, zero, negatives, non-numeric strings).
function parseId(value) {
const id = Number(value);
return Number.isInteger(id) && id >= 1 ? id : null;
}

module.exports = parseId;
22 changes: 22 additions & 0 deletions middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const store = require('../db/store');

// Express middleware that enforces bearer-token authentication.
// Reads the Authorization header, validates the token against the store,
// and either calls next() (valid) or returns 401 (missing / invalid).
function requireToken(req, res, next) {
const authHeader = req.get('authorization');
if (!authHeader || !authHeader.toLowerCase().startsWith('bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}

const value = authHeader.slice('bearer '.length).trim();
const token = store.getTokenByValue(value);
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}

req.token = token;
return next();
}

module.exports = requireToken;
8 changes: 8 additions & 0 deletions middleware/requireAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function requireAdmin(req, res, next) {
if (!req.token || req.token.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
return next();
}

module.exports = requireAdmin;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dev": "node server.js",
"start": "node server.js",
"test": "node --test",
"lint": "eslint server.js routes db tests"
"lint": "eslint server.js routes db middleware lib tests"
},
"dependencies": {
"express": "^4.19.2"
Expand Down
28 changes: 28 additions & 0 deletions routes/tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const express = require('express');
const store = require('../db/store');
const parseId = require('../lib/parseId');
const requireToken = require('../middleware/auth');
const requireAdmin = require('../middleware/requireAdmin');

const router = express.Router();

// POST /tokens — create a new bearer token (open; bootstrapping).
// Always creates a client token — role from the request body is ignored.
router.post('/', (req, res) => {
const { name } = req.body ?? {};
const token = store.createToken({ name, role: 'client' });
return res.status(201).json(token);
});

// DELETE /tokens/:id — revoke a token by id. Admin only.
// Returns 204 whether or not the token existed (idempotent).
router.delete('/:id', requireToken, requireAdmin, (req, res) => {
const id = parseId(req.params.id);
if (id === null) {
return res.status(400).json({ error: 'Invalid id' });
}
store.revokeToken(id);
return res.status(204).send();
});

module.exports = router;
27 changes: 23 additions & 4 deletions routes/users.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const express = require('express');
const store = require('../db/store');
const parseId = require('../lib/parseId');

const router = express.Router();

Expand All @@ -10,7 +11,11 @@ router.get('/', (req, res) => {

// GET /users/:id — fetch one user, or 404 if it doesn't exist.
router.get('/:id', (req, res) => {
const user = store.getUser(Number(req.params.id));
const id = parseId(req.params.id);
if (id === null) {
return res.status(400).json({ error: 'Invalid id' });
}
const user = store.getUser(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
Expand All @@ -19,7 +24,7 @@ router.get('/:id', (req, res) => {

// POST /users — create a user. Requires name and email.
router.post('/', (req, res) => {
const { name, email } = req.body;
const { name, email } = req.body ?? {};
if (!name || !email) {
return res.status(400).json({ error: 'name and email are required' });
}
Expand All @@ -29,15 +34,29 @@ router.post('/', (req, res) => {

// PUT /users/:id — update an existing user (added in Project 2).
router.put('/:id', (req, res) => {
const { name, email } = req.body;
const id = parseId(req.params.id);
if (id === null) {
return res.status(400).json({ error: 'Invalid id' });
}
const { name, email } = req.body ?? {};
if (name === undefined && email === undefined) {
return res.status(400).json({ error: 'name or email is required' });
}
const user = store.updateUser(Number(req.params.id), { name, email });
const user = store.updateUser(id, { name, email });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.json(user);
});

// DELETE /users/:id — remove a user. Returns 204 whether or not the user existed (idempotent).
router.delete('/:id', (req, res) => {
const id = parseId(req.params.id);
if (id === null) {
return res.status(400).json({ error: 'Invalid id' });
}
store.deleteUser(id);
return res.status(204).send();
});

module.exports = router;
10 changes: 10 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
const express = require('express');
const usersRouter = require('./routes/users');
const healthRouter = require('./routes/health');
const tokensRouter = require('./routes/tokens');
const requireToken = require('./middleware/auth');
const store = require('./db/store');

const app = express();
app.use(express.json());

app.use('/health', healthRouter);
app.use('/tokens', tokensRouter);
app.use(requireToken);
app.use('/users', usersRouter);

const PORT = process.env.PORT || 3000;

// Only start listening when run directly (e.g. `npm run dev`), so the tests
// can import the app without opening a port.
if (require.main === module) {
const adminValue = process.env.ADMIN_TOKEN;
const adminToken = store.createToken({ name: 'admin', role: 'admin', value: adminValue });
if (!adminValue) {
console.log(`\nAdmin token (save this — shown once): ${adminToken.token}\n`);
}
app.listen(PORT, () => {
console.log(`API listening on http://localhost:${PORT}`);
});
Expand Down
Loading
Loading