@traisetech/autopilot 2.1.0 → 2.2.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 +60 -0
- package/bin/autopilot.js +9 -1
- package/docs/CONFIGURATION.md +12 -9
- package/docs/DESIGN_PRINCIPLES.md +62 -6
- package/package.json +1 -1
- package/src/commands/dashboard.mjs +16 -12
- package/src/commands/doctor.js +42 -4
- package/src/commands/init.js +21 -21
- package/src/commands/insights.js +33 -67
- package/src/commands/interactive.js +40 -0
- package/src/commands/leaderboard.js +9 -10
- package/src/config/defaults.js +8 -7
- package/src/config/loader.js +7 -0
- package/src/core/commit.js +19 -7
- package/src/core/events.js +16 -11
- package/src/core/gemini.js +15 -7
- package/src/core/git.js +6 -5
- package/src/core/grok.js +140 -44
- package/src/core/keys.js +77 -0
- package/src/core/watcher.js +15 -1
- package/src/index.js +70 -64
- package/src/utils/logger.js +7 -7
- package/src/utils/obfuscator.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.2.0] - 2026-02-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Automatic Leaderboard Sync**:
|
|
7
|
+
- Watcher auto-syncs stats after commit or push.
|
|
8
|
+
- Uses site API with anonymized ID and metrics only.
|
|
9
|
+
- **Durable Backend Storage**:
|
|
10
|
+
- Website API backed by Supabase for persistent leaderboard and event telemetry.
|
|
11
|
+
- **Events API**:
|
|
12
|
+
- CLI emits `push_success` events with commit hash and identity.
|
|
13
|
+
- Website ingests and stores normalized payloads.
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
- **Config Consistency**:
|
|
17
|
+
- Standardized `blockedBranches` with backward-compat mapping.
|
|
18
|
+
- **AI Network Resilience**:
|
|
19
|
+
- Added request timeouts to Gemini/Grok; doctor validates connectivity.
|
|
20
|
+
- **Programmatic Imports**:
|
|
21
|
+
- Fixed command imports wiring in `src/index.js`.
|
|
22
|
+
|
|
23
|
+
### Docs/Website
|
|
24
|
+
- **OG Image & Favicon**:
|
|
25
|
+
- Added `public/og-image.svg` and `public/favicon.svg`.
|
|
26
|
+
- Corrected manifest path to `/manifest.webmanifest`.
|
|
27
|
+
- **Foreground Watcher Wording**:
|
|
28
|
+
- Updated homepage and commands to reflect foreground behavior.
|
|
29
|
+
|
|
30
|
+
### Tests
|
|
31
|
+
- **Grok Test Coverage**:
|
|
32
|
+
- Added parity tests mirroring Gemini scenarios.
|
|
33
|
+
|
|
3
34
|
All notable changes to this project will be documented in this file.
|
|
4
35
|
This project follows [Semantic Versioning](https://semver.org).
|
|
5
36
|
|
|
37
|
+
## [2.1.1] - 2026-02-11
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- **Global Leaderboard Sync**:
|
|
41
|
+
- New `autopilot leaderboard --sync` command to share productivity metrics with the global community.
|
|
42
|
+
- Implemented secure, anonymized data transmission (metrics only, no code).
|
|
43
|
+
- **Dynamic Leaderboard Dashboard**:
|
|
44
|
+
- Completely redesigned `autopilot-docs` leaderboard with real-time analytics.
|
|
45
|
+
- Real-time aggregation of global commits, focus hours, and active streaks.
|
|
46
|
+
- Premium glassmorphism UI with live ranking updates.
|
|
47
|
+
|
|
48
|
+
### Improved
|
|
49
|
+
- **CLI Aesthetics**:
|
|
50
|
+
- Integrated full ANSI color support for the `logger` utility for more professional output.
|
|
51
|
+
- Improved visual hierarchy with bold section headers and color-coded status icons.
|
|
52
|
+
- **Diagnostics**:
|
|
53
|
+
- Enhanced `doctor` command to intelligently check for `credential.helper` validation on HTTPS remotes.
|
|
54
|
+
- **Insights Portability**:
|
|
55
|
+
- Refined Git log parsing to be more robust across different Git versions and configurations.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- **Dashboard Stability**:
|
|
59
|
+
- Fixed "Duplicate Key" warning in the interactive React Ink dashboard.
|
|
60
|
+
- Added TTY-detection to prevent crashes in non-interactive environments (CI/CD, background).
|
|
61
|
+
- Fixed ESM/CommonJS compatibility issues using dynamic `import()` for the dashboard.
|
|
62
|
+
- **Test Integrity**:
|
|
63
|
+
- Implemented `AUTOPILOT_TEST_MODE` bypass for automated dashboard verification.
|
|
64
|
+
- Fixed cross-test contamination and EBUSY errors on Windows platforms.
|
|
65
|
+
|
|
6
66
|
## [2.1.0] - 2026-02-08
|
|
7
67
|
|
|
8
68
|
### Added
|
package/bin/autopilot.js
CHANGED
|
@@ -13,6 +13,7 @@ const { leaderboard } = require('../src/commands/leaderboard');
|
|
|
13
13
|
const doctor = require('../src/commands/doctor');
|
|
14
14
|
const presetCommand = require('../src/commands/preset');
|
|
15
15
|
const configCommand = require('../src/commands/config');
|
|
16
|
+
const interactiveCommand = require('../src/commands/interactive');
|
|
16
17
|
const pkg = require('../package.json');
|
|
17
18
|
const logger = require('../src/utils/logger');
|
|
18
19
|
const { checkForUpdate } = require('../src/utils/update-check');
|
|
@@ -30,7 +31,8 @@ const commands = {
|
|
|
30
31
|
leaderboard: leaderboard,
|
|
31
32
|
doctor: doctor,
|
|
32
33
|
preset: presetCommand,
|
|
33
|
-
config: configCommand
|
|
34
|
+
config: configCommand,
|
|
35
|
+
interactive: interactiveCommand
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
// Runtime assertion to prevent wiring errors
|
|
@@ -122,6 +124,12 @@ program
|
|
|
122
124
|
.option('-g, --global', 'Use global configuration')
|
|
123
125
|
.action(configCommand);
|
|
124
126
|
|
|
127
|
+
program
|
|
128
|
+
.command('interactive [on|off]')
|
|
129
|
+
.description('Toggle AI Safety Mode (on = prompt, off = automated)')
|
|
130
|
+
.option('-g, --global', 'Set the preference globally')
|
|
131
|
+
.action(interactiveCommand);
|
|
132
|
+
|
|
125
133
|
program
|
|
126
134
|
.command('doctor')
|
|
127
135
|
.description('Diagnose and validate autopilot setup')
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -49,7 +49,7 @@ This document reflects the current `.autopilotrc.json` options.
|
|
|
49
49
|
- **Default:** true
|
|
50
50
|
- **Description:** Push to `origin/<branch>` after commit.
|
|
51
51
|
|
|
52
|
-
### `
|
|
52
|
+
### `blockedBranches`
|
|
53
53
|
- **Type:** string[]
|
|
54
54
|
- **Default:** `["main", "master"]`
|
|
55
55
|
- **Description:** Branches where auto-commit is disabled.
|
|
@@ -65,24 +65,27 @@ This document reflects the current `.autopilotrc.json` options.
|
|
|
65
65
|
- **Description:** Shell commands executed sequentially when `requireChecks` is true.
|
|
66
66
|
|
|
67
67
|
### `commitMessageMode`
|
|
68
|
-
- **Type:** `"smart" | "simple"`
|
|
68
|
+
- **Type:** `"smart" | "simple" | "ai"`
|
|
69
69
|
- **Default:** `"smart"`
|
|
70
|
-
- **Description:**
|
|
70
|
+
- **Description:**
|
|
71
|
+
- smart: file/diff-based conventional messages
|
|
72
|
+
- simple: `chore: update changes`
|
|
73
|
+
- ai: uses configured AI provider (Gemini or Grok)
|
|
71
74
|
|
|
72
75
|
### `teamMode`
|
|
73
76
|
- **Type:** boolean
|
|
74
77
|
- **Default:** `false`
|
|
75
78
|
- **Description:** Enables pull-before-push and stricter conflict handling. Recommended for collaborative environments.
|
|
76
79
|
|
|
77
|
-
### `
|
|
78
|
-
- **Type:**
|
|
79
|
-
- **Default:** `
|
|
80
|
-
- **Description:**
|
|
80
|
+
### `preCommitChecks.fileSize`
|
|
81
|
+
- **Type:** boolean
|
|
82
|
+
- **Default:** `true`
|
|
83
|
+
- **Description:** Prevent commits containing files larger than 50MB.
|
|
81
84
|
|
|
82
|
-
### `
|
|
85
|
+
### `preCommitChecks.secrets`
|
|
83
86
|
- **Type:** boolean
|
|
84
87
|
- **Default:** `true`
|
|
85
|
-
- **Description:**
|
|
88
|
+
- **Description:** Secret scan for common key/token patterns before committing.
|
|
86
89
|
|
|
87
90
|
---
|
|
88
91
|
|
|
@@ -40,19 +40,75 @@ All automation is **reversible** via `autopilot undo`.
|
|
|
40
40
|
- **AI (Gemini / Grok)** is an assistant, never an authority.
|
|
41
41
|
- AI output must be reviewable, overridable, and optional.
|
|
42
42
|
|
|
43
|
+
## Privacy & Local-First Design
|
|
44
|
+
|
|
45
|
+
**Privacy Guarantees:**
|
|
46
|
+
- Your source code **never** leaves your machine
|
|
47
|
+
- No code diffs are transmitted externally
|
|
48
|
+
- No file contents are sent to remote servers
|
|
49
|
+
- AI commit message generation happens with metadata only (file paths, line counts, not actual code)
|
|
50
|
+
|
|
51
|
+
**Local-First Architecture:**
|
|
52
|
+
- Works 100% offline (except for git push operations)
|
|
53
|
+
- No authentication to external services required
|
|
54
|
+
- All data stored locally in your project
|
|
55
|
+
- Configuration is local and version-controllable
|
|
56
|
+
|
|
43
57
|
## Leaderboard & Metrics
|
|
44
58
|
|
|
45
59
|
- Metrics are derived **only** from local Git activity created by Autopilot.
|
|
46
60
|
- **No raw code, diffs, or file contents are ever transmitted.**
|
|
47
61
|
- Leaderboard data is:
|
|
48
|
-
- opt-in
|
|
62
|
+
- opt-in (disabled by default)
|
|
49
63
|
- anonymized or pseudonymous
|
|
50
64
|
- explainable (users know exactly what is counted)
|
|
65
|
+
- aggregate only (commit counts, focus time, streak days)
|
|
66
|
+
|
|
67
|
+
**What gets synced (if opted in):**
|
|
68
|
+
- ✅ Commit counts
|
|
69
|
+
- ✅ Focus time duration
|
|
70
|
+
- ✅ Streak days
|
|
71
|
+
- ✅ Anonymized username/identifier
|
|
72
|
+
|
|
73
|
+
**What never gets synced:**
|
|
74
|
+
- ❌ Source code
|
|
75
|
+
- ❌ File names or paths
|
|
76
|
+
- ❌ Commit messages
|
|
77
|
+
- ❌ Repository names
|
|
78
|
+
- ❌ File diffs or changes
|
|
79
|
+
|
|
80
|
+
## User Experience Philosophy
|
|
81
|
+
|
|
82
|
+
**When in Doubt: Pause, Explain, Wait**
|
|
83
|
+
|
|
84
|
+
- Ambiguous situations should trigger clear, actionable error messages
|
|
85
|
+
- Users should always understand what Autopilot is doing and why
|
|
86
|
+
- Status messages should be informative without being verbose
|
|
87
|
+
- Configuration should have sensible defaults but be fully customizable
|
|
88
|
+
|
|
89
|
+
**Trust Through Transparency:**
|
|
90
|
+
- Every action Autopilot takes should be logged
|
|
91
|
+
- Users should be able to audit what happened and when
|
|
92
|
+
- The system should explain its decisions in plain language
|
|
93
|
+
- Documentation should be honest about limitations
|
|
94
|
+
|
|
95
|
+
## Development Guidelines
|
|
96
|
+
|
|
97
|
+
**For Contributors:**
|
|
98
|
+
- Any feature must pass the "trust test" - would you trust this with your production code?
|
|
99
|
+
- Prefer explicit over implicit behavior
|
|
100
|
+
- Add clear error messages for every failure case
|
|
101
|
+
- Document why, not just what
|
|
102
|
+
- Test edge cases extensively, especially around git state
|
|
103
|
+
|
|
104
|
+
**For AI Integration:**
|
|
105
|
+
- AI should enhance, not replace, developer judgment
|
|
106
|
+
- All AI suggestions must be reviewable before commit
|
|
107
|
+
- Provide escape hatches for AI-generated content
|
|
108
|
+
- Log AI usage for transparency
|
|
109
|
+
- Allow disabling AI features entirely
|
|
51
110
|
|
|
52
|
-
|
|
111
|
+
---
|
|
53
112
|
|
|
54
|
-
|
|
55
|
-
- **When in doubt: pause, explain, wait.**
|
|
113
|
+
*Any feature (including Grok or leaderboards) must pass this test: Does it maintain developer trust, safety, and control?*
|
|
56
114
|
|
|
57
|
-
---
|
|
58
|
-
*Any feature (including Grok or leaderboards) must pass this test.*
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ const e = React.createElement;
|
|
|
19
19
|
const Dashboard = () => {
|
|
20
20
|
const { exit } = useApp();
|
|
21
21
|
const root = process.cwd();
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
const [status, setStatus] = useState('loading');
|
|
24
24
|
const [pid, setPid] = useState(null);
|
|
25
25
|
const [lastCommit, setLastCommit] = useState(null);
|
|
@@ -34,18 +34,18 @@ const Dashboard = () => {
|
|
|
34
34
|
// 1. Check process status
|
|
35
35
|
const currentPid = await getRunningPid(root);
|
|
36
36
|
setPid(currentPid);
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
// 2. Check Paused State
|
|
39
39
|
const stateManager = new StateManager(root);
|
|
40
40
|
if (stateManager.isPaused()) {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
setStatus('paused');
|
|
42
|
+
setPausedState(stateManager.getState());
|
|
43
43
|
} else if (currentPid) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
setStatus('running');
|
|
45
|
+
setPausedState(null);
|
|
46
46
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
setStatus('stopped');
|
|
48
|
+
setPausedState(null);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// 3. Last Commit
|
|
@@ -56,7 +56,7 @@ const Dashboard = () => {
|
|
|
56
56
|
// 4. Pending Files
|
|
57
57
|
const statusObj = await git.getPorcelainStatus(root);
|
|
58
58
|
if (statusObj.ok) {
|
|
59
|
-
|
|
59
|
+
setPendingFiles(statusObj.files);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// 5. Today Stats (Simple count from history)
|
|
@@ -126,10 +126,10 @@ const Dashboard = () => {
|
|
|
126
126
|
e(Box, { flexDirection: "column", marginBottom: 1 },
|
|
127
127
|
e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
|
|
128
128
|
e(Box, { flexDirection: "column" },
|
|
129
|
-
pendingFiles.length === 0 ?
|
|
129
|
+
pendingFiles.length === 0 ?
|
|
130
130
|
e(Text, { color: "gray" }, "No pending changes") :
|
|
131
|
-
pendingFiles.slice(0, 5).map((f) =>
|
|
132
|
-
e(Text, { key: f.file
|
|
131
|
+
pendingFiles.slice(0, 5).map((f, idx) =>
|
|
132
|
+
e(Text, { key: `${f.file}-${idx}`, color: "yellow" }, ` ${f.status} ${f.file}`)
|
|
133
133
|
)
|
|
134
134
|
),
|
|
135
135
|
pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
|
|
@@ -143,5 +143,9 @@ const Dashboard = () => {
|
|
|
143
143
|
};
|
|
144
144
|
|
|
145
145
|
export default function runDashboard() {
|
|
146
|
+
if (!process.stdin.isTTY && !process.env.AUTOPILOT_TEST_MODE) {
|
|
147
|
+
console.error('Error: Dashboard requires an interactive terminal (TTY).');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
146
150
|
render(e(Dashboard));
|
|
147
151
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -13,7 +13,7 @@ const git = require('../core/git');
|
|
|
13
13
|
const doctor = async () => {
|
|
14
14
|
const repoPath = process.cwd();
|
|
15
15
|
let issues = 0;
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
logger.section('Autopilot Doctor');
|
|
18
18
|
logger.info('Diagnosing environment...');
|
|
19
19
|
|
|
@@ -50,7 +50,17 @@ const doctor = async () => {
|
|
|
50
50
|
|
|
51
51
|
// Check remote type
|
|
52
52
|
if (remoteUrl.startsWith('http')) {
|
|
53
|
-
|
|
53
|
+
let hasHelper = false;
|
|
54
|
+
try {
|
|
55
|
+
const { stdout: helper } = await execa('git', ['config', '--get', 'credential.helper'], { cwd: repoPath });
|
|
56
|
+
if (helper.trim()) hasHelper = true;
|
|
57
|
+
} catch (e) { /* ignore */ }
|
|
58
|
+
|
|
59
|
+
if (hasHelper) {
|
|
60
|
+
logger.success('Remote uses HTTPS with credential helper configured.');
|
|
61
|
+
} else {
|
|
62
|
+
logger.warn('Remote uses HTTPS. Ensure credential helper is configured for non-interactive push.');
|
|
63
|
+
}
|
|
54
64
|
} else if (remoteUrl.startsWith('git@') || remoteUrl.startsWith('ssh://')) {
|
|
55
65
|
logger.success('Remote uses SSH (recommended).');
|
|
56
66
|
} else {
|
|
@@ -102,8 +112,8 @@ const doctor = async () => {
|
|
|
102
112
|
logger.success('Branch is up to date with remote.');
|
|
103
113
|
}
|
|
104
114
|
} else {
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
// Could be no upstream configured, which is fine for local-only initially
|
|
116
|
+
logger.info('Could not check remote status (upstream might not be set).');
|
|
107
117
|
}
|
|
108
118
|
} catch (error) {
|
|
109
119
|
logger.info('Skipping remote status check.');
|
|
@@ -116,6 +126,34 @@ const doctor = async () => {
|
|
|
116
126
|
} else {
|
|
117
127
|
logger.warn(`Diagnosis complete. Found ${issues} potential issue(s).`);
|
|
118
128
|
}
|
|
129
|
+
|
|
130
|
+
// 7. AI Connectivity (if enabled)
|
|
131
|
+
try {
|
|
132
|
+
const { loadConfig } = require('../config/loader');
|
|
133
|
+
const config = await loadConfig(repoPath);
|
|
134
|
+
if (config?.ai?.enabled) {
|
|
135
|
+
logger.section('AI Connectivity');
|
|
136
|
+
if (config.ai.provider === 'grok') {
|
|
137
|
+
const { validateGrokApiKey } = require('../core/grok');
|
|
138
|
+
const result = await validateGrokApiKey(config.ai.grokApiKey);
|
|
139
|
+
if (result.valid) logger.success('Grok API reachable and key looks valid.');
|
|
140
|
+
else {
|
|
141
|
+
logger.warn(`Grok API check failed: ${result.error}`);
|
|
142
|
+
issues++;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const { validateApiKey } = require('../core/gemini');
|
|
146
|
+
const result = await validateApiKey(config.ai.apiKey);
|
|
147
|
+
if (result.valid) logger.success('Gemini API reachable and key looks valid.');
|
|
148
|
+
else {
|
|
149
|
+
logger.warn(`Gemini API check failed: ${result.error}`);
|
|
150
|
+
issues++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// ignore AI check failures
|
|
156
|
+
}
|
|
119
157
|
};
|
|
120
158
|
|
|
121
159
|
module.exports = doctor;
|
package/src/commands/init.js
CHANGED
|
@@ -137,19 +137,21 @@ async function initRepo() {
|
|
|
137
137
|
const teamMode = await askQuestion('Enable team mode? (pull before push) [y/N]: ');
|
|
138
138
|
const useTeamMode = teamMode.toLowerCase() === 'y';
|
|
139
139
|
|
|
140
|
-
// Phase 3: AI Configuration
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
// Phase 3: AI Configuration (Zero-Config)
|
|
141
|
+
logger.info('\n🤖 AI Commit Messages are ENABLED by default (Zero-Config).');
|
|
142
|
+
const customAI = await askQuestion('Would you like to use your own AI API keys instead? [y/N]: ');
|
|
143
143
|
|
|
144
|
+
let useAI = true;
|
|
144
145
|
let apiKey = '';
|
|
145
146
|
let grokApiKey = '';
|
|
146
|
-
let provider = '
|
|
147
|
-
let interactive =
|
|
147
|
+
let provider = 'grok';
|
|
148
|
+
let interactive = DEFAULT_CONFIG.ai.interactive;
|
|
149
|
+
|
|
148
150
|
|
|
149
|
-
if (
|
|
151
|
+
if (customAI.toLowerCase() === 'y') {
|
|
150
152
|
// Select Provider
|
|
151
|
-
const providerAns = await askQuestion('Select AI Provider (gemini/grok) [
|
|
152
|
-
provider = providerAns.toLowerCase() === '
|
|
153
|
+
const providerAns = await askQuestion('Select AI Provider (gemini/grok) [grok]: ');
|
|
154
|
+
provider = providerAns.toLowerCase() === 'gemini' ? 'gemini' : 'grok';
|
|
153
155
|
|
|
154
156
|
while (true) {
|
|
155
157
|
const keyPrompt = provider === 'grok'
|
|
@@ -159,16 +161,15 @@ async function initRepo() {
|
|
|
159
161
|
const keyInput = await askQuestion(keyPrompt);
|
|
160
162
|
|
|
161
163
|
if (!keyInput) {
|
|
162
|
-
logger.warn('API Key cannot be empty
|
|
163
|
-
const retry = await askQuestion('Try again? (n to
|
|
164
|
+
logger.warn('Custom API Key cannot be empty. Falling back to System AI.');
|
|
165
|
+
const retry = await askQuestion('Try again with custom key? (n to use System AI) [Y/n]: ');
|
|
164
166
|
if (retry.toLowerCase() === 'n') {
|
|
165
|
-
useAI = false;
|
|
166
167
|
break;
|
|
167
168
|
}
|
|
168
169
|
continue;
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
logger.info(`Verifying ${provider} API Key...`);
|
|
172
|
+
logger.info(`Verifying custom ${provider} API Key...`);
|
|
172
173
|
let result;
|
|
173
174
|
if (provider === 'grok') {
|
|
174
175
|
result = await grok.validateGrokApiKey(keyInput);
|
|
@@ -177,34 +178,33 @@ async function initRepo() {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
if (result.valid) {
|
|
180
|
-
logger.success('API Key verified successfully! ✨');
|
|
181
|
+
logger.success('Custom API Key verified successfully! ✨');
|
|
181
182
|
if (provider === 'grok') grokApiKey = keyInput;
|
|
182
183
|
else apiKey = keyInput;
|
|
183
184
|
break;
|
|
184
185
|
} else {
|
|
185
186
|
logger.warn(`API Key validation failed: ${result.error}`);
|
|
186
|
-
const retry = await askQuestion('Try again? (n to
|
|
187
|
+
const retry = await askQuestion('Try again? (n to use System AI, p to proceed anyway) [Y/n/p]: ');
|
|
187
188
|
const choice = retry.toLowerCase();
|
|
188
189
|
|
|
189
190
|
if (choice === 'n') {
|
|
190
|
-
useAI = false;
|
|
191
191
|
break;
|
|
192
192
|
} else if (choice === 'p') {
|
|
193
|
-
logger.warn('Proceeding with
|
|
193
|
+
logger.warn('Proceeding with custom API key.');
|
|
194
194
|
if (provider === 'grok') grokApiKey = keyInput;
|
|
195
195
|
else apiKey = keyInput;
|
|
196
196
|
break;
|
|
197
197
|
}
|
|
198
|
-
// Default is retry (loop)
|
|
199
198
|
}
|
|
200
199
|
}
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
const interactiveAns = await askQuestion('Review AI messages before committing? [y/N]: ');
|
|
202
|
+
interactive = interactiveAns.toLowerCase() === 'y';
|
|
203
|
+
} else {
|
|
204
|
+
logger.info('Using System AI (Zero-Config mode). ✨');
|
|
206
205
|
}
|
|
207
206
|
|
|
207
|
+
|
|
208
208
|
const overrides = {
|
|
209
209
|
teamMode: useTeamMode,
|
|
210
210
|
ai: {
|
package/src/commands/insights.js
CHANGED
|
@@ -6,86 +6,52 @@ const { createObjectCsvWriter } = require('csv-writer');
|
|
|
6
6
|
|
|
7
7
|
async function getGitStats(repoPath) {
|
|
8
8
|
try {
|
|
9
|
-
// Get commit log with stats
|
|
10
|
-
// We use custom delimiters to safely parse multi-line bodies and stats
|
|
11
9
|
const { stdout } = await git.runGit(repoPath, [
|
|
12
10
|
'log',
|
|
13
|
-
'--pretty=format
|
|
11
|
+
'--pretty=format:===C===%H|%an|%ad|%s|%b===E===',
|
|
14
12
|
'--date=iso',
|
|
15
13
|
'--numstat'
|
|
16
14
|
]);
|
|
17
15
|
|
|
16
|
+
if (!stdout) return [];
|
|
17
|
+
|
|
18
18
|
const commits = [];
|
|
19
|
-
const rawCommits = stdout.split('
|
|
19
|
+
const rawCommits = stdout.split('===C===').filter(Boolean);
|
|
20
20
|
|
|
21
21
|
for (const raw of rawCommits) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const lines = metadataPart.trim().split('\n');
|
|
28
|
-
const header = lines[0]; // hash|author|date|subject|body_start...
|
|
29
|
-
// The body might continue on next lines if %b has newlines.
|
|
30
|
-
// Actually, my format puts %b starting on the first line.
|
|
31
|
-
// But let's be safer: split header by | first 4 times only.
|
|
32
|
-
|
|
33
|
-
// header format: hash|author|date|subject|rest...
|
|
34
|
-
// But wait, if body has newlines, "lines" array has them.
|
|
35
|
-
|
|
36
|
-
// Let's reconstruct the full message body
|
|
37
|
-
const fullMetadata = metadataPart.trim();
|
|
38
|
-
const firstPipe = fullMetadata.indexOf('|');
|
|
39
|
-
const secondPipe = fullMetadata.indexOf('|', firstPipe + 1);
|
|
40
|
-
const thirdPipe = fullMetadata.indexOf('|', secondPipe + 1);
|
|
41
|
-
const fourthPipe = fullMetadata.indexOf('|', thirdPipe + 1);
|
|
42
|
-
|
|
43
|
-
if (firstPipe === -1 || fourthPipe === -1) continue;
|
|
44
|
-
|
|
45
|
-
const hash = fullMetadata.substring(0, firstPipe);
|
|
46
|
-
const author = fullMetadata.substring(firstPipe + 1, secondPipe);
|
|
47
|
-
const dateStr = fullMetadata.substring(secondPipe + 1, thirdPipe);
|
|
48
|
-
const subject = fullMetadata.substring(thirdPipe + 1, fourthPipe);
|
|
49
|
-
const body = fullMetadata.substring(fourthPipe + 1);
|
|
50
|
-
|
|
51
|
-
// TRUST VERIFICATION
|
|
52
|
-
// Check for Autopilot trailers
|
|
53
|
-
if (!body.includes('Autopilot-Commit: true')) {
|
|
54
|
-
continue; // Skip non-autopilot commits
|
|
55
|
-
}
|
|
22
|
+
const [metadataPlusBody, ...statsParts] = raw.split('===E===');
|
|
23
|
+
if (!metadataPlusBody) continue;
|
|
24
|
+
|
|
25
|
+
const [hash, author, dateStr, subject, ...bodyParts] = metadataPlusBody.trim().split('|');
|
|
26
|
+
const body = bodyParts.join('|'); // Rejoin in case body had pipes
|
|
56
27
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
// if (!verifySignature(signature, ...)) continue;
|
|
28
|
+
// Trust Verification: Only process autopilot commits
|
|
29
|
+
if (!body.includes('Autopilot-Commit: true')) continue;
|
|
60
30
|
|
|
61
31
|
const commit = {
|
|
62
32
|
hash,
|
|
63
33
|
author,
|
|
64
34
|
date: new Date(dateStr),
|
|
65
|
-
message: subject
|
|
35
|
+
message: `${subject}\n${body}`.trim(),
|
|
66
36
|
files: [],
|
|
67
37
|
additions: 0,
|
|
68
38
|
deletions: 0
|
|
69
39
|
};
|
|
70
40
|
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
const statLines =
|
|
74
|
-
for (const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const deletions = parseInt(parts[1]) || 0;
|
|
80
|
-
const file = parts.slice(2).join(' '); // handle spaces in filenames
|
|
81
|
-
|
|
41
|
+
const statsText = statsParts.join('===E===').trim();
|
|
42
|
+
if (statsText) {
|
|
43
|
+
const statLines = statsText.split('\n');
|
|
44
|
+
for (const line of statLines) {
|
|
45
|
+
const [add, del, file] = line.trim().split(/\s+/);
|
|
46
|
+
if (file) {
|
|
47
|
+
const additions = parseInt(add) || 0;
|
|
48
|
+
const deletions = parseInt(del) || 0;
|
|
82
49
|
commit.files.push({ file, additions, deletions });
|
|
83
50
|
commit.additions += additions;
|
|
84
51
|
commit.deletions += deletions;
|
|
85
52
|
}
|
|
86
53
|
}
|
|
87
54
|
}
|
|
88
|
-
|
|
89
55
|
commits.push(commit);
|
|
90
56
|
}
|
|
91
57
|
|
|
@@ -130,7 +96,7 @@ function calculateMetrics(commits) {
|
|
|
130
96
|
// Time analysis
|
|
131
97
|
const dateStr = c.date.toISOString().split('T')[0];
|
|
132
98
|
const hour = c.date.getHours();
|
|
133
|
-
|
|
99
|
+
|
|
134
100
|
stats.commitsByDay[dateStr] = (stats.commitsByDay[dateStr] || 0) + 1;
|
|
135
101
|
stats.commitsByHour[hour] = (stats.commitsByHour[hour] || 0) + 1;
|
|
136
102
|
dates.add(dateStr);
|
|
@@ -145,7 +111,7 @@ function calculateMetrics(commits) {
|
|
|
145
111
|
// Calculate Averages
|
|
146
112
|
stats.totalFilesCount = stats.totalFilesChanged.size;
|
|
147
113
|
stats.quality.avgLength = commits.length ? Math.round(totalMessageLength / commits.length) : 0;
|
|
148
|
-
|
|
114
|
+
|
|
149
115
|
// Calculate Score (0-100)
|
|
150
116
|
// 40% Conventional, 30% Message Length (>30 chars), 30% Consistency
|
|
151
117
|
const convScore = commits.length ? (stats.quality.conventional / commits.length) * 40 : 0;
|
|
@@ -165,7 +131,7 @@ function calculateMetrics(commits) {
|
|
|
165
131
|
currentStreak = 1;
|
|
166
132
|
} else {
|
|
167
133
|
const diffTime = Math.abs(d - lastDate);
|
|
168
|
-
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
134
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
169
135
|
if (diffDays === 1) {
|
|
170
136
|
currentStreak++;
|
|
171
137
|
} else {
|
|
@@ -176,12 +142,12 @@ function calculateMetrics(commits) {
|
|
|
176
142
|
lastDate = d;
|
|
177
143
|
});
|
|
178
144
|
stats.streak.max = Math.max(maxStreak, currentStreak);
|
|
179
|
-
|
|
145
|
+
|
|
180
146
|
// Check if streak is active (last commit today or yesterday)
|
|
181
147
|
const today = new Date().toISOString().split('T')[0];
|
|
182
148
|
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
|
|
183
149
|
const lastCommitDate = sortedDates[sortedDates.length - 1];
|
|
184
|
-
|
|
150
|
+
|
|
185
151
|
if (lastCommitDate === today || lastCommitDate === yesterday) {
|
|
186
152
|
stats.streak.current = currentStreak;
|
|
187
153
|
} else {
|
|
@@ -217,10 +183,10 @@ async function insights(options) {
|
|
|
217
183
|
console.log(`Lines Added: ${metrics.totalAdditions}`);
|
|
218
184
|
console.log(`Lines Deleted: ${metrics.totalDeletions}`);
|
|
219
185
|
console.log(`Current Streak: ${metrics.streak.current} days (Max: ${metrics.streak.max})`);
|
|
220
|
-
|
|
186
|
+
|
|
221
187
|
// Find most productive hour
|
|
222
188
|
const productiveHour = Object.entries(metrics.commitsByHour)
|
|
223
|
-
.sort(([,a], [,b]) => b - a)[0];
|
|
189
|
+
.sort(([, a], [, b]) => b - a)[0];
|
|
224
190
|
console.log(`Peak Productivity: ${productiveHour ? productiveHour[0] + ':00' : 'N/A'}`);
|
|
225
191
|
|
|
226
192
|
console.log('');
|
|
@@ -241,12 +207,12 @@ async function insights(options) {
|
|
|
241
207
|
const csvWriter = createObjectCsvWriter({
|
|
242
208
|
path: csvPath,
|
|
243
209
|
header: [
|
|
244
|
-
{id: 'hash', title: 'Hash'},
|
|
245
|
-
{id: 'date', title: 'Date'},
|
|
246
|
-
{id: 'author', title: 'Author'},
|
|
247
|
-
{id: 'message', title: 'Message'},
|
|
248
|
-
{id: 'additions', title: 'Additions'},
|
|
249
|
-
{id: 'deletions', title: 'Deletions'}
|
|
210
|
+
{ id: 'hash', title: 'Hash' },
|
|
211
|
+
{ id: 'date', title: 'Date' },
|
|
212
|
+
{ id: 'author', title: 'Author' },
|
|
213
|
+
{ id: 'message', title: 'Message' },
|
|
214
|
+
{ id: 'additions', title: 'Additions' },
|
|
215
|
+
{ id: 'deletions', title: 'Deletions' }
|
|
250
216
|
]
|
|
251
217
|
});
|
|
252
218
|
|