@chris1807/claude-kit 2.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +821 -0
  3. package/bin/cli.js +521 -0
  4. package/package.json +50 -0
  5. package/templates/agents/global/api-tester.md +75 -0
  6. package/templates/agents/global/azure-ops.md +59 -0
  7. package/templates/agents/global/backend.md +245 -0
  8. package/templates/agents/global/build-validator.md +50 -0
  9. package/templates/agents/global/frontend.md +254 -0
  10. package/templates/agents/global/legacy.md +218 -0
  11. package/templates/agents/global/lint-checker.md +86 -0
  12. package/templates/agents/global/manager.md +138 -0
  13. package/templates/agents/global/mockup.md +95 -0
  14. package/templates/agents/global/reviewer.md +149 -0
  15. package/templates/agents/global/security-auditor.md +74 -0
  16. package/templates/agents/global/test-runner.md +98 -0
  17. package/templates/agents/global/uat-generator.md +107 -0
  18. package/templates/agents/project/db-admin.md +106 -0
  19. package/templates/agents/project/deployer.md +113 -0
  20. package/templates/agents/project/devops-tracker.md +101 -0
  21. package/templates/commands/add-to-release.md +55 -0
  22. package/templates/commands/cherry-pick.md +96 -0
  23. package/templates/commands/cleanup-branches.md +73 -0
  24. package/templates/commands/create-release.md +65 -0
  25. package/templates/commands/deploy-release.md +147 -0
  26. package/templates/commands/deploy.md +65 -0
  27. package/templates/commands/explain.md +49 -0
  28. package/templates/commands/implement.md +170 -0
  29. package/templates/commands/promote.md +71 -0
  30. package/templates/commands/quote.md +39 -0
  31. package/templates/commands/review.md +32 -0
  32. package/templates/commands/rework.md +158 -0
  33. package/templates/commands/rollback.md +106 -0
  34. package/templates/commands/status.md +111 -0
  35. package/templates/hooks/auto-format.sh +46 -0
  36. package/templates/hooks/protected-files.sh +52 -0
  37. package/templates/hooks/secret-blocker.sh +68 -0
  38. package/templates/hooks/self-improve.sh +7 -0
  39. package/templates/hooks/sensitive-data-blocker.sh +43 -0
  40. package/templates/hooks/sensitive-data-mcp-blocker.sh +40 -0
  41. package/templates/hooks/sensitive-data-output-blocker.sh +63 -0
  42. package/templates/hooks/test-on-change.sh +46 -0
  43. package/templates/hooks/uat-reminder.sh +9 -0
  44. package/templates/infrastructure/CLAUDE-WORKFLOW.md +274 -0
  45. package/templates/infrastructure/azure-pipelines-template.yml +199 -0
  46. package/templates/infrastructure/mcp.json +35 -0
  47. package/templates/infrastructure/settings.json +94 -0
