@nerviq/cli 1.0.0 → 1.2.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 (48) hide show
  1. package/bin/cli.js +170 -73
  2. package/package.json +3 -5
  3. package/src/activity.js +20 -0
  4. package/src/aider/domain-packs.js +27 -2
  5. package/src/aider/mcp-packs.js +231 -0
  6. package/src/aider/techniques.js +3210 -1397
  7. package/src/audit.js +290 -9
  8. package/src/catalog.js +18 -2
  9. package/src/codex/domain-packs.js +23 -1
  10. package/src/codex/mcp-packs.js +254 -0
  11. package/src/codex/techniques.js +4738 -3257
  12. package/src/copilot/domain-packs.js +23 -1
  13. package/src/copilot/mcp-packs.js +254 -0
  14. package/src/copilot/techniques.js +3433 -1936
  15. package/src/cursor/domain-packs.js +23 -1
  16. package/src/cursor/mcp-packs.js +257 -0
  17. package/src/cursor/techniques.js +3697 -1869
  18. package/src/deprecation.js +98 -0
  19. package/src/domain-pack-expansion.js +571 -0
  20. package/src/domain-packs.js +25 -2
  21. package/src/formatters/otel.js +151 -0
  22. package/src/gemini/domain-packs.js +23 -1
  23. package/src/gemini/mcp-packs.js +257 -0
  24. package/src/gemini/techniques.js +3734 -2238
  25. package/src/integrations.js +194 -0
  26. package/src/mcp-packs.js +233 -0
  27. package/src/opencode/domain-packs.js +23 -1
  28. package/src/opencode/mcp-packs.js +231 -0
  29. package/src/opencode/techniques.js +3500 -1687
  30. package/src/org.js +68 -0
  31. package/src/source-urls.js +410 -260
  32. package/src/stack-checks.js +565 -0
  33. package/src/supplemental-checks.js +767 -0
  34. package/src/techniques.js +2929 -1449
  35. package/src/telemetry.js +160 -0
  36. package/src/windsurf/domain-packs.js +23 -1
  37. package/src/windsurf/mcp-packs.js +257 -0
  38. package/src/windsurf/techniques.js +3647 -1834
  39. package/src/workspace.js +233 -0
  40. package/CHANGELOG.md +0 -198
  41. package/content/case-study-template.md +0 -91
  42. package/content/claims-governance.md +0 -37
  43. package/content/claude-code/audit-repo/SKILL.md +0 -20
  44. package/content/claude-native-integration.md +0 -60
  45. package/content/devto-article.json +0 -9
  46. package/content/launch-posts.md +0 -226
  47. package/content/pilot-rollout-kit.md +0 -30
  48. package/content/release-checklist.md +0 -31
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Nerviq Integrations
3
+ *
4
+ * Webhook dispatch and message formatting for Slack, Discord,
5
+ * and generic HTTP endpoints.
6
+ *
7
+ * All functions are synchronous-friendly; sendWebhook is async
8
+ * (uses built-in https module, no external dependencies).
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const https = require('https');
14
+ const http = require('http');
15
+ const { URL } = require('url');
16
+
17
+ // ─── Webhook delivery ────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * POST JSON payload to a webhook URL.
21
+ * @param {string} url - Destination URL (http or https)
22
+ * @param {object} payload - JSON-serialisable object
23
+ * @param {object} [opts]
24
+ * @param {number} [opts.timeoutMs=10000]
25
+ * @param {object} [opts.headers]
26
+ * @returns {Promise<{ ok: boolean, status: number, body: string }>}
27
+ */
28
+ function sendWebhook(url, payload, opts = {}) {
29
+ return new Promise((resolve, reject) => {
30
+ let parsed;
31
+ try {
32
+ parsed = new URL(url);
33
+ } catch {
34
+ return reject(new Error(`Invalid webhook URL: ${url}`));
35
+ }
36
+
37
+ const body = JSON.stringify(payload);
38
+ const timeoutMs = opts.timeoutMs ?? 10_000;
39
+
40
+ const options = {
41
+ hostname: parsed.hostname,
42
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
43
+ path: parsed.pathname + (parsed.search || ''),
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ 'Content-Length': Buffer.byteLength(body),
48
+ 'User-Agent': `nerviq/${require('../package.json').version}`,
49
+ ...(opts.headers || {}),
50
+ },
51
+ };
52
+
53
+ const transport = parsed.protocol === 'https:' ? https : http;
54
+
55
+ const req = transport.request(options, (res) => {
56
+ const chunks = [];
57
+ res.on('data', (c) => chunks.push(c));
58
+ res.on('end', () => {
59
+ const respBody = Buffer.concat(chunks).toString('utf8');
60
+ resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: respBody });
61
+ });
62
+ });
63
+
64
+ req.setTimeout(timeoutMs, () => {
65
+ req.destroy(new Error(`Webhook request timed out after ${timeoutMs}ms`));
66
+ });
67
+
68
+ req.on('error', reject);
69
+ req.write(body);
70
+ req.end();
71
+ });
72
+ }
73
+
74
+ // ─── Slack formatting ─────────────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Format an audit result as a Slack Block Kit message payload.
78
+ * @param {object} auditResult - Result from audit()
79
+ * @returns {object} Slack-compatible message payload (blocks API)
80
+ */
81
+ function formatSlackMessage(auditResult) {
82
+ const score = auditResult.score ?? 0;
83
+ const platform = auditResult.platform ?? 'claude';
84
+ const emoji = score >= 70 ? ':white_check_mark:' : score >= 40 ? ':warning:' : ':x:';
85
+ const color = score >= 70 ? 'good' : score >= 40 ? 'warning' : 'danger';
86
+
87
+ const criticals = (auditResult.results || [])
88
+ .filter((r) => r.passed === false && r.impact === 'critical')
89
+ .slice(0, 5);
90
+
91
+ const sections = [
92
+ {
93
+ type: 'header',
94
+ text: { type: 'plain_text', text: `${emoji} Nerviq Audit — ${platform}`, emoji: true },
95
+ },
96
+ {
97
+ type: 'section',
98
+ fields: [
99
+ { type: 'mrkdwn', text: `*Score*\n${score}/100` },
100
+ { type: 'mrkdwn', text: `*Checks*\n${auditResult.passed ?? 0} pass / ${auditResult.failed ?? 0} fail` },
101
+ ],
102
+ },
103
+ ];
104
+
105
+ if (criticals.length > 0) {
106
+ sections.push({ type: 'divider' });
107
+ sections.push({
108
+ type: 'section',
109
+ text: {
110
+ type: 'mrkdwn',
111
+ text: `*Critical gaps:*\n${criticals.map((r) => `• ${r.name}`).join('\n')}`,
112
+ },
113
+ });
114
+ }
115
+
116
+ if (auditResult.suggestedNextCommand) {
117
+ sections.push({
118
+ type: 'section',
119
+ text: { type: 'mrkdwn', text: `*Next step:* \`${auditResult.suggestedNextCommand}\`` },
120
+ });
121
+ }
122
+
123
+ // Also include legacy attachment for clients that don't support blocks
124
+ return {
125
+ blocks: sections,
126
+ attachments: [
127
+ {
128
+ color,
129
+ fallback: `Nerviq audit (${platform}): ${score}/100 — ${auditResult.passed ?? 0} pass, ${auditResult.failed ?? 0} fail`,
130
+ },
131
+ ],
132
+ };
133
+ }
134
+
135
+ // ─── Discord formatting ───────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Format an audit result as a Discord webhook embed payload.
139
+ * @param {object} auditResult - Result from audit()
140
+ * @returns {object} Discord-compatible webhook payload (embeds)
141
+ */
142
+ function formatDiscordMessage(auditResult) {
143
+ const score = auditResult.score ?? 0;
144
+ const platform = auditResult.platform ?? 'claude';
145
+ const color = score >= 70 ? 0x2ecc71 : score >= 40 ? 0xf39c12 : 0xe74c3c; // green / yellow / red
146
+ const icon = score >= 70 ? '✅' : score >= 40 ? '⚠️' : '❌';
147
+
148
+ const criticals = (auditResult.results || [])
149
+ .filter((r) => r.passed === false && r.impact === 'critical')
150
+ .slice(0, 5);
151
+
152
+ const highs = (auditResult.results || [])
153
+ .filter((r) => r.passed === false && r.impact === 'high')
154
+ .slice(0, 3);
155
+
156
+ const fields = [
157
+ { name: 'Score', value: `**${score}/100**`, inline: true },
158
+ { name: 'Pass / Fail', value: `${auditResult.passed ?? 0} / ${auditResult.failed ?? 0}`, inline: true },
159
+ { name: 'Platform', value: platform, inline: true },
160
+ ];
161
+
162
+ if (criticals.length > 0) {
163
+ fields.push({
164
+ name: '🚨 Critical',
165
+ value: criticals.map((r) => `• ${r.name}`).join('\n'),
166
+ inline: false,
167
+ });
168
+ }
169
+
170
+ if (highs.length > 0) {
171
+ fields.push({
172
+ name: '⚠️ High',
173
+ value: highs.map((r) => `• ${r.name}`).join('\n'),
174
+ inline: false,
175
+ });
176
+ }
177
+
178
+ if (auditResult.suggestedNextCommand) {
179
+ fields.push({ name: '▶️ Next step', value: `\`${auditResult.suggestedNextCommand}\``, inline: false });
180
+ }
181
+
182
+ return {
183
+ embeds: [
184
+ {
185
+ title: `${icon} Nerviq Audit — ${platform}`,
186
+ color,
187
+ fields,
188
+ footer: { text: `nerviq v${require('../package.json').version} • ${new Date().toISOString()}` },
189
+ },
190
+ ],
191
+ };
192
+ }
193
+
194
+ module.exports = { sendWebhook, formatSlackMessage, formatDiscordMessage };
package/src/mcp-packs.js CHANGED
@@ -328,6 +328,168 @@ const MCP_PACKS = [
328
328
  },
