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
11 changes: 11 additions & 0 deletions .claude/commands/pr-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Review the current git diff (staged and unstaged) against this project's conventions and flag any violations. Check each of the following:

1. **Route placement** — every new route handler lives in `routes/`, not in `server.js` or elsewhere.
2. **Data access** — routes read and write only through `db/store.js`; no in-route state.
3. **Mounting** — any new router is mounted in `server.js` under its base path.
4. **Error shape** — all error responses are JSON `{ "error": "<message>" }` with no other shape.
5. **Status codes** — bad input → 400, missing record → 404, created → 201, success → 200.
6. **Input validation** — required fields are validated in the route before calling the store.
7. **Tests** — a test file exists for every new route file, covering at minimum: list, 404 on missing, create (201), update, update 404.

For each violation found, show the file, line number, and a one-line explanation. If everything looks clean, say so in one sentence.
16 changes: 16 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint",
"statusMessage": "Linting..."
}
]
}
]
}
}
151 changes: 151 additions & 0 deletions .claude/skills/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
name: add-resource
description: Use when asked to add a new REST resource (route, CRUD endpoints, or "add a <thing> resource/route"). Encodes the three-file pattern this project uses: routes/<resource>.js, db/store.js additions, and tests/<resource>.test.js.
---
Adding a new resource requires three files touched in order: the store helpers, the route, and the tests. Then mount the router in server.js.

## 1. Store helpers — `db/store.js`

Append to the existing file. Follow the exact shape already used for users:

```js
let <resources> = [];
let next<Resource>Id = 1;

function seed<Resource>s() {
<resources> = [
{ id: 1, <field>: '<value>', ... },
];
next<Resource>Id = 2;
}
seed<Resource>s();

function list<Resource>s() { return <resources>; }

function get<Resource>(id) {
return <resources>.find((r) => r.id === id);
}

function create<Resource>({ <fields> }) {
const item = { id: next<Resource>Id, <fields> };
next<Resource>Id += 1;
<resources>.push(item);
return item;
}

function update<Resource>(id, fields) {
const item = get<Resource>(id);
if (!item) return undefined;
// apply only the fields that were provided
Object.keys(fields).forEach((k) => {
if (fields[k] !== undefined) item[k] = fields[k];
});
return item;
}
```

Export each new function alongside the existing ones. Add a call to `seed<Resource>s()` inside the existing `reset()` so tests clean up this resource too.

## 2. Route file — `routes/<resource>.js`

```js
const express = require('express');
const store = require('../db/store');

const router = express.Router();

// GET /<resources> — list all.
router.get('/', (req, res) => {
res.json(store.list<Resource>s());
});

// GET /<resources>/:id — fetch one, or 404.
router.get('/:id', (req, res) => {
const item = store.get<Resource>(Number(req.params.id));
if (!item) {
return res.status(404).json({ error: '<Resource> not found' });
}
return res.json(item);
});

// POST /<resources> — create. Validate required fields and return 400 on bad input.
router.post('/', (req, res) => {
const { <requiredFields> } = req.body;
if (!<requiredFields check>) {
return res.status(400).json({ error: '<fields> are required' });
}
const item = store.create<Resource>({ <fields> });
return res.status(201).json(item);
});

// PUT /<resources>/:id — partial update.
router.put('/:id', (req, res) => {
const { <fields> } = req.body;
if (<all fields undefined check>) {
return res.status(400).json({ error: 'at least one field is required' });
}
const item = store.update<Resource>(Number(req.params.id), { <fields> });
if (!item) {
return res.status(404).json({ error: '<Resource> not found' });
}
return res.json(item);
});

module.exports = router;
```

Error responses are always `{ "error": "<message>" }`. Status codes: 200 success, 201 created, 400 bad input, 404 not found.

## 3. Mount in `server.js`

```js
const <resource>sRouter = require('./routes/<resource>s');
// ...
app.use('/<resource>s', <resource>sRouter);
```

## 4. Test file — `tests/<resource>s.test.js`

