@lisa.ai/agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/commands/coverage.js +105 -0
- package/dist/commands/heal.js +97 -0
- package/dist/index.js +54 -0
- package/dist/services/config.service.js +22 -0
- package/dist/services/git.service.js +66 -0
- package/dist/services/llm.service.js +124 -0
- package/dist/services/telemetry.service.js +21 -0
- package/dist/utils/coverage.parser.js +59 -0
- package/dist/utils/math.js +9 -0
- package/dist/utils/math.spec.js +19 -0
- package/dist/utils/parser.js +18 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @lisa.ai/agent
|
|
2
|
+
|
|
3
|
+
The **Lisa.ai Worker Agent** is an autonomous CI/CD sidecar that hooks into your standard build and test scripts. Powered by a configurable OODA loop (Observe, Orient, Decide, Act), it detects compile failures and logic coverage gaps and acts on them.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @lisa.ai/agent
|
|
9
|
+
# or
|
|
10
|
+
npm install --save-dev @lisa.ai/agent
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Auto-Heal a Failing Build
|
|
16
|
+
Wrap your existing broken build script with `lisa-agent heal` and pass your Project ID. The agent will execute your script, pipe the runtime errors securely to an LLM provider (like Claude or Gemini), apply the patch, and recursively verify!
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx lisa-agent heal --command="npm run build" --model="claude" --project-id="demo-123"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Continuous 100% Test Generation
|
|
23
|
+
Wrap your testing script with `lisa-agent coverage` (assuming it generates Istanbul/Jest JSON summaries). The agent will query the AST of files lacking coverage and autonomously generate functioning unit tests.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx lisa-agent coverage --command="npm run test" --model="gemini" --project-id="demo-123"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Dashboard Telemetry
|
|
30
|
+
This agent natively links to the Lisa.ai Control Plane and Telemetry UI via your `--project-id`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
*Created by the Lisa.ai Platform team.*
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.coverageCommand = coverageCommand;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const coverage_parser_1 = require("../utils/coverage.parser");
|
|
41
|
+
const llm_service_1 = require("../services/llm.service");
|
|
42
|
+
const heal_1 = require("./heal");
|
|
43
|
+
const telemetry_service_1 = require("../services/telemetry.service");
|
|
44
|
+
async function coverageCommand(command, modelProvider, attempt = 1, maxRetries = 3, projectId = 'local') {
|
|
45
|
+
console.log(`\n[Lisa.ai Coverage] ${command} (Attempt ${attempt}/${maxRetries}) Using Model: ${modelProvider}`);
|
|
46
|
+
let executionPassed = true;
|
|
47
|
+
try {
|
|
48
|
+
// 1. Run the test command which should ideally produce coverage-summary.json
|
|
49
|
+
const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
|
|
50
|
+
console.log(output);
|
|
51
|
+
console.log(`\n✅ [Lisa.ai Coverage] Tests passed successfully on attempt ${attempt}.`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.log(`\n❌ [Lisa.ai Coverage] Tests failed. Delegating to Auto-Heal...`);
|
|
55
|
+
// If tests themselves fail to compile or run, we fallback exactly to auto-heal
|
|
56
|
+
// We intentionally don't loop here immediately, but await healCommand.
|
|
57
|
+
await (0, heal_1.healCommand)(command, modelProvider, attempt, null, maxRetries, projectId);
|
|
58
|
+
executionPassed = false;
|
|
59
|
+
}
|
|
60
|
+
// 2. If it passed without compile/runtime errors, evaluate coverage
|
|
61
|
+
if (executionPassed) {
|
|
62
|
+
try {
|
|
63
|
+
// Find JSON summary expecting standard output paths
|
|
64
|
+
const coveragePath = path.resolve(process.cwd(), 'coverage/coverage-summary.json');
|
|
65
|
+
if (!fs.existsSync(coveragePath)) {
|
|
66
|
+
console.warn(`[Lisa.ai Coverage Warning] No coverage-summary.json found at ${coveragePath}. Cannot evaluate logic constraints.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(`[Lisa.ai Coverage] Evaluating summary...`);
|
|
70
|
+
const uncoveredFiles = (0, coverage_parser_1.parseCoverageSummary)('coverage/coverage-summary.json');
|
|
71
|
+
if (uncoveredFiles.length === 0) {
|
|
72
|
+
console.log(`✅ [Lisa.ai Coverage] 100% Logic Coverage Verified! No un-covered files remaining.`);
|
|
73
|
+
return; // We exit the loop
|
|
74
|
+
}
|
|
75
|
+
if (attempt >= maxRetries) {
|
|
76
|
+
console.error(`\n🚨 [Lisa.ai Error] Max coverage retries (${maxRetries}) reached with remaining gaps. Auto-generation limits exceeded.`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.log(`[Lisa.ai Coverage] Found ${uncoveredFiles.length} file(s) below 100% threshold:`, uncoveredFiles);
|
|
80
|
+
// 3. Address the first uncovered file
|
|
81
|
+
const targetFilePath = uncoveredFiles[0];
|
|
82
|
+
const absoluteTarget = path.resolve(process.cwd(), targetFilePath);
|
|
83
|
+
console.log(`[Lisa.ai Coverage] Requesting generated test suite for ${targetFilePath}...`);
|
|
84
|
+
const fileContent = fs.readFileSync(absoluteTarget, 'utf-8');
|
|
85
|
+
const testCode = await (0, llm_service_1.generateTestForFile)(targetFilePath, fileContent, modelProvider);
|
|
86
|
+
// Derive spec name from original file name (e.g. app.component.ts -> app.component.spec.ts)
|
|
87
|
+
const parsedPath = path.parse(absoluteTarget);
|
|
88
|
+
const specPath = path.join(parsedPath.dir, `${parsedPath.name}.spec${parsedPath.ext}`);
|
|
89
|
+
fs.writeFileSync(specPath, testCode, 'utf-8');
|
|
90
|
+
console.log(`[Lisa.ai Coverage] Generated and wrote Spec File to ${specPath}`);
|
|
91
|
+
await (0, telemetry_service_1.reportTelemetry)({
|
|
92
|
+
projectId,
|
|
93
|
+
type: 'coverage',
|
|
94
|
+
filePath: targetFilePath,
|
|
95
|
+
modelUsed: modelProvider,
|
|
96
|
+
status: 'success'
|
|
97
|
+
});
|
|
98
|
+
// 4. Recursive iteration to verify newly written test and hunt for next gap
|
|
99
|
+
await coverageCommand(command, modelProvider, attempt + 1, maxRetries, projectId);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.error(`[Lisa.ai Coverage loop failure]:`, e.message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.healCommand = healCommand;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const parser_1 = require("../utils/parser");
|
|
41
|
+
const llm_service_1 = require("../services/llm.service");
|
|
42
|
+
const git_service_1 = require("../services/git.service");
|
|
43
|
+
const telemetry_service_1 = require("../services/telemetry.service");
|
|
44
|
+
async function healCommand(command, modelProvider, attempt = 1, healedFilePath = null, maxRetries = 3, projectId = 'local') {
|
|
45
|
+
console.log(`\n[Lisa.ai Executing] ${command} (Attempt ${attempt}/${maxRetries}) Using Model: ${modelProvider}`);
|
|
46
|
+
try {
|
|
47
|
+
// Execute command synchronously
|
|
48
|
+
// stdio: 'pipe' captures stdout/stderr so they are thrown in error object on fail.
|
|
49
|
+
// However, to show stream we could use inherit, but we need the error log.
|
|
50
|
+
// For now, we capture and print it.
|
|
51
|
+
const output = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
|
|
52
|
+
console.log(output);
|
|
53
|
+
console.log(`\n✅ [Lisa.ai Success] Command executed successfully on attempt ${attempt}.`);
|
|
54
|
+
// If we successfully executed and had previously healed a file, PR it
|
|
55
|
+
if (healedFilePath) {
|
|
56
|
+
await (0, telemetry_service_1.reportTelemetry)({
|
|
57
|
+
projectId,
|
|
58
|
+
type: 'heal',
|
|
59
|
+
filePath: healedFilePath,
|
|
60
|
+
modelUsed: modelProvider,
|
|
61
|
+
status: 'success'
|
|
62
|
+
});
|
|
63
|
+
await (0, git_service_1.createPullRequestForHeal)(healedFilePath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.log(`\n❌ [Lisa.ai Failure] Command failed.`);
|
|
68
|
+
// Combine logs
|
|
69
|
+
const errorLog = (error.stderr || '') + '\n' + (error.stdout || '') + '\n' + (error.message || '');
|
|
70
|
+
console.error(errorLog);
|
|
71
|
+
if (attempt >= maxRetries) {
|
|
72
|
+
console.error(`\n🚨 [Lisa.ai Error] Max retries (${maxRetries}) reached. Auto-heal failed.`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
console.log(`\n[Lisa.ai Auto-Heal] Analyzing errors...`);
|
|
76
|
+
const filePath = (0, parser_1.extractFilePath)(errorLog);
|
|
77
|
+
if (!filePath) {
|
|
78
|
+
console.error(`\n🚨 [Lisa.ai Error] Could not parse a valid failing file path from the logs.`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
82
|
+
if (!fs.existsSync(absolutePath)) {
|
|
83
|
+
console.error(`\n🚨 [Lisa.ai Error] Extracted file path does not exist: ${absolutePath}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
console.log(`[Lisa.ai Auto-Heal] Identified failing file: ${filePath}`);
|
|
87
|
+
// Read file contents to send to LLM
|
|
88
|
+
const fileContent = fs.readFileSync(absolutePath, 'utf-8');
|
|
89
|
+
// Call LLM for a fix
|
|
90
|
+
const fixedCode = await (0, llm_service_1.askLisaForFix)(filePath, fileContent, errorLog, modelProvider);
|
|
91
|
+
// Write the fix to the file
|
|
92
|
+
fs.writeFileSync(absolutePath, fixedCode, 'utf-8');
|
|
93
|
+
console.log(`[Lisa.ai Auto-Heal] Applied patch to ${filePath}`);
|
|
94
|
+
// Recursive retry, passing the healed file path state
|
|
95
|
+
await healCommand(command, modelProvider, attempt + 1, filePath, maxRetries, projectId);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const config_service_1 = require("./services/config.service");
|
|
5
|
+
const heal_1 = require("./commands/heal");
|
|
6
|
+
const coverage_1 = require("./commands/coverage");
|
|
7
|
+
const program = new commander_1.Command();
|
|
8
|
+
program
|
|
9
|
+
.name('lisa-agent')
|
|
10
|
+
.description('Lisa.ai - Autonomous CI/CD Platform Worker Agent')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
program
|
|
13
|
+
.command('heal')
|
|
14
|
+
.description('Run a command and autonomously heal errors')
|
|
15
|
+
.requiredOption('-c, --command <type>', 'Command to execute (e.g. "ng build")')
|
|
16
|
+
.option('-m, --model <provider>', 'LLM provider to use (gemini, claude, openai)', 'gemini')
|
|
17
|
+
.option('-p, --project-id <id>', 'Control Plane Project ID to fetch dynamic config')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
let maxRetries = 3;
|
|
20
|
+
let model = options.model;
|
|
21
|
+
if (options.projectId) {
|
|
22
|
+
const config = await (0, config_service_1.fetchRemoteConfig)(options.projectId);
|
|
23
|
+
if (config) {
|
|
24
|
+
console.log(`[Lisa.ai Agent] Dynamic Config Loaded: Provider=${config.modelProvider}, MaxRetries=${config.maxRetries}`);
|
|
25
|
+
model = config.modelProvider;
|
|
26
|
+
maxRetries = config.maxRetries;
|
|
27
|
+
if (config.autoHealEnabled === false) {
|
|
28
|
+
console.log(`[Lisa.ai Agent] Auto-heal is disabled by Control Plane.`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
(0, heal_1.healCommand)(options.command, model, 1, null, maxRetries, options.projectId || 'local');
|
|
34
|
+
});
|
|
35
|
+
program
|
|
36
|
+
.command('coverage')
|
|
37
|
+
.description('Run test command dynamically generating missing specs for 100% coverage')
|
|
38
|
+
.requiredOption('-c, --command <type>', 'Test command to execute (e.g. "npm run test")')
|
|
39
|
+
.option('-m, --model <provider>', 'LLM provider to use (gemini, claude, openai)', 'gemini')
|
|
40
|
+
.option('-p, --project-id <id>', 'Control Plane Project ID to fetch dynamic config')
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
let maxRetries = 3;
|
|
43
|
+
let model = options.model;
|
|
44
|
+
if (options.projectId) {
|
|
45
|
+
const config = await (0, config_service_1.fetchRemoteConfig)(options.projectId);
|
|
46
|
+
if (config) {
|
|
47
|
+
console.log(`[Lisa.ai Agent] Dynamic Config Loaded: Provider=${config.modelProvider}, MaxRetries=${config.maxRetries}`);
|
|
48
|
+
model = config.modelProvider;
|
|
49
|
+
maxRetries = config.maxRetries;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
(0, coverage_1.coverageCommand)(options.command, model, 1, maxRetries, options.projectId || 'local');
|
|
53
|
+
});
|
|
54
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchRemoteConfig = fetchRemoteConfig;
|
|
4
|
+
async function fetchRemoteConfig(projectId) {
|
|
5
|
+
const MASTER_CONTROL_URL = process.env.LISA_CONTROL_PLANE_URL || 'http://localhost:3000';
|
|
6
|
+
try {
|
|
7
|
+
const url = `${MASTER_CONTROL_URL}/api/config/${projectId}`;
|
|
8
|
+
console.log(`[Lisa.ai Agent] Fetching dynamic configuration from ${url}...`);
|
|
9
|
+
// Using native Node Fetch API available in Node 18+
|
|
10
|
+
const response = await fetch(url);
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
console.warn(`[Lisa.ai Agent Warning] Control Plane returned ${response.status}. Falling back to default settings.`);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const config = await response.json();
|
|
16
|
+
return config;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.warn(`[Lisa.ai Agent Warning] Failed to reach Control Plane (${MASTER_CONTROL_URL}). Using local fallback configuration.`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createPullRequestForHeal = createPullRequestForHeal;
|
|
7
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
8
|
+
const rest_1 = require("@octokit/rest");
|
|
9
|
+
require("dotenv/config");
|
|
10
|
+
async function createPullRequestForHeal(filePath) {
|
|
11
|
+
console.log(`\n[Lisa.ai PR Engine] Initializing Pull Request for ${filePath}...`);
|
|
12
|
+
const git = (0, simple_git_1.default)();
|
|
13
|
+
const timestamp = new Date().getTime();
|
|
14
|
+
const branchName = `lisa-fix/build-error-${timestamp}`;
|
|
15
|
+
const commitMsg = `fix: automated auto-heal by Lisa.ai for ${filePath}`;
|
|
16
|
+
try {
|
|
17
|
+
// 1. Git Config Setup
|
|
18
|
+
await git.addConfig('user.name', 'Lisa.ai');
|
|
19
|
+
await git.addConfig('user.email', 'lisa@lisa.ai');
|
|
20
|
+
// 2. Branch and Commit
|
|
21
|
+
await git.checkoutLocalBranch(branchName);
|
|
22
|
+
await git.add(filePath);
|
|
23
|
+
await git.commit(commitMsg);
|
|
24
|
+
console.log(`[Lisa.ai PR Engine] Committed changes to branch ${branchName}`);
|
|
25
|
+
// 3. Optional Push to origin. We need a remote to open a PR.
|
|
26
|
+
// If no remote exists, this will fail gracefully or we catch it.
|
|
27
|
+
console.log(`[Lisa.ai PR Engine] Pushing branch to remote origin...`);
|
|
28
|
+
await git.push('origin', branchName, { '--set-upstream': null });
|
|
29
|
+
// 4. Open PR via Octokit
|
|
30
|
+
const octokit = new rest_1.Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
31
|
+
const repoInfoStr = process.env.GITHUB_REPOSITORY; // e.g., "owner/repo"
|
|
32
|
+
if (repoInfoStr && process.env.GITHUB_TOKEN) {
|
|
33
|
+
const [owner, repo] = repoInfoStr.split('/');
|
|
34
|
+
console.log(`[Lisa.ai PR Engine] Opening Pull Request against ${owner}/${repo}...`);
|
|
35
|
+
const pr = await octokit.rest.pulls.create({
|
|
36
|
+
owner,
|
|
37
|
+
repo,
|
|
38
|
+
title: commitMsg,
|
|
39
|
+
body: `### Lisa.ai Auto-Healed Pull Request
|
|
40
|
+
This PR was automatically generated by Lisa.ai to resolve a failing compilation/build step.
|
|
41
|
+
|
|
42
|
+
**Healed File:** \`${filePath}\`
|
|
43
|
+
|
|
44
|
+
Please review the changes.`,
|
|
45
|
+
head: branchName,
|
|
46
|
+
base: 'main', // Hardcoding main, could be dynamic
|
|
47
|
+
});
|
|
48
|
+
console.log(`✅ [Lisa.ai PR Engine] Pull Request created successfully: ${pr.data.html_url}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`⚠️ [Lisa.ai PR Engine] GITHUB_TOKEN or GITHUB_REPOSITORY not set. Skipping GitHub Pull Request creation.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`\n🚨 [Lisa.ai PR Engine Error] Failed to create Pull Request:`, error.message);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
// Switch back to original branch so we don't leave the user in a detached state locally
|
|
59
|
+
try {
|
|
60
|
+
await git.checkout('main'); // assumes main is the default, ideally parse current branch beforehand
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// Ignore checkout cleanup error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.askLisaForFix = askLisaForFix;
|
|
37
|
+
exports.generateTestForFile = generateTestForFile;
|
|
38
|
+
const ai_1 = require("ai");
|
|
39
|
+
const openai_1 = require("@ai-sdk/openai");
|
|
40
|
+
const anthropic_1 = require("@ai-sdk/anthropic");
|
|
41
|
+
const google_1 = require("@ai-sdk/google");
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
require("dotenv/config");
|
|
44
|
+
async function askLisaForFix(filePath, fileContent, errorLog, modelProvider) {
|
|
45
|
+
console.log(`[Lisa.ai Auto-Heal] Requesting fix from ${modelProvider} for ${filePath}...`);
|
|
46
|
+
let model;
|
|
47
|
+
switch (modelProvider) {
|
|
48
|
+
case 'openai':
|
|
49
|
+
model = (0, openai_1.openai)('gpt-4o');
|
|
50
|
+
break;
|
|
51
|
+
case 'claude':
|
|
52
|
+
model = (0, anthropic_1.anthropic)('claude-3-haiku-20240307');
|
|
53
|
+
break;
|
|
54
|
+
case 'gemini':
|
|
55
|
+
model = (0, google_1.google)('gemini-1.5-pro-latest');
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
console.warn(`[Lisa.ai Warning] Unrecognized modelProvider '${modelProvider}', defaulting to 'gemini'`);
|
|
59
|
+
model = (0, google_1.google)('gemini-1.5-pro-latest');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const prompt = `You are Lisa.ai, an autonomous CI/CD expert platform.
|
|
63
|
+
A build/compilation error occurred. Your task is to fix the provided file so that the error resolves.
|
|
64
|
+
|
|
65
|
+
--- Error Log ---
|
|
66
|
+
${errorLog}
|
|
67
|
+
|
|
68
|
+
--- Target File Content (${filePath}) ---
|
|
69
|
+
${fileContent}
|
|
70
|
+
|
|
71
|
+
--- Constraints ---
|
|
72
|
+
1. Do not delete business logic.
|
|
73
|
+
2. Do not suppress TypeScript errors with @ts-ignore or any type assertions unless absolutely unavoidable.
|
|
74
|
+
3. Fix the underlying type or logic issue.
|
|
75
|
+
4. Return ONLY the raw, fixed code without markdown backticks. Do not include any explanation or intro text.`;
|
|
76
|
+
// Provide a mock bypass if keys are not present to test the Git loop
|
|
77
|
+
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && !process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {
|
|
78
|
+
console.log(`[Lisa.ai Warning] No API keys found in environment. Using mock LLM fix for testing.`);
|
|
79
|
+
return `const missingImportStr: string = "42";\nconsole.log(missingImportStr);\n`;
|
|
80
|
+
}
|
|
81
|
+
const { text } = await (0, ai_1.generateText)({
|
|
82
|
+
model,
|
|
83
|
+
prompt,
|
|
84
|
+
});
|
|
85
|
+
return text.trim();
|
|
86
|
+
}
|
|
87
|
+
async function generateTestForFile(sourceFilePath, sourceFileContent, modelProvider) {
|
|
88
|
+
console.log(`[Lisa.ai Coverage] Requesting test generation from ${modelProvider} for ${sourceFilePath}...`);
|
|
89
|
+
let model;
|
|
90
|
+
switch (modelProvider) {
|
|
91
|
+
case 'openai':
|
|
92
|
+
model = (0, openai_1.openai)('gpt-4o');
|
|
93
|
+
break;
|
|
94
|
+
case 'claude':
|
|
95
|
+
model = (0, anthropic_1.anthropic)('claude-3-haiku-20240307');
|
|
96
|
+
break;
|
|
97
|
+
case 'gemini':
|
|
98
|
+
model = (0, google_1.google)('gemini-1.5-pro-latest');
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
model = (0, google_1.google)('gemini-1.5-pro-latest');
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const prompt = `You are Lisa.ai, an autonomous CI/CD expert platform.
|
|
105
|
+
A TypeScript/Angular file lacks 100% test coverage. Your task is to generate a comprehensive Jest unit test file (.spec.ts) covering all branches, lines, and functions.
|
|
106
|
+
|
|
107
|
+
--- Target File Content (${sourceFilePath}) ---
|
|
108
|
+
${sourceFileContent}
|
|
109
|
+
|
|
110
|
+
--- Constraints ---
|
|
111
|
+
1. Return ONLY the raw, generated TypeScript test code.
|
|
112
|
+
2. Do not include markdown formatting backticks \`\`\`.
|
|
113
|
+
3. Include all necessary imports assuming Jest is available.
|
|
114
|
+
4. Aim for 100% logic coverage.`;
|
|
115
|
+
// Provide a mock bypass if keys are not present
|
|
116
|
+
if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && !process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {
|
|
117
|
+
return `import { something } from './${path.basename(sourceFilePath, '.ts')}';\n\ndescribe('Auto-Generated Test', () => { \n it('should pass dynamically', () => { \n expect(true).toBe(true); \n }); \n});\n`;
|
|
118
|
+
}
|
|
119
|
+
const { text } = await (0, ai_1.generateText)({
|
|
120
|
+
model,
|
|
121
|
+
prompt,
|
|
122
|
+
});
|
|
123
|
+
return text.trim();
|
|
124
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reportTelemetry = reportTelemetry;
|
|
4
|
+
async function reportTelemetry(event) {
|
|
5
|
+
const MASTER_CONTROL_URL = process.env.LISA_CONTROL_PLANE_URL || 'http://localhost:3000';
|
|
6
|
+
try {
|
|
7
|
+
const url = `${MASTER_CONTROL_URL}/api/telemetry`;
|
|
8
|
+
// Fire and forget, don't block the compilation thread heavily
|
|
9
|
+
await fetch(url, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json'
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify(event)
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
// Silently fail if control plane is down so agent can still work detached
|
|
19
|
+
console.debug(`[Lisa.ai Agent Debug] Failed to report telemetry: ${error.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseCoverageSummary = parseCoverageSummary;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
function parseCoverageSummary(coverageFilePath) {
|
|
40
|
+
const absolutePath = path.resolve(process.cwd(), coverageFilePath);
|
|
41
|
+
if (!fs.existsSync(absolutePath)) {
|
|
42
|
+
throw new Error(`[Lisa.ai Coverage Error] Coverage file not found at ${absolutePath}`);
|
|
43
|
+
}
|
|
44
|
+
const rawData = fs.readFileSync(absolutePath, 'utf-8');
|
|
45
|
+
const summary = JSON.parse(rawData);
|
|
46
|
+
const uncoveredFiles = [];
|
|
47
|
+
for (const [file, metrics] of Object.entries(summary)) {
|
|
48
|
+
if (file === 'total')
|
|
49
|
+
continue;
|
|
50
|
+
// Check if any metric is below 100%
|
|
51
|
+
if (metrics.lines.pct < 100 ||
|
|
52
|
+
metrics.statements.pct < 100 ||
|
|
53
|
+
metrics.functions.pct < 100 ||
|
|
54
|
+
metrics.branches.pct < 100) {
|
|
55
|
+
uncoveredFiles.push(file);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return uncoveredFiles;
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const math_1 = require("./math");
|
|
4
|
+
describe('MathUtils', () => {
|
|
5
|
+
describe('add', () => {
|
|
6
|
+
it('should add two positive numbers', () => {
|
|
7
|
+
expect(math_1.MathUtils.add(2, 3)).toBe(5);
|
|
8
|
+
});
|
|
9
|
+
it('should add two negative numbers', () => {
|
|
10
|
+
expect(math_1.MathUtils.add(-2, -3)).toBe(-5);
|
|
11
|
+
});
|
|
12
|
+
it('should add a positive and a negative number', () => {
|
|
13
|
+
expect(math_1.MathUtils.add(2, -3)).toBe(-1);
|
|
14
|
+
});
|
|
15
|
+
it('should add zero to a number', () => {
|
|
16
|
+
expect(math_1.MathUtils.add(2, 0)).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractFilePath = extractFilePath;
|
|
4
|
+
function extractFilePath(errorLog) {
|
|
5
|
+
// Matches typical TS/Angular error patterns:
|
|
6
|
+
// e.g., "src/app/app.component.ts:12:3 - error TS2322:"
|
|
7
|
+
// e.g., "Error: src/app/app.component.ts:12:3"
|
|
8
|
+
// Try to match path near an error
|
|
9
|
+
const tsErrorRegex = /([a-zA-Z0-9_.\-\/\\]+\.ts)(?:[:(])/;
|
|
10
|
+
const match = tsErrorRegex.exec(errorLog);
|
|
11
|
+
if (match && match[1]) {
|
|
12
|
+
return match[1];
|
|
13
|
+
}
|
|
14
|
+
// Fallback: Just try to find anything that looks like a .ts file path
|
|
15
|
+
const fallbackRegex = /([a-zA-Z0-9_.\-\/\\]+\.ts)/;
|
|
16
|
+
const fallbackMatch = fallbackRegex.exec(errorLog);
|
|
17
|
+
return fallbackMatch ? fallbackMatch[1] : null;
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lisa.ai/agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lisa.ai Autonomous CI/CD Worker Agent",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lisa-agent": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "ts-node src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"lisa",
|
|
19
|
+
"ci",
|
|
20
|
+
"cd",
|
|
21
|
+
"agent"
|
|
22
|
+
],
|
|
23
|
+
"author": "Lisa.ai",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@ai-sdk/anthropic": "^3.0.46",
|
|
27
|
+
"@ai-sdk/google": "^3.0.30",
|
|
28
|
+
"@ai-sdk/openai": "^3.0.30",
|
|
29
|
+
"@octokit/rest": "^22.0.1",
|
|
30
|
+
"ai": "^6.0.94",
|
|
31
|
+
"commander": "^11.1.0",
|
|
32
|
+
"dotenv": "^17.3.1",
|
|
33
|
+
"simple-git": "^3.31.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/jest": "^30.0.0",
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"ts-node": "^10.9.1",
|
|
39
|
+
"typescript": "^5.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|