switchman-dev 0.1.13 → 0.1.15

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.
Files changed (39) hide show
  1. package/.switchman/audit.key +1 -0
  2. package/.switchman/switchman.db +0 -0
  3. package/README.md +11 -0
  4. package/bin/switchman.js +3 -0
  5. package/examples/taskapi/.switchman/audit.key +1 -0
  6. package/examples/taskapi/.switchman/switchman.db +0 -0
  7. package/examples/taskapi/package-lock.json +4736 -0
  8. package/examples/worktrees/agent-rate-limiting/.cursor/mcp.json +8 -0
  9. package/examples/worktrees/agent-rate-limiting/.mcp.json +8 -0
  10. package/examples/worktrees/agent-rate-limiting/package-lock.json +4736 -0
  11. package/examples/worktrees/agent-rate-limiting/package.json +18 -0
  12. package/examples/worktrees/agent-rate-limiting/src/db.js +179 -0
  13. package/examples/worktrees/agent-rate-limiting/src/middleware/auth.js +100 -0
  14. package/examples/worktrees/agent-rate-limiting/src/middleware/validate.js +135 -0
  15. package/examples/worktrees/agent-rate-limiting/src/routes/tasks.js +67 -0
  16. package/examples/worktrees/agent-rate-limiting/src/routes/users.js +38 -0
  17. package/examples/worktrees/agent-rate-limiting/src/server.js +11 -0
  18. package/examples/worktrees/agent-tests/.cursor/mcp.json +8 -0
  19. package/examples/worktrees/agent-tests/.mcp.json +8 -0
  20. package/examples/worktrees/agent-tests/package-lock.json +4736 -0
  21. package/examples/worktrees/agent-tests/package.json +18 -0
  22. package/examples/worktrees/agent-tests/src/db.js +179 -0
  23. package/examples/worktrees/agent-tests/src/middleware/auth.js +98 -0
  24. package/examples/worktrees/agent-tests/src/middleware/validate.js +135 -0
  25. package/examples/worktrees/agent-tests/src/routes/tasks.js +67 -0
  26. package/examples/worktrees/agent-tests/src/routes/users.js +38 -0
  27. package/examples/worktrees/agent-tests/src/server.js +9 -0
  28. package/examples/worktrees/agent-validation/.cursor/mcp.json +8 -0
  29. package/examples/worktrees/agent-validation/.mcp.json +8 -0
  30. package/examples/worktrees/agent-validation/package-lock.json +4736 -0
  31. package/examples/worktrees/agent-validation/package.json +18 -0
  32. package/examples/worktrees/agent-validation/src/db.js +179 -0
  33. package/examples/worktrees/agent-validation/src/middleware/auth.js +100 -0
  34. package/examples/worktrees/agent-validation/src/middleware/validate.js +137 -0
  35. package/examples/worktrees/agent-validation/src/routes/tasks.js +69 -0
  36. package/examples/worktrees/agent-validation/src/routes/users.js +38 -0
  37. package/examples/worktrees/agent-validation/src/server.js +11 -0
  38. package/package.json +2 -2
  39. package/src/core/sync.js +177 -49
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "taskapi",
3
+ "version": "1.0.0",
4
+ "description": "Example project for practising Switchman — a simple task management REST API",
5
+ "main": "src/server.js",
6
+ "scripts": {
7
+ "start": "node src/server.js",
8
+ "dev": "node --watch src/server.js",
9
+ "test": "jest --runInBand"
10
+ },
11
+ "dependencies": {
12
+ "express": "^4.18.2"
13
+ },
14
+ "devDependencies": {
15
+ "jest": "^29.7.0",
16
+ "supertest": "^6.3.4"
17
+ }
18
+ }
@@ -0,0 +1,179 @@
1
+ // db.js — SQLite-backed store for taskapi.
2
+ // Drop-in alternative to dbstore/store.js.
3
+ // Requires: npm install better-sqlite3
4
+ //
5
+ // Usage: swap require('./dbstore/store') → require('./db') in routes/app.js
6
+
7
+ const path = require('path');
8
+ const Database = require('better-sqlite3');
9
+
10
+ const DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'taskapi.db');
11
+
12
+ let _db;
13
+
14
+ function getDb() {
15
+ if (!_db) {
16
+ _db = new Database(DB_PATH);
17
+ _db.pragma('journal_mode = WAL');
18
+ _db.pragma('busy_timeout = 5000');
19
+ _db.pragma('foreign_keys = ON');
20
+ migrate(_db);
21
+ seed(_db);
22
+ }
23
+ return _db;
24
+ }
25
+
26
+ function migrate(db) {
27
+ db.exec(`
28
+ CREATE TABLE IF NOT EXISTS users (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ name TEXT NOT NULL,
31
+ email TEXT NOT NULL UNIQUE,
32
+ role TEXT NOT NULL DEFAULT 'member',
33
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS projects (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ name TEXT NOT NULL,
39
+ ownerId INTEGER REFERENCES users(id),
40
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS tasks (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ title TEXT NOT NULL,
46
+ status TEXT NOT NULL DEFAULT 'todo',
47
+ priority TEXT NOT NULL DEFAULT 'medium',
48
+ projectId INTEGER NOT NULL REFERENCES projects(id),
49
+ assigneeId INTEGER REFERENCES users(id),
50
+ createdAt TEXT NOT NULL DEFAULT (datetime('now'))
51
+ );
52
+ `);
53
+ }
54
+
55
+ function seed(db) {
56
+ const count = db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
57
+ if (count > 0) return; // already seeded
58
+
59
+ db.prepare(`INSERT INTO users (name, email, role) VALUES (?, ?, ?)`).run('Alice', 'alice@example.com', 'admin');
60
+ db.prepare(`INSERT INTO users (name, email, role) VALUES (?, ?, ?)`).run('Bob', 'bob@example.com', 'member');
61
+
62
+ db.prepare(`INSERT INTO projects (name, ownerId) VALUES (?, ?)`).run('Backend Rewrite', 1);
63
+ db.prepare(`INSERT INTO projects (name, ownerId) VALUES (?, ?)`).run('Mobile App', 2);
64
+
65
+ db.prepare(`INSERT INTO tasks (title, status, priority, projectId, assigneeId) VALUES (?, ?, ?, ?, ?)`).run('Set up CI pipeline', 'todo', 'high', 1, null);
66
+ db.prepare(`INSERT INTO tasks (title, status, priority, projectId, assigneeId) VALUES (?, ?, ?, ?, ?)`).run('Write API docs', 'in_progress', 'medium', 1, 2);
67
+ db.prepare(`INSERT INTO tasks (title, status, priority, projectId, assigneeId) VALUES (?, ?, ?, ?, ?)`).run('Fix login bug', 'todo', 'high', 2, null);
68
+ }
69
+
70
+ // ── Public API (same shape as dbstore/store.js) ──────────────────────────────
71
+
72
+ const db = {
73
+ tasks: {
74
+ findAll() {
75
+ return getDb().prepare('SELECT * FROM tasks ORDER BY createdAt DESC').all();
76
+ },
77
+ findById(id) {
78
+ return getDb().prepare('SELECT * FROM tasks WHERE id = ?').get(id) || null;
79
+ },
80
+ findByProject(projectId) {
81
+ return getDb().prepare('SELECT * FROM tasks WHERE projectId = ?').all(projectId);
82
+ },
83
+ create(data) {
84
+ const { title, projectId, priority = 'medium', status = 'todo', assigneeId = null } = data;
85
+ const result = getDb()
86
+ .prepare('INSERT INTO tasks (title, projectId, priority, status, assigneeId) VALUES (?, ?, ?, ?, ?)')
87
+ .run(title, projectId, priority, status, assigneeId);
88
+ return this.findById(result.lastInsertRowid);
89
+ },
90
+ update(id, data) {
91
+ const allowed = ['title', 'status', 'priority', 'assigneeId'];
92
+ const fields = Object.keys(data).filter(k => allowed.includes(k));
93
+ if (fields.length === 0) return this.findById(id);
94
+ const set = fields.map(f => `${f} = ?`).join(', ');
95
+ const values = fields.map(f => data[f]);
96
+ getDb().prepare(`UPDATE tasks SET ${set} WHERE id = ?`).run(...values, id);
97
+ return this.findById(id);
98
+ },
99
+ delete(id) {
100
+ const result = getDb().prepare('DELETE FROM tasks WHERE id = ?').run(id);
101
+ return result.changes > 0;
102
+ },
103
+ },
104
+
105
+ projects: {
106
+ findAll() {
107
+ return getDb().prepare('SELECT * FROM projects ORDER BY createdAt DESC').all();
108
+ },
109
+ findById(id) {
110
+ return getDb().prepare('SELECT * FROM projects WHERE id = ?').get(id) || null;
111
+ },
112
+ create(data) {
113
+ const { name, ownerId = null } = data;
114
+ const result = getDb()
115
+ .prepare('INSERT INTO projects (name, ownerId) VALUES (?, ?)')
116
+ .run(name, ownerId);
117
+ return this.findById(result.lastInsertRowid);
118
+ },
119
+ update(id, data) {
120
+ const allowed = ['name', 'ownerId'];
121
+ const fields = Object.keys(data).filter(k => allowed.includes(k));
122
+ if (fields.length === 0) return this.findById(id);
123
+ const set = fields.map(f => `${f} = ?`).join(', ');
124
+ const values = fields.map(f => data[f]);
125
+ getDb().prepare(`UPDATE projects SET ${set} WHERE id = ?`).run(...values, id);
126
+ return this.findById(id);
127
+ },
128
+ delete(id) {
129
+ const result = getDb().prepare('DELETE FROM projects WHERE id = ?').run(id);
130
+ return result.changes > 0;
131
+ },
132
+ },
133
+
134
+ users: {
135
+ findAll() {
136
+ return getDb().prepare('SELECT * FROM users ORDER BY id').all();
137
+ },
138
+ findById(id) {
139
+ return getDb().prepare('SELECT * FROM users WHERE id = ?').get(id) || null;
140
+ },
141
+ findByEmail(email) {
142
+ return getDb().prepare('SELECT * FROM users WHERE email = ?').get(email) || null;
143
+ },
144
+ create(data) {
145
+ const { name, email, role = 'member' } = data;
146
+ const result = getDb()
147
+ .prepare('INSERT INTO users (name, email, role) VALUES (?, ?, ?)')
148
+ .run(name, email, role);
149
+ return this.findById(result.lastInsertRowid);
150
+ },
151
+ update(id, data) {
152
+ const allowed = ['name', 'email', 'role'];
153
+ const fields = Object.keys(data).filter(k => allowed.includes(k));
154
+ if (fields.length === 0) return this.findById(id);
155
+ const set = fields.map(f => `${f} = ?`).join(', ');
156
+ const values = fields.map(f => data[f]);
157
+ getDb().prepare(`UPDATE users SET ${set} WHERE id = ?`).run(...values, id);
158
+ return this.findById(id);
159
+ },
160
+ },
161
+
162
+ // For tests: wipe and re-seed
163
+ _reset() {
164
+ const d = getDb();
165
+ d.exec('DELETE FROM tasks; DELETE FROM projects; DELETE FROM users;');
166
+ // Reset autoincrement counters
167
+ d.exec(`
168
+ DELETE FROM sqlite_sequence WHERE name IN ('tasks','projects','users');
169
+ `);
170
+ seed(d);
171
+ },
172
+
173
+ // Close the connection (useful in tests)
174
+ close() {
175
+ if (_db) { _db.close(); _db = null; }
176
+ },
177
+ };
178
+
179
+ module.exports = db;
@@ -0,0 +1,100 @@
1
+ // middleware/auth.js — API key authentication middleware.
2
+ //
3
+ // Every protected route requires an `x-api-key` header.
4
+ // Keys are looked up against the users table; the resolved user
5
+ // is attached to req.user for downstream handlers.
6
+ //
7
+ // Usage:
8
+ // const { requireAuth, requireRole } = require('./middleware/auth');
9
+ // router.post('/admin-only', requireAuth, requireRole('admin'), handler);
10
+
11
+ const db = require('../dbstore/store');
12
+
13
+ // Static API key → user mapping (matches session summary keys).
14
+ // In production, store hashed keys in the DB and look them up there.
15
+ const API_KEYS = {
16
+ 'dev-key-alice-123': { email: 'alice@example.com' },
17
+ 'dev-key-bob-456': { email: 'bob@example.com' },
18
+ 'test-key-789': { email: 'bob@example.com' }, // test user maps to Bob
19
+ };
20
+
21
+ /**
22
+ * requireAuth
23
+ * Reads the `x-api-key` header, resolves the key to a user record,
24
+ * and attaches it to req.user. Returns 401 if the key is missing or
25
+ * unknown, 403 if the user record can no longer be found in the store.
26
+ */
27
+ function requireAuth(req, res, next) {
28
+ const key = req.headers['x-api-key'];
29
+
30
+ if (!key) {
31
+ return res.status(401).json({
32
+ error: 'Missing API key. Provide an x-api-key header.',
33
+ });
34
+ }
35
+
36
+ const mapping = API_KEYS[key];
37
+ if (!mapping) {
38
+ return res.status(401).json({ error: 'Invalid API key.' });
39
+ }
40
+
41
+ const user = db.users.findByEmail(mapping.email);
42
+ if (!user) {
43
+ return res.status(403).json({ error: 'API key refers to a deleted user.' });
44
+ }
45
+
46
+ req.user = user;
47
+ next();
48
+ }
49
+
50
+ /**
51
+ * requireRole(role)
52
+ * Factory that returns a middleware enforcing a minimum role.
53
+ * Roles (weakest → strongest): member → admin
54
+ * Must be used AFTER requireAuth.
55
+ *
56
+ * @param {'member'|'admin'} role
57
+ */
58
+ const ROLE_RANK = { member: 1, admin: 2 };
59
+
60
+ function requireRole(role) {
61
+ return function roleGuard(req, res, next) {
62
+ if (!req.user) {
63
+ // Programmer error: requireRole used without requireAuth
64
+ return res.status(500).json({ error: 'Auth middleware misconfigured.' });
65
+ }
66
+
67
+ const userRank = ROLE_RANK[req.user.role] ?? 0;
68
+ const required = ROLE_RANK[role] ?? 99;
69
+
70
+ if (userRank < required) {
71
+ return res.status(403).json({
72
+ error: `Requires '${role}' role. Your role: '${req.user.role}'.`,
73
+ });
74
+ }
75
+
76
+ next();
77
+ };
78
+ }
79
+
80
+ /**
81
+ * optionalAuth
82
+ * Like requireAuth but doesn't block unauthenticated requests.
83
+ * Attaches req.user if a valid key is provided, otherwise leaves it null.
84
+ */
85
+ function optionalAuth(req, res, next) {
86
+ const key = req.headers['x-api-key'];
87
+ if (!key) { req.user = null; return next(); }
88
+
89
+ const mapping = API_KEYS[key];
90
+ if (!mapping) { req.user = null; return next(); }
91
+
92
+ req.user = db.users.findByEmail(mapping.email) || null;
93
+ next();
94
+ }
95
+
96
+ module.exports = { requireAuth, requireRole, optionalAuth };
97
+
98
+ // demo: rate-limiting agent touched auth middleware
99
+
100
+ // demo: rate-limiting agent touched auth middleware
@@ -0,0 +1,135 @@
1
+ // middleware/validate.js — lightweight request validation helpers.
2
+ //
3
+ // Provides schema-based body validation without a heavy dependency.
4
+ // Works alongside auth.js as the other primary shared middleware —
5
+ // making both files natural conflict hotspots for parallel agents.
6
+ //
7
+ // Usage:
8
+ // const { validate, schemas } = require('./middleware/validate');
9
+ // router.post('/', validate(schemas.createTask), handler);
10
+
11
+ /**
12
+ * validate(schema)
13
+ * Returns an Express middleware that checks req.body against `schema`.
14
+ * A schema is a plain object whose values are validator functions:
15
+ * { field: (value, body) => errorString | null }
16
+ *
17
+ * All errors are collected and returned together as a 400 response.
18
+ */
19
+ function validate(schema) {
20
+ return function validationMiddleware(req, res, next) {
21
+ const errors = [];
22
+
23
+ for (const [field, validator] of Object.entries(schema)) {
24
+ const value = req.body?.[field];
25
+ const error = validator(value, req.body);
26
+ if (error) errors.push({ field, message: error });
27
+ }
28
+
29
+ if (errors.length > 0) {
30
+ return res.status(400).json({ errors });
31
+ }
32
+
33
+ next();
34
+ };
35
+ }
36
+
37
+ // ── Reusable validator primitives ────────────────────────────────────────────
38
+
39
+ const required = (field) => (value) =>
40
+ value === undefined || value === null || value === ''
41
+ ? `${field} is required`
42
+ : null;
43
+
44
+ const isString = (field) => (value) =>
45
+ value !== undefined && typeof value !== 'string'
46
+ ? `${field} must be a string`
47
+ : null;
48
+
49
+ const isNumber = (field) => (value) =>
50
+ value !== undefined && (typeof value !== 'number' || !Number.isFinite(value))
51
+ ? `${field} must be a number`
52
+ : null;
53
+
54
+ const isEmail = (field) => (value) => {
55
+ if (!value) return null; // let `required` handle missing values
56
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
57
+ ? null
58
+ : `${field} must be a valid email address`;
59
+ };
60
+
61
+ const oneOf = (field, options) => (value) =>
62
+ value !== undefined && !options.includes(value)
63
+ ? `${field} must be one of: ${options.join(', ')}`
64
+ : null;
65
+
66
+ const maxLength = (field, max) => (value) =>
67
+ value && typeof value === 'string' && value.length > max
68
+ ? `${field} must be ${max} characters or fewer`
69
+ : null;
70
+
71
+ const minLength = (field, min) => (value) =>
72
+ value && typeof value === 'string' && value.length < min
73
+ ? `${field} must be at least ${min} characters`
74
+ : null;
75
+
76
+ /** Compose multiple validators for one field (returns first error). */
77
+ const compose = (...validators) =>
78
+ (value, body) => {
79
+ for (const v of validators) {
80
+ const err = v(value, body);
81
+ if (err) return err;
82
+ }
83
+ return null;
84
+ };
85
+
86
+ // ── Pre-built schemas ────────────────────────────────────────────────────────
87
+
88
+ const VALID_STATUSES = ['todo', 'in_progress', 'done', 'cancelled'];
89
+ const VALID_PRIORITIES = ['low', 'medium', 'high'];
90
+ const VALID_ROLES = ['member', 'admin'];
91
+
92
+ const schemas = {
93
+ createTask: {
94
+ title: compose(required('title'), isString('title'), maxLength('title', 200)),
95
+ projectId: compose(required('projectId'), isNumber('projectId')),
96
+ priority: oneOf('priority', VALID_PRIORITIES),
97
+ status: oneOf('status', VALID_STATUSES),
98
+ },
99
+
100
+ updateTask: {
101
+ title: compose(isString('title'), maxLength('title', 200)),
102
+ status: oneOf('status', VALID_STATUSES),
103
+ priority: oneOf('priority', VALID_PRIORITIES),
104
+ },
105
+
106
+ createProject: {
107
+ name: compose(required('name'), isString('name'), maxLength('name', 100)),
108
+ ownerId: isNumber('ownerId'),
109
+ },
110
+
111
+ updateProject: {
112
+ name: compose(isString('name'), maxLength('name', 100)),
113
+ },
114
+
115
+ createUser: {
116
+ name: compose(required('name'), isString('name'), maxLength('name', 100)),
117
+ email: compose(required('email'), isEmail('email'), maxLength('email', 255)),
118
+ role: oneOf('role', VALID_ROLES),
119
+ },
120
+
121
+ updateUser: {
122
+ name: compose(isString('name'), maxLength('name', 100)),
123
+ email: compose(isEmail('email'), maxLength('email', 255)),
124
+ role: oneOf('role', VALID_ROLES),
125
+ },
126
+ };
127
+
128
+ module.exports = {
129
+ validate,
130
+ schemas,
131
+ // Export primitives so routes can build ad-hoc schemas
132
+ validators: { required, isString, isNumber, isEmail, oneOf, maxLength, minLength, compose },
133
+ };
134
+
135
+ // demo: validation agent touched validation middleware
@@ -0,0 +1,67 @@
1
+ const express = require('express');
2
+ const db = require('../dbstore/store');
3
+
4
+ const router = express.Router();
5
+
6
+ // GET /api/tasks — list all tasks (optionally filter by projectId or status)
7
+ router.get('/', (req, res) => {
8
+ let tasks = db.tasks.findAll();
9
+ if (req.query.projectId) {
10
+ tasks = tasks.filter(t => t.projectId === Number(req.query.projectId));
11
+ }
12
+ if (req.query.status) {
13
+ tasks = tasks.filter(t => t.status === req.query.status);
14
+ }
15
+ res.json(tasks);
16
+ });
17
+
18
+ // GET /api/tasks/:id
19
+ router.get('/:id', (req, res) => {
20
+ const task = db.tasks.findById(Number(req.params.id));
21
+ if (!task) return res.status(404).json({ error: 'Task not found' });
22
+ res.json(task);
23
+ });
24
+
25
+ // POST /api/tasks
26
+ router.post('/', (req, res) => {
27
+ const { title, projectId, priority = 'medium', assigneeId } = req.body;
28
+ if (!title) return res.status(400).json({ error: 'title is required' });
29
+ if (!projectId) return res.status(400).json({ error: 'projectId is required' });
30
+
31
+ const project = db.projects.findById(Number(projectId));
32
+ if (!project) return res.status(400).json({ error: 'Project not found' });
33
+
34
+ const task = db.tasks.create({
35
+ title,
36
+ projectId: Number(projectId),
37
+ priority,
38
+ status: 'todo',
39
+ assigneeId: assigneeId ? Number(assigneeId) : null,
40
+ });
41
+ res.status(201).json(task);
42
+ });
43
+
44
+ // PATCH /api/tasks/:id
45
+ router.patch('/:id', (req, res) => {
46
+ const task = db.tasks.findById(Number(req.params.id));
47
+ if (!task) return res.status(404).json({ error: 'Task not found' });
48
+
49
+ const VALID_STATUSES = ['todo', 'in_progress', 'done', 'cancelled'];
50
+ if (req.body.status && !VALID_STATUSES.includes(req.body.status)) {
51
+ return res.status(400).json({ error: `status must be one of: ${VALID_STATUSES.join(', ')}` });
52
+ }
53
+
54
+ const updated = db.tasks.update(Number(req.params.id), req.body);
55
+ res.json(updated);
56
+ });
57
+
58
+ // DELETE /api/tasks/:id
59
+ router.delete('/:id', (req, res) => {
60
+ const deleted = db.tasks.delete(Number(req.params.id));
61
+ if (!deleted) return res.status(404).json({ error: 'Task not found' });
62
+ res.status(204).send();
63
+ });
64
+
65
+ module.exports = router;
66
+
67
+ // demo: validation agent touched tasks route
@@ -0,0 +1,38 @@
1
+ const express = require('express');
2
+ const db = require('../dbstore/store');
3
+
4
+ const router = express.Router();
5
+
6
+ // GET /api/users
7
+ router.get('/', (req, res) => {
8
+ res.json(db.users.findAll());
9
+ });
10
+
11
+ // GET /api/users/:id
12
+ router.get('/:id', (req, res) => {
13
+ const user = db.users.findById(Number(req.params.id));
14
+ if (!user) return res.status(404).json({ error: 'User not found' });
15
+ res.json(user);
16
+ });
17
+
18
+ // POST /api/users
19
+ router.post('/', (req, res) => {
20
+ const { name, email, role = 'member' } = req.body;
21
+ if (!name) return res.status(400).json({ error: 'name is required' });
22
+ if (!email) return res.status(400).json({ error: 'email is required' });
23
+
24
+ const existing = db.users.findByEmail(email);
25
+ if (existing) return res.status(409).json({ error: 'Email already in use' });
26
+
27
+ const user = db.users.create({ name, email, role });
28
+ res.status(201).json(user);
29
+ });
30
+
31
+ // PATCH /api/users/:id
32
+ router.patch('/:id', (req, res) => {
33
+ const user = db.users.findById(Number(req.params.id));
34
+ if (!user) return res.status(404).json({ error: 'User not found' });
35
+ res.json(db.users.update(Number(req.params.id), req.body));
36
+ });
37
+
38
+ module.exports = router;
@@ -0,0 +1,11 @@
1
+ const app = require('./app');
2
+
3
+ const PORT = process.env.PORT || 3000;
4
+
5
+ app.listen(PORT, () => {
6
+ console.log(`TaskAPI running on port ${PORT}`);
7
+ });
8
+
9
+ // demo: rate-limiting agent touched server
10
+
11
+ // demo: rate-limiting agent touched server
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "switchman": {
4
+ "command": "switchman-mcp",
5
+ "args": []
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "switchman": {
4
+ "command": "switchman-mcp",
5
+ "args": []
6
+ }
7
+ }
8
+ }