mvagnon-agents 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ ```
2
+ __ _
3
+ _ ____ ____ _ __ _ _ _ ___ _ _ / /_ _ __ _ ___ _ _| |_ ___
4
+ | ' \ V / _` / _` | ' \/ _ \ ' \ / / _` / _` / -_) ' \ _(_-<
5
+ |_|_|_\_/\__,_\__, |_||_\___/_||_/_/\__,_\__, \___|_||_\__/__/
6
+ |___/ |___/
7
+ ```
8
+
9
+ # AI Workflow
10
+
11
+ Shared configuration and conventions to bootstrap AI coding assistants on TypeScript/React projects. One repo, one command — every tool gets the same rules, skills, and MCP servers.
12
+
13
+ ## Supported Tools
14
+
15
+ | Tool | Rules | Skills / Agents | Root File | MCP Config |
16
+ | ----------- | ------------------ | ---------------------------------------- | ----------- | -------------------- |
17
+ | Claude Code | `.claude/rules/` | `.claude/skills/`, `.claude/agents/` | `CLAUDE.md` | `.mcp.json` |
18
+ | OpenCode | `.opencode/rules/` | `.opencode/skills/`, `.opencode/agents/` | `AGENTS.md` | `opencode.json` |
19
+ | Cursor | `.cursor/rules/` | `.cursor/skills/`, `.cursor/agents/` | — | `.cursor/mcp.json` |
20
+ | Codex | — | `.agents/skills/` | `AGENTS.md` | `.codex/config.toml` |
21
+
22
+ ## Tech Stack Coverage
23
+
24
+ | Category | Technologies |
25
+ | -------- | --------------------------------------------------------- |
26
+ | Frontend | React, Next.js, Vite, TypeScript, Tailwind CSS, ShadCN UI |
27
+ | Backend | Node.js, Fastify, Express, NestJS, Supabase |
28
+ | State | Zustand, TanStack Query, React Context |
29
+ | Testing | Vitest, Jest, React Testing Library |
30
+ | Tooling | ESLint, Prettier, pnpm, Docker |
31
+
32
+ ## Getting Started
33
+
34
+ ### Prerequisites
35
+
36
+ - Node.js >= 18
37
+ - pnpm or npm
38
+
39
+ ### Installation
40
+
41
+ ```bash
42
+ pnpm install
43
+ ```
44
+
45
+ ### Usage
46
+
47
+ ```bash
48
+ # Run the bootstrap script with target path
49
+ pnpm run bootstrap ../my-project
50
+
51
+ # Or directly with node
52
+ node bootstrap.mjs ~/projects/my-app
53
+ ```
54
+
55
+ ### Interactive Walkthrough
56
+
57
+ The script prompts you to:
58
+
59
+ 1. **Select technologies** — filter rules by stack (React, TypeScript)
60
+ 2. **Select architecture** — optional (e.g. Hexagonal)
61
+ 3. **Choose mode** — symlinks (recommended) or copy files
62
+ 4. **Select target tools** — one or more (Claude Code, OpenCode, Cursor, Codex)
63
+
64
+ ```
65
+ AI Workflow → /Users/you/projects/my-app
66
+
67
+ ◆ Select technologies
68
+ │ [ ] React - Components, hooks, patterns
69
+ │ [x] TypeScript - Conventions, testing
70
+
71
+ ◆ Select custom architecture
72
+ │ ● None
73
+
74
+ ◆ Use symlinks? Yes
75
+
76
+ ◆ Select target tools
77
+ │ [x] Claude Code
78
+ │ [x] Cursor
79
+ │ [ ] OpenCode
80
+ │ [ ] Codex
81
+
82
+ ◇ Claude Code Setup
83
+ │ Rules: 2 linked
84
+ │ Skills: 1 linked
85
+ │ Agents: 0 linked
86
+ │ CLAUDE.md: linked
87
+ │ .mcp.json: copied
88
+ │ .gitignore: entries added
89
+
90
+ ◇ Cursor Setup
91
+ │ Rules: 2 linked
92
+ │ .cursor/mcp.json: copied
93
+ │ .gitignore: entries added
94
+
95
+ Done
96
+ ```
97
+
98
+ ## Project Structure
99
+
100
+ ```
101
+ config/
102
+ ├── rules/
103
+ │ ├── project.md # Generic project rules (always included)
104
+ │ └── react-hexagonal-architecture.md # Hexagonal architecture for React
105
+ ├── skills/
106
+ │ └── readme-writing/ # README generation skill
107
+ ├── agents/ # Custom agents (optional)
108
+ ├── AGENTS.md # Master rules for all agents
109
+ ├── claudecode.settings.json # Claude Code MCP config
110
+ ├── opencode.settings.json # OpenCode MCP config
111
+ ├── cursor.mcp.json # Cursor MCP config
112
+ └── codex.config.toml # Codex MCP config (TOML)
113
+
114
+ bootstrap.mjs # Interactive setup script
115
+ ```
116
+
117
+ ## Rule Filtering
118
+
119
+ Items without a technology or architecture prefix are considered generic and always included.
120
+
121
+ | Selection | Rules Included |
122
+ | ------------ | -------------------------------------- |
123
+ | None | Generic rules only (`project.md`) |
124
+ | React | Generic + `react-*.md` |
125
+ | TypeScript | Generic + `ts-*.md` |
126
+ | Hexagonal | Generic + items containing `hexagonal` |
127
+ | React + Both | Generic + `react-*.md` + `ts-*.md` |
128
+
129
+ ## Symlinks vs Copy
130
+
131
+ | Mode | Pros | Cons |
132
+ | -------- | ----------------------------------- | ----------------------------- |
133
+ | Symlinks | Centralized updates, no duplication | Requires source repo presence |
134
+ | Copy | Self-contained, portable | Manual updates needed |
135
+
136
+ Items listed in `COPIED_RULES`, `COPIED_SKILLS`, or `COPIED_AGENTS` are always copied (never symlinked) to allow per-project customization.
137
+
138
+ ## Manual Installation
139
+
140
+ If you prefer not to use the bootstrap script:
141
+
142
+ 1. Copy the `config/` folder contents to your project
143
+ 2. Rename files according to your target tool (see Supported Tools table)
144
+ 3. Update `.gitignore` as needed
package/bootstrap.mjs ADDED
@@ -0,0 +1,469 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as p from "@clack/prompts";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ // =============================================================================
11
+ // CONFIGURATION
12
+ // =============================================================================
13
+
14
+ // Technologies filter items by prefix (e.g., "react-*" matches "react-components")
15
+ const TECHNOLOGIES = [
16
+ { value: "react", label: "React", hint: "Components, hooks, patterns" },
17
+ { value: "ts", label: "TypeScript", hint: "Conventions, testing" },
18
+ ];
19
+
20
+ // Architectures filter items by substring match
21
+ const ARCHITECTURES = [
22
+ { value: "none", label: "None", hint: "No custom architecture" },
23
+ { value: "hexagonal", label: "Hexagonal", hint: "Ports & adapters pattern" },
24
+ ];
25
+
26
+ // Items always copied (never symlinked) to allow per-project customization
27
+ const COPIED_RULES = ["project"];
28
+ const COPIED_SKILLS = [];
29
+ const COPIED_AGENTS = [];
30
+
31
+ // Intermediate directory in the target project for per-project customizable resources
32
+ const INTERMEDIATE_DIR = ".mvagnon/agents";
33
+
34
+ // Tool definitions: directory structure, root files, config files, gitignore
35
+ const TOOLS = {
36
+ claudecode: {
37
+ value: "claudecode",
38
+ label: "Claude Code",
39
+ hint: "Anthropic's CLI for Claude",
40
+ paths: {
41
+ rules: ".claude/rules",
42
+ skills: ".claude/skills",
43
+ agents: ".claude/agents",
44
+ },
45
+ rootFiles: { "AGENTS.md": "CLAUDE.md" },
46
+ configFiles: { "claudecode.settings.json": ".mcp.json" },
47
+ gitignoreEntries: [".claude", "CLAUDE.md", ".mcp.json"],
48
+ },
49
+
50
+ opencode: {
51
+ value: "opencode",
52
+ label: "OpenCode",
53
+ hint: "Open-source AI coding assistant",
54
+ paths: {
55
+ rules: ".opencode/rules",
56
+ skills: ".opencode/skills",
57
+ agents: ".opencode/agents",
58
+ },
59
+ rootFiles: { "AGENTS.md": "AGENTS.md" },
60
+ configFiles: { "opencode.settings.json": "opencode.json" },
61
+ gitignoreEntries: [".opencode", "AGENTS.md", "opencode.json"],
62
+ },
63
+
64
+ cursor: {
65
+ value: "cursor",
66
+ label: "Cursor",
67
+ hint: "AI-powered code editor",
68
+ paths: {
69
+ rules: ".cursor/rules",
70
+ skills: ".cursor/skills",
71
+ agents: ".cursor/agents",
72
+ },
73
+ rootFiles: {},
74
+ configFiles: { "cursor.mcp.json": ".cursor/mcp.json" },
75
+ gitignoreEntries: [".cursor"],
76
+ },
77
+
78
+ codex: {
79
+ value: "codex",
80
+ label: "Codex",
81
+ hint: "OpenAI's coding agent CLI",
82
+ paths: {
83
+ skills: ".agents/skills",
84
+ },
85
+ rootFiles: { "AGENTS.md": "AGENTS.md" },
86
+ configFiles: { "codex.config.toml": ".codex/config.toml" },
87
+ gitignoreEntries: [".codex", ".agents", "AGENTS.md"],
88
+ },
89
+ };
90
+
91
+ // =============================================================================
92
+ // MAIN
93
+ // =============================================================================
94
+
95
+ async function main() {
96
+ const targetArg = process.argv[2];
97
+
98
+ if (!targetArg) {
99
+ console.error("Usage: ./bootstrap.sh <target-path>");
100
+ console.error("Example: ./bootstrap.sh ../my-project");
101
+ process.exit(1);
102
+ }
103
+
104
+ const targetPath = resolvePath(targetArg);
105
+
106
+ if (!fs.existsSync(targetPath)) {
107
+ console.error(`Error: Directory not found: ${targetPath}`);
108
+ process.exit(1);
109
+ }
110
+
111
+ if (!fs.statSync(targetPath).isDirectory()) {
112
+ console.error(`Error: Path must be a directory: ${targetPath}`);
113
+ process.exit(1);
114
+ }
115
+
116
+ console.clear();
117
+
118
+ const banner = [
119
+ " __ _ ",
120
+ " _ ____ ____ _ __ _ _ _ ___ _ _ / /_ _ __ _ ___ _ _| |_ ___",
121
+ "| ' \\ V / _` / _` | ' \\/ _ \\ ' \\ / / _` / _` / -_) ' \\ _(_-<",
122
+ "|_|_|_\\_/\\__,_\\__, |_||_\\___/_||_/_/\\__,_\\__, \\___|_||_\\__/__/",
123
+ " |___/ |___/ ",
124
+ ];
125
+ console.log("\x1b[36m" + banner.join("\n") + "\x1b[0m\n");
126
+
127
+ p.intro(`AI Workflow → ${targetPath}`);
128
+
129
+ const config = await p.group(
130
+ {
131
+ techs: () =>
132
+ p.multiselect({
133
+ message: "Select technologies",
134
+ options: TECHNOLOGIES,
135
+ required: false,
136
+ }),
137
+
138
+ archs: () =>
139
+ p.select({
140
+ message: "Select custom architecture",
141
+ options: ARCHITECTURES,
142
+ required: false,
143
+ }),
144
+
145
+ useSymlinks: () =>
146
+ p.confirm({
147
+ message:
148
+ "Use symlinks? (yes: `.gitignore` updated, no: config versioned)",
149
+ initialValue: true,
150
+ }),
151
+
152
+ tools: () =>
153
+ p.multiselect({
154
+ message: "Select target tools",
155
+ options: Object.values(TOOLS).map((t) => ({
156
+ value: t.value,
157
+ label: t.label,
158
+ hint: t.hint,
159
+ })),
160
+ required: true,
161
+ }),
162
+ },
163
+ {
164
+ onCancel: () => {
165
+ p.cancel("Setup cancelled");
166
+ process.exit(0);
167
+ },
168
+ },
169
+ );
170
+
171
+ const selectedTechs = config.techs || [];
172
+ const selectedArchs = config.archs ? [config.archs] : [];
173
+ const useSymlinks = config.useSymlinks;
174
+ const selectedTools = config.tools.map((key) => TOOLS[key]);
175
+ const s = p.spinner();
176
+ const safeCopy = (src, tgt) =>
177
+ copyWithConfirm(src, tgt, { spinner: s, projectRoot: targetPath });
178
+ const linkOrCopy = useSymlinks
179
+ ? async (src, tgt) => createSymlink(src, tgt)
180
+ : safeCopy;
181
+
182
+ s.start(useSymlinks ? "Creating symlinks" : "Copying files");
183
+
184
+ const mode = useSymlinks ? "linked" : "copied";
185
+ const summaryLines = [];
186
+ const processedIntermediateFiles = new Set();
187
+
188
+ for (const tool of selectedTools) {
189
+ const stats = { rules: 0, skills: 0, agents: 0 };
190
+ const { paths } = tool;
191
+
192
+ for (const dir of Object.values(paths)) {
193
+ fs.mkdirSync(path.join(targetPath, dir), { recursive: true });
194
+ }
195
+
196
+ if (paths.rules) {
197
+ stats.rules = await linkMatchingItems(
198
+ path.join(__dirname, "config", "rules"),
199
+ path.join(targetPath, paths.rules),
200
+ selectedTechs,
201
+ selectedArchs,
202
+ linkOrCopy,
203
+ {
204
+ filterExtension: ".md",
205
+ copiedItems: COPIED_RULES,
206
+ intermediateDir: path.join(targetPath, INTERMEDIATE_DIR, "rules"),
207
+ processedIntermediateFiles,
208
+ safeCopy,
209
+ },
210
+ );
211
+ }
212
+
213
+ if (paths.skills) {
214
+ stats.skills = await linkMatchingItems(
215
+ path.join(__dirname, "config", "skills"),
216
+ path.join(targetPath, paths.skills),
217
+ selectedTechs,
218
+ selectedArchs,
219
+ linkOrCopy,
220
+ {
221
+ directoriesOnly: true,
222
+ copiedItems: COPIED_SKILLS,
223
+ intermediateDir: path.join(targetPath, INTERMEDIATE_DIR, "skills"),
224
+ processedIntermediateFiles,
225
+ safeCopy,
226
+ },
227
+ );
228
+ }
229
+
230
+ if (paths.agents) {
231
+ stats.agents = await linkMatchingItems(
232
+ path.join(__dirname, "config", "agents"),
233
+ path.join(targetPath, paths.agents),
234
+ selectedTechs,
235
+ selectedArchs,
236
+ linkOrCopy,
237
+ {
238
+ directoriesOnly: true,
239
+ copiedItems: COPIED_AGENTS,
240
+ intermediateDir: path.join(targetPath, INTERMEDIATE_DIR, "agents"),
241
+ processedIntermediateFiles,
242
+ safeCopy,
243
+ },
244
+ );
245
+ }
246
+
247
+ for (const [src, dest] of Object.entries(tool.rootFiles)) {
248
+ const srcPath = path.join(__dirname, "config", src);
249
+ if (fs.existsSync(srcPath)) {
250
+ await linkOrCopy(srcPath, path.join(targetPath, dest));
251
+ }
252
+ }
253
+
254
+ for (const [src, dest] of Object.entries(tool.configFiles)) {
255
+ const srcPath = path.join(__dirname, "config", src);
256
+ if (fs.existsSync(srcPath)) {
257
+ const destPath = path.join(targetPath, dest);
258
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
259
+ await safeCopy(srcPath, destPath);
260
+ }
261
+ }
262
+
263
+ if (useSymlinks) {
264
+ updateGitignore(targetPath, tool);
265
+ addGitignoreEntry(targetPath, INTERMEDIATE_DIR, "mvagnon/agents");
266
+ }
267
+
268
+ const toolSummary = [
269
+ `Rules: ${stats.rules} ${mode}`,
270
+ paths.skills ? `Skills: ${stats.skills} ${mode}` : null,
271
+ paths.agents ? `Agents: ${stats.agents} ${mode}` : null,
272
+ ...Object.values(tool.rootFiles).map((f) => `${f}: ${mode}`),
273
+ ...Object.values(tool.configFiles).map((f) => `${f}: copied`),
274
+ useSymlinks ? `.gitignore: entries added` : `.gitignore: not modified`,
275
+ ].filter(Boolean);
276
+
277
+ summaryLines.push({ tool, lines: toolSummary });
278
+ }
279
+
280
+ s.stop("Setup complete");
281
+
282
+ for (const { tool, lines } of summaryLines) {
283
+ p.note(lines.join("\n"), `${tool.label} Setup`);
284
+ }
285
+
286
+ p.note(
287
+ [
288
+ "1. Modify `project.md` to add project-specific rules;",
289
+ "2. Add rules, skills, agents, MCPs or plugins based on your needs for each tool.",
290
+ ].join("\n"),
291
+ "Next Steps",
292
+ );
293
+
294
+ p.outro("Done");
295
+ }
296
+
297
+ function resolvePath(inputPath) {
298
+ if (inputPath.startsWith("~")) {
299
+ inputPath = inputPath.replace("~", process.env.HOME);
300
+ }
301
+ return path.resolve(inputPath);
302
+ }
303
+
304
+ function isTechOrArchSpecific(name) {
305
+ const allTechs = TECHNOLOGIES.map((t) => t.value);
306
+ const allArchs = ARCHITECTURES.filter((a) => a.value !== "none").map(
307
+ (a) => a.value,
308
+ );
309
+
310
+ return (
311
+ allTechs.some((tech) => name.startsWith(`${tech}-`)) ||
312
+ allArchs.some((arch) => name.includes(arch))
313
+ );
314
+ }
315
+
316
+ function shouldInclude(name, selectedTechs, selectedArchs) {
317
+ if (!isTechOrArchSpecific(name)) return true;
318
+
319
+ const matchesTech = selectedTechs.some((tech) => name.startsWith(`${tech}-`));
320
+ const matchesArch = selectedArchs
321
+ .filter((arch) => arch !== "none")
322
+ .some((arch) => name.includes(arch));
323
+
324
+ return matchesTech || matchesArch;
325
+ }
326
+
327
+ async function linkMatchingItems(
328
+ sourceDir,
329
+ targetDir,
330
+ selectedTechs,
331
+ selectedArchs,
332
+ linkOrCopy,
333
+ {
334
+ filterExtension,
335
+ directoriesOnly,
336
+ copiedItems = [],
337
+ intermediateDir,
338
+ safeCopy,
339
+ processedIntermediateFiles = new Set(),
340
+ } = {},
341
+ ) {
342
+ if (!fs.existsSync(sourceDir)) return 0;
343
+
344
+ let count = 0;
345
+
346
+ for (const entry of fs.readdirSync(sourceDir)) {
347
+ const fullPath = path.join(sourceDir, entry);
348
+ const isDir = fs.statSync(fullPath).isDirectory();
349
+
350
+ if (directoriesOnly && !isDir) continue;
351
+ if (filterExtension && !entry.endsWith(filterExtension)) continue;
352
+
353
+ const name = entry.replace(/\.md$/, "");
354
+
355
+ if (!shouldInclude(name, selectedTechs, selectedArchs)) continue;
356
+
357
+ const isCopied = copiedItems.includes(name) && intermediateDir;
358
+
359
+ if (isCopied) {
360
+ const intermediatePath = path.join(intermediateDir, entry);
361
+ fs.mkdirSync(intermediateDir, { recursive: true });
362
+
363
+ if (!processedIntermediateFiles.has(intermediatePath)) {
364
+ await safeCopy(fullPath, intermediatePath);
365
+ processedIntermediateFiles.add(intermediatePath);
366
+ }
367
+
368
+ await linkOrCopy(intermediatePath, path.join(targetDir, entry));
369
+ } else {
370
+ await linkOrCopy(fullPath, path.join(targetDir, entry));
371
+ }
372
+
373
+ count++;
374
+ }
375
+
376
+ return count;
377
+ }
378
+
379
+ async function copyWithConfirm(source, target, { spinner, projectRoot } = {}) {
380
+ if (fs.existsSync(target)) {
381
+ try {
382
+ if (!fs.lstatSync(target).isSymbolicLink()) {
383
+ const label = projectRoot
384
+ ? path.relative(projectRoot, target)
385
+ : path.basename(target);
386
+ if (spinner) spinner.stop("Existing file found");
387
+ const overwrite = await p.confirm({
388
+ message: `${label} already exists. Overwrite?`,
389
+ initialValue: false,
390
+ });
391
+ if (p.isCancel(overwrite)) {
392
+ p.cancel("Setup cancelled");
393
+ process.exit(0);
394
+ }
395
+ if (spinner) spinner.start("Continuing setup");
396
+ if (!overwrite) return false;
397
+ }
398
+ } catch {
399
+ // lstat failed, proceed with copy
400
+ }
401
+ }
402
+
403
+ copyPath(source, target);
404
+ return true;
405
+ }
406
+
407
+ function removePath(target) {
408
+ if (
409
+ fs.existsSync(target) ||
410
+ fs.lstatSync(target, { throwIfNoEntry: false })
411
+ ) {
412
+ fs.rmSync(target, { recursive: true, force: true });
413
+ }
414
+ }
415
+
416
+ function createSymlink(source, target) {
417
+ removePath(target);
418
+ fs.symlinkSync(source, target);
419
+ }
420
+
421
+ function copyPath(source, target) {
422
+ removePath(target);
423
+
424
+ if (fs.statSync(source).isDirectory()) {
425
+ fs.cpSync(source, target, { recursive: true });
426
+ } else {
427
+ fs.copyFileSync(source, target);
428
+ }
429
+ }
430
+
431
+ function updateGitignore(targetPath, tool) {
432
+ const gitignorePath = path.join(targetPath, ".gitignore");
433
+ const sectionHeader = `# ${tool.label} Configuration`;
434
+ let content = "";
435
+
436
+ if (fs.existsSync(gitignorePath)) {
437
+ content = fs.readFileSync(gitignorePath, "utf-8");
438
+
439
+ if (content.includes(sectionHeader)) return;
440
+
441
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
442
+ content += "\n";
443
+ }
444
+
445
+ content += sectionHeader + "\n";
446
+ content += tool.gitignoreEntries.join("\n") + "\n";
447
+
448
+ fs.writeFileSync(gitignorePath, content);
449
+ }
450
+
451
+ function addGitignoreEntry(targetPath, entry, sectionComment) {
452
+ const gitignorePath = path.join(targetPath, ".gitignore");
453
+ let content = "";
454
+
455
+ if (fs.existsSync(gitignorePath)) {
456
+ content = fs.readFileSync(gitignorePath, "utf-8");
457
+ if (content.split("\n").some((line) => line.trim() === entry)) return;
458
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
459
+ content += "\n";
460
+ }
461
+
462
+ if (sectionComment) {
463
+ content += `# ${sectionComment}\n`;
464
+ }
465
+ content += entry + "\n";
466
+ fs.writeFileSync(gitignorePath, content);
467
+ }
468
+
469
+ main().catch(console.error);
@@ -0,0 +1,77 @@
1
+ # Master Rules
2
+
3
+ ## Project-Specific Rules
4
+
5
+ Find the project-specific master rules here → `project.md`.
6
+
7
+ ## Before Writing Code
8
+
9
+ Before writing or modifying code that uses any external library:
10
+
11
+ 1. **First**, resolve the library with _Context7 MCP_ to get the up-to-date documentation;
12
+ 2. **Only** if Context7 returns no results or insufficient info, fall back to _Exa MCP_ as a secondary source.
13
+
14
+ > **Important:** ALWAYS prefer Context7 documentation over training data as the source of truth, especially for critical features (security, billing, authentication, payments, etc.).
15
+
16
+ ## While Writing Code
17
+
18
+ ### Code Conventions
19
+
20
+ - ALWAYS group all imports in a single block at the top of the file, with no blank lines between them;
21
+ - ALWAYS organize the code into blocs with each bloc representing a distinct logic. Each bloc is separated with a line jump (example below).
22
+
23
+ > **Example (React):**
24
+ >
25
+ > ```tsx
26
+ > export function UserProfile({ userId }: UserProfileProps) {
27
+ > const { data: user, isLoading } = useUser(userId);
28
+ > const { mutate: updateUser } = useUpdateUser();
29
+ >
30
+ > const displayName = user?.name ?? "Anonymous";
31
+ > const initials = displayName.slice(0, 2).toUpperCase();
32
+ >
33
+ > const handleSave = (values: UserFormValues) => {
34
+ > updateUser({ id: userId, ...values });
35
+ > };
36
+ >
37
+ > if (isLoading) return <Skeleton />;
38
+ >
39
+ > return (
40
+ > <Card>
41
+ > <Avatar initials={initials} />
42
+ > <UserForm defaultValues={user} onSubmit={handleSave} />
43
+ > </Card>
44
+ > );
45
+ > }
46
+ > ```
47
+ >
48
+ > **Example (FastAPI):**
49
+ >
50
+ > ```python
51
+ > @router.get("/users/{user_id}")
52
+ > async def get_user(user_id: UUID, db: Session = Depends(get_db)) -> UserResponse:
53
+ > """Retrieve a user by ID."""
54
+ > user = await db.get(User, user_id)
55
+ >
56
+ > if not user:
57
+ > raise HTTPException(status_code=404, detail="User not found")
58
+ >
59
+ > return UserResponse.model_validate(user)
60
+ > ```
61
+
62
+ ### Documentation & Logging
63
+
64
+ - Write TSDoc/docstrings ONLY for **exported** functions, classes, types, and interfaces;
65
+ - NEVER add comments on trivial logic (hook, constant declaration, etc.);
66
+ - NEVER add logs unless requested;
67
+ - ALWAYS comment complex logic (RegEx, etc.);
68
+ - Generic code (style variables, UI components, etc.) should ALWAYS be reusable.
69
+
70
+ ## After Writing Code
71
+
72
+ CRITICAL: ALWAYS check the following after modifying the codebase. Iterate until complete:
73
+
74
+ - [ ] KISS (Keep It Stupid Simple) principle is applied
75
+ - [ ] DRY (Don't Repeat Yourself) principle is applied
76
+ - [ ] No logs were added without prior consent
77
+ - [ ] Code is properly spaced into logical blocks
@@ -0,0 +1,15 @@
1
+ {
2
+ "mcpServers": {
3
+ "exa": {
4
+ "type": "http",
5
+ "url": "https://mcp.exa.ai/mcp"
6
+ },
7
+ "context7": {
8
+ "type": "http",
9
+ "url": "https://mcp.context7.com/mcp",
10
+ "headers": {
11
+ "CONTEXT7_API_KEY": "ctx7sk-09be8e62-174a-424a-81ef-166f2b2c94a2"
12
+ }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,10 @@
1
+ [mcp_servers.exa]
2
+ url = "https://mcp.exa.ai/mcp"
3
+ enabled = true
4
+
5
+ [mcp_servers.context7]
6
+ url = "https://mcp.context7.com/mcp"
7
+ enabled = true
8
+
9
+ [mcp_servers.context7.http_headers]
10
+ CONTEXT7_API_KEY = "ctx7sk-09be8e62-174a-424a-81ef-166f2b2c94a2"
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcpServers": {
3
+ "exa": {
4
+ "url": "https://mcp.exa.ai/mcp"
5
+ },
6
+ "context7": {
7
+ "url": "https://mcp.context7.com/mcp",
8
+ "headers": {
9
+ "CONTEXT7_API_KEY": "ctx7sk-09be8e62-174a-424a-81ef-166f2b2c94a2"
10
+ }
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "instructions": [".opencode/rules/*.md"],
4
+ "mcp": {
5
+ "exa": {
6
+ "enabled": true,
7
+ "type": "remote",
8
+ "url": "https://mcp.exa.ai/mcp",
9
+ "headers": {}
10
+ },
11
+ "context7": {
12
+ "enabled": true,
13
+ "type": "remote",
14
+ "url": "https://mcp.context7.com/mcp",
15
+ "headers": {
16
+ "CONTEXT7_API_KEY": "ctx7sk-09be8e62-174a-424a-81ef-166f2b2c94a2"
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ # Project-Specific Master Rules
2
+
3
+ - Write;
4
+ - Down;
5
+ - Project;
6
+ - Specific;
7
+ - Rules.
@@ -0,0 +1,50 @@
1
+ # React Hexagonal Architecture
2
+
3
+ ## Structure
4
+
5
+ ```
6
+ src/
7
+ ├── application/ # UI — Can freely use React
8
+ │ ├── components/ # React components
9
+ │ ├── hooks/ # Business hooks (consume domain)
10
+ │ ├── pages/ # Route components
11
+ │ └── providers/ # Context providers
12
+ ├── domain/ # Business — Pure TypeScript, ZERO React dependencies
13
+ │ ├── entities/ # Business models
14
+ │ ├── ports/ # Interfaces/contracts
15
+ │ └── lib/ # Pure functions
16
+ └── infrastructure/ # External — Implements ports
17
+ ├── api/ # HTTP/GraphQL clients
18
+ └── config/ # Configuration, feature flags
19
+ ```
20
+
21
+ ## Layer Rules
22
+
23
+ ### Domain (core)
24
+
25
+ - **No** React dependencies (no JSX, no hooks)
26
+ - Pure functions, testable in isolation
27
+ - Defines `ports` (interfaces) that infrastructure implements
28
+
29
+ ### Application (UI)
30
+
31
+ - Consumes domain via custom hooks
32
+ - Components split by responsibility
33
+ - State management: `useState` → `useReducer` → Context → Zustand (progressive escalation)
34
+
35
+ ### Infrastructure (adapters)
36
+
37
+ - Implements ports defined by domain
38
+ - Handles side effects (API, storage, analytics)
39
+ - Transforms external responses into domain entities
40
+
41
+ ## Dependency Principle
42
+
43
+ ```
44
+ Application → Domain ← Infrastructure
45
+
46
+ (ports/interfaces)
47
+ ```
48
+
49
+ - Application and Infrastructure depend on Domain
50
+ - Domain depends on nothing external
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: readme-writing
3
+ description: Write README.md for a project.
4
+ ---
5
+
6
+ # README Generator Skill
7
+
8
+ Generates or updates the `README.md` file at the project root.
9
+
10
+ ## Workflow
11
+
12
+ 1. **Analyze the project**
13
+ - Read `package.json` (or equivalent) for metadata;
14
+ - Read `.claude/CLAUDE.md` and `.claude/rules/` to understand context;
15
+ - Scan folder structure (`src/`, `lib/`, `app/`, etc.);
16
+ - Identify the tech stack in use.
17
+
18
+ 2. **Collect existing information**
19
+ - If `README.md` exists: read it to preserve custom content;
20
+ - Identify sections to keep vs regenerate.
21
+
22
+ 3. **Generate content**
23
+ - Follow the structure below;
24
+ - Use a professional and concise tone;
25
+ - No emojis unless explicitly requested.
26
+
27
+ ## README Structure
28
+
29
+ ```markdown
30
+ # Project Name
31
+
32
+ Short and impactful description (1-2 sentences).
33
+
34
+ ## Features
35
+
36
+ - Feature 1;
37
+ - Feature 2.
38
+
39
+ ## Tech Stack
40
+
41
+ | Category | Technologies |
42
+ | -------- | ------------ |
43
+ | Frontend | ... |
44
+ | Backend | ... |
45
+
46
+ ## Getting Started
47
+
48
+ ### Prerequisites
49
+
50
+ - Node.js >= X.X;
51
+ - pnpm/npm.
52
+
53
+ ### Environment Files
54
+
55
+ #### `.env`
56
+
57
+ \`\`\`text
58
+ ENV_VAR=
59
+ \`\`\`
60
+
61
+ #### `.env.local`
62
+
63
+ \`\`\`text
64
+ ENV_LOCAL_VAR=
65
+ \`\`\`
66
+
67
+ ### Installation
68
+
69
+ \`\`\`bash
70
+ pnpm install
71
+ \`\`\`
72
+
73
+ ### Development
74
+
75
+ \`\`\`bash
76
+ pnpm dev
77
+ \`\`\`
78
+
79
+ ## Project Structure
80
+
81
+ \`\`\`
82
+ src/
83
+ ├── ...
84
+ \`\`\`
85
+
86
+ ## Scripts
87
+
88
+ | Script | Description |
89
+ | ------ | ----------- |
90
+ | `dev` | ... |
91
+
92
+ ## Rules
93
+
94
+ - **Never invent** features not present in the code;
95
+ - **Preserve** existing custom sections (Contributing, Acknowledgments, etc.);
96
+ - **Adapt** the structure based on project type (lib, app, monorepo);
97
+ ```
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "mvagnon-agents",
3
+ "version": "1.0.0",
4
+ "description": "Shared configuration and conventions to bootstrap AI coding assistants.",
5
+ "type": "module",
6
+ "bin": {
7
+ "opencode-workflow": "./bootstrap.mjs"
8
+ },
9
+ "files": [
10
+ "bootstrap.mjs",
11
+ "config/"
12
+ ],
13
+ "scripts": {
14
+ "bootstrap": "node bootstrap.mjs"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "coding-assistant",
19
+ "claude-code",
20
+ "cursor",
21
+ "opencode",
22
+ "codex",
23
+ "bootstrap",
24
+ "typescript",
25
+ "react"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@clack/prompts": "^0.11.0"
30
+ }
31
+ }