@traisetech/autopilot 0.1.6 → 0.1.8
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/CHANGELOG.md +92 -67
- package/README.md +233 -2
- package/bin/autopilot.js +72 -68
- package/package.json +59 -58
- package/src/commands/doctor.js +121 -121
- package/src/commands/init.js +183 -129
- package/src/commands/insights.js +90 -0
- package/src/config/defaults.js +42 -36
- package/src/config/ignore.js +153 -136
- package/src/core/commit.js +321 -116
- package/src/core/focus.js +197 -0
- package/src/core/gemini.js +109 -0
- package/src/core/git.js +180 -154
- package/src/core/watcher.js +362 -274
- package/src/index.js +6 -0
- package/src/integrations/base.js +23 -0
- package/src/integrations/calendar.js +23 -0
- package/src/integrations/manager.js +48 -0
- package/src/utils/update-check.js +151 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini AI Integration for Autopilot
|
|
3
|
+
* Generates commit messages using the Gemini Pro model
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a commit message using Gemini API
|
|
12
|
+
* @param {string} diff - The git diff content
|
|
13
|
+
* @param {string} apiKey - Google Gemini API Key
|
|
14
|
+
* @returns {Promise<string>} Generated commit message
|
|
15
|
+
*/
|
|
16
|
+
async function generateAICommitMessage(diff, apiKey) {
|
|
17
|
+
if (!diff || !diff.trim()) {
|
|
18
|
+
return 'chore: update changes';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Truncate diff if it's too large (Gemini has token limits, though high)
|
|
22
|
+
// A safe limit might be around 30k chars for now to be safe and fast
|
|
23
|
+
const truncatedDiff = diff.length > 30000 ? diff.slice(0, 30000) + '\n...(truncated)' : diff;
|
|
24
|
+
|
|
25
|
+
const prompt = `
|
|
26
|
+
You are an expert software engineer.
|
|
27
|
+
Generate a concise, standardized commit message following the Conventional Commits specification based on the provided git diff.
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
1. Format: <type>(<scope>): <subject>
|
|
31
|
+
2. Keep the subject line under 72 characters.
|
|
32
|
+
3. If there are multiple changes, use a bulleted body.
|
|
33
|
+
4. Detect breaking changes and add "BREAKING CHANGE:" footer if necessary.
|
|
34
|
+
5. Use types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
|
|
35
|
+
6. Return ONLY the commit message, no explanations or markdown code blocks.
|
|
36
|
+
|
|
37
|
+
Diff:
|
|
38
|
+
${truncatedDiff}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
contents: [{
|
|
49
|
+
parts: [{
|
|
50
|
+
text: prompt
|
|
51
|
+
}]
|
|
52
|
+
}],
|
|
53
|
+
generationConfig: {
|
|
54
|
+
temperature: 0.2,
|
|
55
|
+
maxOutputTokens: 256,
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const errorData = await response.json().catch(() => ({}));
|
|
62
|
+
throw new Error(`Gemini API Error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
|
|
67
|
+
if (!data.candidates || data.candidates.length === 0 || !data.candidates[0].content) {
|
|
68
|
+
throw new Error('No response content from Gemini');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let message = data.candidates[0].content.parts[0].text.trim();
|
|
72
|
+
|
|
73
|
+
// Cleanup markdown if present (e.g. ```git commit ... ```)
|
|
74
|
+
message = message.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '').trim();
|
|
75
|
+
|
|
76
|
+
return message;
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error(`AI Generation failed: ${error.message}`);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate Gemini API Key
|
|
86
|
+
* @param {string} apiKey
|
|
87
|
+
* @returns {Promise<boolean>}
|
|
88
|
+
*/
|
|
89
|
+
async function validateApiKey(apiKey) {
|
|
90
|
+
try {
|
|
91
|
+
// Simple test call with empty prompt
|
|
92
|
+
const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
contents: [{ parts: [{ text: "Hi" }] }],
|
|
97
|
+
generationConfig: { maxOutputTokens: 1 }
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
return response.ok;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
generateAICommitMessage,
|
|
108
|
+
validateApiKey
|
|
109
|
+
};
|
package/src/core/git.js
CHANGED
|
@@ -1,154 +1,180 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git helper module - Clean, testable Git operations
|
|
3
|
-
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Get current branch name
|
|
10
|
-
* @param {string} root - Repository root path
|
|
11
|
-
* @returns {Promise<string|null>} Branch name or null on error
|
|
12
|
-
*/
|
|
13
|
-
async function getBranch(root) {
|
|
14
|
-
try {
|
|
15
|
-
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root });
|
|
16
|
-
return stdout.trim();
|
|
17
|
-
} catch (error) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check if repository has uncommitted changes
|
|
24
|
-
* @param {string} root - Repository root path
|
|
25
|
-
* @returns {Promise<boolean>} True if there are changes
|
|
26
|
-
*/
|
|
27
|
-
async function hasChanges(root) {
|
|
28
|
-
try {
|
|
29
|
-
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
|
|
30
|
-
return stdout.trim().length > 0;
|
|
31
|
-
} catch (error) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get porcelain status - parsed list of changed files
|
|
38
|
-
* @param {string} root - Repository root path
|
|
39
|
-
* @returns {Promise<{ok: boolean, files: string[], raw: string}>} Status object
|
|
40
|
-
*/
|
|
41
|
-
async function getPorcelainStatus(root) {
|
|
42
|
-
try {
|
|
43
|
-
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
|
|
44
|
-
const raw = stdout.trim();
|
|
45
|
-
|
|
46
|
-
if (!raw) {
|
|
47
|
-
return { ok: true, files: [], raw: '' };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const files = raw
|
|
51
|
-
.split(/\r?\n/)
|
|
52
|
-
.map(line =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Git helper module - Clean, testable Git operations
|
|
3
|
+
* Built by Praise Masunga (PraiseTechzw)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const execa = require('execa');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get current branch name
|
|
10
|
+
* @param {string} root - Repository root path
|
|
11
|
+
* @returns {Promise<string|null>} Branch name or null on error
|
|
12
|
+
*/
|
|
13
|
+
async function getBranch(root) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root });
|
|
16
|
+
return stdout.trim();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if repository has uncommitted changes
|
|
24
|
+
* @param {string} root - Repository root path
|
|
25
|
+
* @returns {Promise<boolean>} True if there are changes
|
|
26
|
+
*/
|
|
27
|
+
async function hasChanges(root) {
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
|
|
30
|
+
return stdout.trim().length > 0;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get porcelain status - parsed list of changed files
|
|
38
|
+
* @param {string} root - Repository root path
|
|
39
|
+
* @returns {Promise<{ok: boolean, files: string[], raw: string}>} Status object
|
|
40
|
+
*/
|
|
41
|
+
async function getPorcelainStatus(root) {
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: root });
|
|
44
|
+
const raw = stdout.trim();
|
|
45
|
+
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return { ok: true, files: [], raw: '' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = raw
|
|
51
|
+
.split(/\r?\n/)
|
|
52
|
+
.map(line => {
|
|
53
|
+
const status = line.slice(0, 2).trim();
|
|
54
|
+
const file = line.slice(3).trim();
|
|
55
|
+
return { status, file };
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return { ok: true, files, raw };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return { ok: false, files: [], raw: error.message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stage all changes (git add -A)
|
|
66
|
+
* @param {string} root - Repository root path
|
|
67
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
68
|
+
*/
|
|
69
|
+
async function addAll(root) {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout, stderr } = await execa('git', ['add', '-A'], { cwd: root });
|
|
72
|
+
return { ok: true, stdout, stderr };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Commit staged changes
|
|
80
|
+
* @param {string} root - Repository root path
|
|
81
|
+
* @param {string} message - Commit message
|
|
82
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
83
|
+
*/
|
|
84
|
+
async function commit(root, message) {
|
|
85
|
+
try {
|
|
86
|
+
const { stdout, stderr } = await execa('git', ['commit', '-m', message], { cwd: root });
|
|
87
|
+
return { ok: true, stdout, stderr };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Fetch updates from remote
|
|
95
|
+
* @param {string} root - Repository root path
|
|
96
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
97
|
+
*/
|
|
98
|
+
async function fetch(root) {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout, stderr } = await execa('git', ['fetch'], { cwd: root });
|
|
101
|
+
return { ok: true, stdout, stderr };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if remote is ahead/behind
|
|
109
|
+
* @param {string} root - Repository root path
|
|
110
|
+
* @returns {Promise<{ok: boolean, ahead: boolean, behind: boolean, raw: string}>} Status object
|
|
111
|
+
*/
|
|
112
|
+
async function isRemoteAhead(root) {
|
|
113
|
+
try {
|
|
114
|
+
const branch = await getBranch(root);
|
|
115
|
+
if (!branch) return { ok: false, ahead: false, behind: false, raw: 'No branch' };
|
|
116
|
+
|
|
117
|
+
// Ensure we have latest info
|
|
118
|
+
await fetch(root);
|
|
119
|
+
|
|
120
|
+
const { stdout } = await execa('git', ['rev-list', '--left-right', '--count', `${branch}...origin/${branch}`], { cwd: root });
|
|
121
|
+
const [aheadCount, behindCount] = stdout.trim().split(/\s+/).map(Number);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
ahead: aheadCount > 0,
|
|
126
|
+
behind: behindCount > 0,
|
|
127
|
+
raw: stdout.trim()
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return { ok: false, ahead: false, behind: false, raw: error.message };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Push changes to remote
|
|
136
|
+
* @param {string} root - Repository root path
|
|
137
|
+
* @param {string} [branch] - Branch to push (optional, defaults to current)
|
|
138
|
+
* @returns {Promise<{ok: boolean, stdout: string, stderr: string}>} Result object
|
|
139
|
+
*/
|
|
140
|
+
async function push(root, branch) {
|
|
141
|
+
try {
|
|
142
|
+
const targetBranch = branch || await getBranch(root);
|
|
143
|
+
if (!targetBranch) throw new Error('Could not determine branch to push');
|
|
144
|
+
|
|
145
|
+
const { stdout, stderr } = await execa('git', ['push', 'origin', targetBranch], { cwd: root });
|
|
146
|
+
return { ok: true, stdout, stderr };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { ok: false, stdout: '', stderr: error.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get diff of changes
|
|
154
|
+
* @param {string} root - Repository root path
|
|
155
|
+
* @param {boolean} staged - Whether to get staged diff (default: true)
|
|
156
|
+
* @returns {Promise<string>} Diff content
|
|
157
|
+
*/
|
|
158
|
+
async function getDiff(root, staged = true) {
|
|
159
|
+
try {
|
|
160
|
+
const args = ['diff'];
|
|
161
|
+
if (staged) args.push('--cached');
|
|
162
|
+
args.push('-U3');
|
|
163
|
+
const { stdout } = await execa('git', args, { cwd: root });
|
|
164
|
+
return stdout || '';
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
getBranch,
|
|
172
|
+
hasChanges,
|
|
173
|
+
getPorcelainStatus,
|
|
174
|
+
addAll,
|
|
175
|
+
commit,
|
|
176
|
+
fetch,
|
|
177
|
+
isRemoteAhead,
|
|
178
|
+
push,
|
|
179
|
+
getDiff
|
|
180
|
+
};
|