```js
const test = require('node:test');
const assert = require('node:assert');
const request = require('supertest');
const app = require('../server');
const store = require('../db/store');

test.beforeEach(() => store.reset());

test('GET /<resource>s returns the seeded list', async () => {
const res = await request(app).get('/<resource>s');
assert.equal(res.status, 200);
assert.ok(Array.isArray(res.body));
assert.ok(res.body.length > 0);
});

test('GET /<resource>s/:id returns 404 for a missing <resource>', async () => {
const res = await request(app).get('/<resource>s/999');
assert.equal(res.status, 404);
});

test('POST /<resource>s creates a <resource>', async () => {
const res = await request(app)
.post('/<resource>s')
.send({ <fields with example values> });
assert.equal(res.status, 201);
assert.equal(res.body.<keyField>, <expectedValue>);
assert.ok(res.body.id);
});

test('PUT /<resource>s/:id updates an existing <resource>', async () => {
const res = await request(app).put('/<resource>s/1').send({ <oneField>: <newValue> });
assert.equal(res.status, 200);
assert.equal(res.body.<oneField>, <newValue>);
});

test('PUT /<resource>s/:id returns 404 for a missing <resource>', async () => {
const res = await request(app).put('/<resource>s/999').send({ <oneField>: 'x' });
assert.equal(res.status, 404);
});
```

After writing all files, run `npm test` to confirm everything passes.
151 changes: 151 additions & 0 deletions .claude/skills/add-resource/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
name: add-resource
description: Use when asked to add a new REST resource (route, CRUD endpoints, or "add a <thing> resource/route"). Encodes the three-file pattern this project uses: routes/<resource>.js, db/store.js additions, and tests/<resource>.test.js.
---
Adding a new resource requires three files touched in order: the store helpers, the route, and the tests. Then mount the router in server.js.

## 1. Store helpers — `db/store.js`

Append to the existing file. Follow the exact shape already used for users:

```js
let <resources> = [];
let next<Resource>Id = 1;

function seed<Resource>s() {
<resources> = [
{ id: 1, <field>: '<value>', ... },
];
next<Resource>Id = 2;
}
seed<Resource>s();

function list<Resource>s() { return <resources>; }

function get<Resource>(id) {
return <resources>.find((r) => r.id === id);
}

function create<Resource>({ <fields> }) {
const item = { id: next<Resource>Id, <fields> };
next<Resource>Id += 1;
<resources>.push(item);
return item;
}

function update<Resource>(id, fields) {
const item = get<Resource>(id);
if (!item) return undefined;
// apply only the fields that were provided
Object.keys(fields).forEach((k) => {
if (fields[k] !== undefined) item[k] = fields[k];
});
return item;
}
```

Export each new function alongside the existing ones. Add a call to `seed<Resource>s()` inside the existing `reset()` so tests clean up this resource too.

## 2. Route file — `routes/<resource>.js`

```js
const express = require('express');
const store = require('../db/store');

const router = express.Router();

// GET /<resources> — list all.
router.get('/', (req, res) => {
res.json(store.list<Resource>s());
});

// GET /<resources>/:id — fetch one, or 404.
router.get('/:id', (req, res) => {
const item = store.get<Resource>(Number(req.params.id));
if (!item) {
return res.status(404).json({ error: '<Resource> not found' });
}
return res.json(item);
});

// POST /<resources> — create. Validate required fields and return 400 on bad input.
router.post('/', (req, res) => {
const { <requiredFields> } = req.body;
if (!<requiredFields check>) {
return res.status(400).json({ error: '<fields> are required' });
}
const item = store.create<Resource>({ <fields> });
return res.status(201).json(item);
});

// PUT /<resources>/:id — partial update.
router.put('/:id', (req, res) => {
const { <fields> } = req.body;
if (<all fields undefined check>) {
return res.status(400).json({ error: 'at least one field is required' });
}
const item = store.update<Resource>(Number(req.params.id), { <fields> });
if (!item) {
return res.status(404).json({ error: '<Resource> not found' });
}
return res.json(item);
});

module.exports = router;
```

Error responses are always `{ "error": "<message>" }`. Status codes: 200 success, 201 created, 400 bad input, 404 not found.

