@nerviq/cli 1.0.1 → 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 (39) hide show
  1. package/bin/cli.js +170 -73
  2. package/package.json +1 -1
  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 +257 -2
  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
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Nerviq Opt-In Telemetry Foundation
3
+ *
4
+ * Collects anonymous usage events ONLY when NERVIQ_TELEMETRY=1 is set.
5
+ * No PII, no file contents, no absolute paths are ever stored.
6
+ * Events are stored locally in <projectDir>/.nerviq/telemetry.json.
7
+ *
8
+ * This module is the foundation layer — actual transmission to a dashboard
9
+ * is an explicit opt-in step configured separately.
10
+ *
11
+ * Privacy guarantees:
12
+ * - No usernames, emails, or identifiers
13
+ * - No file contents or code
14
+ * - No absolute paths (only hashed project fingerprint)
15
+ * - Stored only on local disk
16
+ * - Never sent anywhere without additional explicit configuration
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
24
+ const crypto = require('crypto');
25
+
26
+ const TELEMETRY_FILE = path.join(os.homedir(), '.nerviq', 'telemetry.json');
27
+ const MAX_EVENTS = 500; // cap file size at ~500 events
28
+
29
+ // ─── Opt-in check ─────────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Returns true only when the user has explicitly set NERVIQ_TELEMETRY=1.
33
+ * Telemetry is opt-IN, not opt-out.
34
+ * @returns {boolean}
35
+ */
36
+ function shouldCollectTelemetry() {
37
+ return process.env.NERVIQ_TELEMETRY === '1';
38
+ }
39
+
40
+ // ─── Anonymous fingerprinting ─────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Creates a one-way hash of the project directory.
44
+ * This allows grouping events by project without exposing the path.
45
+ * @param {string} dir
46
+ * @returns {string} 8-char hex fingerprint
47
+ */
48
+ function hashProject(dir) {
49
+ try {
50
+ return crypto.createHash('sha256').update(dir).digest('hex').slice(0, 8);
51
+ } catch {
52
+ return 'unknown';
53
+ }
54
+ }
55
+
56
+ // ─── Event collection ────────────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Collect an anonymous usage event and append it to the local telemetry file.
60
+ * Does nothing unless shouldCollectTelemetry() returns true.
61
+ *
62
+ * @param {string} event - Event name (e.g. 'audit', 'setup', 'convert')
63
+ * @param {object} [data] - Additional anonymous data
64
+ * @param {string} [data.platform] - Platform name (claude, codex, etc.)
65
+ * @param {number} [data.score] - Audit score
66
+ * @param {number} [data.checkCount] - Total checks evaluated
67
+ * @param {number} [data.durationMs] - Execution time in ms
68
+ * @param {string} [data.dir] - Project dir (hashed before storage)
69
+ * @returns {object|null} The recorded event object, or null if telemetry is off
70
+ */
71
+ function collectAnonymousEvent(event, data = {}) {
72
+ if (!shouldCollectTelemetry()) return null;
73
+
74
+ const record = {
75
+ event: String(event),
76
+ platform: data.platform || null,
77
+ score: typeof data.score === 'number' ? data.score : null,
78
+ checkCount: typeof data.checkCount === 'number' ? data.checkCount : null,
79
+ durationMs: typeof data.durationMs === 'number' ? Math.round(data.durationMs) : null,
80
+ timestamp: new Date().toISOString(),
81
+ nodeVersion: process.version,
82
+ os: `${os.platform()}-${os.arch()}`,
83
+ projectFingerprint: data.dir ? hashProject(data.dir) : null,
84
+ // Explicitly omit: paths, file contents, usernames, email, tokens
85
+ };
86
+
87
+ try {
88
+ const telemetryDir = path.dirname(TELEMETRY_FILE);
89
+ fs.mkdirSync(telemetryDir, { recursive: true });
90
+
91
+ let events = [];
92
+ if (fs.existsSync(TELEMETRY_FILE)) {
93
+ try {
94
+ const raw = fs.readFileSync(TELEMETRY_FILE, 'utf8');
95
+ const parsed = JSON.parse(raw);
96
+ events = Array.isArray(parsed.events) ? parsed.events : [];
97
+ } catch {
98
+ events = [];
99
+ }
100
+ }
101
+
102
+ events.push(record);
103
+
104
+ // Cap at MAX_EVENTS to prevent unbounded growth
105
+ if (events.length > MAX_EVENTS) {
106
+ events = events.slice(events.length - MAX_EVENTS);
107
+ }
108
+
109
+ const payload = {
110
+ version: 1,
111
+ telemetryOptIn: true,
112
+ note: 'Local telemetry only. Set NERVIQ_TELEMETRY=0 or unset to disable.',
113
+ events,
114
+ };
115
+
116
+ fs.writeFileSync(TELEMETRY_FILE, JSON.stringify(payload, null, 2), 'utf8');
117
+ } catch {
118
+ // Telemetry failures are always silent — never block main flow
119
+ }
120
+
121
+ return record;
122
+ }
123
+
124
+ // ─── Read local telemetry ─────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Read the local telemetry file.
128
+ * @returns {{ version: number, events: object[] } | null}
129
+ */
130
+ function readLocalTelemetry() {
131
+ try {
132
+ if (!fs.existsSync(TELEMETRY_FILE)) return null;
133
+ return JSON.parse(fs.readFileSync(TELEMETRY_FILE, 'utf8'));
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Clear all local telemetry events.
141
+ * @returns {boolean} true if cleared successfully
142
+ */
143
+ function clearLocalTelemetry() {
144
+ try {
145
+ if (fs.existsSync(TELEMETRY_FILE)) {
146
+ fs.writeFileSync(TELEMETRY_FILE, JSON.stringify({ version: 1, events: [] }, null, 2), 'utf8');
147
+ }
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ module.exports = {
155
+ shouldCollectTelemetry,
156
+ collectAnonymousEvent,
157
+ readLocalTelemetry,
158
+ clearLocalTelemetry,
159
+ TELEMETRY_FILE,
160
+ };
@@ -10,7 +10,9 @@
10
10
  * - No background agents (unlike Cursor)
11
11
  */
12
12
 
13
- const WINDSURF_DOMAIN_PACKS = [
13
+ const { buildAdditionalDomainPacks, detectAdditionalDomainPacks } = require('../domain-pack-expansion');
14
+
15
+ const BASE_WINDSURF_DOMAIN_PACKS = [
14
16
  {
15
17
  key: 'baseline-general',
16
18
  label: 'Baseline General',
@@ -173,6 +175,13 @@ const WINDSURF_DOMAIN_PACKS = [
173
175
  },
174
176
  ];
175
177
 
178
+ const WINDSURF_DOMAIN_PACKS = [
179
+ ...BASE_WINDSURF_DOMAIN_PACKS,
180
+ ...buildAdditionalDomainPacks('windsurf', {
181
+ existingKeys: new Set(BASE_WINDSURF_DOMAIN_PACKS.map((pack) => pack.key)),
182
+ }),
183
+ ];
184
+
176
185
  function uniqueByKey(items) {
177
186
  const seen = new Set();
178
187
  const result = [];
@@ -330,6 +339,19 @@ function detectWindsurfDomainPacks(ctx, stacks = [], assets = {}) {
330
339
  addMatch('security-focused', ['Detected security-focused repo signals.', ctx.fileContent('SECURITY.md') ? 'SECURITY.md present.' : null]);
331
340
  }
332
341
 
342
+ detectAdditionalDomainPacks({
343
+ ctx,
344
+ pkg,
345
+ deps,
346
+ stackKeys,
347
+ addMatch,
348
+ hasBackend,
349
+ hasFrontend,
350
+ hasInfra,
351
+ hasCi,
352
+ isEnterpriseGoverned,
353
+ });
354
+
333
355
  if (matches.length === 0) {
334
356
  addMatch('baseline-general', [
335
357
  'No stronger platform-specific domain dominated, so a safe general Windsurf baseline is the best starting point.',
@@ -359,6 +359,237 @@ const WINDSURF_MCP_PACKS = [
359
359
  jsonProjection: { command: 'npx', args: ['-y', 'huggingface-mcp-server'], env: { HF_TOKEN: '${env:HF_TOKEN}' } },
360
360
  excludeTools: [],
361
361
  },
362
+ // ── 23 new packs ─────────────────────────────────────────────────────────
363
+ {
364
+ key: 'supabase-mcp', label: 'Supabase',
365
+ description: 'Database, auth, and storage for Supabase.',
366
+ useWhen: 'Repos using Supabase.',
367
+ adoption: 'Requires: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY.',
368
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
369
+ serverName: 'supabase',
370
+ jsonProjection: { command: 'npx', args: ['-y', '@supabase/mcp-server-supabase@latest'], env: { SUPABASE_URL: '${env:SUPABASE_URL}', SUPABASE_SERVICE_ROLE_KEY: '${env:SUPABASE_SERVICE_ROLE_KEY}' } },
371
+ excludeTools: ['delete_project', 'drop_table'],
372
+ },
373
+ {
374
+ key: 'prisma-mcp', label: 'Prisma ORM',
375
+ description: 'Schema inspection and migrations via Prisma.',
376
+ useWhen: 'Repos with a Prisma schema.',
377
+ adoption: 'Requires: DATABASE_URL.',
378
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['DATABASE_URL'],
379
+ serverName: 'prisma',
380
+ jsonProjection: { command: 'npx', args: ['-y', 'prisma-mcp-server@latest'], env: { DATABASE_URL: '${env:DATABASE_URL}' } },
381
+ excludeTools: ['drop_database'],
382
+ },
383
+ {
384
+ key: 'vercel-mcp', label: 'Vercel',
385
+ description: 'Deployment management via Vercel.',
386
+ useWhen: 'Repos deployed on Vercel.',
387
+ adoption: 'Requires: VERCEL_TOKEN.',
388
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['VERCEL_TOKEN'],
389
+ serverName: 'vercel',
390
+ jsonProjection: { command: 'npx', args: ['-y', '@vercel/mcp-server@latest'], env: { VERCEL_TOKEN: '${env:VERCEL_TOKEN}' } },
391
+ excludeTools: ['delete_project', 'delete_deployment'],
392
+ },
393
+ {
394
+ key: 'cloudflare-mcp', label: 'Cloudflare',
395
+ description: 'Workers, KV, R2, and D1 management.',
396
+ useWhen: 'Repos using Cloudflare edge.',
397
+ adoption: 'Requires: CLOUDFLARE_API_TOKEN.',
398
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['CLOUDFLARE_API_TOKEN'],
399
+ serverName: 'cloudflare',
400
+ jsonProjection: { command: 'npx', args: ['-y', '@cloudflare/mcp-server-cloudflare@latest'], env: { CLOUDFLARE_API_TOKEN: '${env:CLOUDFLARE_API_TOKEN}' } },
401
+ excludeTools: ['delete_worker', 'purge_cache'],
402
+ },
403
+ {
404
+ key: 'aws-mcp', label: 'AWS',
405
+ description: 'S3, Lambda, DynamoDB access.',
406
+ useWhen: 'Repos using AWS.',
407
+ adoption: 'Requires: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION.',
408
+ trustLevel: 'low', transport: 'stdio', requiredAuth: ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'],
409
+ serverName: 'aws',
410
+ jsonProjection: { command: 'npx', args: ['-y', '@aws-samples/mcp-server-aws@latest'], env: { AWS_ACCESS_KEY_ID: '${env:AWS_ACCESS_KEY_ID}', AWS_SECRET_ACCESS_KEY: '${env:AWS_SECRET_ACCESS_KEY}', AWS_REGION: '${env:AWS_REGION}' } },
411
+ excludeTools: ['delete_stack', 'terminate_instances', 'delete_bucket'],
412
+ },
413
+ {
414
+ key: 'redis-mcp', label: 'Redis',
415
+ description: 'Cache and session management.',
416
+ useWhen: 'Repos using Redis.',
417
+ adoption: 'Requires: REDIS_URL.',
418
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['REDIS_URL'],
419
+ serverName: 'redis',
420
+ jsonProjection: { command: 'npx', args: ['-y', 'redis-mcp-server@latest'], env: { REDIS_URL: '${env:REDIS_URL}' } },
421
+ excludeTools: ['flushall', 'flushdb'],
422
+ },
423
+ {
424
+ key: 'mongodb-mcp', label: 'MongoDB',
425
+ description: 'Document database access.',
426
+ useWhen: 'Repos using MongoDB.',
427
+ adoption: 'Requires: MONGODB_URI.',
428
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['MONGODB_URI'],
429
+ serverName: 'mongodb',
430
+ jsonProjection: { command: 'npx', args: ['-y', '@mongodb-js/mongodb-mcp-server@latest'], env: { MONGODB_URI: '${env:MONGODB_URI}' } },
431
+ excludeTools: ['drop_collection', 'drop_database'],
432
+ },
433
+ {
434
+ key: 'twilio-mcp', label: 'Twilio',
435
+ description: 'SMS, voice, and messaging.',
436
+ useWhen: 'Repos using Twilio.',
437
+ adoption: 'Requires: TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN.',
438
+ trustLevel: 'low', transport: 'stdio', requiredAuth: ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'],
439
+ serverName: 'twilio',
440
+ jsonProjection: { command: 'npx', args: ['-y', 'twilio-mcp-server@latest'], env: { TWILIO_ACCOUNT_SID: '${env:TWILIO_ACCOUNT_SID}', TWILIO_AUTH_TOKEN: '${env:TWILIO_AUTH_TOKEN}' } },
441
+ excludeTools: ['delete_message'],
442
+ },
443
+ {
444
+ key: 'sendgrid-mcp', label: 'SendGrid',
445
+ description: 'Transactional email delivery.',
446
+ useWhen: 'Repos using SendGrid.',
447
+ adoption: 'Requires: SENDGRID_API_KEY.',
448
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['SENDGRID_API_KEY'],
449
+ serverName: 'sendgrid',
450
+ jsonProjection: { command: 'npx', args: ['-y', 'sendgrid-mcp-server@latest'], env: { SENDGRID_API_KEY: '${env:SENDGRID_API_KEY}' } },
451
+ excludeTools: [],
452
+ },
453
+ {
454
+ key: 'algolia-mcp', label: 'Algolia Search',
455
+ description: 'Search indexing via Algolia.',
456
+ useWhen: 'Repos using Algolia.',
457
+ adoption: 'Requires: ALGOLIA_APP_ID, ALGOLIA_API_KEY.',
458
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['ALGOLIA_APP_ID', 'ALGOLIA_API_KEY'],
459
+ serverName: 'algolia',
460
+ jsonProjection: { command: 'npx', args: ['-y', 'algolia-mcp-server@latest'], env: { ALGOLIA_APP_ID: '${env:ALGOLIA_APP_ID}', ALGOLIA_API_KEY: '${env:ALGOLIA_API_KEY}' } },
461
+ excludeTools: ['delete_index'],
462
+ },
463
+ {
464
+ key: 'planetscale-mcp', label: 'PlanetScale',
465
+ description: 'Serverless MySQL via PlanetScale.',
466
+ useWhen: 'Repos on PlanetScale.',
467
+ adoption: 'Requires: PLANETSCALE_TOKEN.',
468
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['PLANETSCALE_TOKEN'],
469
+ serverName: 'planetscale',
470
+ jsonProjection: { command: 'npx', args: ['-y', 'planetscale-mcp-server@latest'], env: { PLANETSCALE_TOKEN: '${env:PLANETSCALE_TOKEN}' } },
471
+ excludeTools: ['delete_database'],
472
+ },
473
+ {
474
+ key: 'neon-mcp', label: 'Neon Serverless Postgres',
475
+ description: 'Serverless Postgres via Neon.',
476
+ useWhen: 'Repos using Neon.',
477
+ adoption: 'Requires: NEON_API_KEY.',
478
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['NEON_API_KEY'],
479
+ serverName: 'neon',
480
+ jsonProjection: { command: 'npx', args: ['-y', '@neondatabase/mcp-server-neon@latest'], env: { NEON_API_KEY: '${env:NEON_API_KEY}' } },
481
+ excludeTools: ['delete_project'],
482
+ },
483
+ {
484
+ key: 'turso-mcp', label: 'Turso Edge SQLite',
485
+ description: 'Edge SQLite via Turso.',
486
+ useWhen: 'Repos using Turso.',
487
+ adoption: 'Requires: TURSO_DATABASE_URL, TURSO_AUTH_TOKEN.',
488
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['TURSO_DATABASE_URL', 'TURSO_AUTH_TOKEN'],
489
+ serverName: 'turso',
490
+ jsonProjection: { command: 'npx', args: ['-y', 'turso-mcp-server@latest'], env: { TURSO_DATABASE_URL: '${env:TURSO_DATABASE_URL}', TURSO_AUTH_TOKEN: '${env:TURSO_AUTH_TOKEN}' } },
491
+ excludeTools: ['destroy_database'],
492
+ },
493
+ {
494
+ key: 'upstash-mcp', label: 'Upstash Redis+Kafka',
495
+ description: 'Serverless Redis and Kafka.',
496
+ useWhen: 'Repos using Upstash.',
497
+ adoption: 'Requires: UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN.',
498
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['UPSTASH_REDIS_REST_URL', 'UPSTASH_REDIS_REST_TOKEN'],
499
+ serverName: 'upstash',
500
+ jsonProjection: { command: 'npx', args: ['-y', '@upstash/mcp-server@latest'], env: { UPSTASH_REDIS_REST_URL: '${env:UPSTASH_REDIS_REST_URL}', UPSTASH_REDIS_REST_TOKEN: '${env:UPSTASH_REDIS_REST_TOKEN}' } },
501
+ excludeTools: [],
502
+ },
503
+ {
504
+ key: 'convex-mcp', label: 'Convex',
505
+ description: 'Reactive backend via Convex.',
506
+ useWhen: 'Repos using Convex.',
507
+ adoption: 'Requires: CONVEX_DEPLOYMENT.',
508
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['CONVEX_DEPLOYMENT'],
509
+ serverName: 'convex',
510
+ jsonProjection: { command: 'npx', args: ['-y', '@convex-dev/mcp-server@latest'], env: { CONVEX_DEPLOYMENT: '${env:CONVEX_DEPLOYMENT}' } },
511
+ excludeTools: ['delete_deployment'],
512
+ },
513
+ {
514
+ key: 'clerk-mcp', label: 'Clerk Authentication',
515
+ description: 'User auth via Clerk.',
516
+ useWhen: 'Repos using Clerk.',
517
+ adoption: 'Requires: CLERK_SECRET_KEY.',
518
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['CLERK_SECRET_KEY'],
519
+ serverName: 'clerk',
520
+ jsonProjection: { command: 'npx', args: ['-y', '@clerk/mcp-server@latest'], env: { CLERK_SECRET_KEY: '${env:CLERK_SECRET_KEY}' } },
521
+ excludeTools: ['delete_user'],
522
+ },
523
+ {
524
+ key: 'resend-mcp', label: 'Resend Email',
525
+ description: 'Transactional email via Resend.',
526
+ useWhen: 'Repos using Resend.',
527
+ adoption: 'Requires: RESEND_API_KEY.',
528
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['RESEND_API_KEY'],
529
+ serverName: 'resend',
530
+ jsonProjection: { command: 'npx', args: ['-y', 'resend-mcp-server@latest'], env: { RESEND_API_KEY: '${env:RESEND_API_KEY}' } },
531
+ excludeTools: [],
532
+ },
533
+ {
534
+ key: 'temporal-mcp', label: 'Temporal Workflow',
535
+ description: 'Workflow orchestration via Temporal.',
536
+ useWhen: 'Repos using Temporal.',
537
+ adoption: 'Requires: TEMPORAL_ADDRESS.',
538
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['TEMPORAL_ADDRESS'],
539
+ serverName: 'temporal',
540
+ jsonProjection: { command: 'npx', args: ['-y', 'temporal-mcp-server@latest'], env: { TEMPORAL_ADDRESS: '${env:TEMPORAL_ADDRESS}' } },
541
+ excludeTools: ['terminate_workflow'],
542
+ },
543
+ {
544
+ key: 'launchdarkly-mcp', label: 'LaunchDarkly',
545
+ description: 'Feature flags via LaunchDarkly.',
546
+ useWhen: 'Repos using LaunchDarkly.',
547
+ adoption: 'Requires: LAUNCHDARKLY_ACCESS_TOKEN.',
548
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['LAUNCHDARKLY_ACCESS_TOKEN'],
549
+ serverName: 'launchdarkly',
550
+ jsonProjection: { command: 'npx', args: ['-y', 'launchdarkly-mcp-server@latest'], env: { LAUNCHDARKLY_ACCESS_TOKEN: '${env:LAUNCHDARKLY_ACCESS_TOKEN}' } },
551
+ excludeTools: ['delete_flag'],
552
+ },
553
+ {
554
+ key: 'datadog-mcp', label: 'Datadog',
555
+ description: 'Monitoring and APM via Datadog.',
556
+ useWhen: 'Repos using Datadog.',
557
+ adoption: 'Requires: DATADOG_API_KEY, DATADOG_APP_KEY.',
558
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['DATADOG_API_KEY', 'DATADOG_APP_KEY'],
559
+ serverName: 'datadog',
560
+ jsonProjection: { command: 'npx', args: ['-y', '@datadog/mcp-server@latest'], env: { DATADOG_API_KEY: '${env:DATADOG_API_KEY}', DATADOG_APP_KEY: '${env:DATADOG_APP_KEY}' } },
561
+ excludeTools: ['delete_monitor'],
562
+ },
563
+ {
564
+ key: 'grafana-mcp', label: 'Grafana',
565
+ description: 'Dashboards via Grafana.',
566
+ useWhen: 'Repos using Grafana.',
567
+ adoption: 'Requires: GRAFANA_URL, GRAFANA_API_KEY.',
568
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['GRAFANA_URL', 'GRAFANA_API_KEY'],
569
+ serverName: 'grafana',
570
+ jsonProjection: { command: 'npx', args: ['-y', 'grafana-mcp-server@latest'], env: { GRAFANA_URL: '${env:GRAFANA_URL}', GRAFANA_API_KEY: '${env:GRAFANA_API_KEY}' } },
571
+ excludeTools: ['delete_dashboard'],
572
+ },
573
+ {
574
+ key: 'circleci-mcp', label: 'CircleCI',
575
+ description: 'CI/CD via CircleCI.',
576
+ useWhen: 'Repos using CircleCI.',
577
+ adoption: 'Requires: CIRCLECI_TOKEN.',
578
+ trustLevel: 'medium', transport: 'stdio', requiredAuth: ['CIRCLECI_TOKEN'],
579
+ serverName: 'circleci',
580
+ jsonProjection: { command: 'npx', args: ['-y', 'circleci-mcp-server@latest'], env: { CIRCLECI_TOKEN: '${env:CIRCLECI_TOKEN}' } },
581
+ excludeTools: ['cancel_pipeline'],
582
+ },
583
+ {
584
+ key: 'anthropic-mcp', label: 'Anthropic Claude API',
585
+ description: 'Claude API for AI-powered apps.',
586
+ useWhen: 'Repos building on Claude API.',
587
+ adoption: 'Requires: ANTHROPIC_API_KEY.',
588
+ trustLevel: 'high', transport: 'stdio', requiredAuth: ['ANTHROPIC_API_KEY'],
589
+ serverName: 'anthropic',
590
+ jsonProjection: { command: 'npx', args: ['-y', '@anthropic-ai/mcp-server@latest'], env: { ANTHROPIC_API_KEY: '${env:ANTHROPIC_API_KEY}' } },
591
+ excludeTools: [],
592
+ },
362
593
  ];
363
594
 
364
595
  // --- Helpers ---
@@ -462,6 +693,32 @@ function recommendWindsurfMcpPacks(stacks = [], domainPacks = [], options = {})
462
693
  if (domainKeys.has('ecommerce') && (hasDependency(deps, 'shopify') || hasDependency(deps, '@shopify/shopify-api'))) recommended.add('shopify-mcp');
463
694
  if (domainKeys.has('ai-ml')) recommended.add('huggingface-mcp');
464
695
  if (domainKeys.has('design-system')) recommended.add('figma-mcp');
696
+ // ── 23 new packs recommendation logic ────────────────────────────────────
697
+ if (ctx) {
698
+ if (hasDependency(deps, '@supabase/supabase-js') || hasDependency(deps, '@supabase/auth-helpers-nextjs') || hasFileContentMatch(ctx, '.env', /SUPABASE/i) || hasFileContentMatch(ctx, '.env.example', /SUPABASE/i)) recommended.add('supabase-mcp');
699
+ if (hasFileContentMatch(ctx, 'schema.prisma', /\S/) || hasDependency(deps, '@prisma/client') || hasDependency(deps, 'prisma')) recommended.add('prisma-mcp');
700
+ if (ctx.files.includes('vercel.json') || hasFileContentMatch(ctx, 'package.json', /"deploy":\s*"vercel/i) || hasFileContentMatch(ctx, '.env', /VERCEL_TOKEN/i)) recommended.add('vercel-mcp');
701
+ if (hasFileContentMatch(ctx, 'wrangler.toml', /\S/) || hasDependency(deps, 'wrangler') || hasFileContentMatch(ctx, '.env', /CLOUDFLARE/i)) recommended.add('cloudflare-mcp');
702
+ if (hasFileContentMatch(ctx, '.env', /AWS_ACCESS_KEY/i) || ctx.files.some(f => /serverless\.yml|template\.ya?ml|cdk\.json/.test(f))) recommended.add('aws-mcp');
703
+ if (hasDependency(deps, 'redis') || hasDependency(deps, 'ioredis') || hasDependency(deps, '@redis/client') || hasFileContentMatch(ctx, '.env', /REDIS_URL/i)) recommended.add('redis-mcp');
704
+ if (hasDependency(deps, 'mongoose') || hasDependency(deps, 'mongodb') || hasFileContentMatch(ctx, '.env', /MONGODB_URI/i)) recommended.add('mongodb-mcp');
705
+ if (hasDependency(deps, 'twilio') || hasFileContentMatch(ctx, '.env', /TWILIO_/i)) recommended.add('twilio-mcp');
706
+ if (hasDependency(deps, '@sendgrid/mail') || hasFileContentMatch(ctx, '.env', /SENDGRID_API_KEY/i)) recommended.add('sendgrid-mcp');
707
+ if (hasDependency(deps, 'algoliasearch') || hasDependency(deps, '@algolia/client-search') || hasFileContentMatch(ctx, '.env', /ALGOLIA_/i)) recommended.add('algolia-mcp');
708
+ if (hasFileContentMatch(ctx, '.env', /PLANETSCALE_TOKEN/i)) recommended.add('planetscale-mcp');
709
+ if (hasDependency(deps, '@neondatabase/serverless') || hasFileContentMatch(ctx, '.env', /NEON_/i)) recommended.add('neon-mcp');
710
+ if (hasDependency(deps, '@libsql/client') || hasFileContentMatch(ctx, '.env', /TURSO_/i)) recommended.add('turso-mcp');
711
+ if (hasDependency(deps, '@upstash/redis') || hasDependency(deps, '@upstash/kafka') || hasFileContentMatch(ctx, '.env', /UPSTASH_/i)) recommended.add('upstash-mcp');
712
+ if (hasDependency(deps, 'convex') || hasFileContentMatch(ctx, 'convex.json', /\S/) || hasFileContentMatch(ctx, '.env', /CONVEX_/i)) recommended.add('convex-mcp');
713
+ if (hasDependency(deps, '@clerk/nextjs') || hasDependency(deps, '@clerk/backend') || hasFileContentMatch(ctx, '.env', /CLERK_/i)) recommended.add('clerk-mcp');
714
+ if (hasDependency(deps, 'resend') || hasFileContentMatch(ctx, '.env', /RESEND_API_KEY/i)) recommended.add('resend-mcp');
715
+ if (hasDependency(deps, '@temporalio/client') || hasFileContentMatch(ctx, '.env', /TEMPORAL_/i)) recommended.add('temporal-mcp');
716
+ if (hasDependency(deps, '@launchdarkly/node-server-sdk') || hasFileContentMatch(ctx, '.env', /LAUNCHDARKLY_/i)) recommended.add('launchdarkly-mcp');
717
+ if (hasDependency(deps, 'dd-trace') || hasFileContentMatch(ctx, '.env', /DATADOG_/i)) recommended.add('datadog-mcp');
718
+ if (hasFileContentMatch(ctx, 'docker-compose.yml', /grafana/i) || hasFileContentMatch(ctx, '.env', /GRAFANA_/i)) recommended.add('grafana-mcp');
719
+ if (ctx.files.some(f => /\.circleci\/config/.test(f)) || hasFileContentMatch(ctx, '.env', /CIRCLECI_/i)) recommended.add('circleci-mcp');
720
+ if (hasDependency(deps, '@anthropic-ai/sdk') || hasDependency(deps, 'anthropic') || hasFileContentMatch(ctx, '.env', /ANTHROPIC_API_KEY/i)) recommended.add('anthropic-mcp');
721
+ }
465
722
  if (recommended.size >= 2) recommended.add('mcp-security');
466
723
  if (recommended.size === 0) recommended.add('context7-docs');
467
724