@traisetech/autopilot 2.0.0 → 2.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/CHANGELOG.md +32 -5
- package/README.md +201 -214
- package/bin/autopilot.js +9 -1
- package/docs/DESIGN_PRINCIPLES.md +58 -0
- package/package.json +69 -69
- package/src/commands/config.js +110 -0
- package/src/commands/dashboard.mjs +13 -8
- package/src/commands/init.js +29 -7
- package/src/commands/insights.js +72 -32
- package/src/commands/leaderboard.js +47 -7
- package/src/config/defaults.js +5 -2
- package/src/config/ignore.js +10 -10
- package/src/config/loader.js +36 -10
- package/src/core/commit.js +41 -6
- package/src/core/events.js +105 -0
- package/src/core/git.js +38 -1
- package/src/core/grok.js +109 -0
- package/src/core/safety.js +6 -0
- package/src/core/watcher.js +30 -2
- package/src/utils/crypto.js +18 -0
- package/src/utils/identity.js +41 -0
- package/src/utils/paths.js +3 -0
package/src/config/loader.js
CHANGED
|
@@ -2,27 +2,52 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const logger = require('../utils/logger');
|
|
4
4
|
const { DEFAULT_CONFIG } = require('./defaults');
|
|
5
|
-
const { getConfigPath } = require('../utils/paths');
|
|
5
|
+
const { getConfigPath, getConfigDir, ensureConfigDir } = require('../utils/paths');
|
|
6
|
+
|
|
7
|
+
const getGlobalConfigPath = () => path.join(getConfigDir(), 'config.json');
|
|
6
8
|
|
|
7
9
|
const loadConfig = async (repoPath) => {
|
|
10
|
+
let config = { ...DEFAULT_CONFIG };
|
|
11
|
+
|
|
12
|
+
// 1. Load Global Config
|
|
13
|
+
try {
|
|
14
|
+
const globalPath = getGlobalConfigPath();
|
|
15
|
+
if (await fs.pathExists(globalPath)) {
|
|
16
|
+
const globalConfig = await fs.readJson(globalPath);
|
|
17
|
+
config = { ...config, ...globalConfig };
|
|
18
|
+
logger.debug(`Loaded global config from ${globalPath}`);
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
logger.warn(`Error loading global config: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Load Local Config (Overrides Global)
|
|
8
25
|
const configPath = getConfigPath(repoPath);
|
|
9
26
|
try {
|
|
10
27
|
if (await fs.pathExists(configPath)) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
const localConfig = await fs.readJson(configPath);
|
|
29
|
+
config = { ...config, ...localConfig };
|
|
30
|
+
logger.debug(`Loaded local config from ${configPath}`);
|
|
14
31
|
}
|
|
15
32
|
} catch (error) {
|
|
16
|
-
logger.warn(`Error loading config: ${error.message}`);
|
|
33
|
+
logger.warn(`Error loading local config: ${error.message}`);
|
|
17
34
|
}
|
|
18
|
-
|
|
35
|
+
|
|
36
|
+
return config;
|
|
19
37
|
};
|
|
20
38
|
|
|
21
|
-
const saveConfig = async (repoPath, config) => {
|
|
22
|
-
const configPath = getConfigPath(repoPath);
|
|
39
|
+
const saveConfig = async (repoPath, config, isGlobal = false) => {
|
|
23
40
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
let targetPath;
|
|
42
|
+
if (isGlobal) {
|
|
43
|
+
await ensureConfigDir();
|
|
44
|
+
targetPath = getGlobalConfigPath();
|
|
45
|
+
} else {
|
|
46
|
+
targetPath = getConfigPath(repoPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await fs.writeJson(targetPath, config, { spaces: 2 });
|
|
50
|
+
logger.success(`Config saved to ${targetPath}`);
|
|
26
51
|
} catch (error) {
|
|
27
52
|
logger.error(`Failed to save config: ${error.message}`);
|
|
28
53
|
}
|
|
@@ -44,4 +69,5 @@ module.exports = {
|
|
|
44
69
|
loadConfig,
|
|
45
70
|
saveConfig,
|
|
46
71
|
createDefaultConfig,
|
|
72
|
+
getGlobalConfigPath
|
|
47
73
|
};
|
package/src/core/commit.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const logger = require('../utils/logger');
|
|
8
|
-
const
|
|
8
|
+
const gemini = require('./gemini');
|
|
9
|
+
const grok = require('./grok');
|
|
9
10
|
const HistoryManager = require('./history');
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -19,13 +20,22 @@ async function generateCommitMessage(files, diffContent, config = {}) {
|
|
|
19
20
|
let message = '';
|
|
20
21
|
if (!files || files.length === 0) {
|
|
21
22
|
message = 'chore: update changes';
|
|
22
|
-
} else if (config.commitMessageMode === 'ai' && config.ai?.enabled
|
|
23
|
+
} else if (config.commitMessageMode === 'ai' && config.ai?.enabled) {
|
|
23
24
|
// AI Mode
|
|
24
25
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const provider = config.ai.provider || 'gemini';
|
|
27
|
+
logger.info(`Generating AI commit message using ${provider}...`);
|
|
28
|
+
|
|
29
|
+
if (provider === 'grok') {
|
|
30
|
+
if (!config.ai.grokApiKey) throw new Error('Grok API Key not configured');
|
|
31
|
+
message = await grok.generateGrokCommitMessage(diffContent, config.ai.grokApiKey, config.ai.grokModel);
|
|
32
|
+
} else {
|
|
33
|
+
// Default to Gemini
|
|
34
|
+
if (!config.ai.apiKey) throw new Error('Gemini API Key not configured');
|
|
35
|
+
message = await gemini.generateAICommitMessage(diffContent, config.ai.apiKey, config.ai.model);
|
|
36
|
+
}
|
|
27
37
|
} catch (error) {
|
|
28
|
-
logger.warn(
|
|
38
|
+
logger.warn(`AI generation failed (${error.message}), falling back to smart generation.`);
|
|
29
39
|
message = generateSmartCommitMessage(files, diffContent);
|
|
30
40
|
}
|
|
31
41
|
} else {
|
|
@@ -340,7 +350,32 @@ function generateBody(analysis, files) {
|
|
|
340
350
|
return bullets;
|
|
341
351
|
}
|
|
342
352
|
|
|
353
|
+
async function addTrailers(message) {
|
|
354
|
+
const { getIdentity } = require('../utils/identity');
|
|
355
|
+
const { generateSignature } = require('../utils/crypto');
|
|
356
|
+
const { version } = require('../../package.json');
|
|
357
|
+
|
|
358
|
+
const identity = await getIdentity();
|
|
359
|
+
const timestamp = Date.now().toString();
|
|
360
|
+
|
|
361
|
+
// Content to sign: message|timestamp|version|userId
|
|
362
|
+
// This ensures integrity of the whole commit data
|
|
363
|
+
const contentToSign = `${message}|${timestamp}|${version}|${identity.id}`;
|
|
364
|
+
const signature = generateSignature(contentToSign, identity.id);
|
|
365
|
+
|
|
366
|
+
const trailers = [
|
|
367
|
+
'',
|
|
368
|
+
'Autopilot-Commit: true',
|
|
369
|
+
`Autopilot-Version: ${version}`,
|
|
370
|
+
`Autopilot-User: ${identity.id}`,
|
|
371
|
+
`Autopilot-Signature: ${signature}`
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
return message + trailers.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
343
377
|
module.exports = {
|
|
344
378
|
generateCommitMessage,
|
|
345
|
-
parseDiff
|
|
379
|
+
parseDiff,
|
|
380
|
+
addTrailers
|
|
346
381
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { getConfigDir } = require('../utils/paths');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
const getQueuePath = () => path.join(getConfigDir(), 'events-queue.json');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Telemetry/Event System for Autopilot
|
|
10
|
+
* Handles reliable event emission with local queuing
|
|
11
|
+
*/
|
|
12
|
+
class EventSystem {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.queue = [];
|
|
15
|
+
this.isFlushing = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async loadQueue() {
|
|
19
|
+
try {
|
|
20
|
+
const queuePath = getQueuePath();
|
|
21
|
+
if (await fs.pathExists(queuePath)) {
|
|
22
|
+
this.queue = await fs.readJson(queuePath);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
logger.debug(`Failed to load event queue: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async saveQueue() {
|
|
30
|
+
try {
|
|
31
|
+
await fs.ensureDir(getConfigDir());
|
|
32
|
+
await fs.writeJson(getQueuePath(), this.queue, { spaces: 2 });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error(`Failed to save event queue: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Queue an event for emission
|
|
40
|
+
* @param {object} event - Event payload
|
|
41
|
+
*/
|
|
42
|
+
async emit(event) {
|
|
43
|
+
await this.loadQueue();
|
|
44
|
+
|
|
45
|
+
// Add metadata
|
|
46
|
+
const enrichedEvent = {
|
|
47
|
+
...event,
|
|
48
|
+
queuedAt: Date.now(),
|
|
49
|
+
retryCount: 0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this.queue.push(enrichedEvent);
|
|
53
|
+
await this.saveQueue();
|
|
54
|
+
|
|
55
|
+
// Try to flush immediately
|
|
56
|
+
this.flush();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Attempt to send queued events to backend
|
|
61
|
+
*/
|
|
62
|
+
async flush() {
|
|
63
|
+
if (this.isFlushing || this.queue.length === 0) return;
|
|
64
|
+
this.isFlushing = true;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const remaining = [];
|
|
68
|
+
|
|
69
|
+
for (const event of this.queue) {
|
|
70
|
+
try {
|
|
71
|
+
await this.sendToBackend(event);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Keep in queue if failed
|
|
74
|
+
event.retryCount++;
|
|
75
|
+
remaining.push(event);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.queue = remaining;
|
|
80
|
+
await this.saveQueue();
|
|
81
|
+
} finally {
|
|
82
|
+
this.isFlushing = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mock backend transmission
|
|
88
|
+
* In production, this would POST to an API
|
|
89
|
+
*/
|
|
90
|
+
async sendToBackend(event) {
|
|
91
|
+
// Simulate network delay
|
|
92
|
+
// await new Promise(resolve => setTimeout(resolve, 100));
|
|
93
|
+
|
|
94
|
+
// For now, we just log that we would have sent it
|
|
95
|
+
// logger.debug(`[Telemetry] Event emitted: ${event.type}`);
|
|
96
|
+
|
|
97
|
+
// In a real implementation:
|
|
98
|
+
// const response = await fetch('https://api.autopilot.com/events', { ... });
|
|
99
|
+
// if (!response.ok) throw new Error('Network error');
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = new EventSystem();
|
package/src/core/git.js
CHANGED
|
@@ -241,6 +241,42 @@ async function getLatestCommitHash(root) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
const fs = require('fs-extra');
|
|
245
|
+
const path = require('path');
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if repository is in a merge/rebase/cherry-pick state
|
|
249
|
+
* @param {string} root - Repository root path
|
|
250
|
+
* @returns {Promise<boolean>} True if operation in progress
|
|
251
|
+
*/
|
|
252
|
+
async function isMergeInProgress(root) {
|
|
253
|
+
try {
|
|
254
|
+
const gitDir = path.join(root, '.git');
|
|
255
|
+
const files = [
|
|
256
|
+
'MERGE_HEAD',
|
|
257
|
+
'REBASE_HEAD',
|
|
258
|
+
'CHERRY_PICK_HEAD',
|
|
259
|
+
'REVERT_HEAD',
|
|
260
|
+
'BISECT_LOG'
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// Check if .git/rebase-merge or .git/rebase-apply exists (directory check)
|
|
264
|
+
if (await fs.pathExists(path.join(gitDir, 'rebase-merge')) ||
|
|
265
|
+
await fs.pathExists(path.join(gitDir, 'rebase-apply'))) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
if (await fs.pathExists(path.join(gitDir, file))) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
244
280
|
module.exports = {
|
|
245
281
|
getBranch,
|
|
246
282
|
hasChanges,
|
|
@@ -255,5 +291,6 @@ module.exports = {
|
|
|
255
291
|
revert,
|
|
256
292
|
resetSoft,
|
|
257
293
|
commitExists,
|
|
258
|
-
getLatestCommitHash
|
|
294
|
+
getLatestCommitHash,
|
|
295
|
+
isMergeInProgress
|
|
259
296
|
};
|
package/src/core/grok.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xAI Grok Integration for Autopilot
|
|
3
|
+
* Generates commit messages using the Grok API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
const BASE_API_URL = 'https://api.x.ai/v1/chat/completions';
|
|
9
|
+
const DEFAULT_MODEL = 'grok-beta'; // or current stable model
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a commit message using Grok API
|
|
13
|
+
* @param {string} diff - The git diff content
|
|
14
|
+
* @param {string} apiKey - xAI API Key
|
|
15
|
+
* @param {string} [model] - Grok Model ID
|
|
16
|
+
* @returns {Promise<string>} Generated commit message
|
|
17
|
+
*/
|
|
18
|
+
async function generateGrokCommitMessage(diff, apiKey, model = DEFAULT_MODEL) {
|
|
19
|
+
if (!diff || !diff.trim()) {
|
|
20
|
+
return 'chore: update changes';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Truncate diff to avoid token limits (safe limit)
|
|
24
|
+
const truncatedDiff = diff.length > 30000 ? diff.slice(0, 30000) + '\n...(truncated)' : diff;
|
|
25
|
+
|
|
26
|
+
const systemPrompt = `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
|
+
try {
|
|
38
|
+
const response = await fetch(BASE_API_URL, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Authorization': `Bearer ${apiKey}`
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model: model,
|
|
46
|
+
messages: [
|
|
47
|
+
{ role: 'system', content: systemPrompt },
|
|
48
|
+
{ role: 'user', content: `Diff:\n${truncatedDiff}` }
|
|
49
|
+
],
|
|
50
|
+
temperature: 0.2,
|
|
51
|
+
stream: false
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const errorData = await response.json().catch(() => ({}));
|
|
57
|
+
throw new Error(`Grok API Error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
|
|
62
|
+
if (!data.choices || data.choices.length === 0 || !data.choices[0].message) {
|
|
63
|
+
throw new Error('No response content from Grok');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let message = data.choices[0].message.content.trim();
|
|
67
|
+
|
|
68
|
+
// Cleanup markdown if present
|
|
69
|
+
message = message.replace(/^```[a-z]*\n?/, '').replace(/\n?```$/, '').trim();
|
|
70
|
+
|
|
71
|
+
return message;
|
|
72
|
+
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.error(`Grok Generation failed: ${error.message}`);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate Grok API Key
|
|
81
|
+
* @param {string} apiKey
|
|
82
|
+
* @returns {Promise<{valid: boolean, error?: string}>}
|
|
83
|
+
*/
|
|
84
|
+
async function validateGrokApiKey(apiKey) {
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(BASE_API_URL, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
'Authorization': `Bearer ${apiKey}`
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
model: DEFAULT_MODEL,
|
|
94
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
95
|
+
max_tokens: 1
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (response.ok) return { valid: true };
|
|
100
|
+
return { valid: false, error: `Status: ${response.status}` };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { valid: false, error: error.message };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
generateGrokCommitMessage,
|
|
108
|
+
validateGrokApiKey
|
|
109
|
+
};
|
package/src/core/safety.js
CHANGED
|
@@ -56,6 +56,12 @@ const validateBeforeCommit = async (repoPath, config) => {
|
|
|
56
56
|
return { ok: true, errors: [] };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// 0. Merge/Rebase Safety Check (Hard Guarantee)
|
|
60
|
+
const isMerge = await git.isMergeInProgress(repoPath);
|
|
61
|
+
if (isMerge) {
|
|
62
|
+
return { ok: false, errors: ['Repository is in a merge/rebase state. Autopilot paused for safety.'] };
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
try {
|
|
60
66
|
// Get staged files
|
|
61
67
|
const statusObj = await git.getPorcelainStatus(repoPath);
|
package/src/core/watcher.js
CHANGED
|
@@ -10,7 +10,10 @@ const readline = require('readline');
|
|
|
10
10
|
const logger = require('../utils/logger');
|
|
11
11
|
const git = require('./git');
|
|
12
12
|
const FocusEngine = require('./focus');
|
|
13
|
-
const { generateCommitMessage } = require('./commit');
|
|
13
|
+
const { generateCommitMessage, addTrailers } = require('./commit');
|
|
14
|
+
const eventSystem = require('./events');
|
|
15
|
+
const { getIdentity } = require('../utils/identity');
|
|
16
|
+
const { version } = require('../../package.json');
|
|
14
17
|
const { savePid, removePid, registerProcessHandlers } = require('../utils/process');
|
|
15
18
|
const { loadConfig } = require('../config/loader');
|
|
16
19
|
const { readIgnoreFile, createIgnoredFilter, normalizePath } = require('../config/ignore');
|
|
@@ -356,6 +359,9 @@ class Watcher {
|
|
|
356
359
|
message = approval.message;
|
|
357
360
|
}
|
|
358
361
|
|
|
362
|
+
// Add Trust/Attribution Trailers
|
|
363
|
+
message = await addTrailers(message);
|
|
364
|
+
|
|
359
365
|
const commitResult = await git.commit(this.repoPath, message);
|
|
360
366
|
|
|
361
367
|
if (commitResult.ok) {
|
|
@@ -388,9 +394,31 @@ class Watcher {
|
|
|
388
394
|
logger.info('Pushing to remote...');
|
|
389
395
|
const pushResult = await git.push(this.repoPath);
|
|
390
396
|
if (!pushResult.ok) {
|
|
391
|
-
logger.warn(`Push failed
|
|
397
|
+
logger.warn(`Push failed: ${pushResult.stderr}`);
|
|
398
|
+
|
|
399
|
+
// Safety: Pause on critical push failures (Auth, Permissions, or Persistent errors)
|
|
400
|
+
// This aligns with "Failure Behavior: If a push fails -> pause watcher"
|
|
401
|
+
logger.error('Push failed! Pausing Autopilot to prevent issues.');
|
|
402
|
+
this.stateManager.pause(`Push failed: ${pushResult.stderr.split('\n')[0]}`);
|
|
403
|
+
logger.info('Run "autopilot resume" to restart after fixing the issue.');
|
|
392
404
|
} else {
|
|
393
405
|
logger.success('Push complete');
|
|
406
|
+
|
|
407
|
+
// Emit Event for Leaderboard/Telemetry
|
|
408
|
+
try {
|
|
409
|
+
const identity = await getIdentity();
|
|
410
|
+
const latestHash = await git.getLatestCommitHash(this.repoPath);
|
|
411
|
+
|
|
412
|
+
await eventSystem.emit({
|
|
413
|
+
type: 'push_success',
|
|
414
|
+
userId: identity.id,
|
|
415
|
+
commitHash: latestHash,
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
version: version
|
|
418
|
+
});
|
|
419
|
+
} catch (err) {
|
|
420
|
+
logger.debug(`Failed to emit push event: ${err.message}`);
|
|
421
|
+
}
|
|
394
422
|
}
|
|
395
423
|
}
|
|
396
424
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate HMAC signature for commit verification
|
|
5
|
+
* @param {string} content - Content to sign (message + timestamp + version)
|
|
6
|
+
* @param {string} secret - Secret key (using anonymous ID as salt for now)
|
|
7
|
+
* @returns {string} HMAC SHA256 signature
|
|
8
|
+
*/
|
|
9
|
+
function generateSignature(content, secret) {
|
|
10
|
+
return crypto
|
|
11
|
+
.createHmac('sha256', secret)
|
|
12
|
+
.update(content)
|
|
13
|
+
.digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
generateSignature
|
|
18
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { getConfigDir } = require('./paths');
|
|
5
|
+
const logger = require('./logger');
|
|
6
|
+
|
|
7
|
+
const getIdentityPath = () => path.join(getConfigDir(), 'identity.json');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get or create the anonymous user identity
|
|
11
|
+
* @returns {Promise<{id: string, created: number}>} Identity object
|
|
12
|
+
*/
|
|
13
|
+
async function getIdentity() {
|
|
14
|
+
try {
|
|
15
|
+
const identityPath = getIdentityPath();
|
|
16
|
+
|
|
17
|
+
if (await fs.pathExists(identityPath)) {
|
|
18
|
+
return await fs.readJson(identityPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create new identity
|
|
22
|
+
const identity = {
|
|
23
|
+
id: crypto.randomUUID(),
|
|
24
|
+
created: Date.now()
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
await fs.ensureDir(getConfigDir());
|
|
28
|
+
await fs.writeJson(identityPath, identity, { spaces: 2 });
|
|
29
|
+
logger.debug(`Created new anonymous identity: ${identity.id}`);
|
|
30
|
+
|
|
31
|
+
return identity;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.error(`Failed to manage identity: ${error.message}`);
|
|
34
|
+
// Fallback to memory-only ID if filesystem fails
|
|
35
|
+
return { id: crypto.randomUUID(), created: Date.now() };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
getIdentity
|
|
41
|
+
};
|
package/src/utils/paths.js
CHANGED
|
@@ -39,6 +39,9 @@ const getGitPath = (repoPath) => {
|
|
|
39
39
|
* @returns {string} Config directory path
|
|
40
40
|
*/
|
|
41
41
|
const getConfigDir = () => {
|
|
42
|
+
if (process.env.AUTOPILOT_CONFIG_DIR) {
|
|
43
|
+
return process.env.AUTOPILOT_CONFIG_DIR;
|
|
44
|
+
}
|
|
42
45
|
return path.join(os.homedir(), '.autopilot');
|
|
43
46
|
};
|
|
44
47
|
|