329
329
  },
330
330
  },
331
+ // ── 24 new packs ─────────────────────────────────────────────────────────
332
+ {
333
+ key: 'supabase-mcp',
334
+ label: 'Supabase',
335
+ useWhen: 'Repos using Supabase for database, auth, or storage.',
336
+ adoption: 'Recommended for full-stack repos using Supabase. Requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.',
337
+ servers: { supabase: { command: 'npx', args: ['-y', '@supabase/mcp-server-supabase@latest'], env: { SUPABASE_URL: '${SUPABASE_URL}', SUPABASE_SERVICE_ROLE_KEY: '${SUPABASE_SERVICE_ROLE_KEY}' } } },
338
+ },
339
+ {
340
+ key: 'prisma-mcp',
341
+ label: 'Prisma ORM',
342
+ useWhen: 'Repos using Prisma for database schema management and migrations.',
343
+ adoption: 'Recommended for any repo with a Prisma schema. No auth required beyond DATABASE_URL.',
344
+ servers: { prisma: { command: 'npx', args: ['-y', 'prisma-mcp-server@latest'], env: { DATABASE_URL: '${DATABASE_URL}' } } },
345
+ },
346
+ {
347
+ key: 'vercel-mcp',
348
+ label: 'Vercel',
349
+ useWhen: 'Repos deployed on Vercel that benefit from deployment management and log access.',
350
+ adoption: 'Recommended for Next.js and other Vercel-hosted repos. Requires VERCEL_TOKEN.',
351
+ servers: { vercel: { command: 'npx', args: ['-y', '@vercel/mcp-server@latest'], env: { VERCEL_TOKEN: '${VERCEL_TOKEN}' } } },
352
+ },
353
+ {
354
+ key: 'cloudflare-mcp',
355
+ label: 'Cloudflare',
356
+ useWhen: 'Repos using Cloudflare Workers, KV, R2, or D1.',
357
+ adoption: 'Recommended for edge-compute repos. Requires CLOUDFLARE_API_TOKEN.',
358
+ servers: { cloudflare: { command: 'npx', args: ['-y', '@cloudflare/mcp-server-cloudflare@latest'], env: { CLOUDFLARE_API_TOKEN: '${CLOUDFLARE_API_TOKEN}' } } },
359
+ },
360
+ {
361
+ key: 'aws-mcp',
362
+ label: 'AWS (S3, Lambda, DynamoDB)',
363
+ useWhen: 'Repos using AWS services — S3, Lambda, DynamoDB, or CloudFormation.',
364
+ adoption: 'Recommended for cloud-infra repos. Requires AWS credentials.',
365
+ servers: { aws: { command: 'npx', args: ['-y', '@aws-samples/mcp-server-aws@latest'], env: { AWS_ACCESS_KEY_ID: '${AWS_ACCESS_KEY_ID}', AWS_SECRET_ACCESS_KEY: '${AWS_SECRET_ACCESS_KEY}', AWS_REGION: '${AWS_REGION}' } } },
366
+ },
367
+ {
368
+ key: 'redis-mcp',
369
+ label: 'Redis',
370
+ useWhen: 'Repos using Redis for caching, sessions, or pub/sub.',
371
+ adoption: 'Recommended for performance-critical repos with Redis. Requires REDIS_URL.',
372
+ servers: { redis: { command: 'npx', args: ['-y', 'redis-mcp-server@latest'], env: { REDIS_URL: '${REDIS_URL}' } } },
373
+ },
374
+ {
375
+ key: 'mongodb-mcp',
376
+ label: 'MongoDB',
377
+ useWhen: 'Repos using MongoDB as document database.',
378
+ adoption: 'Recommended for document-model repos. Requires MONGODB_URI.',
379
+ servers: { mongodb: { command: 'npx', args: ['-y', '@mongodb-js/mongodb-mcp-server@latest'], env: { MONGODB_URI: '${MONGODB_URI}' } } },
380
+ },
381
+ {
382
+ key: 'twilio-mcp',
383
+ label: 'Twilio',
384
+ useWhen: 'Repos integrating SMS, voice, or messaging via Twilio.',
385
+ adoption: 'Recommended for communication-feature repos. Requires TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.',
386
+ servers: { twilio: { command: 'npx', args: ['-y', 'twilio-mcp-server@latest'], env: { TWILIO_ACCOUNT_SID: '${TWILIO_ACCOUNT_SID}', TWILIO_AUTH_TOKEN: '${TWILIO_AUTH_TOKEN}' } } },
387
+ },
388
+ {
389
+ key: 'sendgrid-mcp',
390
+ label: 'SendGrid',
391
+ useWhen: 'Repos using SendGrid for transactional or marketing email.',
392
+ adoption: 'Recommended for repos with email delivery workflows. Requires SENDGRID_API_KEY.',
393
+ servers: { sendgrid: { command: 'npx', args: ['-y', 'sendgrid-mcp-server@latest'], env: { SENDGRID_API_KEY: '${SENDGRID_API_KEY}' } } },
394
+ },
395
+ {
396
+ key: 'algolia-mcp',
397
+ label: 'Algolia Search',
398
+ useWhen: 'Repos using Algolia for search indexing and discovery.',
399
+ adoption: 'Recommended for e-commerce and content-heavy repos. Requires ALGOLIA_APP_ID and ALGOLIA_API_KEY.',
400
+ servers: { algolia: { command: 'npx', args: ['-y', 'algolia-mcp-server@latest'], env: { ALGOLIA_APP_ID: '${ALGOLIA_APP_ID}', ALGOLIA_API_KEY: '${ALGOLIA_API_KEY}' } } },
401
+ },
402
+ {
403
+ key: 'planetscale-mcp',
404
+ label: 'PlanetScale',
405
+ useWhen: 'Repos using PlanetScale serverless MySQL database.',
406
+ adoption: 'Recommended for MySQL-based repos on PlanetScale. Requires PLANETSCALE_TOKEN.',
407
+ servers: { planetscale: { command: 'npx', args: ['-y', 'planetscale-mcp-server@latest'], env: { PLANETSCALE_TOKEN: '${PLANETSCALE_TOKEN}' } } },
408
+ },
409
+ {
410
+ key: 'neon-mcp',
411
+ label: 'Neon Serverless Postgres',
412
+ useWhen: 'Repos using Neon for serverless PostgreSQL.',
413
+ adoption: 'Recommended for serverless and edge Postgres repos. Requires NEON_API_KEY.',
414
+ servers: { neon: { command: 'npx', args: ['-y', '@neondatabase/mcp-server-neon@latest'], env: { NEON_API_KEY: '${NEON_API_KEY}' } } },
415
+ },
416
+ {
417
+ key: 'turso-mcp',
418
+ label: 'Turso Edge SQLite',
419
+ useWhen: 'Repos using Turso for distributed edge SQLite.',
420
+ adoption: 'Recommended for edge and multi-region apps. Requires TURSO_DATABASE_URL and TURSO_AUTH_TOKEN.',
421
+ servers: { turso: { command: 'npx', args: ['-y', 'turso-mcp-server@latest'], env: { TURSO_DATABASE_URL: '${TURSO_DATABASE_URL}', TURSO_AUTH_TOKEN: '${TURSO_AUTH_TOKEN}' } } },
422
+ },
423
+ {
424
+ key: 'upstash-mcp',
425
+ label: 'Upstash (Redis + Kafka)',
426
+ useWhen: 'Repos using Upstash for serverless Redis, Kafka, or QStash.',
427
+ adoption: 'Recommended for serverless caching and messaging. Requires UPSTASH_REDIS_REST_URL.',
428
+ servers: { upstash: { command: 'npx', args: ['-y', '@upstash/mcp-server@latest'], env: { UPSTASH_REDIS_REST_URL: '${UPSTASH_REDIS_REST_URL}', UPSTASH_REDIS_REST_TOKEN: '${UPSTASH_REDIS_REST_TOKEN}' } } },
429
+ },
430
+ {
431
+ key: 'convex-mcp',
432
+ label: 'Convex',
433
+ useWhen: 'Repos using Convex as reactive backend-as-a-service.',
434
+ adoption: 'Recommended for real-time full-stack repos on Convex. Requires CONVEX_DEPLOYMENT.',
435
+ servers: { convex: { command: 'npx', args: ['-y', '@convex-dev/mcp-server@latest'], env: { CONVEX_DEPLOYMENT: '${CONVEX_DEPLOYMENT}' } } },
436
+ },
437
+ {
438
+ key: 'clerk-mcp',
439
+ label: 'Clerk Authentication',
440
+ useWhen: 'Repos using Clerk for user authentication and session management.',
441
+ adoption: 'Recommended for SaaS repos with Clerk auth. Requires CLERK_SECRET_KEY.',
442
+ servers: { clerk: { command: 'npx', args: ['-y', '@clerk/mcp-server@latest'], env: { CLERK_SECRET_KEY: '${CLERK_SECRET_KEY}' } } },
443
+ },
444
+ {
445
+ key: 'resend-mcp',
446
+ label: 'Resend Email',
447
+ useWhen: 'Repos using Resend for developer-focused transactional email.',
448
+ adoption: 'Recommended for modern full-stack repos using Resend. Requires RESEND_API_KEY.',
449
+ servers: { resend: { command: 'npx', args: ['-y', 'resend-mcp-server@latest'], env: { RESEND_API_KEY: '${RESEND_API_KEY}' } } },
450
+ },
451
+ {
452
+ key: 'temporal-mcp',
453
+ label: 'Temporal Workflow',
454
+ useWhen: 'Repos using Temporal for durable workflow orchestration.',
455
+ adoption: 'Recommended for async-workflow and microservice repos. Requires TEMPORAL_ADDRESS.',
456
+ servers: { temporal: { command: 'npx', args: ['-y', 'temporal-mcp-server@latest'], env: { TEMPORAL_ADDRESS: '${TEMPORAL_ADDRESS}' } } },
457
+ },
458
+ {
459
+ key: 'launchdarkly-mcp',
460
+ label: 'LaunchDarkly Feature Flags',
461
+ useWhen: 'Repos using LaunchDarkly for feature flags and experimentation.',
462
+ adoption: 'Recommended for feature-flag-driven development. Requires LAUNCHDARKLY_ACCESS_TOKEN.',
463
+ servers: { launchdarkly: { command: 'npx', args: ['-y', 'launchdarkly-mcp-server@latest'], env: { LAUNCHDARKLY_ACCESS_TOKEN: '${LAUNCHDARKLY_ACCESS_TOKEN}' } } },
464
+ },
465
+ {
466
+ key: 'datadog-mcp',
467
+ label: 'Datadog',
468
+ useWhen: 'Repos using Datadog for monitoring, APM, and log management.',
469
+ adoption: 'Recommended for production repos with Datadog observability. Requires DATADOG_API_KEY.',
470
+ servers: { datadog: { command: 'npx', args: ['-y', '@datadog/mcp-server@latest'], env: { DATADOG_API_KEY: '${DATADOG_API_KEY}', DATADOG_APP_KEY: '${DATADOG_APP_KEY}' } } },
471
+ },
472
+ {
473
+ key: 'grafana-mcp',
474
+ label: 'Grafana',
475
+ useWhen: 'Repos using Grafana for dashboards and observability.',
476
+ adoption: 'Recommended for observability-focused repos. Requires GRAFANA_URL and GRAFANA_API_KEY.',
477
+ servers: { grafana: { command: 'npx', args: ['-y', 'grafana-mcp-server@latest'], env: { GRAFANA_URL: '${GRAFANA_URL}', GRAFANA_API_KEY: '${GRAFANA_API_KEY}' } } },
478
+ },
479
+ {
480
+ key: 'circleci-mcp',
481
+ label: 'CircleCI',
482
+ useWhen: 'Repos using CircleCI for CI/CD pipelines.',
483
+ adoption: 'Recommended for CircleCI-powered projects. Requires CIRCLECI_TOKEN.',
484
+ servers: { circleci: { command: 'npx', args: ['-y', 'circleci-mcp-server@latest'], env: { CIRCLECI_TOKEN: '${CIRCLECI_TOKEN}' } } },
485
+ },
486
+ {
487
+ key: 'anthropic-mcp',
488
+ label: 'Anthropic Claude API',
489
+ useWhen: 'Repos that build on or integrate the Anthropic Claude API.',
490
+ adoption: 'Recommended for AI-powered apps using Claude. Requires ANTHROPIC_API_KEY.',
491
+ servers: { anthropic: { command: 'npx', args: ['-y', '@anthropic-ai/mcp-server@latest'], env: { ANTHROPIC_API_KEY: '${ANTHROPIC_API_KEY}' } } },
492
+ },
331
493
  ];
