@aiready/agents 0.3.5 → 0.3.7
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/.turbo/turbo-test.log +42 -16
- package/package.json +8 -5
- package/src/__tests__/remediation-swarm.test.ts +118 -0
- package/src/impact-agent.ts +2 -2
- package/src/prioritization-agent.ts +1 -1
- package/src/refactor-agent.ts +18 -13
- package/src/rename-agent.ts +1 -1
- package/src/restructure-agent.ts +1 -1
- package/src/risk-agent.ts +1 -1
- package/src/tools/fs.ts +137 -0
- package/src/tools/github.ts +79 -27
- package/src/tools/mcp-adapter.ts +105 -0
- package/src/validation-agent.ts +1 -1
- package/src/workflows/remediation-swarm.ts +128 -18
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[2m
|
|
13
|
-
[
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
[
|
|
1
|
+
|
|
2
|
+
> @aiready/agents@0.3.6 test /Users/pengcao/projects/aiready/packages/agents
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/agents[39m
|
|
7
|
+
|
|
8
|
+
[32m✓[39m src/__tests__/smoke.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 1[2mms[22m[39m
|
|
9
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould successfully connect to MCP, execute agent, and parse JSON response
|
|
10
|
+
[22m[39m[RemediationSwarm] Connecting to MCP servers...
|
|
11
|
+
|
|
12
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould successfully connect to MCP, execute agent, and parse JSON response
|
|
13
|
+
[22m[39m[RemediationSwarm] Executing agent logic...
|
|
14
|
+
|
|
15
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould successfully connect to MCP, execute agent, and parse JSON response
|
|
16
|
+
[22m[39m[RemediationSwarm] Cleaning up MCP connections...
|
|
17
|
+
|
|
18
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould handle agent failures and return error status
|
|
19
|
+
[22m[39m[RemediationSwarm] Connecting to MCP servers...
|
|
20
|
+
|
|
21
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould handle agent failures and return error status
|
|
22
|
+
[22m[39m[RemediationSwarm] Executing agent logic...
|
|
23
|
+
|
|
24
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould handle agent failures and return error status
|
|
25
|
+
[22m[39m[RemediationSwarm] Cleaning up MCP connections...
|
|
26
|
+
|
|
27
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould fallback to raw text if agent fails to return valid JSON
|
|
28
|
+
[22m[39m[RemediationSwarm] Connecting to MCP servers...
|
|
29
|
+
|
|
30
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould fallback to raw text if agent fails to return valid JSON
|
|
31
|
+
[22m[39m[RemediationSwarm] Executing agent logic...
|
|
32
|
+
|
|
33
|
+
[90mstdout[2m | src/__tests__/remediation-swarm.test.ts[2m > [22m[2mRemediationSwarm (MCP Powered)[2m > [22m[2mshould fallback to raw text if agent fails to return valid JSON
|
|
34
|
+
[22m[39m[RemediationSwarm] Cleaning up MCP connections...
|
|
35
|
+
|
|
36
|
+
[32m✓[39m src/__tests__/remediation-swarm.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
37
|
+
|
|
38
|
+
[2m Test Files [22m [1m[32m2 passed[39m[22m[90m (2)[39m
|
|
39
|
+
[2m Tests [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
40
|
+
[2m Start at [22m 15:39:52
|
|
41
|
+
[2m Duration [22m 307ms[2m (transform 217ms, setup 0ms, import 267ms, tests 6ms, environment 0ms)[22m
|
|
42
|
+
|
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/agents",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Agentic remediation and analysis system for AIReady",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@mastra/core": "^1.0.0",
|
|
9
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
10
|
+
"@octokit/rest": "^21.0.0",
|
|
11
|
+
"isomorphic-git": "^1.25.0",
|
|
9
12
|
"zod": "^4.0.0",
|
|
10
|
-
"@aiready/
|
|
11
|
-
"@aiready/
|
|
13
|
+
"@aiready/cli": "0.14.10",
|
|
14
|
+
"@aiready/core": "0.23.8"
|
|
12
15
|
},
|
|
13
16
|
"devDependencies": {
|
|
17
|
+
"@types/node": "^24.0.0",
|
|
14
18
|
"typescript": "^5.0.0",
|
|
15
|
-
"vitest": "^4.0.0"
|
|
16
|
-
"@types/node": "^24.0.0"
|
|
19
|
+
"vitest": "^4.0.0"
|
|
17
20
|
},
|
|
18
21
|
"scripts": {
|
|
19
22
|
"test": "vitest run",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { RemediationSwarm } from '../workflows/remediation-swarm';
|
|
3
|
+
import { MCPAdapter } from '../tools/mcp-adapter';
|
|
4
|
+
import { Agent } from '@mastra/core/agent';
|
|
5
|
+
|
|
6
|
+
// Mock MCPAdapter
|
|
7
|
+
vi.mock('../tools/mcp-adapter', () => {
|
|
8
|
+
const MockAdapter = vi.fn();
|
|
9
|
+
MockAdapter.prototype.connect = vi.fn().mockResolvedValue(undefined);
|
|
10
|
+
MockAdapter.prototype.disconnect = vi.fn().mockResolvedValue(undefined);
|
|
11
|
+
MockAdapter.prototype.getMastraTools = vi.fn().mockResolvedValue({
|
|
12
|
+
'list-files': { id: 'list-files', execute: vi.fn() },
|
|
13
|
+
'create-pr': { id: 'create-pr', execute: vi.fn() },
|
|
14
|
+
});
|
|
15
|
+
return { MCPAdapter: MockAdapter };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Mock Mastra Agent
|
|
19
|
+
vi.mock('@mastra/core/agent', () => {
|
|
20
|
+
const MockAgent = vi.fn();
|
|
21
|
+
MockAgent.prototype.generate = vi.fn().mockResolvedValue({
|
|
22
|
+
text: JSON.stringify({
|
|
23
|
+
status: 'success',
|
|
24
|
+
diff: 'test-diff',
|
|
25
|
+
prUrl: 'https://github.com/test/repo/pull/1',
|
|
26
|
+
prNumber: 1,
|
|
27
|
+
explanation: 'Fixed duplication in auth.ts',
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
return { Agent: MockAgent };
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('RemediationSwarm (MCP Powered)', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should successfully connect to MCP, execute agent, and parse JSON response', async () => {
|
|
39
|
+
const input = {
|
|
40
|
+
remediation: { id: 'rem-1', type: 'consolidation' },
|
|
41
|
+
repo: { url: 'https://github.com/test/repo' },
|
|
42
|
+
rootDir: '/tmp/test-repo',
|
|
43
|
+
config: {
|
|
44
|
+
githubToken: 'gh-token-123',
|
|
45
|
+
openaiApiKey: 'oa-key-456',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = await RemediationSwarm.execute(input);
|
|
50
|
+
|
|
51
|
+
// Verify MCP Adapters were created and connected
|
|
52
|
+
expect(MCPAdapter).toHaveBeenCalledTimes(2);
|
|
53
|
+
|
|
54
|
+
const mockInstances = vi.mocked(MCPAdapter).mock.instances;
|
|
55
|
+
for (const instance of mockInstances) {
|
|
56
|
+
expect(instance.connect).toHaveBeenCalled();
|
|
57
|
+
expect(instance.getMastraTools).toHaveBeenCalled();
|
|
58
|
+
expect(instance.disconnect).toHaveBeenCalled();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Verify result structure
|
|
62
|
+
expect(result.ok).toBe(true);
|
|
63
|
+
if (result.ok) {
|
|
64
|
+
expect(result.value.status).toBe('success');
|
|
65
|
+
expect(result.value.prUrl).toBe('https://github.com/test/repo/pull/1');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle agent failures and return error status', async () => {
|
|
70
|
+
// Override mock for this specific test
|
|
71
|
+
const MockAgent = vi.mocked(Agent);
|
|
72
|
+
(MockAgent.prototype.generate as any).mockResolvedValueOnce({
|
|
73
|
+
text: JSON.stringify({
|
|
74
|
+
status: 'failure',
|
|
75
|
+
explanation: 'Could not find the duplicated files',
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const input = {
|
|
80
|
+
remediation: { id: 'rem-2' },
|
|
81
|
+
repo: { url: 'https://github.com/test/repo' },
|
|
82
|
+
rootDir: '/tmp/test',
|
|
83
|
+
config: { githubToken: 'token' },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const result = await RemediationSwarm.execute(input);
|
|
87
|
+
|
|
88
|
+
expect(result.ok).toBe(true);
|
|
89
|
+
if (result.ok) {
|
|
90
|
+
expect(result.value.status).toBe('failure');
|
|
91
|
+
expect(result.value.explanation).toBe(
|
|
92
|
+
'Could not find the duplicated files'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should fallback to raw text if agent fails to return valid JSON', async () => {
|
|
98
|
+
const MockAgent = vi.mocked(Agent);
|
|
99
|
+
(MockAgent.prototype.generate as any).mockResolvedValueOnce({
|
|
100
|
+
text: 'Applied fixes manually. PR created at https://github.com/test/repo/pull/99',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const input = {
|
|
104
|
+
remediation: { id: 'rem-3' },
|
|
105
|
+
repo: { url: 'https://github.com/test/repo' },
|
|
106
|
+
rootDir: '/tmp/test',
|
|
107
|
+
config: { githubToken: 'token' },
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = await RemediationSwarm.execute(input);
|
|
111
|
+
|
|
112
|
+
expect(result.ok).toBe(true);
|
|
113
|
+
if (result.ok) {
|
|
114
|
+
expect(result.value.status).toBe('success');
|
|
115
|
+
expect(result.value.explanation).toContain('fallback to text response');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/impact-agent.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const ImpactAgent = new Agent({
|
|
|
11
11
|
Analysis Logic:
|
|
12
12
|
1. Base Cost: Assume an average developer interacts with this code 20 times/day via AI.
|
|
13
13
|
2. Token Impact: Calculate token reduction based on "Cognitive Load" and "Context Window" savings.
|
|
14
|
-
3. Model Pricing: Use a blended rate of $
|
|
14
|
+
3. Model Pricing: Use a blended rate of $0.50 per 1M tokens (GPT-5.4-Mini/Gemini 3.1 Pro average).
|
|
15
15
|
4. Formulas:
|
|
16
16
|
- MonthlySavings = (TokensSavedPerInteraction * 20 * 22 days) * (Rate / 1,000,000)
|
|
17
17
|
|
|
@@ -20,7 +20,7 @@ export const ImpactAgent = new Agent({
|
|
|
20
20
|
- confidenceScore: number (0.0 to 1.0)
|
|
21
21
|
- breakdown: string (explanation of the math)
|
|
22
22
|
`,
|
|
23
|
-
model: 'openai/gpt-
|
|
23
|
+
model: 'openai/gpt-5.4-mini',
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
export const ImpactSchema = z.object({
|
|
@@ -17,7 +17,7 @@ export const PrioritizationAgent = new Agent({
|
|
|
17
17
|
- rank: 'P0' | 'P1' | 'P2' | 'P3'
|
|
18
18
|
- reasoning: string explanation of the ranking decision.
|
|
19
19
|
`,
|
|
20
|
-
model: 'openai/gpt-
|
|
20
|
+
model: 'openai/gpt-5.4-mini',
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
export const PrioritySchema = z.object({
|
package/src/refactor-agent.ts
CHANGED
|
@@ -1,32 +1,37 @@
|
|
|
1
1
|
import { Agent } from '@mastra/core/agent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { githubTools } from './tools/github';
|
|
4
|
+
import { fsTools } from './tools/fs';
|
|
3
5
|
|
|
4
6
|
export const RefactorAgent = new Agent({
|
|
5
7
|
id: 'refactor-agent',
|
|
6
8
|
name: 'Refactor Agent',
|
|
7
9
|
instructions: `
|
|
8
10
|
You are an expert full-stack engineer specialized in code consolidation and refactoring.
|
|
9
|
-
Your task is to take detected code
|
|
10
|
-
|
|
11
|
-
Follow these rules:
|
|
12
|
-
1. Preserve functionality: The behavior of the code must remain identical.
|
|
13
|
-
2. Follow patterns: Match the existing project's implementation patterns (e.g., React hooks, utility functions).
|
|
14
|
-
3. Type safety: Ensure all changes are correctly typed in TypeScript.
|
|
15
|
-
4. Minimal changes: Only modify what is necessary to resolve the duplication.
|
|
11
|
+
Your task is to take a detected code duplication or fragmentation issue and fix it.
|
|
16
12
|
|
|
17
13
|
Workflow:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
1. Research: Read the affected files using 'read-file'.
|
|
15
|
+
2. Branching: Create a new branch remotely using 'create-branch' AND locally using 'checkout-branch'.
|
|
16
|
+
3. Remediation: Consolidate the logic and write the changes using 'write-file'.
|
|
17
|
+
Ensure type safety and preserve functionality.
|
|
18
|
+
4. Persist: Commit and push the changes using 'commit-and-push'.
|
|
19
|
+
5. Finalize: Create a Pull Request with a clear description using 'create-pr'.
|
|
20
|
+
|
|
21
|
+
Return a summary of your actions, including the PR URL and a unified diff of your changes.
|
|
22
22
|
`,
|
|
23
|
-
model: 'openai/gpt-
|
|
23
|
+
model: 'openai/gpt-5.4-mini',
|
|
24
|
+
tools: {
|
|
25
|
+
...githubTools,
|
|
26
|
+
...fsTools,
|
|
27
|
+
},
|
|
24
28
|
});
|
|
25
29
|
|
|
26
30
|
export const RefactorResultSchema = z.object({
|
|
27
31
|
status: z.enum(['success', 'failure']),
|
|
28
32
|
diff: z.string(),
|
|
29
|
-
|
|
33
|
+
prUrl: z.string().optional(),
|
|
34
|
+
prNumber: z.number().optional(),
|
|
30
35
|
explanation: z.string(),
|
|
31
36
|
});
|
|
32
37
|
|
package/src/rename-agent.ts
CHANGED
package/src/restructure-agent.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const RestructureAgent = new Agent({
|
|
|
18
18
|
- strategy: string (explanation of the architectural vision)
|
|
19
19
|
- impact: { cognitiveLoadReduction: number (0-100), fragmentationReduction: number (0-100) }
|
|
20
20
|
`,
|
|
21
|
-
model: 'openai/gpt-
|
|
21
|
+
model: 'openai/gpt-5.4-mini',
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
export const RestructureSchema = z.object({
|
package/src/risk-agent.ts
CHANGED
package/src/tools/fs.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createTool } from '@mastra/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as git from 'isomorphic-git';
|
|
6
|
+
import http from 'isomorphic-git/http/node';
|
|
7
|
+
|
|
8
|
+
export const fsTools = {
|
|
9
|
+
readFile: createTool({
|
|
10
|
+
id: 'read-file',
|
|
11
|
+
description: 'Read the contents of a file',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
rootDir: z.string(),
|
|
14
|
+
filePath: z.string(),
|
|
15
|
+
}),
|
|
16
|
+
outputSchema: z.object({
|
|
17
|
+
content: z.string().optional(),
|
|
18
|
+
error: z.string().optional(),
|
|
19
|
+
}),
|
|
20
|
+
execute: async ({ rootDir, filePath }) => {
|
|
21
|
+
try {
|
|
22
|
+
const fullPath = path.join(rootDir, filePath);
|
|
23
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
24
|
+
return { content };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
writeFile: createTool({
|
|
34
|
+
id: 'write-file',
|
|
35
|
+
description: 'Write contents to a file',
|
|
36
|
+
inputSchema: z.object({
|
|
37
|
+
rootDir: z.string(),
|
|
38
|
+
filePath: z.string(),
|
|
39
|
+
content: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
outputSchema: z.object({
|
|
42
|
+
success: z.boolean(),
|
|
43
|
+
error: z.string().optional(),
|
|
44
|
+
}),
|
|
45
|
+
execute: async ({ rootDir, filePath, content }) => {
|
|
46
|
+
try {
|
|
47
|
+
const fullPath = path.join(rootDir, filePath);
|
|
48
|
+
// Ensure directory exists
|
|
49
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
50
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
51
|
+
return { success: true };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
commitAndPush: createTool({
|
|
62
|
+
id: 'commit-and-push',
|
|
63
|
+
description: 'Commit all changes and push to a branch',
|
|
64
|
+
inputSchema: z.object({
|
|
65
|
+
rootDir: z.string(),
|
|
66
|
+
branchName: z.string(),
|
|
67
|
+
message: z.string(),
|
|
68
|
+
githubToken: z.string(),
|
|
69
|
+
}),
|
|
70
|
+
outputSchema: z.object({
|
|
71
|
+
success: z.boolean(),
|
|
72
|
+
error: z.string().optional(),
|
|
73
|
+
}),
|
|
74
|
+
execute: async ({ rootDir, branchName, message, githubToken }) => {
|
|
75
|
+
try {
|
|
76
|
+
// 1. Add all changed files
|
|
77
|
+
await git.add({ fs, dir: rootDir, filepath: '.' });
|
|
78
|
+
|
|
79
|
+
// 2. Commit
|
|
80
|
+
await git.commit({
|
|
81
|
+
fs,
|
|
82
|
+
dir: rootDir,
|
|
83
|
+
message,
|
|
84
|
+
author: {
|
|
85
|
+
name: 'AIReady Remediation Agent',
|
|
86
|
+
email: 'agents@getaiready.dev',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 3. Push
|
|
91
|
+
await git.push({
|
|
92
|
+
fs,
|
|
93
|
+
http,
|
|
94
|
+
dir: rootDir,
|
|
95
|
+
ref: branchName,
|
|
96
|
+
onAuth: () => ({ username: githubToken, password: '' }),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { success: true };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('[FSTool] Error during commit/push:', error);
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
checkoutBranch: createTool({
|
|
111
|
+
id: 'checkout-branch',
|
|
112
|
+
description: 'Create and/or checkout a local branch',
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
rootDir: z.string(),
|
|
115
|
+
branchName: z.string(),
|
|
116
|
+
create: z.boolean().default(true),
|
|
117
|
+
}),
|
|
118
|
+
outputSchema: z.object({
|
|
119
|
+
success: z.boolean(),
|
|
120
|
+
error: z.string().optional(),
|
|
121
|
+
}),
|
|
122
|
+
execute: async ({ rootDir, branchName, create }) => {
|
|
123
|
+
try {
|
|
124
|
+
if (create) {
|
|
125
|
+
await git.branch({ fs, dir: rootDir, ref: branchName });
|
|
126
|
+
}
|
|
127
|
+
await git.checkout({ fs, dir: rootDir, ref: branchName, force: true });
|
|
128
|
+
return { success: true };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
};
|
package/src/tools/github.ts
CHANGED
|
@@ -1,29 +1,57 @@
|
|
|
1
1
|
import { createTool } from '@mastra/core/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { Octokit } from '@octokit/rest';
|
|
3
4
|
|
|
4
|
-
// Note: In a real implementation, this would use Octokit with appropriate auth
|
|
5
5
|
export const githubTools = {
|
|
6
6
|
createBranch: createTool({
|
|
7
7
|
id: 'create-branch',
|
|
8
8
|
description: 'Create a new branch in the repository',
|
|
9
9
|
inputSchema: z.object({
|
|
10
|
-
|
|
11
|
-
branchName: z.string(),
|
|
12
|
-
baseBranch: z
|
|
10
|
+
repoUrl: z.string().describe('The full URL of the repository'),
|
|
11
|
+
branchName: z.string().describe('The name of the new branch'),
|
|
12
|
+
baseBranch: z
|
|
13
|
+
.string()
|
|
14
|
+
.default('main')
|
|
15
|
+
.describe('The base branch to branch off from'),
|
|
16
|
+
githubToken: z.string().describe('GitHub Personal Access Token'),
|
|
13
17
|
}),
|
|
14
18
|
outputSchema: z.object({
|
|
15
19
|
success: z.boolean(),
|
|
16
|
-
|
|
20
|
+
branchName: z.string().optional(),
|
|
21
|
+
error: z.string().optional(),
|
|
17
22
|
}),
|
|
18
|
-
execute: async (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
execute: async ({ repoUrl, branchName, baseBranch, githubToken }) => {
|
|
24
|
+
try {
|
|
25
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
26
|
+
const [owner, repo] = repoUrl
|
|
27
|
+
.replace('https://github.com/', '')
|
|
28
|
+
.replace('git@github.com:', '')
|
|
29
|
+
.replace('.git', '')
|
|
30
|
+
.split('/');
|
|
31
|
+
|
|
32
|
+
// 1. Get the SHA of the base branch
|
|
33
|
+
const { data: ref } = await octokit.git.getRef({
|
|
34
|
+
owner,
|
|
35
|
+
repo,
|
|
36
|
+
ref: `heads/${baseBranch}`,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 2. Create the new branch
|
|
40
|
+
await octokit.git.createRef({
|
|
41
|
+
owner,
|
|
42
|
+
repo,
|
|
43
|
+
ref: `refs/heads/${branchName}`,
|
|
44
|
+
sha: ref.object.sha,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { success: true, branchName };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('[GitHubTool] Error creating branch:', error);
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
27
55
|
},
|
|
28
56
|
}),
|
|
29
57
|
|
|
@@ -31,25 +59,49 @@ export const githubTools = {
|
|
|
31
59
|
id: 'create-pr',
|
|
32
60
|
description: 'Create a Pull Request for a branch',
|
|
33
61
|
inputSchema: z.object({
|
|
34
|
-
|
|
35
|
-
title: z.string(),
|
|
36
|
-
body: z.string(),
|
|
37
|
-
head: z.string(),
|
|
38
|
-
base: z.string().default('main'),
|
|
62
|
+
repoUrl: z.string().describe('The full URL of the repository'),
|
|
63
|
+
title: z.string().describe('PR Title'),
|
|
64
|
+
body: z.string().describe('PR Description/Body'),
|
|
65
|
+
head: z.string().describe('The branch containing the changes'),
|
|
66
|
+
base: z.string().default('main').describe('The branch to merge into'),
|
|
67
|
+
githubToken: z.string().describe('GitHub Personal Access Token'),
|
|
39
68
|
}),
|
|
40
69
|
outputSchema: z.object({
|
|
41
70
|
success: z.boolean(),
|
|
42
71
|
prNumber: z.number().optional(),
|
|
43
72
|
prUrl: z.string().optional(),
|
|
73
|
+
error: z.string().optional(),
|
|
44
74
|
}),
|
|
45
|
-
execute: async (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
execute: async ({ repoUrl, title, body, head, base, githubToken }) => {
|
|
76
|
+
try {
|
|
77
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
78
|
+
const [owner, repo] = repoUrl
|
|
79
|
+
.replace('https://github.com/', '')
|
|
80
|
+
.replace('git@github.com:', '')
|
|
81
|
+
.replace('.git', '')
|
|
82
|
+
.split('/');
|
|
83
|
+
|
|
84
|
+
const { data: pr } = await octokit.pulls.create({
|
|
85
|
+
owner,
|
|
86
|
+
repo,
|
|
87
|
+
title,
|
|
88
|
+
body,
|
|
89
|
+
head,
|
|
90
|
+
base: base || 'main',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
prNumber: pr.number,
|
|
96
|
+
prUrl: pr.html_url,
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[GitHubTool] Error creating PR:', error);
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
53
105
|
},
|
|
54
106
|
}),
|
|
55
107
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createTool } from '@mastra/core/tools';
|
|
2
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* MCPAdapter allows connecting to external MCP servers and exposing their tools to Mastra agents.
|
|
12
|
+
*/
|
|
13
|
+
export class MCPAdapter {
|
|
14
|
+
private client: Client;
|
|
15
|
+
private transport: StdioClientTransport;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
command: string,
|
|
19
|
+
args: string[] = [],
|
|
20
|
+
env: Record<string, string> = {}
|
|
21
|
+
) {
|
|
22
|
+
this.transport = new StdioClientTransport({
|
|
23
|
+
command,
|
|
24
|
+
args,
|
|
25
|
+
env: { ...process.env, ...env } as any,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.client = new Client({ name: 'aiready-mcp-adapter', version: '1.0.0' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async connect() {
|
|
32
|
+
await this.client.connect(this.transport);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async disconnect() {
|
|
36
|
+
await this.transport.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Discovers tools from the MCP server and wraps them as Mastra tools.
|
|
41
|
+
*/
|
|
42
|
+
async getMastraTools(): Promise<Record<string, any>> {
|
|
43
|
+
const { tools } = await (this.client as any).request(
|
|
44
|
+
ListToolsRequestSchema,
|
|
45
|
+
{}
|
|
46
|
+
);
|
|
47
|
+
const mastraTools: Record<string, any> = {};
|
|
48
|
+
|
|
49
|
+
for (const tool of tools) {
|
|
50
|
+
mastraTools[tool.name] = createTool({
|
|
51
|
+
id: tool.name,
|
|
52
|
+
description: tool.description || `MCP Tool: ${tool.name}`,
|
|
53
|
+
inputSchema: this.mapJsonSchemaToZod(tool.inputSchema),
|
|
54
|
+
execute: async (args) => {
|
|
55
|
+
const result = await (this.client as any).request(
|
|
56
|
+
CallToolRequestSchema,
|
|
57
|
+
{
|
|
58
|
+
name: tool.name,
|
|
59
|
+
arguments: args,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
return result;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return mastraTools;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Simple mapper from JSON Schema (MCP) to Zod (Mastra)
|
|
72
|
+
* Note: This is a basic implementation and might need enhancement for complex schemas.
|
|
73
|
+
*/
|
|
74
|
+
private mapJsonSchemaToZod(schema: any): z.ZodObject<any> {
|
|
75
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
76
|
+
|
|
77
|
+
if (schema.type === 'object' && schema.properties) {
|
|
78
|
+
for (const [key, value] of Object.entries<any>(schema.properties)) {
|
|
79
|
+
let zodType: z.ZodTypeAny = z.any();
|
|
80
|
+
|
|
81
|
+
if (value.type === 'string') {
|
|
82
|
+
zodType = z.string();
|
|
83
|
+
} else if (value.type === 'number') {
|
|
84
|
+
zodType = z.number();
|
|
85
|
+
} else if (value.type === 'boolean') {
|
|
86
|
+
zodType = z.boolean();
|
|
87
|
+
} else if (value.type === 'array') {
|
|
88
|
+
zodType = z.array(z.any());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (value.description) {
|
|
92
|
+
zodType = zodType.describe(value.description);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (schema.required?.includes(key)) {
|
|
96
|
+
shape[key] = zodType;
|
|
97
|
+
} else {
|
|
98
|
+
shape[key] = zodType.optional();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return z.object(shape);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/validation-agent.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const ValidationAgent = new Agent({
|
|
|
18
18
|
- errors: array of { file: string, message: string, line?: number }
|
|
19
19
|
- feedbackForRefactor: string (detailed instructions for the RefactorAgent on how to fix these errors)
|
|
20
20
|
`,
|
|
21
|
-
model: 'openai/gpt-
|
|
21
|
+
model: 'openai/gpt-5.4-mini',
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
export const ValidationSchema = z.object({
|
|
@@ -1,18 +1,128 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { Agent } from '@mastra/core/agent';
|
|
2
|
+
import { MCPAdapter } from '../tools/mcp-adapter';
|
|
3
|
+
|
|
4
|
+
// Standard MCP server commands (configured for a standard Node environment)
|
|
5
|
+
// In a real SST/Lambda environment, these should be pre-installed or bundled.
|
|
6
|
+
const GITHUB_MCP_COMMAND = 'npx';
|
|
7
|
+
const GITHUB_MCP_ARGS = ['-y', '@modelcontextprotocol/server-github'];
|
|
8
|
+
|
|
9
|
+
const FILESYSTEM_MCP_COMMAND = 'npx';
|
|
10
|
+
const FILESYSTEM_MCP_ARGS = ['-y', '@modelcontextprotocol/server-filesystem'];
|
|
11
|
+
|
|
12
|
+
export const RemediationSwarm = {
|
|
13
|
+
execute: async (input: any) => {
|
|
14
|
+
const { remediation, repo, rootDir, config } = input;
|
|
15
|
+
|
|
16
|
+
// 1. Setup MCP Adapters
|
|
17
|
+
const githubAdapter = new MCPAdapter(GITHUB_MCP_COMMAND, GITHUB_MCP_ARGS, {
|
|
18
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: config.githubToken,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const fsAdapter = new MCPAdapter(FILESYSTEM_MCP_COMMAND, [
|
|
22
|
+
...FILESYSTEM_MCP_ARGS,
|
|
23
|
+
rootDir,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
console.log('[RemediationSwarm] Connecting to MCP servers...');
|
|
28
|
+
await Promise.all([githubAdapter.connect(), fsAdapter.connect()]);
|
|
29
|
+
|
|
30
|
+
const githubTools = await githubAdapter.getMastraTools();
|
|
31
|
+
const fsTools = await fsAdapter.getMastraTools();
|
|
32
|
+
|
|
33
|
+
// 2. Create the Agent with MCP Tools
|
|
34
|
+
const agent = new Agent({
|
|
35
|
+
id: 'refactor-agent-mcp',
|
|
36
|
+
name: 'Refactor Agent (MCP Powered)',
|
|
37
|
+
instructions: `
|
|
38
|
+
You are an expert full-stack engineer specialized in code consolidation and refactoring.
|
|
39
|
+
Your task is to take a detected code duplication or fragmentation issue and fix it.
|
|
40
|
+
|
|
41
|
+
Context:
|
|
42
|
+
- Repository: ${repo.url}
|
|
43
|
+
- Local Path: ${rootDir} (You have full access via filesystem tools)
|
|
44
|
+
|
|
45
|
+
Available Toolsets:
|
|
46
|
+
- GitHub: Use for creating branches, commits, and pull requests.
|
|
47
|
+
- Filesystem: Use for reading and writing files in ${rootDir}.
|
|
48
|
+
|
|
49
|
+
Workflow:
|
|
50
|
+
1. Research: Read the affected files using filesystem tools.
|
|
51
|
+
2. Branching: Create a new branch remotely using GitHub tools AND locally using Git/Filesystem tools if available.
|
|
52
|
+
3. Remediation: Consolidate the logic and write the changes using filesystem tools.
|
|
53
|
+
4. Persist: Commit and push the changes using GitHub/Git tools.
|
|
54
|
+
5. Finalize: Create a Pull Request with a clear description using GitHub tools.
|
|
55
|
+
|
|
56
|
+
You MUST provide your final response as a VALID JSON object with this structure:
|
|
57
|
+
{
|
|
58
|
+
"status": "success" | "failure",
|
|
59
|
+
"diff": "unified diff string",
|
|
60
|
+
"prUrl": "URL of the created PR",
|
|
61
|
+
"prNumber": 123,
|
|
62
|
+
"explanation": "Brief explanation of changes"
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
model: 'openai/gpt-5.4-mini',
|
|
66
|
+
tools: {
|
|
67
|
+
...githubTools,
|
|
68
|
+
...fsTools,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log('[RemediationSwarm] Executing agent logic...');
|
|
73
|
+
|
|
74
|
+
const prompt = `
|
|
75
|
+
Remediate this issue: ${JSON.stringify(remediation)}
|
|
76
|
+
The repository is cloned at: ${rootDir}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const result = await agent.generate(prompt);
|
|
80
|
+
|
|
81
|
+
const text = result.text || '';
|
|
82
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
83
|
+
|
|
84
|
+
if (jsonMatch) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
value: parsed,
|
|
90
|
+
};
|
|
91
|
+
} catch (parseErr) {
|
|
92
|
+
console.error(
|
|
93
|
+
'[RemediationSwarm] JSON Parse Error:',
|
|
94
|
+
parseErr,
|
|
95
|
+
'Text:',
|
|
96
|
+
text
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
value: {
|
|
104
|
+
status: 'success',
|
|
105
|
+
diff: text,
|
|
106
|
+
explanation:
|
|
107
|
+
'Applied refactoring successfully (fallback to text response)',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('[RemediationSwarm] Execution error:', error);
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error:
|
|
115
|
+
error instanceof Error
|
|
116
|
+
? error.message
|
|
117
|
+
: 'Unknown error during swarm execution',
|
|
118
|
+
};
|
|
119
|
+
} finally {
|
|
120
|
+
// 3. Cleanup MCP Connections
|
|
121
|
+
console.log('[RemediationSwarm] Cleaning up MCP connections...');
|
|
122
|
+
await Promise.allSettled([
|
|
123
|
+
githubAdapter.disconnect(),
|
|
124
|
+
fsAdapter.disconnect(),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|