wiggum-cli 0.3.2 → 0.4.1
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 +6 -4
- package/dist/ai/agents/codebase-analyst.d.ts +3 -0
- package/dist/ai/agents/codebase-analyst.d.ts.map +1 -1
- package/dist/ai/agents/codebase-analyst.js +3 -0
- package/dist/ai/agents/codebase-analyst.js.map +1 -1
- package/dist/ai/agents/context-enricher.d.ts +11 -0
- package/dist/ai/agents/context-enricher.d.ts.map +1 -0
- package/dist/ai/agents/context-enricher.js +163 -0
- package/dist/ai/agents/context-enricher.js.map +1 -0
- package/dist/ai/agents/evaluator-optimizer.d.ts +13 -0
- package/dist/ai/agents/evaluator-optimizer.d.ts.map +1 -0
- package/dist/ai/agents/evaluator-optimizer.js +231 -0
- package/dist/ai/agents/evaluator-optimizer.js.map +1 -0
- package/dist/ai/agents/index.d.ts +21 -3
- package/dist/ai/agents/index.d.ts.map +1 -1
- package/dist/ai/agents/index.js +148 -86
- package/dist/ai/agents/index.js.map +1 -1
- package/dist/ai/agents/mcp-detector.d.ts +26 -0
- package/dist/ai/agents/mcp-detector.d.ts.map +1 -0
- package/dist/ai/agents/mcp-detector.js +186 -0
- package/dist/ai/agents/mcp-detector.js.map +1 -0
- package/dist/ai/agents/orchestrator.d.ts +3 -0
- package/dist/ai/agents/orchestrator.d.ts.map +1 -1
- package/dist/ai/agents/orchestrator.js +3 -0
- package/dist/ai/agents/orchestrator.js.map +1 -1
- package/dist/ai/agents/planning-orchestrator.d.ts +12 -0
- package/dist/ai/agents/planning-orchestrator.d.ts.map +1 -0
- package/dist/ai/agents/planning-orchestrator.js +133 -0
- package/dist/ai/agents/planning-orchestrator.js.map +1 -0
- package/dist/ai/agents/stack-researcher.d.ts +3 -0
- package/dist/ai/agents/stack-researcher.d.ts.map +1 -1
- package/dist/ai/agents/stack-researcher.js +3 -0
- package/dist/ai/agents/stack-researcher.js.map +1 -1
- package/dist/ai/agents/stack-utils.d.ts +11 -0
- package/dist/ai/agents/stack-utils.d.ts.map +1 -0
- package/dist/ai/agents/stack-utils.js +27 -0
- package/dist/ai/agents/stack-utils.js.map +1 -0
- package/dist/ai/agents/synthesis-agent.d.ts +11 -0
- package/dist/ai/agents/synthesis-agent.d.ts.map +1 -0
- package/dist/ai/agents/synthesis-agent.js +202 -0
- package/dist/ai/agents/synthesis-agent.js.map +1 -0
- package/dist/ai/agents/tech-researcher.d.ts +16 -0
- package/dist/ai/agents/tech-researcher.d.ts.map +1 -0
- package/dist/ai/agents/tech-researcher.js +208 -0
- package/dist/ai/agents/tech-researcher.js.map +1 -0
- package/dist/ai/agents/types.d.ts +127 -0
- package/dist/ai/agents/types.d.ts.map +1 -1
- package/dist/ai/agents/types.js +6 -0
- package/dist/ai/agents/types.js.map +1 -1
- package/dist/ai/enhancer.d.ts +4 -0
- package/dist/ai/enhancer.d.ts.map +1 -1
- package/dist/ai/enhancer.js +3 -0
- package/dist/ai/enhancer.js.map +1 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +14 -2
- package/dist/ai/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/tracing.d.ts +5 -0
- package/dist/utils/tracing.d.ts.map +1 -1
- package/dist/utils/tracing.js +40 -1
- package/dist/utils/tracing.js.map +1 -1
- package/package.json +5 -2
- package/src/ai/agents/codebase-analyst.ts +3 -0
- package/src/ai/agents/context-enricher.ts +189 -0
- package/src/ai/agents/evaluator-optimizer.ts +277 -0
- package/src/ai/agents/index.ts +198 -111
- package/src/ai/agents/mcp-detector.test.ts +290 -0
- package/src/ai/agents/mcp-detector.ts +210 -0
- package/src/ai/agents/orchestrator.ts +3 -0
- package/src/ai/agents/planning-orchestrator.ts +140 -0
- package/src/ai/agents/stack-researcher.ts +3 -0
- package/src/ai/agents/stack-utils.ts +34 -0
- package/src/ai/agents/synthesis-agent.ts +240 -0
- package/src/ai/agents/tech-researcher.ts +262 -0
- package/src/ai/agents/types.ts +160 -0
- package/src/ai/enhancer.ts +6 -1
- package/src/ai/index.ts +26 -5
- package/src/commands/init.ts +20 -4
- package/src/utils/tracing.ts +44 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for MCP Detector
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx vitest run src/ai/agents/mcp-detector.test.ts
|
|
5
|
+
* (Requires vitest to be installed: npm install -D vitest)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { detectRalphMcpServers, convertToLegacyMcpRecommendations } from './mcp-detector.js';
|
|
10
|
+
import type { DetectedStack } from '../../scanner/types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper to create a DetectionResult
|
|
14
|
+
*/
|
|
15
|
+
function detection(name: string) {
|
|
16
|
+
return { name, confidence: 1, evidence: [`detected ${name}`] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to create a minimal DetectedStack for testing
|
|
21
|
+
*/
|
|
22
|
+
function createStack(overrides: Partial<DetectedStack> = {}): DetectedStack {
|
|
23
|
+
return {
|
|
24
|
+
language: detection('TypeScript'),
|
|
25
|
+
...overrides,
|
|
26
|
+
} as DetectedStack;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('detectRalphMcpServers', () => {
|
|
30
|
+
describe('e2eTesting', () => {
|
|
31
|
+
it('always returns playwright for e2eTesting', () => {
|
|
32
|
+
const result = detectRalphMcpServers(createStack());
|
|
33
|
+
expect(result.e2eTesting).toBe('playwright');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('database detection', () => {
|
|
38
|
+
it('detects Supabase', () => {
|
|
39
|
+
const stack = createStack({
|
|
40
|
+
database: detection('Supabase'),
|
|
41
|
+
});
|
|
42
|
+
const result = detectRalphMcpServers(stack);
|
|
43
|
+
expect(result.database).toBe('supabase');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('detects PostgreSQL', () => {
|
|
47
|
+
const stack = createStack({
|
|
48
|
+
database: detection('PostgreSQL'),
|
|
49
|
+
});
|
|
50
|
+
const result = detectRalphMcpServers(stack);
|
|
51
|
+
expect(result.database).toBe('postgres');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('detects Neon as postgres', () => {
|
|
55
|
+
const stack = createStack({
|
|
56
|
+
database: detection('Neon'),
|
|
57
|
+
});
|
|
58
|
+
const result = detectRalphMcpServers(stack);
|
|
59
|
+
expect(result.database).toBe('postgres');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('detects SQLite', () => {
|
|
63
|
+
const stack = createStack({
|
|
64
|
+
database: detection('SQLite'),
|
|
65
|
+
});
|
|
66
|
+
const result = detectRalphMcpServers(stack);
|
|
67
|
+
expect(result.database).toBe('sqlite');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('detects Turso as sqlite', () => {
|
|
71
|
+
const stack = createStack({
|
|
72
|
+
database: detection('Turso'),
|
|
73
|
+
});
|
|
74
|
+
const result = detectRalphMcpServers(stack);
|
|
75
|
+
expect(result.database).toBe('sqlite');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('detects Firebase/Firestore', () => {
|
|
79
|
+
const stack = createStack({
|
|
80
|
+
database: detection('Firestore'),
|
|
81
|
+
});
|
|
82
|
+
const result = detectRalphMcpServers(stack);
|
|
83
|
+
expect(result.database).toBe('firebase');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('detects MongoDB', () => {
|
|
87
|
+
const stack = createStack({
|
|
88
|
+
database: detection('MongoDB'),
|
|
89
|
+
});
|
|
90
|
+
const result = detectRalphMcpServers(stack);
|
|
91
|
+
expect(result.database).toBe('mongodb');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns undefined for unknown database', () => {
|
|
95
|
+
const stack = createStack({
|
|
96
|
+
database: detection('UnknownDB'),
|
|
97
|
+
});
|
|
98
|
+
const result = detectRalphMcpServers(stack);
|
|
99
|
+
expect(result.database).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns undefined when no database is detected', () => {
|
|
103
|
+
const result = detectRalphMcpServers(createStack());
|
|
104
|
+
expect(result.database).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('framework detection', () => {
|
|
109
|
+
it('adds vercel for Next.js projects', () => {
|
|
110
|
+
const stack = createStack({
|
|
111
|
+
framework: detection('Next.js'),
|
|
112
|
+
});
|
|
113
|
+
const result = detectRalphMcpServers(stack);
|
|
114
|
+
expect(result.additional).toContain('vercel');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not add vercel for non-Next.js projects', () => {
|
|
118
|
+
const stack = createStack({
|
|
119
|
+
framework: detection('React'),
|
|
120
|
+
});
|
|
121
|
+
const result = detectRalphMcpServers(stack);
|
|
122
|
+
expect(result.additional).not.toContain('vercel');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('deployment detection', () => {
|
|
127
|
+
it('detects Docker deployment', () => {
|
|
128
|
+
const stack = createStack({
|
|
129
|
+
deployment: [detection('Docker')],
|
|
130
|
+
});
|
|
131
|
+
const result = detectRalphMcpServers(stack);
|
|
132
|
+
expect(result.additional).toContain('docker');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('detects Vercel deployment', () => {
|
|
136
|
+
const stack = createStack({
|
|
137
|
+
deployment: [detection('Vercel')],
|
|
138
|
+
});
|
|
139
|
+
const result = detectRalphMcpServers(stack);
|
|
140
|
+
expect(result.additional).toContain('vercel');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('detects multiple deployments', () => {
|
|
144
|
+
const stack = createStack({
|
|
145
|
+
deployment: [
|
|
146
|
+
detection('Docker'),
|
|
147
|
+
detection('Railway'),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
const result = detectRalphMcpServers(stack);
|
|
151
|
+
expect(result.additional).toContain('docker');
|
|
152
|
+
expect(result.additional).toContain('railway');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('auth provider detection', () => {
|
|
157
|
+
it('detects Clerk auth', () => {
|
|
158
|
+
const stack = createStack({
|
|
159
|
+
auth: detection('Clerk'),
|
|
160
|
+
});
|
|
161
|
+
const result = detectRalphMcpServers(stack);
|
|
162
|
+
expect(result.additional).toContain('clerk');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('detects Auth0', () => {
|
|
166
|
+
const stack = createStack({
|
|
167
|
+
auth: detection('Auth0'),
|
|
168
|
+
});
|
|
169
|
+
const result = detectRalphMcpServers(stack);
|
|
170
|
+
expect(result.additional).toContain('auth0');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('analytics detection', () => {
|
|
175
|
+
it('detects PostHog analytics', () => {
|
|
176
|
+
const stack = createStack({
|
|
177
|
+
analytics: [detection('PostHog')],
|
|
178
|
+
});
|
|
179
|
+
const result = detectRalphMcpServers(stack);
|
|
180
|
+
expect(result.additional).toContain('posthog');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('detects Sentry', () => {
|
|
184
|
+
const stack = createStack({
|
|
185
|
+
analytics: [detection('Sentry')],
|
|
186
|
+
});
|
|
187
|
+
const result = detectRalphMcpServers(stack);
|
|
188
|
+
expect(result.additional).toContain('sentry');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('payments detection', () => {
|
|
193
|
+
it('detects Stripe payments', () => {
|
|
194
|
+
const stack = createStack({
|
|
195
|
+
payments: detection('Stripe'),
|
|
196
|
+
});
|
|
197
|
+
const result = detectRalphMcpServers(stack);
|
|
198
|
+
expect(result.additional).toContain('stripe');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('scanner recommendations', () => {
|
|
203
|
+
it('includes scanner MCP recommendations', () => {
|
|
204
|
+
const stack = createStack({
|
|
205
|
+
mcp: {
|
|
206
|
+
isProject: false,
|
|
207
|
+
recommended: ['custom-mcp', 'another-mcp'],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const result = detectRalphMcpServers(stack);
|
|
211
|
+
expect(result.additional).toContain('custom-mcp');
|
|
212
|
+
expect(result.additional).toContain('another-mcp');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('deduplicates scanner recommendations', () => {
|
|
216
|
+
const stack = createStack({
|
|
217
|
+
database: detection('Supabase'),
|
|
218
|
+
mcp: {
|
|
219
|
+
isProject: false,
|
|
220
|
+
recommended: ['supabase', 'other-mcp'],
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const result = detectRalphMcpServers(stack);
|
|
224
|
+
// supabase should be in database, not duplicated in additional
|
|
225
|
+
expect(result.database).toBe('supabase');
|
|
226
|
+
expect(result.additional).toContain('other-mcp');
|
|
227
|
+
expect(result.additional).not.toContain('supabase');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('deduplication', () => {
|
|
232
|
+
it('does not duplicate MCPs in additional', () => {
|
|
233
|
+
const stack = createStack({
|
|
234
|
+
framework: detection('Next.js'),
|
|
235
|
+
deployment: [detection('Vercel')],
|
|
236
|
+
});
|
|
237
|
+
const result = detectRalphMcpServers(stack);
|
|
238
|
+
const vercelCount = result.additional.filter(m => m === 'vercel').length;
|
|
239
|
+
expect(vercelCount).toBe(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('convertToLegacyMcpRecommendations', () => {
|
|
245
|
+
it('always includes filesystem and git as essential', () => {
|
|
246
|
+
const result = convertToLegacyMcpRecommendations({
|
|
247
|
+
e2eTesting: 'playwright',
|
|
248
|
+
additional: [],
|
|
249
|
+
});
|
|
250
|
+
expect(result.essential).toContain('filesystem');
|
|
251
|
+
expect(result.essential).toContain('git');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('includes playwright in essential', () => {
|
|
255
|
+
const result = convertToLegacyMcpRecommendations({
|
|
256
|
+
e2eTesting: 'playwright',
|
|
257
|
+
additional: [],
|
|
258
|
+
});
|
|
259
|
+
expect(result.essential).toContain('playwright');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('includes database in essential when detected', () => {
|
|
263
|
+
const result = convertToLegacyMcpRecommendations({
|
|
264
|
+
e2eTesting: 'playwright',
|
|
265
|
+
database: 'supabase',
|
|
266
|
+
additional: [],
|
|
267
|
+
});
|
|
268
|
+
expect(result.essential).toContain('supabase');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('moves additional MCPs to recommended', () => {
|
|
272
|
+
const result = convertToLegacyMcpRecommendations({
|
|
273
|
+
e2eTesting: 'playwright',
|
|
274
|
+
additional: ['docker', 'vercel'],
|
|
275
|
+
});
|
|
276
|
+
expect(result.recommended).toContain('docker');
|
|
277
|
+
expect(result.recommended).toContain('vercel');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns correct structure for full stack', () => {
|
|
281
|
+
const result = convertToLegacyMcpRecommendations({
|
|
282
|
+
e2eTesting: 'playwright',
|
|
283
|
+
database: 'postgres',
|
|
284
|
+
additional: ['docker', 'stripe'],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(result.essential).toEqual(['filesystem', 'git', 'playwright', 'postgres']);
|
|
288
|
+
expect(result.recommended).toEqual(['docker', 'stripe']);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Detector (Phase 3 helper)
|
|
3
|
+
* Detects ralph-essential MCP servers based on the detected stack
|
|
4
|
+
*
|
|
5
|
+
* This is a pure function (no LLM) - uses rule-based detection for efficiency
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DetectedStack } from '../../scanner/types.js';
|
|
9
|
+
import type { RalphMcpServers } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Database name to MCP server mapping
|
|
13
|
+
*/
|
|
14
|
+
const DATABASE_MCP_MAP: Record<string, string> = {
|
|
15
|
+
supabase: 'supabase',
|
|
16
|
+
convex: 'convex',
|
|
17
|
+
postgres: 'postgres',
|
|
18
|
+
postgresql: 'postgres',
|
|
19
|
+
sqlite: 'sqlite',
|
|
20
|
+
firebase: 'firebase',
|
|
21
|
+
firestore: 'firebase',
|
|
22
|
+
mongodb: 'mongodb',
|
|
23
|
+
mysql: 'mysql',
|
|
24
|
+
redis: 'redis',
|
|
25
|
+
planetscale: 'planetscale',
|
|
26
|
+
neon: 'postgres', // Neon is PostgreSQL-compatible
|
|
27
|
+
turso: 'sqlite', // Turso is SQLite-compatible
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Framework-specific MCP recommendations
|
|
32
|
+
*/
|
|
33
|
+
const FRAMEWORK_MCP_MAP: Record<string, string[]> = {
|
|
34
|
+
'next.js': ['vercel'],
|
|
35
|
+
'nextjs': ['vercel'],
|
|
36
|
+
'vercel': ['vercel'],
|
|
37
|
+
'remix': [],
|
|
38
|
+
'astro': [],
|
|
39
|
+
'nuxt': [],
|
|
40
|
+
'sveltekit': [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Service-specific MCP recommendations
|
|
45
|
+
*/
|
|
46
|
+
const SERVICE_MCP_MAP: Record<string, string> = {
|
|
47
|
+
stripe: 'stripe',
|
|
48
|
+
clerk: 'clerk',
|
|
49
|
+
auth0: 'auth0',
|
|
50
|
+
github: 'github',
|
|
51
|
+
gitlab: 'gitlab',
|
|
52
|
+
aws: 'aws',
|
|
53
|
+
gcp: 'gcp',
|
|
54
|
+
azure: 'azure',
|
|
55
|
+
docker: 'docker',
|
|
56
|
+
kubernetes: 'kubernetes',
|
|
57
|
+
k8s: 'kubernetes',
|
|
58
|
+
posthog: 'posthog',
|
|
59
|
+
sentry: 'sentry',
|
|
60
|
+
resend: 'resend',
|
|
61
|
+
sendgrid: 'sendgrid',
|
|
62
|
+
twilio: 'twilio',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Detect ralph-essential MCP servers from the stack
|
|
67
|
+
*
|
|
68
|
+
* Ralph loop essentials:
|
|
69
|
+
* - Playwright: Always recommended for E2E testing
|
|
70
|
+
* - Database MCP: If database is detected
|
|
71
|
+
* - Additional MCPs based on services and deployment
|
|
72
|
+
*/
|
|
73
|
+
export function detectRalphMcpServers(stack: DetectedStack): RalphMcpServers {
|
|
74
|
+
const result: RalphMcpServers = {
|
|
75
|
+
e2eTesting: 'playwright', // Always recommend Playwright for ralph loop
|
|
76
|
+
additional: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Detect database MCP
|
|
80
|
+
if (stack.database) {
|
|
81
|
+
const dbName = stack.database.name.toLowerCase();
|
|
82
|
+
|
|
83
|
+
// Check direct mapping
|
|
84
|
+
for (const [key, mcp] of Object.entries(DATABASE_MCP_MAP)) {
|
|
85
|
+
if (dbName.includes(key)) {
|
|
86
|
+
result.database = mcp;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check ORM for database hints
|
|
93
|
+
if (!result.database && stack.orm) {
|
|
94
|
+
const ormName = stack.orm.name.toLowerCase();
|
|
95
|
+
if (ormName.includes('prisma') || ormName.includes('drizzle')) {
|
|
96
|
+
// These ORMs often use PostgreSQL by default
|
|
97
|
+
// But we don't set a default - let it be detected from actual DB config
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Detect framework-specific MCPs
|
|
102
|
+
if (stack.framework) {
|
|
103
|
+
const frameworkName = stack.framework.name.toLowerCase();
|
|
104
|
+
for (const [key, mcps] of Object.entries(FRAMEWORK_MCP_MAP)) {
|
|
105
|
+
if (frameworkName.includes(key)) {
|
|
106
|
+
result.additional.push(...mcps);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Detect deployment MCPs
|
|
113
|
+
if (stack.deployment) {
|
|
114
|
+
for (const deploy of stack.deployment) {
|
|
115
|
+
const deployName = deploy.name.toLowerCase();
|
|
116
|
+
if (deployName.includes('docker')) {
|
|
117
|
+
addIfNotExists(result.additional, 'docker');
|
|
118
|
+
}
|
|
119
|
+
if (deployName.includes('vercel')) {
|
|
120
|
+
addIfNotExists(result.additional, 'vercel');
|
|
121
|
+
}
|
|
122
|
+
if (deployName.includes('railway')) {
|
|
123
|
+
addIfNotExists(result.additional, 'railway');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Detect auth provider MCPs
|
|
129
|
+
if (stack.auth) {
|
|
130
|
+
const authName = stack.auth.name.toLowerCase();
|
|
131
|
+
for (const [key, mcp] of Object.entries(SERVICE_MCP_MAP)) {
|
|
132
|
+
if (authName.includes(key)) {
|
|
133
|
+
addIfNotExists(result.additional, mcp);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Detect analytics MCPs
|
|
140
|
+
if (stack.analytics) {
|
|
141
|
+
for (const analytics of stack.analytics) {
|
|
142
|
+
const analyticsName = analytics.name.toLowerCase();
|
|
143
|
+
for (const [key, mcp] of Object.entries(SERVICE_MCP_MAP)) {
|
|
144
|
+
if (analyticsName.includes(key)) {
|
|
145
|
+
addIfNotExists(result.additional, mcp);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Detect payment MCPs
|
|
153
|
+
if (stack.payments) {
|
|
154
|
+
const paymentName = stack.payments.name.toLowerCase();
|
|
155
|
+
for (const [key, mcp] of Object.entries(SERVICE_MCP_MAP)) {
|
|
156
|
+
if (paymentName.includes(key)) {
|
|
157
|
+
addIfNotExists(result.additional, mcp);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add any MCP recommendations from scanner
|
|
164
|
+
if (stack.mcp?.recommended) {
|
|
165
|
+
for (const rec of stack.mcp.recommended) {
|
|
166
|
+
const normalizedRec = rec.toLowerCase();
|
|
167
|
+
// Skip if it's the database or playwright (already handled)
|
|
168
|
+
if (normalizedRec !== result.database && normalizedRec !== 'playwright') {
|
|
169
|
+
addIfNotExists(result.additional, rec);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Convert RalphMcpServers to the legacy McpRecommendations format
|
|
179
|
+
* for backward compatibility with existing code
|
|
180
|
+
*/
|
|
181
|
+
export function convertToLegacyMcpRecommendations(ralphMcp: RalphMcpServers): {
|
|
182
|
+
essential: string[];
|
|
183
|
+
recommended: string[];
|
|
184
|
+
} {
|
|
185
|
+
const essential: string[] = ['filesystem', 'git'];
|
|
186
|
+
|
|
187
|
+
// Add E2E testing as essential for ralph
|
|
188
|
+
if (ralphMcp.e2eTesting) {
|
|
189
|
+
essential.push(ralphMcp.e2eTesting);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add database as essential if detected
|
|
193
|
+
if (ralphMcp.database) {
|
|
194
|
+
essential.push(ralphMcp.database);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
essential,
|
|
199
|
+
recommended: ralphMcp.additional,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Helper to add item to array if not already present
|
|
205
|
+
*/
|
|
206
|
+
function addIfNotExists(arr: string[], item: string): void {
|
|
207
|
+
if (!arr.includes(item)) {
|
|
208
|
+
arr.push(item);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Orchestrator Agent
|
|
3
3
|
* Coordinates the multi-agent analysis and merges results
|
|
4
|
+
*
|
|
5
|
+
* @deprecated Use runPlanningOrchestrator + runSynthesisAgent instead.
|
|
6
|
+
* This agent is kept for backward compatibility.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import type { LanguageModel } from 'ai';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning Orchestrator Agent (Phase 1)
|
|
3
|
+
* Creates an analysis plan that guides the parallel workers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type LanguageModel } from 'ai';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import type { ScanResult } from '../../scanner/types.js';
|
|
9
|
+
import type { AnalysisPlan } from './types.js';
|
|
10
|
+
import { isReasoningModel } from '../providers.js';
|
|
11
|
+
import { logger } from '../../utils/logger.js';
|
|
12
|
+
import { getTracedAI } from '../../utils/tracing.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Schema for the analysis plan output
|
|
16
|
+
*/
|
|
17
|
+
const analysisPlanSchema = z.object({
|
|
18
|
+
areasToExplore: z.array(z.string()).describe('Key directories and files to explore'),
|
|
19
|
+
technologiesToResearch: z.array(z.string()).describe('Technologies to research in depth'),
|
|
20
|
+
questionsToAnswer: z.array(z.string()).describe('Specific questions that need answers'),
|
|
21
|
+
estimatedComplexity: z.enum(['low', 'medium', 'high']).describe('Estimated project complexity'),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* System prompt for the Planning Orchestrator
|
|
26
|
+
*/
|
|
27
|
+
const PLANNING_ORCHESTRATOR_SYSTEM_PROMPT = `You are a senior software architect analyzing a codebase to create an analysis plan.
|
|
28
|
+
|
|
29
|
+
Based on the scan result, create a focused analysis plan that identifies:
|
|
30
|
+
1. Key areas to explore (directories, config files, entry points)
|
|
31
|
+
2. Technologies that need in-depth research (frameworks, libraries, tools)
|
|
32
|
+
3. Specific questions that need answers for implementation guidance
|
|
33
|
+
|
|
34
|
+
## Guidelines
|
|
35
|
+
- Focus on areas that would benefit from deeper exploration
|
|
36
|
+
- Identify technologies where best practices would be valuable
|
|
37
|
+
- Ask questions that would help an AI developer implement features correctly
|
|
38
|
+
- Keep lists focused (3-7 items each)
|
|
39
|
+
- Consider the project type when prioritizing areas
|
|
40
|
+
|
|
41
|
+
## Example Output
|
|
42
|
+
{
|
|
43
|
+
"areasToExplore": ["src/", "config/", "lib/auth/"],
|
|
44
|
+
"technologiesToResearch": ["Next.js 14", "Prisma", "NextAuth"],
|
|
45
|
+
"questionsToAnswer": ["What is the authentication strategy?", "How is state managed?", "What testing patterns are used?"],
|
|
46
|
+
"estimatedComplexity": "medium"
|
|
47
|
+
}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run the Planning Orchestrator to create an analysis plan
|
|
51
|
+
*/
|
|
52
|
+
export async function runPlanningOrchestrator(
|
|
53
|
+
model: LanguageModel,
|
|
54
|
+
modelId: string,
|
|
55
|
+
scanResult: ScanResult,
|
|
56
|
+
verbose: boolean = false
|
|
57
|
+
): Promise<AnalysisPlan> {
|
|
58
|
+
// Build technology summary from scan result
|
|
59
|
+
const technologies: string[] = [];
|
|
60
|
+
const stack = scanResult.stack;
|
|
61
|
+
|
|
62
|
+
if (stack.framework) technologies.push(stack.framework.name);
|
|
63
|
+
if (stack.database) technologies.push(stack.database.name);
|
|
64
|
+
if (stack.orm) technologies.push(stack.orm.name);
|
|
65
|
+
if (stack.testing?.unit) technologies.push(stack.testing.unit.name);
|
|
66
|
+
if (stack.testing?.e2e) technologies.push(stack.testing.e2e.name);
|
|
67
|
+
if (stack.stateManagement) technologies.push(stack.stateManagement.name);
|
|
68
|
+
if (stack.auth) technologies.push(stack.auth.name);
|
|
69
|
+
if (stack.styling) technologies.push(stack.styling.name);
|
|
70
|
+
if (stack.mcp?.isProject) technologies.push('MCP Server');
|
|
71
|
+
|
|
72
|
+
const prompt = `Analyze this scan result and create an analysis plan:
|
|
73
|
+
|
|
74
|
+
Project: ${scanResult.projectRoot}
|
|
75
|
+
Framework: ${stack.framework?.name || 'Unknown'}
|
|
76
|
+
Database: ${stack.database?.name || 'None detected'}
|
|
77
|
+
Testing: ${stack.testing?.unit?.name || 'None detected'}
|
|
78
|
+
Package Manager: ${stack.packageManager?.name || 'npm'}
|
|
79
|
+
Detected Technologies: ${technologies.join(', ') || 'None'}
|
|
80
|
+
${stack.mcp?.isProject ? 'This is an MCP Server project.' : ''}
|
|
81
|
+
|
|
82
|
+
Create a focused analysis plan that will help understand this codebase.`;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { generateObject } = getTracedAI();
|
|
86
|
+
|
|
87
|
+
const { object: plan } = await generateObject({
|
|
88
|
+
model,
|
|
89
|
+
schema: analysisPlanSchema,
|
|
90
|
+
system: PLANNING_ORCHESTRATOR_SYSTEM_PROMPT,
|
|
91
|
+
prompt,
|
|
92
|
+
...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
|
|
93
|
+
experimental_telemetry: {
|
|
94
|
+
isEnabled: true,
|
|
95
|
+
metadata: {
|
|
96
|
+
agent: 'planning-orchestrator',
|
|
97
|
+
projectRoot: scanResult.projectRoot,
|
|
98
|
+
framework: stack.framework?.name || 'unknown',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (verbose) {
|
|
104
|
+
logger.info(`Planning Orchestrator: ${plan.areasToExplore.length} areas, ${plan.technologiesToResearch.length} techs, ${plan.questionsToAnswer.length} questions`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return plan;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (verbose) {
|
|
110
|
+
logger.error(`Planning Orchestrator error: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Return a sensible default plan
|
|
114
|
+
return getDefaultPlan(scanResult);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a default analysis plan when the orchestrator fails
|
|
120
|
+
*/
|
|
121
|
+
function getDefaultPlan(scanResult: ScanResult): AnalysisPlan {
|
|
122
|
+
const stack = scanResult.stack;
|
|
123
|
+
const technologies: string[] = [];
|
|
124
|
+
|
|
125
|
+
if (stack.framework) technologies.push(stack.framework.name);
|
|
126
|
+
if (stack.database) technologies.push(stack.database.name);
|
|
127
|
+
if (stack.orm) technologies.push(stack.orm.name);
|
|
128
|
+
if (stack.testing?.unit) technologies.push(stack.testing.unit.name);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
areasToExplore: ['src/', 'package.json'],
|
|
132
|
+
technologiesToResearch: technologies.length > 0 ? technologies : ['TypeScript'],
|
|
133
|
+
questionsToAnswer: [
|
|
134
|
+
'What is the project structure?',
|
|
135
|
+
'What are the main entry points?',
|
|
136
|
+
'How are tests organized?',
|
|
137
|
+
],
|
|
138
|
+
estimatedComplexity: 'medium',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Stack Researcher Agent
|
|
3
3
|
* Researches best practices and tools for the detected stack
|
|
4
4
|
* Gracefully degrades when optional services are unavailable
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Use runTechResearcher/runTechResearchPool from tech-researcher.ts instead.
|
|
7
|
+
* This agent is kept for backward compatibility.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import { stepCountIs, type LanguageModel, type Tool } from 'ai';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack Utilities
|
|
3
|
+
* Shared helper functions for working with DetectedStack
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DetectedStack } from '../../scanner/types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect project type from the stack
|
|
10
|
+
* Returns a human-readable project type string
|
|
11
|
+
*/
|
|
12
|
+
export function detectProjectType(stack: DetectedStack | undefined): string {
|
|
13
|
+
if (!stack) {
|
|
14
|
+
return 'Unknown';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (stack.mcp?.isProject) {
|
|
18
|
+
return 'MCP Server';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (stack.framework?.name?.includes('Next')) {
|
|
22
|
+
return 'Next.js App';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (stack.framework?.name?.includes('React')) {
|
|
26
|
+
return 'React SPA';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (stack.framework?.name) {
|
|
30
|
+
return `${stack.framework.name} Project`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return 'Unknown';
|
|
34
|
+
}
|