andur 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/cli/index.js +366 -0
- package/dist/core/ai-engine.js +23 -0
- package/dist/core/config-loader.js +116 -0
- package/dist/core/grouping-engine.js +65 -0
- package/dist/core/plugin-loader.js +38 -0
- package/dist/index.js +40 -0
- package/dist/plugins/ai/gemini-provider.js +82 -0
- package/dist/plugins/ai/mock-provider.js +39 -0
- package/dist/plugins/ai/openai-provider.js +28 -0
- package/dist/plugins/jules/jules-provider.js +38 -0
- package/dist/plugins/logs/newrelic-provider.js +85 -0
- package/dist/plugins/repo/local-provider.js +62 -0
- package/dist/types/constants.js +29 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/fix-tracker.js +64 -0
- package/dist/utils/pricing.js +21 -0
- package/dist/utils/usage-tracker.js +65 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rivendev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Andur 🗡️ Scout & Bridge
|
|
2
|
+
|
|
3
|
+
> **The Intelligent Bridge between Production Logs and Agentic Fixing.**
|
|
4
|
+
|
|
5
|
+
Andur is a developer tool designed to automate the triage and handover of production errors to autonomous agents. It acts as a **Scout**, extracting rich context and impact analysis from your logs, and a **Bridge**, initiating fix sessions directly on agentic platforms like Jules.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🌟 The "Scout & Bridge" Workflow
|
|
10
|
+
|
|
11
|
+
1. **Scout**: Andur fetches errors from New Relic, groups them by signature, and performs an AI-driven impact and cause analysis.
|
|
12
|
+
2. **Bridge**: If a Jules API token is configured, Andur automatically prompts you to select a target repository and branch, then creates a new fix session with a comprehensive "Handover Report".
|
|
13
|
+
|
|
14
|
+
## 🚀 Key Features
|
|
15
|
+
|
|
16
|
+
- **Log Ingestion**: Deep integration with New Relic (NRQL) to fetch and group transaction errors.
|
|
17
|
+
- **AI Diagnosis**: Automatic analysis of stack traces using Gemini or OpenAI to identify root causes.
|
|
18
|
+
- **Automated Handover**: Direct integration with the **Jules Service API** (`v1alpha`) to start fixing sessions.
|
|
19
|
+
- **Impact Tracking**: Calculates how much each error group contributes to your total error volume.
|
|
20
|
+
- **Fix Status**: Persistently tracks which errors have already been sent to Jules to avoid duplicate work.
|
|
21
|
+
- **Usage & Cost Tracking**: Built-in monitoring of AI token consumption and estimated costs.
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g andur
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 🛠️ Configuration
|
|
30
|
+
|
|
31
|
+
Initialize your project configuration interactively:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
andur init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Environment Variables
|
|
38
|
+
Andur requires API keys to function. Set these in your `.env` or environment:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Log Provider (New Relic)
|
|
42
|
+
ANDUR_LOGS_ACCOUNT_ID="your-id"
|
|
43
|
+
ANDUR_LOGS_API_KEY="your-key"
|
|
44
|
+
|
|
45
|
+
# AI Provider (Gemini/OpenAI)
|
|
46
|
+
ANDUR_AI_API_KEY="your-key"
|
|
47
|
+
|
|
48
|
+
# Jules Service Handover
|
|
49
|
+
ANDUR_JULES_API_TOKEN="your-token"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### andur.config.js
|
|
53
|
+
```javascript
|
|
54
|
+
module.exports = {
|
|
55
|
+
logs: {
|
|
56
|
+
provider: 'new-relic',
|
|
57
|
+
since: '1 day ago',
|
|
58
|
+
limit: 100
|
|
59
|
+
},
|
|
60
|
+
ai: {
|
|
61
|
+
provider: 'gemini',
|
|
62
|
+
model: 'gemini-2.0-flash-exp'
|
|
63
|
+
},
|
|
64
|
+
repo: {
|
|
65
|
+
branch: 'develop' // Default target branch for fixes
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## ⌨️ Command Reference
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
| :--- | :--- |
|
|
74
|
+
| `andur init` | Set up configuration interactively |
|
|
75
|
+
| `andur analyze` | Scout recent logs and generate AI insights |
|
|
76
|
+
| `andur fix` | Select an error and bridge it to Jules Service |
|
|
77
|
+
| `andur fix --all` | Send all unhandled error groups to Jules in batch |
|
|
78
|
+
| `andur usage-details` | View token usage and cost estimation |
|
|
79
|
+
|
|
80
|
+
## 🛡️ License
|
|
81
|
+
|
|
82
|
+
MIT © [Rivendev](https://github.com/rivendev)
|
|
@@ -0,0 +1,366 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.setupCLI = setupCLI;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const config_loader_1 = require("../core/config-loader");
|
|
45
|
+
const plugin_loader_1 = require("../core/plugin-loader");
|
|
46
|
+
const grouping_engine_1 = require("../core/grouping-engine");
|
|
47
|
+
const ai_engine_1 = require("../core/ai-engine");
|
|
48
|
+
const newrelic_provider_1 = require("../plugins/logs/newrelic-provider");
|
|
49
|
+
const mock_provider_1 = require("../plugins/ai/mock-provider");
|
|
50
|
+
const openai_provider_1 = require("../plugins/ai/openai-provider");
|
|
51
|
+
const gemini_provider_1 = require("../plugins/ai/gemini-provider");
|
|
52
|
+
const local_provider_1 = require("../plugins/repo/local-provider");
|
|
53
|
+
const fix_tracker_1 = require("../utils/fix-tracker");
|
|
54
|
+
function setupCLI() {
|
|
55
|
+
const program = new commander_1.Command();
|
|
56
|
+
program
|
|
57
|
+
.name('andur')
|
|
58
|
+
.description('Transform production logs into code fixes')
|
|
59
|
+
.version('0.1.0');
|
|
60
|
+
program
|
|
61
|
+
.command('init')
|
|
62
|
+
.description('Initialize Andur configuration interactively')
|
|
63
|
+
.action(async () => {
|
|
64
|
+
const inquirer = require('inquirer');
|
|
65
|
+
const { LOG_PROVIDERS, AI_PROVIDERS, MODELS, REPO_PROVIDERS } = require('../types/constants');
|
|
66
|
+
console.log(chalk_1.default.blue('\n🗡️ Andur Initialization\n'));
|
|
67
|
+
const answers = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: 'list',
|
|
70
|
+
name: 'logProvider',
|
|
71
|
+
message: 'Select your log provider:',
|
|
72
|
+
choices: LOG_PROVIDERS,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'aiProvider',
|
|
77
|
+
message: 'Select your AI provider:',
|
|
78
|
+
choices: AI_PROVIDERS,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'list',
|
|
82
|
+
name: 'model',
|
|
83
|
+
message: (currAnswers) => `Select model for ${currAnswers.aiProvider}:`,
|
|
84
|
+
choices: (currAnswers) => MODELS[currAnswers.aiProvider],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'repoProvider',
|
|
89
|
+
message: 'Select your repo provider:',
|
|
90
|
+
choices: REPO_PROVIDERS,
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
const configContent = config_loader_1.ConfigLoader.generateDefaultConfig(answers);
|
|
94
|
+
const configPath = path.join(process.cwd(), 'andur.config.js');
|
|
95
|
+
if (fs.existsSync(configPath)) {
|
|
96
|
+
console.error(chalk_1.default.red('andur.config.js already exists!'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(configPath, configContent);
|
|
100
|
+
console.log(chalk_1.default.green(`\n✅ Created andur.config.js`));
|
|
101
|
+
console.log(chalk_1.default.yellow('\nNext steps:'));
|
|
102
|
+
console.log(`1. Set your environment variables:`);
|
|
103
|
+
console.log(` - ${chalk_1.default.cyan('ANDUR_LOGS_ACCOUNT_ID')}`);
|
|
104
|
+
console.log(` - ${chalk_1.default.cyan('ANDUR_LOGS_API_KEY')}`);
|
|
105
|
+
console.log(` - ${chalk_1.default.cyan('ANDUR_AI_API_KEY')}`);
|
|
106
|
+
console.log(`2. Run ${chalk_1.default.cyan('andur analyze')} to start.\n`);
|
|
107
|
+
});
|
|
108
|
+
program
|
|
109
|
+
.command('analyze')
|
|
110
|
+
.description('Analyze recent logs and group errors')
|
|
111
|
+
.option('--query <nrql>', 'Custom NRQL query')
|
|
112
|
+
.action(async (options) => {
|
|
113
|
+
try {
|
|
114
|
+
console.log(chalk_1.default.blue('Loading configuration...'));
|
|
115
|
+
const config = await config_loader_1.ConfigLoader.loadConfig();
|
|
116
|
+
const plugins = new plugin_loader_1.PluginLoader();
|
|
117
|
+
plugins.registerLogProvider(new newrelic_provider_1.NewRelicProvider(config.logs.options));
|
|
118
|
+
const provider = plugins.getLogProvider(config.logs.provider);
|
|
119
|
+
const finalQuery = options.query || config_loader_1.ConfigLoader.buildQuery(config);
|
|
120
|
+
console.log(chalk_1.default.blue(`Fetching logs from ${provider.name}...`));
|
|
121
|
+
console.debug(chalk_1.default.gray(`Query: ${finalQuery}`));
|
|
122
|
+
const logs = await provider.fetchLogs({ query: finalQuery });
|
|
123
|
+
console.log(chalk_1.default.blue(`Grouping ${logs.length} errors...`));
|
|
124
|
+
const groups = grouping_engine_1.GroupingEngine.group(logs, config.grouping?.strategy);
|
|
125
|
+
console.log(chalk_1.default.green(`\nFound ${groups.length} error groups.\n`));
|
|
126
|
+
// --- NEW: AI Insights ---
|
|
127
|
+
console.log(chalk_1.default.blue('Generating AI insights...'));
|
|
128
|
+
// Setup AI Provider for insights
|
|
129
|
+
if (config.ai.provider === 'openai') {
|
|
130
|
+
plugins.registerAIProvider(new openai_provider_1.OpenAIProvider({
|
|
131
|
+
apiKey: config.ai.options.apiKey,
|
|
132
|
+
model: config.ai.model
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
else if (config.ai.provider === 'gemini') {
|
|
136
|
+
plugins.registerAIProvider(new gemini_provider_1.GeminiProvider({
|
|
137
|
+
apiKey: config.ai.options.apiKey,
|
|
138
|
+
model: config.ai.model
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
plugins.registerAIProvider(new mock_provider_1.MockAIProvider());
|
|
143
|
+
}
|
|
144
|
+
const aiProvider = plugins.getAIProvider(config.ai.provider);
|
|
145
|
+
const aiEngine = new ai_engine_1.AIEngine(aiProvider);
|
|
146
|
+
const insights = await aiEngine.getInsights(groups);
|
|
147
|
+
// Record usage
|
|
148
|
+
if (insights.usage) {
|
|
149
|
+
const { UsageTracker: Tracker } = require('../utils/usage-tracker');
|
|
150
|
+
Tracker.record(config.ai.provider, config.ai.model, insights.usage.inputTokens, insights.usage.outputTokens);
|
|
151
|
+
}
|
|
152
|
+
console.log(chalk_1.default.bold('AI Summary:'));
|
|
153
|
+
console.log(chalk_1.default.white(insights.summary));
|
|
154
|
+
console.log(chalk_1.default.bold('\nCategorization:'));
|
|
155
|
+
insights.categories.forEach(cat => {
|
|
156
|
+
console.log(`- ${chalk_1.default.cyan(cat.name)}: ${cat.count} groups`);
|
|
157
|
+
});
|
|
158
|
+
console.log(chalk_1.default.bold('\nDetailed Groups:'));
|
|
159
|
+
groups.forEach(g => {
|
|
160
|
+
console.log(`${chalk_1.default.yellow(g.hash)}: ${g.representativeError.message} (${g.count} occurrences)`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error(chalk_1.default.red('Error:'), error.message);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
program
|
|
168
|
+
.command('jules-fix')
|
|
169
|
+
.alias('fix')
|
|
170
|
+
.description('Request Jules to generate a fix for a specific error group')
|
|
171
|
+
.argument('[hash]', 'Error group hash to fix')
|
|
172
|
+
.option('-a, --all', 'Send all detected error groups to Jules for fixing')
|
|
173
|
+
.action(async (hash, options) => {
|
|
174
|
+
try {
|
|
175
|
+
const config = await config_loader_1.ConfigLoader.loadConfig();
|
|
176
|
+
const plugins = new plugin_loader_1.PluginLoader();
|
|
177
|
+
const inquirer = require('inquirer');
|
|
178
|
+
plugins.registerLogProvider(new newrelic_provider_1.NewRelicProvider(config.logs.options));
|
|
179
|
+
// Register the correct AI provider based on config
|
|
180
|
+
if (config.ai.provider === 'openai') {
|
|
181
|
+
plugins.registerAIProvider(new openai_provider_1.OpenAIProvider({
|
|
182
|
+
apiKey: config.ai.options.apiKey,
|
|
183
|
+
model: config.ai.model
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
else if (config.ai.provider === 'gemini') {
|
|
187
|
+
plugins.registerAIProvider(new gemini_provider_1.GeminiProvider({
|
|
188
|
+
apiKey: config.ai.options.apiKey,
|
|
189
|
+
model: config.ai.model
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
plugins.registerAIProvider(new mock_provider_1.MockAIProvider());
|
|
194
|
+
}
|
|
195
|
+
plugins.registerRepoProvider(new local_provider_1.LocalRepoProvider(config.repo.path));
|
|
196
|
+
const logProvider = plugins.getLogProvider(config.logs.provider);
|
|
197
|
+
const aiProvider = plugins.getAIProvider(config.ai.provider);
|
|
198
|
+
const repoProvider = plugins.getRepoProvider(config.repo.provider);
|
|
199
|
+
console.log(chalk_1.default.blue('Fetching error details...'));
|
|
200
|
+
const query = config_loader_1.ConfigLoader.buildQuery(config);
|
|
201
|
+
const logs = await logProvider.fetchLogs({ query });
|
|
202
|
+
const totalErrorCount = logs.length;
|
|
203
|
+
const groups = grouping_engine_1.GroupingEngine.group(logs, config.grouping?.strategy);
|
|
204
|
+
let targetGroups = [];
|
|
205
|
+
if (hash) {
|
|
206
|
+
const group = groups.find(g => g.hash === hash);
|
|
207
|
+
if (!group) {
|
|
208
|
+
console.error(chalk_1.default.red(`Error group ${hash} not found.`));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
targetGroups = [group];
|
|
212
|
+
}
|
|
213
|
+
else if (options?.all) {
|
|
214
|
+
targetGroups = groups.filter(g => !fix_tracker_1.FixTracker.isHandled(g.hash));
|
|
215
|
+
if (targetGroups.length === 0) {
|
|
216
|
+
console.log(chalk_1.default.green('\n✅ All error groups have already been handled by Jules.'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log(chalk_1.default.blue(`\nProcessing ${targetGroups.length} unhandled error groups...`));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
const { selectedHash } = await inquirer.prompt([
|
|
223
|
+
{
|
|
224
|
+
type: 'list',
|
|
225
|
+
name: 'selectedHash',
|
|
226
|
+
message: 'Select an error group for Jules to fix:',
|
|
227
|
+
loop: false,
|
|
228
|
+
choices: groups.map(g => {
|
|
229
|
+
const impact = ((g.count / totalErrorCount) * 100).toFixed(1);
|
|
230
|
+
const isHandled = fix_tracker_1.FixTracker.isHandled(g.hash);
|
|
231
|
+
const prefix = isHandled ? chalk_1.default.green('[JULES] ') : '';
|
|
232
|
+
return {
|
|
233
|
+
name: `${prefix}${chalk_1.default.yellow(g.hash)}: ${g.representativeError.message} (${g.count} logs - ${impact}%)`,
|
|
234
|
+
value: g.hash
|
|
235
|
+
};
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
]);
|
|
239
|
+
targetGroups = [groups.find(g => g.hash === selectedHash)];
|
|
240
|
+
}
|
|
241
|
+
// Prepare Jules Handover context (prompt once if multiple)
|
|
242
|
+
let selectedSource;
|
|
243
|
+
let selectedBranch;
|
|
244
|
+
if (config.jules && config.jules.token) {
|
|
245
|
+
const { JulesProvider } = require('../plugins/jules/jules-provider');
|
|
246
|
+
const julesSvc = new JulesProvider(config.jules.token);
|
|
247
|
+
console.log(chalk_1.default.blue('\nBridge: Connecting to Jules Service...'));
|
|
248
|
+
try {
|
|
249
|
+
const sources = await julesSvc.listSources();
|
|
250
|
+
if (sources.length === 0) {
|
|
251
|
+
console.warn(chalk_1.default.yellow('Warning: No Jules sources found. Automated handover skipped.'));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const sourceAnswer = await inquirer.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: 'list',
|
|
257
|
+
name: 'selectedSource',
|
|
258
|
+
message: 'Select the Jules source for this handover:',
|
|
259
|
+
loop: false,
|
|
260
|
+
choices: sources.map((s) => ({
|
|
261
|
+
name: s.id,
|
|
262
|
+
value: s.name
|
|
263
|
+
}))
|
|
264
|
+
}
|
|
265
|
+
]);
|
|
266
|
+
selectedSource = sourceAnswer.selectedSource;
|
|
267
|
+
const branchAnswer = await inquirer.prompt([
|
|
268
|
+
{
|
|
269
|
+
type: 'input',
|
|
270
|
+
name: 'selectedBranch',
|
|
271
|
+
message: 'Enter the target branch for Jules to fix:',
|
|
272
|
+
default: config.repo.branch || 'main'
|
|
273
|
+
}
|
|
274
|
+
]);
|
|
275
|
+
selectedBranch = branchAnswer.selectedBranch;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
console.error(chalk_1.default.red('\nFailed to connect to Jules Service:'), e.message);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const aiEngine = new ai_engine_1.AIEngine(aiProvider);
|
|
283
|
+
const { JulesProvider } = require('../plugins/jules/jules-provider');
|
|
284
|
+
const jules = (config.jules && config.jules.token) ? new JulesProvider(config.jules.token) : null;
|
|
285
|
+
for (const group of targetGroups) {
|
|
286
|
+
const impactPercentage = ((group.count / totalErrorCount) * 100).toFixed(1);
|
|
287
|
+
console.log(chalk_1.default.magenta.bold(`\n--- Processing Group: ${group.hash} ---`));
|
|
288
|
+
console.log(chalk_1.default.blue('Scouting and analyzing...'));
|
|
289
|
+
const result = await aiEngine.analyze(group, {});
|
|
290
|
+
if (result.usage) {
|
|
291
|
+
const { UsageTracker: Tracker } = require('../utils/usage-tracker');
|
|
292
|
+
Tracker.record(config.ai.provider, config.ai.model, result.usage.inputTokens, result.usage.outputTokens);
|
|
293
|
+
}
|
|
294
|
+
console.log(chalk_1.default.green('\nInsights Summary: ') + result.explanation.substring(0, 150) + '...');
|
|
295
|
+
if (jules && selectedSource && selectedBranch) {
|
|
296
|
+
const handoverPrompt = `
|
|
297
|
+
JULES HANDOVER REPORT
|
|
298
|
+
=====================
|
|
299
|
+
Error Hash: ${group.hash}
|
|
300
|
+
Impact: ${group.count} occurrences (${impactPercentage}%)
|
|
301
|
+
Message: ${group.representativeError.message}
|
|
302
|
+
|
|
303
|
+
Screener Insights:
|
|
304
|
+
${result.explanation}
|
|
305
|
+
|
|
306
|
+
Direction:
|
|
307
|
+
${result.suggestedFix}
|
|
308
|
+
`.trim();
|
|
309
|
+
console.log(chalk_1.default.blue('Submitting to Jules...'));
|
|
310
|
+
try {
|
|
311
|
+
const session = await jules.createSession(selectedSource, handoverPrompt, `Fix: ${group.representativeError.message}`, selectedBranch);
|
|
312
|
+
console.log(chalk_1.default.green(`✅ Session created: ${session.id}`));
|
|
313
|
+
fix_tracker_1.FixTracker.markAsHandled(group.hash);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
console.error(chalk_1.default.red(`❌ Failed to submit ${group.hash}:`), e.message);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
console.log(chalk_1.default.cyan('\nFix Direction: ') + result.suggestedFix.substring(0, 150) + '...');
|
|
321
|
+
console.log(chalk_1.default.yellow('Note: Automated handover skipped (Service not configured or selection cancelled).'));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
console.log(chalk_1.default.magenta.bold('\n=================================================='));
|
|
325
|
+
console.log(chalk_1.default.white(' Handover cycle complete. Jules is active. '));
|
|
326
|
+
console.log(chalk_1.default.magenta.bold('==================================================\n'));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error(chalk_1.default.red('Error:'), error.message);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
program
|
|
333
|
+
.command('usage-details')
|
|
334
|
+
.description('Show AI token usage and estimated costs')
|
|
335
|
+
.action(async () => {
|
|
336
|
+
const { UsageTracker } = require('../utils/usage-tracker');
|
|
337
|
+
const { PRICING, COST_DISCLAIMER } = require('../utils/pricing');
|
|
338
|
+
const usage = UsageTracker.load();
|
|
339
|
+
if (Object.keys(usage).length === 0) {
|
|
340
|
+
console.log(chalk_1.default.yellow('\nNo usage data found yet. Run "andur fix" to start tracking.\n'));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
console.log(chalk_1.default.blue('\n📊 Andur Usage & Cost Estimation\n'));
|
|
344
|
+
let totalCost = 0;
|
|
345
|
+
for (const [provider, models] of Object.entries(usage)) {
|
|
346
|
+
console.log(chalk_1.default.bold(`${provider.toUpperCase()}:`));
|
|
347
|
+
const modelEntries = Object.entries(models);
|
|
348
|
+
for (const [model, stats] of modelEntries) {
|
|
349
|
+
const pricing = PRICING[model] || { input: 0, output: 0 };
|
|
350
|
+
const inputTokens = stats.inputTokens || 0;
|
|
351
|
+
const outputTokens = stats.outputTokens || 0;
|
|
352
|
+
const inputCost = (inputTokens / 1000000) * pricing.input;
|
|
353
|
+
const outputCost = (outputTokens / 1000000) * pricing.output;
|
|
354
|
+
const modelTotal = inputCost + outputCost;
|
|
355
|
+
totalCost += modelTotal;
|
|
356
|
+
console.log(` - ${chalk_1.default.cyan(model)}:`);
|
|
357
|
+
console.log(` Tokens: ${inputTokens.toLocaleString()} in / ${outputTokens.toLocaleString()} out`);
|
|
358
|
+
console.log(` Estimated Cost: ${chalk_1.default.green('$' + modelTotal.toFixed(4))}`);
|
|
359
|
+
}
|
|
360
|
+
console.log('');
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk_1.default.bold(`TOTAL ESTIMATED COST: ${chalk_1.default.green('$' + totalCost.toFixed(4))}`));
|
|
363
|
+
console.log(chalk_1.default.gray(`\n${COST_DISCLAIMER}\n`));
|
|
364
|
+
});
|
|
365
|
+
program.parse(process.argv);
|
|
366
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AIEngine = void 0;
|
|
4
|
+
class AIEngine {
|
|
5
|
+
constructor(provider) {
|
|
6
|
+
this.provider = provider;
|
|
7
|
+
}
|
|
8
|
+
async analyze(errorGroup, fileContexts) {
|
|
9
|
+
const context = this.prepareContext(fileContexts);
|
|
10
|
+
return await this.provider.analyzeError(errorGroup, context);
|
|
11
|
+
}
|
|
12
|
+
async getInsights(groups) {
|
|
13
|
+
return await this.provider.generateInsights(groups);
|
|
14
|
+
}
|
|
15
|
+
prepareContext(fileContexts) {
|
|
16
|
+
let contextStr = 'Relevant code context:\n\n';
|
|
17
|
+
for (const [path, content] of Object.entries(fileContexts)) {
|
|
18
|
+
contextStr += `--- File: ${path} ---\n${content}\n\n`;
|
|
19
|
+
}
|
|
20
|
+
return contextStr;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.AIEngine = AIEngine;
|
|
@@ -0,0 +1,116 @@
|
|
|
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.ConfigLoader = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const validateConfig = (config) => {
|
|
40
|
+
if (!config.logs || !config.ai || !config.repo) {
|
|
41
|
+
throw new Error('Invalid config: missing logs, ai, or repo section');
|
|
42
|
+
}
|
|
43
|
+
return config;
|
|
44
|
+
};
|
|
45
|
+
class ConfigLoader {
|
|
46
|
+
static async loadConfig(configPath) {
|
|
47
|
+
const searchPaths = [
|
|
48
|
+
configPath,
|
|
49
|
+
path.join(process.cwd(), 'andur.config.js'),
|
|
50
|
+
path.join(process.cwd(), 'andur.config.ts'),
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
for (const p of searchPaths) {
|
|
53
|
+
if (fs.existsSync(p)) {
|
|
54
|
+
try {
|
|
55
|
+
const fullPath = path.resolve(p);
|
|
56
|
+
let module;
|
|
57
|
+
if (p.endsWith('.js')) {
|
|
58
|
+
module = require(fullPath);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
module = await Promise.resolve(`${`file://${fullPath}`}`).then(s => __importStar(require(s)));
|
|
62
|
+
}
|
|
63
|
+
const config = module.default || module.config || module;
|
|
64
|
+
// Inject environment variables if not present in config file
|
|
65
|
+
if (!config.jules)
|
|
66
|
+
config.jules = {};
|
|
67
|
+
if (!config.jules.token)
|
|
68
|
+
config.jules.token = process.env.ANDUR_JULES_API_TOKEN;
|
|
69
|
+
return validateConfig(config);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(`Failed to load config from ${p}:`, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Config file not found or invalid. Run "andur init" to create one.');
|
|
77
|
+
}
|
|
78
|
+
static buildQuery(config) {
|
|
79
|
+
const baseQuery = config.logs.query || 'SELECT * FROM TransactionError';
|
|
80
|
+
const since = config.logs.since ? ` SINCE ${config.logs.since}` : ' SINCE 1 day ago';
|
|
81
|
+
const limit = config.logs.limit ? ` LIMIT ${config.logs.limit}` : ' LIMIT 10';
|
|
82
|
+
return `${baseQuery}${since}${limit}`;
|
|
83
|
+
}
|
|
84
|
+
static generateDefaultConfig(data) {
|
|
85
|
+
return `/** @type {import('./src/types').AndurConfig} */
|
|
86
|
+
module.exports = {
|
|
87
|
+
logs: {
|
|
88
|
+
provider: '${data.logProvider}',
|
|
89
|
+
query: 'SELECT * FROM TransactionError',
|
|
90
|
+
since: '1 day ago',
|
|
91
|
+
limit: 10,
|
|
92
|
+
options: {
|
|
93
|
+
accountId: process.env.ANDUR_LOGS_ACCOUNT_ID,
|
|
94
|
+
apiKey: process.env.ANDUR_LOGS_API_KEY,
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
ai: {
|
|
98
|
+
provider: '${data.aiProvider}',
|
|
99
|
+
model: '${data.model}',
|
|
100
|
+
options: {
|
|
101
|
+
apiKey: process.env.ANDUR_AI_API_KEY,
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
repo: {
|
|
105
|
+
provider: '${data.repoProvider}',
|
|
106
|
+
path: './',
|
|
107
|
+
options: {}
|
|
108
|
+
},
|
|
109
|
+
jules: {
|
|
110
|
+
token: process.env.ANDUR_JULES_API_TOKEN,
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.ConfigLoader = ConfigLoader;
|
|
@@ -0,0 +1,65 @@
|
|
|
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.GroupingEngine = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
class GroupingEngine {
|
|
39
|
+
static group(errors, strategy = 'auto') {
|
|
40
|
+
const groups = new Map();
|
|
41
|
+
for (const error of errors) {
|
|
42
|
+
let hash = '';
|
|
43
|
+
if (strategy === 'message' || (strategy === 'auto' && !error.stackTrace)) {
|
|
44
|
+
hash = this.hashString(error.message);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Simple hash of first few lines of stack trace to avoid grouping variants of same error
|
|
48
|
+
const stackLines = error.stackTrace?.split('\n').slice(0, 3).join('\n') || error.message;
|
|
49
|
+
hash = this.hashString(stackLines);
|
|
50
|
+
}
|
|
51
|
+
const existing = groups.get(hash) || [];
|
|
52
|
+
groups.set(hash, [...existing, error]);
|
|
53
|
+
}
|
|
54
|
+
return Array.from(groups.entries()).map(([hash, errors]) => ({
|
|
55
|
+
hash,
|
|
56
|
+
errors,
|
|
57
|
+
representativeError: errors[0],
|
|
58
|
+
count: errors.length,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
static hashString(str) {
|
|
62
|
+
return crypto.createHash('md5').update(str).digest('hex').substring(0, 8);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.GroupingEngine = GroupingEngine;
|