332
494
 
333
495
  function clone(value) {
@@ -581,6 +743,77 @@ function recommendMcpPacks(stacks = [], domainPacks = [], options = {}) {
581
743
  recommended.add('infisical-secrets');
582
744
  }
583
745
 
746
+ // ── New 24 packs recommendation logic ──────────────────────────────────────
747
+ if (ctx && (hasDependency(deps, '@supabase/supabase-js') || hasDependency(deps, '@supabase/auth-helpers-nextjs') || hasFileContentMatch(ctx, '.env', /SUPABASE/i) || hasFileContentMatch(ctx, '.env.example', /SUPABASE/i))) {
748
+ recommended.add('supabase-mcp');
749
+ }
750
+ if (ctx && (hasFileContentMatch(ctx, 'schema.prisma', /\S/) || hasDependency(deps, '@prisma/client') || hasDependency(deps, 'prisma'))) {
751
+ recommended.add('prisma-mcp');
752
+ }
753
+ if (ctx && (ctx.files.includes('vercel.json') || ctx.files.includes('.vercel') || hasFileContentMatch(ctx, 'package.json', /"deploy":\s*"vercel/i) || hasFileContentMatch(ctx, '.env', /VERCEL_TOKEN/i))) {
754
+ recommended.add('vercel-mcp');
755
+ }
756
+ if (ctx && (hasFileContentMatch(ctx, 'wrangler.toml', /\S/) || hasFileContentMatch(ctx, 'wrangler.json', /\S/) || hasDependency(deps, 'wrangler') || hasFileContentMatch(ctx, '.env', /CLOUDFLARE/i))) {
757
+ recommended.add('cloudflare-mcp');
758
+ }
759
+ if (ctx && (hasFileContentMatch(ctx, '.env', /AWS_ACCESS_KEY/i) || hasFileContentMatch(ctx, '.env.example', /AWS_/i) || ctx.files.some(f => /serverless\.yml|template\.ya?ml|cdk\.json/.test(f)))) {
760
+ recommended.add('aws-mcp');
761
+ }
762
+ if (ctx && (hasDependency(deps, 'redis') || hasDependency(deps, 'ioredis') || hasDependency(deps, '@redis/client') || hasFileContentMatch(ctx, '.env', /REDIS_URL/i))) {
763
+ recommended.add('redis-mcp');
764
+ }
765
+ if (ctx && (hasDependency(deps, 'mongoose') || hasDependency(deps, 'mongodb') || hasFileContentMatch(ctx, '.env', /MONGODB_URI/i) || hasFileContentMatch(ctx, '.env.example', /MONGO/i))) {
766
+ recommended.add('mongodb-mcp');
767
+ }
768
+ if (ctx && (hasDependency(deps, 'twilio') || hasFileContentMatch(ctx, '.env', /TWILIO_/i) || hasFileContentMatch(ctx, '.env.example', /TWILIO_/i))) {
769
+ recommended.add('twilio-mcp');
770
+ }
771
+ if (ctx && (hasDependency(deps, '@sendgrid/mail') || hasDependency(deps, 'sendgrid') || hasFileContentMatch(ctx, '.env', /SENDGRID_API_KEY/i))) {
772
+ recommended.add('sendgrid-mcp');
773
+ }
774
+ if (ctx && (hasDependency(deps, 'algoliasearch') || hasDependency(deps, '@algolia/client-search') || hasFileContentMatch(ctx, '.env', /ALGOLIA_/i))) {
775
+ recommended.add('algolia-mcp');
776
+ }
777
+ if (ctx && (hasFileContentMatch(ctx, '.env', /PLANETSCALE_TOKEN/i) || hasFileContentMatch(ctx, '.env.example', /PLANETSCALE/i))) {
778
+ recommended.add('planetscale-mcp');
779
+ }
780
+ if (ctx && (hasDependency(deps, '@neondatabase/serverless') || hasFileContentMatch(ctx, '.env', /NEON_/i) || hasFileContentMatch(ctx, '.env.example', /NEON_/i))) {
781
+ recommended.add('neon-mcp');
782
+ }
783
+ if (ctx && (hasDependency(deps, '@libsql/client') || hasFileContentMatch(ctx, '.env', /TURSO_/i) || hasFileContentMatch(ctx, '.env.example', /TURSO_/i))) {
784
+ recommended.add('turso-mcp');
785
+ }
786
+ if (ctx && (hasDependency(deps, '@upstash/redis') || hasDependency(deps, '@upstash/kafka') || hasFileContentMatch(ctx, '.env', /UPSTASH_/i))) {
787
+ recommended.add('upstash-mcp');
788
+ }
789
+ if (ctx && (hasDependency(deps, 'convex') || hasDependency(deps, 'convex-dev') || hasFileContentMatch(ctx, 'convex.json', /\S/) || hasFileContentMatch(ctx, '.env', /CONVEX_/i))) {
790
+ recommended.add('convex-mcp');
791
+ }
792
+ if (ctx && (hasDependency(deps, '@clerk/nextjs') || hasDependency(deps, '@clerk/clerk-sdk-node') || hasDependency(deps, '@clerk/backend') || hasFileContentMatch(ctx, '.env', /CLERK_/i))) {
793
+ recommended.add('clerk-mcp');
794
+ }
795
+ if (ctx && (hasDependency(deps, 'resend') || hasFileContentMatch(ctx, '.env', /RESEND_API_KEY/i) || hasFileContentMatch(ctx, '.env.example', /RESEND_/i))) {
796
+ recommended.add('resend-mcp');
797
+ }
798
+ if (ctx && (hasDependency(deps, '@temporalio/client') || hasDependency(deps, '@temporalio/worker') || hasFileContentMatch(ctx, '.env', /TEMPORAL_/i))) {
799
+ recommended.add('temporal-mcp');
800
+ }
801
+ if (ctx && (hasDependency(deps, '@launchdarkly/node-server-sdk') || hasDependency(deps, 'launchdarkly-js-client-sdk') || hasFileContentMatch(ctx, '.env', /LAUNCHDARKLY_/i))) {
802
+ recommended.add('launchdarkly-mcp');
803
+ }
804
+ if (ctx && (hasDependency(deps, 'dd-trace') || hasDependency(deps, 'datadog-metrics') || hasFileContentMatch(ctx, '.env', /DATADOG_/i))) {
805
+ recommended.add('datadog-mcp');
806
+ }
807
+ if (ctx && (hasFileContentMatch(ctx, 'docker-compose.yml', /grafana/i) || hasFileContentMatch(ctx, '.env', /GRAFANA_/i) || ctx.files.some(f => /grafana/.test(f)))) {
808
+ recommended.add('grafana-mcp');
809
+ }
810
+ if (ctx && (ctx.files.some(f => /\.circleci\/config/.test(f)) || hasFileContentMatch(ctx, '.env', /CIRCLECI_/i))) {
811
+ recommended.add('circleci-mcp');
812
+ }
813
+ if (ctx && (hasDependency(deps, '@anthropic-ai/sdk') || hasDependency(deps, 'anthropic') || hasFileContentMatch(ctx, '.env', /ANTHROPIC_API_KEY/i) || hasFileContentMatch(ctx, '.env.example', /ANTHROPIC_/i))) {
814
+ recommended.add('anthropic-mcp');
815
+ }
816
+
584
817
  return MCP_PACKS
585
818
  .filter(pack => recommended.has(pack.key))
586
819
  .map(pack => clone(pack));
@@ -5,7 +5,9 @@
5
5
  * JSONC config, AGENTS.md, permission engine, and plugin system.
6
6
  */
7
7
 
8
- const OPENCODE_DOMAIN_PACKS = [
8
+ const { buildAdditionalDomainPacks, detectAdditionalDomainPacks } = require('../domain-pack-expansion');
9
+
10
+ const BASE_OPENCODE_DOMAIN_PACKS = [
9
11
  {
10
12
  key: 'baseline-general',
11
13
  label: 'Baseline General',
@@ -168,6 +170,13 @@ const OPENCODE_DOMAIN_PACKS = [
168
170
  },
169
171
  ];
170
172
 
173
+ const OPENCODE_DOMAIN_PACKS = [
174
+ ...BASE_OPENCODE_DOMAIN_PACKS,
175
+ ...buildAdditionalDomainPacks('opencode', {
176
+ existingKeys: new Set(BASE_OPENCODE_DOMAIN_PACKS.map((pack) => pack.key)),
177
+ }),
178
+ ];
179
+
171
180
  function uniqueByKey(items) {
172
181
  const seen = new Set();
173
182
  const result = [];
@@ -231,6 +240,19 @@ function detectOpenCodeDomainPacks(ctx, stacks = []) {
231
240
  addMatch('enterprise-governed', ['Detected CI workflows and policy files.']);
232
241
  }
233
242
 
243
+ detectAdditionalDomainPacks({
244
+ ctx,
245
+ pkg,
246
+ deps,
247
+ stackKeys,
248
+ addMatch,
249
+ hasBackend,
250
+ hasFrontend,
251
+ hasInfra,
252
+ hasCi,
253
+ isEnterpriseGoverned: hasCi && hasPolicyFiles,
254
+ });
255
+
234
256
  return uniqueByKey(matches);
235
257
  }
236
258