package/bin/cli.js ADDED
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join, basename, resolve } from 'path';
5
+ import fs from 'fs-extra';
6
+ import inquirer from 'inquirer';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
13
+ const GLOBAL_CLAUDE_DIR = join(process.env.HOME || process.env.USERPROFILE, '.claude');
14
+
15
+ // ============================================================================
16
+ // CLI Arguments
17
+ // ============================================================================
18
+ const args = process.argv.slice(2);
19
+ const showHelp = args.includes('--help') || args.includes('-h');
20
+ const globalOnly = args.includes('--global-only');
21
+ const installAll = args.includes('--all');
22
+ const dbFlag = args.find(a => a.startsWith('--db='))?.split('=')[1] || null;
23
+ const targetArg = args.find(a => !a.startsWith('--') && a !== 'init');
24
+
25
+ if (showHelp) {
26
+ console.log(`
27
+ ${chalk.blue('Claude Kit')}
28
+
29
+ Usage:
30
+ npx @chris1807/claude-kit init [target-dir] Interactive install
31
+ npx @chris1807/claude-kit init --all Install everything
32
+ npx @chris1807/claude-kit init --global-only Global agents only
33
+
34
+ Options:
35
+ --all Install all components (skips identical files, still asks DB type)
36
+ --all --db=mongo Install all with MongoDB (no prompts at all)
37
+ --all --db=mssql Install all with SQL Server
38
+ --all --db=azuresql Install all with Azure SQL
39
+ --all --db=postgres Install all with PostgreSQL
40
+ --global-only Only install global agents to ~/.claude/agents/
41
+ --help, -h Show this help
42
+ `);
43
+ process.exit(0);
44
+ }
45
+
46
+ // ============================================================================
47
+ // Banner
48
+ // ============================================================================
49
+ console.log('');
50
+ console.log(chalk.blue('╔══════════════════════════════════════════════════╗'));
51
+ console.log(chalk.blue('║') + chalk.bold.white(' Claude Kit Installer ') + chalk.blue('║'));
52
+ console.log(chalk.blue('╚══════════════════════════════════════════════════╝'));
53
+ console.log('');
54
+
55
+ // ============================================================================
56
+ // Helper: install file with overwrite check
57
+ // ============================================================================
58
+ async function installFile(src, dest, label) {
59
+ if (await fs.pathExists(dest)) {
60
+ const srcContent = await fs.readFile(src, 'utf8');
61
+ const destContent = await fs.readFile(dest, 'utf8');
62
+ if (srcContent === destContent) {
63
+ console.log(chalk.gray(` = ${label} (identical, skipped)`));
64
+ return false;
65
+ }
66
+ if (installAll) {
67
+ // --all mode: overwrite non-identical files silently
68
+ await fs.ensureDir(dirname(dest));
69
+ await fs.copy(src, dest);
70
+ console.log(chalk.green(` ✓ ${label} (updated)`));
71
+ return true;
72
+ }
73
+ const { action } = await inquirer.prompt([{
74
+ type: 'list',
75
+ name: 'action',
76
+ message: `${label} already exists:`,
77
+ choices: [
78
+ { name: 'Overwrite', value: 'overwrite' },
79
+ { name: 'Skip', value: 'skip' },
80
+ ],
81
+ }]);
82
+ if (action === 'skip') {
83
+ console.log(chalk.gray(` ⊘ ${label} (skipped)`));
84
+ return false;
85
+ }
86
+ }
87
+ await fs.ensureDir(dirname(dest));
88
+ await fs.copy(src, dest);
89
+ console.log(chalk.green(` ✓ ${label}`));
90
+ return true;
91
+ }
92
+
93
+ // ============================================================================
94
+ // Step 1: Global Agents (always)
95
+ // ============================================================================
96
+ async function installGlobalAgents() {
97
+ console.log(chalk.yellow.bold('\n📦 Global Agents → ~/.claude/agents/\n'));
98
+
99
+ const globalAgentsDir = join(GLOBAL_CLAUDE_DIR, 'agents');
100
+ await fs.ensureDir(globalAgentsDir);
101
+
102
+ const agents = await fs.readdir(join(TEMPLATES_DIR, 'agents', 'global'));
103
+ for (const file of agents) {
104
+ if (file.endsWith('.md')) {
105
+ await installFile(
106
+ join(TEMPLATES_DIR, 'agents', 'global', file),
107
+ join(globalAgentsDir, file),
108
+ file
109
+ );
110
+ }
111
+ }
112
+ }
113
+
114
+ // ============================================================================
115
+ // Main Interactive Installer
116
+ // ============================================================================
117
+ async function main() {
118
+ // Install global agents first
119
+ await installGlobalAgents();
120
+
121
+ if (globalOnly) {
122
+ console.log(chalk.green('\n✅ Global agents installed!\n'));
123
+ process.exit(0);
124
+ }
125
+
126
+ // ── Target Directory ──────────────────────────────────────────────────
127
+ let targetDir;
128
+ if (targetArg) {
129
+ targetDir = resolve(targetArg);
130
+ } else if (installAll) {
131
+ targetDir = process.cwd();
132
+ } else {
133
+ const { dir } = await inquirer.prompt([{
134
+ type: 'input',
135
+ name: 'dir',
136
+ message: 'Target project directory:',
137
+ default: process.cwd(),
138
+ }]);
139
+ targetDir = resolve(dir);
140
+ }
141
+
142
+ if (!await fs.pathExists(targetDir)) {
143
+ console.log(chalk.red(`\n ✗ Directory does not exist: ${targetDir}\n`));
144
+ process.exit(1);
145
+ }
146
+
147
+ console.log(chalk.gray(`\n Target: ${targetDir}\n`));
148
+
149
+ // ── Component Selection ───────────────────────────────────────────────
150
+ let components;
151
+ if (installAll) {
152
+ components = ['agents', 'hooks', 'commands', 'mcp', 'settings', 'workflow', 'gitignore'];
153
+ } else {
154
+ const { selected } = await inquirer.prompt([{
155
+ type: 'checkbox',
156
+ name: 'selected',
157
+ message: 'Select components to install:',
158
+ choices: [
159
+ { name: 'Project Agents (deployer, db-admin, devops-tracker)', value: 'agents', checked: true },
160
+ { name: 'Hooks (secret blocker, auto-format, test suggestions)', value: 'hooks', checked: true },
161
+ { name: 'Slash Commands (11 commands — implement, review, deploy, releases, cherry-pick, promote, rollback, status, cleanup)', value: 'commands', checked: true },
162
+ { name: 'MCP Servers (Playwright, DB, Teams, Stripe, Azure)', value: 'mcp', checked: true },
163
+ { name: 'Settings (hook registration)', value: 'settings', checked: true },
164
+ { name: 'CLAUDE.md Workflow Section', value: 'workflow', checked: true },
165
+ { name: '.gitignore Updates', value: 'gitignore', checked: true },
166
+ ],
167
+ }]);
168
+ components = selected;
169
+ }
170
+
171
+ // ── Project Agents ────────────────────────────────────────────────────
172
+ if (components.includes('agents')) {
173
+ console.log(chalk.yellow.bold('\n📦 Project Agents → .claude/agents/\n'));
174
+ const agentsDir = join(targetDir, '.claude', 'agents');
175
+ await fs.ensureDir(agentsDir);
176
+ const agents = await fs.readdir(join(TEMPLATES_DIR, 'agents', 'project'));
177
+ for (const file of agents) {
178
+ if (file.endsWith('.md')) {
179
+ await installFile(
180
+ join(TEMPLATES_DIR, 'agents', 'project', file),
181
+ join(agentsDir, file),
182
+ file
183
+ );
184
+ }
185
+ }
186
+ }
187
+
188
+ // ── Hooks ─────────────────────────────────────────────────────────────
189
+ if (components.includes('hooks')) {
190
+ console.log(chalk.yellow.bold('\n🪝 Hooks → .claude/hooks/\n'));
191
+ const hooksDir = join(targetDir, '.claude', 'hooks');
192
+ await fs.ensureDir(hooksDir);
193
+ const hooks = await fs.readdir(join(TEMPLATES_DIR, 'hooks'));
194
+ for (const file of hooks) {
195
+ if (file.endsWith('.sh')) {
196
+ await installFile(
197
+ join(TEMPLATES_DIR, 'hooks', file),
198
+ join(hooksDir, file),
199
+ file
200
+ );
201
+ await fs.chmod(join(hooksDir, file), 0o755);
202
+ }
203
+ }
204
+ }
205
+
206
+ // ── Commands ──────────────────────────────────────────────────────────
207
+ if (components.includes('commands')) {
208
+ console.log(chalk.yellow.bold('\n⚡ Slash Commands → .claude/commands/\n'));
209
+ const cmdsDir = join(targetDir, '.claude', 'commands');
210
+ await fs.ensureDir(cmdsDir);
211
+ const cmds = await fs.readdir(join(TEMPLATES_DIR, 'commands'));
212
+ for (const file of cmds) {
213
+ if (file.endsWith('.md')) {
214
+ await installFile(
215
+ join(TEMPLATES_DIR, 'commands', file),
216
+ join(cmdsDir, file),
217
+ file
218
+ );
219
+ }
220
+ }
221
+ }
222
+
223
+ // ── MCP Servers (interactive selection) ───────────────────────────────
224
+ let dbType = 'none';
225
+ if (components.includes('mcp')) {
226
+ console.log(chalk.yellow.bold('\n🔌 MCP Servers → .mcp.json\n'));
227
+
228
+ let mcpChoices;
229
+ if (installAll) {
230
+ // --all mode: use --db flag or prompt just for database type
231
+ let db = dbFlag;
232
+ if (!db) {
233
+ const { dbAnswer } = await inquirer.prompt([{
234
+ type: 'list',
235
+ name: 'dbAnswer',
236
+ message: 'What database does this project use?',
237
+ choices: [
238
+ { name: 'MongoDB', value: 'mongo' },
239
+ { name: 'SQL Server (local/VM)', value: 'mssql' },
240
+ { name: 'Azure SQL', value: 'azuresql' },
241
+ { name: 'PostgreSQL', value: 'postgres' },
242
+ { name: 'None / Skip', value: 'none' },
243
+ ],
244
+ }]);
245
+ db = dbAnswer;
246
+ }
247
+ mcpChoices = { servers: ['playwright', 'teams', 'azure'], db, stripe: false };
248
+ } else {
249
+ // Database selection
250
+ const { db } = await inquirer.prompt([{
251
+ type: 'list',
252
+ name: 'db',
253
+ message: 'What database does this project use?',
254
+ choices: [
255
+ { name: 'MongoDB', value: 'mongo' },
256
+ { name: 'SQL Server (local/VM)', value: 'mssql' },
257
+ { name: 'Azure SQL', value: 'azuresql' },
258
+ { name: 'PostgreSQL', value: 'postgres' },
259
+ { name: 'None / Skip', value: 'none' },
260
+ ],
261
+ }]);
262
+ dbType = db;
263
+
264
+ // Other servers
265
+ const { servers } = await inquirer.prompt([{
266
+ type: 'checkbox',
267
+ name: 'servers',
268
+ message: 'Select additional MCP servers:',
269
+ choices: [
270
+ { name: 'Playwright (browser testing)', value: 'playwright', checked: true },
271
+ { name: 'Microsoft Teams (notifications, messages)', value: 'teams', checked: true },
272
+ { name: 'Stripe (payment management)', value: 'stripe', checked: false },
273
+ { name: 'Azure CLI (App Service, Key Vault, DNS)', value: 'azure', checked: true },
274
+ ],
275
+ }]);
276
+
277
+ mcpChoices = { servers, db, stripe: servers.includes('stripe') };
278
+ }
279
+
280
+ // Build MCP config
281
+ const mcpConfig = { mcpServers: {} };
282
+
283
+ if (mcpChoices.servers.includes('playwright')) {
284
+ mcpConfig.mcpServers.playwright = {
285
+ command: 'npx',
286
+ args: ['@playwright/mcp'],
287
+ };
288
+ }
289
+
290
+ if (mcpChoices.db === 'mongo') {
291
+ mcpConfig.mcpServers.mongodb = {
292
+ command: 'npx',
293
+ args: ['-y', 'mongodb-mcp-server'],
294
+ env: { MDB_MCP_CONNECTION_STRING: '${MONGODB_CONNECTION_STRING}' },
295
+ };
296
+ } else if (mcpChoices.db === 'mssql' || mcpChoices.db === 'azuresql') {
297
+ mcpConfig.mcpServers.mssql = {
298
+ command: 'npx',
299
+ args: ['-y', '@anthropic/mcp-mssql-server'],
300
+ env: { MSSQL_CONNECTION_STRING: '${MSSQL_CONNECTION_STRING}' },
301
+ };
302
+ } else if (mcpChoices.db === 'postgres') {
303
+ mcpConfig.mcpServers.postgres = {
304
+ command: 'npx',
305
+ args: ['-y', '@modelcontextprotocol/server-postgres'],
306
+ env: { POSTGRES_CONNECTION_STRING: '${POSTGRES_CONNECTION_STRING}' },
307
+ };
308
+ }
309
+
310
+ if (mcpChoices.servers.includes('teams')) {
311
+ mcpConfig.mcpServers.teams = {
312
+ command: 'npx',
313
+ args: ['-y', '@anthropic/mcp-teams-server'],
314
+ env: {
315
+ TEAMS_TENANT_ID: '${TEAMS_TENANT_ID}',
316
+ TEAMS_CLIENT_ID: '${TEAMS_CLIENT_ID}',
317
+ TEAMS_CLIENT_SECRET: '${TEAMS_CLIENT_SECRET}',
318
+ },
319
+ };
320
+ }
321
+
322
+ if (mcpChoices.servers.includes('stripe') || mcpChoices.stripe) {
323
+ mcpConfig.mcpServers.stripe = {
324
+ command: 'npx',
325
+ args: ['-y', '@stripe/mcp'],
326
+ env: { STRIPE_SECRET_KEY: '${STRIPE_SECRET_KEY}' },
327
+ };
328
+ }
329
+
330
+ if (mcpChoices.servers.includes('azure')) {
331
+ mcpConfig.mcpServers.azure = {
332
+ command: 'npx',
333
+ args: ['-y', '@azure/mcp@latest', 'server', 'start'],
334
+ };
335
+ }
336
+
337
+ dbType = mcpChoices.db;
338
+
339
+ const mcpPath = join(targetDir, '.mcp.json');
340
+ if (await fs.pathExists(mcpPath)) {
341
+ // In --all mode, check if content is identical first
342
+ const newContent = JSON.stringify(mcpConfig, null, 2);
343
+ const existingContent = await fs.readFile(mcpPath, 'utf8');
344
+ if (newContent.trim() === existingContent.trim()) {
345
+ console.log(chalk.gray(' = .mcp.json (identical, skipped)'));
346
+ } else {
347
+ const { action } = installAll
348
+ ? { action: 'overwrite' }
349
+ : await inquirer.prompt([{
350
+ type: 'list',
351
+ name: 'action',
352
+ message: '.mcp.json already exists:',
353
+ choices: [
354
+ { name: 'Overwrite with new config', value: 'overwrite' },
355
+ { name: 'Merge (add missing servers)', value: 'merge' },
356
+ { name: 'Skip', value: 'skip' },
357
+ ],
358
+ }]);
359
+
360
+ if (action === 'merge') {
361
+ const existing = await fs.readJson(mcpPath);
362
+ existing.mcpServers = { ...existing.mcpServers, ...mcpConfig.mcpServers };
363
+ await fs.writeJson(mcpPath, existing, { spaces: 2 });
364
+ console.log(chalk.green(' ✓ .mcp.json (merged)'));
365
+ } else if (action === 'overwrite') {
366
+ await fs.writeJson(mcpPath, mcpConfig, { spaces: 2 });
367
+ console.log(chalk.green(' ✓ .mcp.json (overwritten)'));
368
+ } else {
369
+ console.log(chalk.gray(' ⊘ .mcp.json (skipped)'));
370
+ }
371
+ } // close identical check else
372
+ } else {
373
+ await fs.writeJson(mcpPath, mcpConfig, { spaces: 2 });
374
+ console.log(chalk.green(' ✓ .mcp.json'));
375
+ }
376
+ }
377
+
378
+ // ── Settings ──────────────────────────────────────────────────────────
379
+ if (components.includes('settings')) {
380
+ console.log(chalk.yellow.bold('\n⚙️ Settings → .claude/settings.json\n'));
381
+ await installFile(
382
+ join(TEMPLATES_DIR, 'infrastructure', 'settings.json'),
383
+ join(targetDir, '.claude', 'settings.json'),
384
+ 'settings.json'
385
+ );
386
+ }
387
+
388
+ // ── CLAUDE.md Workflow ────────────────────────────────────────────────
389
+ if (components.includes('workflow')) {
390
+ console.log(chalk.yellow.bold('\n📄 CLAUDE.md Workflow\n'));
391
+ const claudeMdPath = join(targetDir, 'CLAUDE.md');
392
+ const workflowContent = await fs.readFile(
393
+ join(TEMPLATES_DIR, 'infrastructure', 'CLAUDE-WORKFLOW.md'),
394
+ 'utf8'
395
+ );
396
+
397
+ const sensitiveDataPolicy = `## SENSITIVE DATA — MANDATORY RULE
398
+
399
+ **NEVER query, display, read, grep, or expose sensitive PII fields from the database or codebase — even if the values are encrypted.** Blocked fields: TIN, SSN, EIN, TaxId, BankAccountNumber, RoutingNumber, and any \`Encrypted*\` variants. Always use explicit inclusion projections listing only non-sensitive fields. Direct users to the application UI for sensitive data access.
400
+ `;
401
+
402
+ if (!await fs.pathExists(claudeMdPath)) {
403
+ await fs.writeFile(claudeMdPath, `# ${basename(targetDir)}\n\n${sensitiveDataPolicy}\n${workflowContent}`);
404
+ console.log(chalk.green(' ✓ Created CLAUDE.md with sensitive data policy and workflow'));
405
+ } else {
406
+ let existing = await fs.readFile(claudeMdPath, 'utf8');
407
+
408
+ // Inject sensitive data policy if not present
409
+ if (!existing.includes('SENSITIVE DATA — MANDATORY RULE')) {
410
+ // Insert after the first heading line, or at the top
411
+ const firstHeadingEnd = existing.indexOf('\n');
412
+ if (firstHeadingEnd !== -1 && existing.startsWith('#')) {
413
+ existing = existing.slice(0, firstHeadingEnd + 1) + '\n' + sensitiveDataPolicy + existing.slice(firstHeadingEnd + 1);
414
+ } else {
415
+ existing = sensitiveDataPolicy + '\n' + existing;
416
+ }
417
+ await fs.writeFile(claudeMdPath, existing);
418
+ console.log(chalk.green(' ✓ Injected sensitive data policy into CLAUDE.md'));
419
+ } else {
420
+ console.log(chalk.gray(' = Sensitive data policy already exists'));
421
+ }
422
+
423
+ // Re-read in case we just modified it
424
+ existing = await fs.readFile(claudeMdPath, 'utf8');
425
+
426
+ if (existing.includes('Claude Kit Workflow') || existing.includes('Care Solutions AI Workflow')) {
427
+ console.log(chalk.gray(' = Workflow section already exists'));
428
+ if (!installAll) {
429
+ const { replace } = await inquirer.prompt([{
430
+ type: 'confirm',
431
+ name: 'replace',
432
+ message: 'Replace existing workflow section?',
433
+ default: false,
434
+ }]);
435
+ if (replace) {
436
+ const cleaned = existing.replace(/\n## (?:Claude Kit Workflow|Care Solutions AI Workflow)[\s\S]*$/, '').trimEnd();
437
+ await fs.writeFile(claudeMdPath, `${cleaned}\n\n${workflowContent}`);
438
+ console.log(chalk.green(' ✓ Workflow section replaced'));
439
+ }
440
+ }
441
+ } else {
442
+ await fs.appendFile(claudeMdPath, `\n\n${workflowContent}`);
443
+ console.log(chalk.green(' ✓ Workflow appended to CLAUDE.md'));
444
+ }
445
+ }
446
+ }
447
+
448
+ // ── .gitignore ────────────────────────────────────────────────────────
449
+ if (components.includes('gitignore')) {
450
+ console.log(chalk.yellow.bold('\n🙈 .gitignore\n'));
451
+ const gitignorePath = join(targetDir, '.gitignore');
452
+ const claudeBlock = `
453
+ # =========================
454
+ # Claude Code
455
+ # =========================
456
+ .claude/*
457
+ !.claude/agents/
458
+ !.claude/hooks/
459
+ !.claude/commands/
460
+ !.claude/settings.json
461
+ .claude/settings.local.json`;
462
+
463
+ if (await fs.pathExists(gitignorePath)) {
464
+ const content = await fs.readFile(gitignorePath, 'utf8');
465
+ if (content.includes('.claude/*')) {
466
+ console.log(chalk.gray(' = Claude entries already exist'));
467
+ } else {
468
+ await fs.appendFile(gitignorePath, claudeBlock);
469
+ console.log(chalk.green(' ✓ Added Claude entries'));
470
+ }
471
+ } else {
472
+ await fs.writeFile(gitignorePath, claudeBlock.trim());
473
+ console.log(chalk.green(' ✓ Created .gitignore'));
474
+ }
475
+ }
476
+
477
+ // ── Summary ───────────────────────────────────────────────────────────
478
+ const agentCount = (await fs.pathExists(join(targetDir, '.claude', 'agents')))
479
+ ? (await fs.readdir(join(targetDir, '.claude', 'agents'))).filter(f => f.endsWith('.md')).length
480
+ : 0;
481
+ const hookCount = (await fs.pathExists(join(targetDir, '.claude', 'hooks')))
482
+ ? (await fs.readdir(join(targetDir, '.claude', 'hooks'))).filter(f => f.endsWith('.sh')).length
483
+ : 0;
484
+ const cmdCount = (await fs.pathExists(join(targetDir, '.claude', 'commands')))
485
+ ? (await fs.readdir(join(targetDir, '.claude', 'commands'))).filter(f => f.endsWith('.md')).length
486
+ : 0;
487
+ const globalCount = (await fs.pathExists(join(GLOBAL_CLAUDE_DIR, 'agents')))
488
+ ? (await fs.readdir(join(GLOBAL_CLAUDE_DIR, 'agents'))).filter(f => f.endsWith('.md')).length
489
+ : 0;
490
+
491
+ console.log('');
492
+ console.log(chalk.blue('╔══════════════════════════════════════════════════╗'));
493
+ console.log(chalk.blue('║') + chalk.green.bold(' ✅ Installation Complete! ') + chalk.blue('║'));
494
+ console.log(chalk.blue('╚══════════════════════════════════════════════════╝'));
495
+ console.log('');
496
+ console.log(` Target: ${chalk.bold(targetDir)}`);
497
+ console.log(` Project agents: ${chalk.bold(agentCount)}`);
498
+ console.log(` Hooks: ${chalk.bold(hookCount)}`);
499
+ console.log(` Commands: ${chalk.bold(cmdCount)}`);
500
+ console.log(` Global agents: ${chalk.bold(globalCount)}`);
501
+ console.log(` Database: ${chalk.bold(dbType)}`);
502
+ console.log('');
503
+
504
+ // Show required env vars
505
+ console.log(chalk.yellow(' Environment variables to set:'));
506
+ if (dbType === 'mongo') console.log(chalk.gray(' export MONGODB_CONNECTION_STRING="mongodb+srv://..."'));
507
+ if (dbType === 'mssql' || dbType === 'azuresql') console.log(chalk.gray(' export MSSQL_CONNECTION_STRING="Server=...;Database=..."'));
508
+ if (dbType === 'postgres') console.log(chalk.gray(' export POSTGRES_CONNECTION_STRING="postgresql://..."'));
509
+ console.log(chalk.gray(' export TEAMS_TENANT_ID="..." TEAMS_CLIENT_ID="..." TEAMS_CLIENT_SECRET="..."'));
510
+ console.log(chalk.gray(' az login'));
511
+ console.log('');
512
+ console.log(` Then: ${chalk.blue(`cd ${targetDir} && claude`)}`);
513
+ console.log(` Run: ${chalk.blue('/implement AB#1234')} to start working`);
514
+ console.log(` Run: ${chalk.blue('/review 142')} to review a PR`);
515
+ console.log('');
516
+ }
517
+
518
+ main().catch(err => {
519
+ console.error(chalk.red(`\n Error: ${err.message}\n`));
520
+ process.exit(1);
521
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@chris1807/claude-kit",
3
+ "version": "2.0.0",
4
+ "description": "Claude Code starter kit — agents, hooks, MCP servers, slash commands, and workflow automation",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-kit": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/cli.js --help",
11
+ "publish:public": "npm publish --access public"
12
+ },
13
+ "keywords": [
14
+ "claude-code",
15
+ "claude",
16
+ "ai-infrastructure",
17
+ "mcp",
18
+ "agents",
19
+ "hooks",
20
+ "starter-kit"
21
+ ],
22
+ "author": "Chris Waters <chriswaters@caresolutions.com>",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Christopher-Waters/claude-kit.git"
27
+ },
28
+ "homepage": "https://github.com/Christopher-Waters/claude-kit#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/Christopher-Waters/claude-kit/issues"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "files": [
39
+ "bin/",
40
+ "templates/",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "dependencies": {
45
+ "chalk": "^5.6.2",
46
+ "fs-extra": "^11.3.4",
47
+ "inquirer": "^13.3.2",
48
+ "ora": "^9.3.0"
49
+ }
50
+ }
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: api-tester
3
+ description: Tests API endpoints — auth flows, CRUD operations, error handling. Use to verify APIs work correctly after changes.
4
+ tools:
5
+ - Bash
6
+ - Read
7
+ - Grep
8
+ - Glob
9
+ model: sonnet
10
+ ---
11
+
12
+ # API Tester Agent
13
+
14
+ You test REST API endpoints using `curl`. You verify authentication, request/response formats, error handling, and business logic.
15
+
16
+ ## Testing Flow
17
+
18
+ ### 1. Authenticate First
19
+ ```bash
20
+ # Get a JWT token
21
+ TOKEN=$(curl -s -X POST "$API_URL/api/v1/auth/login" \
22
+ -H "Content-Type: application/json" \
23
+ -H "X-Organization-Subdomain: $SUBDOMAIN" \
24
+ -d '{"email":"EMAIL","password":"PASSWORD"}' \
25
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['accessToken'])")
26
+ ```
27
+
28
+ ### 2. Test Endpoint
29
+ ```bash
30
+ curl -s "$API_URL/api/v1/ENDPOINT" \
31
+ -H "Authorization: Bearer $TOKEN" \
32
+ -H "Content-Type: application/json" \
33
+ -H "X-Organization-Subdomain: $SUBDOMAIN"
34
+ ```
35
+
36
+ ### 3. Report Results
37
+
38
+ For each endpoint tested, report:
39
+ ```
40
+ ENDPOINT: METHOD /api/v1/path
41
+ STATUS: 200 | 400 | 401 | 403 | 404 | 500
42
+ RESPONSE: (summarized)
43
+ RESULT: PASS | FAIL
44
+ NOTES: any issues found
45
+ ```
46
+
47
+ ## Test Categories
48
+
49
+ ### Happy Path
50
+ - Valid request → expected response
51
+ - Correct status code
52
+ - Response body matches expected schema
53
+
54
+ ### Authentication
55
+ - No token → 401
56
+ - Invalid token → 401
57
+ - Wrong role → 403
58
+ - Expired token → 401
59
+
60
+ ### Validation
61
+ - Missing required fields → 400 with field errors
62
+ - Invalid data types → 400
63
+ - Boundary values (empty strings, very long strings, negative numbers)
64
+
65
+ ### Error Handling
66
+ - Non-existent resource → 404
67
+ - Duplicate creation → 400 or 409
68
+ - Server error → 500 (should not happen)
69
+
70
+ ## Rules
71
+ - Never use real SSNs, bank accounts, or PII in test data — use placeholders
72
+ - Always clean up test data you create (or note what was created)
73
+ - Test both the success AND failure paths
74
+ - Use `python3 -c "import sys,json; ..."` for JSON parsing (more reliable than jq)
75
+ - Report ALL failures, even intermittent ones