## 3. Mount in `server.js`

```js
const <resource>sRouter = require('./routes/<resource>s');
// ...
app.use('/<resource>s', <resource>sRouter);
```

## 4. Test file — `tests/<resource>s.test.js`

```js
const test = require('node:test');
const assert = require('node:assert');
const request = require('supertest');
const app = require('../server');
const store = require('../db/store');

test.beforeEach(() => store.reset());

test('GET /<resource>s returns the seeded list', async () => {
const res = await request(app).get('/<resource>s');
assert.equal(res.status, 200);
assert.ok(Array.isArray(res.body));
assert.ok(res.body.length > 0);
});

test('GET /<resource>s/:id returns 404 for a missing <resource>', async () => {
const res = await request(app).get('/<resource>s/999');
assert.equal(res.status, 404);
});

test('POST /<resource>s creates a <resource>', async () => {
const res = await request(app)
.post('/<resource>s')
.send({ <fields with example values> });
assert.equal(res.status, 201);
assert.equal(res.body.<keyField>, <expectedValue>);
assert.ok(res.body.id);
});

test('PUT /<resource>s/:id updates an existing <resource>', async () => {
const res = await request(app).put('/<resource>s/1').send({ <oneField>: <newValue> });
assert.equal(res.status, 200);
assert.equal(res.body.<oneField>, <newValue>);
});

test('PUT /<resource>s/:id returns 404 for a missing <resource>', async () => {
const res = await request(app).put('/<resource>s/999').send({ <oneField>: 'x' });
assert.equal(res.status, 404);
});
```

After writing all files, run `npm test` to confirm everything passes.
16 changes: 16 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/psallay/Development/claude/kodree/docs"
]
}
}
}
45 changes: 45 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Notes

## MCP server

Two servers are wired in `.mcp.json`: `fetch` (mcp-server-fetch via uvx) and `filesystem`
(@modelcontextprotocol/server-filesystem scoped to `../docs`). The filesystem server is the
meaningful one here — it lets Claude read the project's reference docs without touching the source
tree. The permission rule in `settings.local.json` is `enabledMcpjsonServers: ["fetch",
"filesystem"]`, which opts both servers in without granting them any broader file-system access
beyond the single directory the server was started with.

## Skill

The `add-resource` skill captures the three-file sequence every new REST resource requires:
store helpers in `db/store.js`, a route file in `routes/`, and a test file in `tests/`, then a
one-line mount in `server.js`. Without the skill, that pattern has to be re-derived from reading
existing files every time. The description reads *"Use when asked to add a new REST resource
(route, CRUD endpoints, or 'add a \<thing\> resource/route')"* — the phrase "add a \<thing\>
resource/route" matches the natural way the request gets phrased, so the skill fires on the first
line of the prompt rather than after Claude has already started guessing the pattern.

## Command

`/pr-check` runs a focused review of the current git diff against this project's seven explicit
conventions: route placement, store-only data access, router mounting, error JSON shape, status
codes, input validation, and test coverage. It's worth a shortcut because it checks the same
checklist on every PR — the kind of thing that's easy to verify once but easy to forget across ten
PRs. Running it as a slash command costs one keystroke and returns a concrete file-and-line verdict
rather than a generic code review.

## Hook

The hook is a `PostToolUse` on the `Edit` matcher in `.claude/settings.json`. It *reacts* — it
runs `npm run lint` after every file edit rather than preventing the edit. The event is
`PostToolUse`, so ESLint sees the file as it was actually written. A `PreToolUse` guard would catch
nothing useful here (the file hasn't changed yet); a post-edit lint run is what surfaces a real
violation.

## Headless run

The headless run added a `products` resource — a well-scoped task with a clear definition of done
(all tests pass). The locked-down `--allowedTools` set was `Read,Write,Edit,Bash(npm test)`: the
three file-manipulation tools the task actually requires, plus a prefix-matched Bash rule that
allows exactly `npm test` and nothing else. That means the agent couldn't start the dev server,
run `rm`, or make any network call — the blast radius was defined before execution started.
Loading
Loading