@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/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "@traisetech/autopilot",
3
- "version": "2.0.0",
4
- "publishConfig": {
5
- "access": "public"
6
- },
7
- "files": [
8
- "bin",
9
- "src",
10
- "docs",
11
- "README.md",
12
- "LICENSE",
13
- "CHANGELOG.md"
14
- ],
15
- "description": "An intelligent CLI tool that automatically commits and pushes your code so you can focus on building.",
16
- "keywords": [
17
- "git",
18
- "automation",
19
- "autocommit",
20
- "autopush",
21
- "developer-tools",
22
- "cli",
23
- "productivity",
24
- "git-workflow"
25
- ],
26
- "author": "Praise Masunga (PraiseTechzw)",
27
- "license": "MIT",
28
- "type": "commonjs",
29
- "bin": {
30
- "autopilot": "bin/autopilot.js"
31
- },
32
- "main": "src/index.js",
33
- "scripts": {
34
- "dev": "node bin/autopilot.js",
35
- "test": "node --test",
36
- "lint": "node -c bin/autopilot.js && node -c src/index.js",
37
- "verify": "node bin/autopilot.js --help && node bin/autopilot.js doctor && node --test",
38
- "prepublishOnly": "npm run verify",
39
- "release:patch": "npm run verify && npm version patch && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
40
- "release:minor": "npm run verify && npm version minor && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
41
- "release:major": "npm run verify && npm version major && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\""
42
- },
43
- "engines": {
44
- "node": ">=18.0.0"
45
- },
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/PraiseTechzw/autopilot-cli.git"
49
- },
50
- "bugs": {
51
- "url": "https://github.com/PraiseTechzw/autopilot-cli/issues"
52
- },
53
- "homepage": "https://github.com/PraiseTechzw/autopilot-cli#readme",
54
- "dependencies": {
55
- "@traisetech/autopilot": "^0.1.7",
56
- "chokidar": "^3.6.0",
57
- "commander": "^14.0.3",
58
- "csv-writer": "^1.6.0",
59
- "execa": "^5.1.1",
60
- "fs-extra": "^11.3.3",
61
- "ink": "^6.6.0",
62
- "ink-big-text": "^2.0.0",
63
- "ink-gradient": "^4.0.0",
64
- "ink-spinner": "^5.0.0",
65
- "open": "^11.0.0",
66
- "prop-types": "^15.8.1",
67
- "react": "^19.2.4"
68
- }
69
- }
1
+ {
2
+ "name": "@traisetech/autopilot",
3
+ "version": "2.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "files": [
8
+ "bin",
9
+ "src",
10
+ "docs",
11
+ "README.md",
12
+ "LICENSE",
13
+ "CHANGELOG.md"
14
+ ],
15
+ "description": "An intelligent Git automation CLI that safely commits and pushes your code so you can focus on building.",
16
+ "keywords": [
17
+ "git",
18
+ "automation",
19
+ "autocommit",
20
+ "autopush",
21
+ "developer-tools",
22
+ "cli",
23
+ "productivity",
24
+ "git-workflow"
25
+ ],
26
+ "author": "Praise Masunga (PraiseTechzw)",
27
+ "license": "MIT",
28
+ "type": "commonjs",
29
+ "bin": {
30
+ "autopilot": "bin/autopilot.js"
31
+ },
32
+ "main": "src/index.js",
33
+ "scripts": {
34
+ "dev": "node bin/autopilot.js",
35
+ "test": "node --test --test-concurrency=1",
36
+ "lint": "node -c bin/autopilot.js && node -c src/index.js",
37
+ "verify": "node bin/autopilot.js --help && node bin/autopilot.js doctor && npm test",
38
+ "prepublishOnly": "npm run verify",
39
+ "release:patch": "npm run verify && npm version patch && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
40
+ "release:minor": "npm run verify && npm version minor && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\"",
41
+ "release:major": "npm run verify && npm version major && git push --follow-tags && echo \"\nšŸš€ Release initiated! GitHub Action will publish to NPM.\""
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/PraiseTechzw/autopilot-cli.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/PraiseTechzw/autopilot-cli/issues"
52
+ },
53
+ "homepage": "https://github.com/PraiseTechzw/autopilot-cli#readme",
54
+ "dependencies": {
55
+ "@traisetech/autopilot": "^0.1.7",
56
+ "chokidar": "^3.6.0",
57
+ "commander": "^14.0.3",
58
+ "csv-writer": "^1.6.0",
59
+ "execa": "^5.1.1",
60
+ "fs-extra": "^11.3.3",
61
+ "ink": "^6.6.0",
62
+ "ink-big-text": "^2.0.0",
63
+ "ink-gradient": "^4.0.0",
64
+ "ink-spinner": "^5.0.0",
65
+ "open": "^11.0.0",
66
+ "prop-types": "^15.8.1",
67
+ "react": "^19.2.4"
68
+ }
69
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Config Command
3
+ * View and modify Autopilot configuration
4
+ */
5
+
6
+ const logger = require('../utils/logger');
7
+ const { loadConfig, saveConfig, getGlobalConfigPath } = require('../config/loader');
8
+ const fs = require('fs-extra');
9
+ const { getConfigPath } = require('../utils/paths');
10
+
11
+ /**
12
+ * Helper to get value by dot notation
13
+ */
14
+ const getByDot = (obj, path) => {
15
+ return path.split('.').reduce((acc, part) => acc && acc[part], obj);
16
+ };
17
+
18
+ /**
19
+ * Helper to set value by dot notation
20
+ */
21
+ const setByDot = (obj, path, value) => {
22
+ const parts = path.split('.');
23
+ const last = parts.pop();
24
+ const target = parts.reduce((acc, part) => {
25
+ if (!acc[part]) acc[part] = {};
26
+ return acc[part];
27
+ }, obj);
28
+ target[last] = value;
29
+ };
30
+
31
+ /**
32
+ * Parse value string to appropriate type
33
+ */
34
+ const parseValue = (val) => {
35
+ if (val === 'true') return true;
36
+ if (val === 'false') return false;
37
+ if (!isNaN(val) && val.trim() !== '') return Number(val);
38
+ return val;
39
+ };
40
+
41
+ async function config(cmd, key, value, options) {
42
+ const repoPath = options?.cwd || process.cwd();
43
+ const isGlobal = options?.global || false;
44
+
45
+ if (cmd === 'list') {
46
+ // If list --global, show only global config?
47
+ // Or just show effective config?
48
+ // Let's show effective config, maybe annotated if we had time.
49
+ // But for now, just loadConfig which merges them.
50
+ const currentConfig = await loadConfig(repoPath);
51
+ console.log(JSON.stringify(currentConfig, null, 2));
52
+ if (isGlobal) {
53
+ logger.info('(Note: Showing effective merged config. Use --global to set global values.)');
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (cmd === 'get') {
59
+ if (!key) {
60
+ logger.error('Usage: autopilot config get <key>');
61
+ return;
62
+ }
63
+ // Get always shows effective value
64
+ const currentConfig = await loadConfig(repoPath);
65
+ const val = getByDot(currentConfig, key);
66
+ if (val === undefined) {
67
+ logger.warn(`Key '${key}' not set`);
68
+ } else {
69
+ console.log(typeof val === 'object' ? JSON.stringify(val, null, 2) : val);
70
+ }
71
+ return;
72
+ }
73
+
74
+ if (cmd === 'set') {
75
+ if (!key || value === undefined) {
76
+ logger.error('Usage: autopilot config set <key> <value>');
77
+ return;
78
+ }
79
+
80
+ const typedValue = parseValue(value);
81
+
82
+ // We need to read the specific file (local or global) to avoid merging defaults into it
83
+ let rawConfig = {};
84
+ let configPath;
85
+
86
+ if (isGlobal) {
87
+ configPath = getGlobalConfigPath();
88
+ } else {
89
+ configPath = getConfigPath(repoPath);
90
+ }
91
+
92
+ if (await fs.pathExists(configPath)) {
93
+ try {
94
+ rawConfig = await fs.readJson(configPath);
95
+ } catch (e) {
96
+ // Ignore read errors, start fresh
97
+ }
98
+ }
99
+
100
+ setByDot(rawConfig, key, typedValue);
101
+
102
+ await saveConfig(repoPath, rawConfig, isGlobal);
103
+ logger.success(`Set ${key} = ${typedValue} ${isGlobal ? '(Global)' : '(Local)'}`);
104
+ return;
105
+ }
106
+
107
+ logger.error('Unknown config command. Use list, get, or set.');
108
+ }
109
+
110
+ module.exports = config;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React from 'react';
2
2
  import { render, Box, Text, useInput, useApp } from 'ink';
3
3
  import Gradient from 'ink-gradient';
4
4
  import BigText from 'ink-big-text';
@@ -8,7 +8,10 @@ import path from 'path';
8
8
  import StateManager from '../core/state.js';
9
9
  import git from '../core/git.js';
10
10
  import HistoryManager from '../core/history.js';
11
- import { getPid } from '../utils/process.js';
11
+ import processUtils from '../utils/process.js';
12
+
13
+ const { useState, useEffect } = React;
14
+ const { getRunningPid } = processUtils;
12
15
 
13
16
  const e = React.createElement;
14
17
 
@@ -29,7 +32,7 @@ const Dashboard = () => {
29
32
  const fetchData = async () => {
30
33
  try {
31
34
  // 1. Check process status
32
- const currentPid = await getPid(root);
35
+ const currentPid = await getRunningPid(root);
33
36
  setPid(currentPid);
34
37
 
35
38
  // 2. Check Paused State
@@ -122,11 +125,13 @@ const Dashboard = () => {
122
125
  // Pending Changes
123
126
  e(Box, { flexDirection: "column", marginBottom: 1 },
124
127
  e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
125
- pendingFiles.length === 0 ?
126
- e(Text, { color: "gray" }, "No pending changes") :
127
- pendingFiles.slice(0, 5).map((f, i) =>
128
- e(Text, { key: i, color: "yellow" }, ` ${f.status} ${f.file}`)
129
- ),
128
+ e(Box, { flexDirection: "column" },
129
+ pendingFiles.length === 0 ?
130
+ e(Text, { color: "gray" }, "No pending changes") :
131
+ pendingFiles.slice(0, 5).map((f) =>
132
+ e(Text, { key: f.file, color: "yellow" }, ` ${f.status} ${f.file}`)
133
+ )
134
+ ),
130
135
  pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
131
136
  ),
132
137
 
@@ -9,7 +9,8 @@ const readline = require('readline');
9
9
  const logger = require('../utils/logger');
10
10
  const { getConfigPath, getIgnorePath, getGitPath } = require('../utils/paths');
11
11
  const { DEFAULT_CONFIG, DEFAULT_IGNORE_PATTERNS } = require('../config/defaults');
12
- const { validateApiKey } = require('../core/gemini');
12
+ const gemini = require('../core/gemini');
13
+ const grok = require('../core/grok');
13
14
 
14
15
  function askQuestion(query) {
15
16
  if (!process.stdin.isTTY) {
@@ -137,17 +138,27 @@ async function initRepo() {
137
138
  const useTeamMode = teamMode.toLowerCase() === 'y';
138
139
 
139
140
  // Phase 3: AI Configuration
140
- const enableAI = await askQuestion('Enable AI commit messages (Gemini)? [y/N]: ');
141
+ const enableAI = await askQuestion('Enable AI commit messages? [y/N]: ');
141
142
  let useAI = enableAI.toLowerCase() === 'y';
142
143
 
143
144
  let apiKey = '';
145
+ let grokApiKey = '';
146
+ let provider = 'gemini';
144
147
  let interactive = false;
145
148
 
146
149
  if (useAI) {
150
+ // Select Provider
151
+ const providerAns = await askQuestion('Select AI Provider (gemini/grok) [gemini]: ');
152
+ provider = providerAns.toLowerCase() === 'grok' ? 'grok' : 'gemini';
153
+
147
154
  while (true) {
148
- apiKey = await askQuestion('Enter your Google Gemini API Key: ');
155
+ const keyPrompt = provider === 'grok'
156
+ ? 'Enter your xAI Grok API Key: '
157
+ : 'Enter your Google Gemini API Key: ';
158
+
159
+ const keyInput = await askQuestion(keyPrompt);
149
160
 
150
- if (!apiKey) {
161
+ if (!keyInput) {
151
162
  logger.warn('API Key cannot be empty if AI is enabled.');
152
163
  const retry = await askQuestion('Try again? (n to disable AI) [Y/n]: ');
153
164
  if (retry.toLowerCase() === 'n') {
@@ -157,11 +168,18 @@ async function initRepo() {
157
168
  continue;
158
169
  }
159
170
 
160
- logger.info('Verifying API Key...');
161
- const result = await validateApiKey(apiKey);
171
+ logger.info(`Verifying ${provider} API Key...`);
172
+ let result;
173
+ if (provider === 'grok') {
174
+ result = await grok.validateGrokApiKey(keyInput);
175
+ } else {
176
+ result = await gemini.validateApiKey(keyInput);
177
+ }
162
178
 
163
179
  if (result.valid) {
164
180
  logger.success('API Key verified successfully! ✨');
181
+ if (provider === 'grok') grokApiKey = keyInput;
182
+ else apiKey = keyInput;
165
183
  break;
166
184
  } else {
167
185
  logger.warn(`API Key validation failed: ${result.error}`);
@@ -173,6 +191,8 @@ async function initRepo() {
173
191
  break;
174
192
  } else if (choice === 'p') {
175
193
  logger.warn('Proceeding with potentially invalid API key.');
194
+ if (provider === 'grok') grokApiKey = keyInput;
195
+ else apiKey = keyInput;
176
196
  break;
177
197
  }
178
198
  // Default is retry (loop)
@@ -189,8 +209,10 @@ async function initRepo() {
189
209
  teamMode: useTeamMode,
190
210
  ai: {
191
211
  enabled: useAI,
212
+ provider: provider,
192
213
  apiKey: apiKey,
193
- model: 'gemini-2.5-flash',
214
+ grokApiKey: grokApiKey,
215
+ model: provider === 'grok' ? 'grok-beta' : 'gemini-2.5-flash',
194
216
  interactive: interactive
195
217
  },
196
218
  commitMessageMode: useAI ? 'ai' : 'smart'
@@ -7,47 +7,87 @@ const { createObjectCsvWriter } = require('csv-writer');
7
7
  async function getGitStats(repoPath) {
8
8
  try {
9
9
  // Get commit log with stats
10
- // Format: hash|author|date|subject|body
10
+ // We use custom delimiters to safely parse multi-line bodies and stats
11
11
  const { stdout } = await git.runGit(repoPath, [
12
12
  'log',
13
- '--pretty=format:%H|%an|%ad|%s',
13
+ '--pretty=format:====COMMIT====%n%H|%an|%ad|%s|%b%n====BODY_END====',
14
14
  '--date=iso',
15
15
  '--numstat'
16
16
  ]);
17
17
 
18
- const lines = stdout.split('\n');
19
18
  const commits = [];
20
- let currentCommit = null;
21
-
22
- // Parse git log output
23
- for (const line of lines) {
24
- if (!line.trim()) continue;
25
-
26
- // Check if line is a commit header (hash|author|date|subject)
27
- // Hashes are 40 chars hex.
28
- const parts = line.split('|');
29
- if (parts.length >= 4 && /^[0-9a-f]{40}$/.test(parts[0])) {
30
- if (currentCommit) commits.push(currentCommit);
31
- currentCommit = {
32
- hash: parts[0],
33
- author: parts[1],
34
- date: new Date(parts[2]),
35
- message: parts.slice(3).join('|'),
36
- files: [],
37
- additions: 0,
38
- deletions: 0
39
- };
40
- } else if (currentCommit && /^\d+\s+\d+\s+/.test(line)) {
41
- // Stat line: "10 5 src/file.js"
42
- const [add, del, file] = line.split(/\s+/);
43
- const additions = parseInt(add) || 0;
44
- const deletions = parseInt(del) || 0;
45
- currentCommit.files.push({ file, additions, deletions });
46
- currentCommit.additions += additions;
47
- currentCommit.deletions += deletions;
19
+ const rawCommits = stdout.split('====COMMIT====');
20
+
21
+ for (const raw of rawCommits) {
22
+ if (!raw.trim()) continue;
23
+
24
+ const [metadataPart, statsPart] = raw.split('====BODY_END====');
25
+ if (!metadataPart) continue;
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
48
55
  }
56
+
57
+ // TODO: Verify Signature (Optional but recommended for strict mode)
58
+ // const signature = extractTrailer(body, 'Autopilot-Signature');
59
+ // if (!verifySignature(signature, ...)) continue;
60
+
61
+ const commit = {
62
+ hash,
63
+ author,
64
+ date: new Date(dateStr),
65
+ message: subject + '\n' + body,
66
+ files: [],
67
+ additions: 0,
68
+ deletions: 0
69
+ };
70
+
71
+ // Parse Stats
72
+ if (statsPart) {
73
+ const statLines = statsPart.trim().split('\n');
74
+ for (const statLine of statLines) {
75
+ if (!statLine.trim()) continue;
76
+ const parts = statLine.split(/\s+/);
77
+ if (parts.length >= 3) {
78
+ const additions = parseInt(parts[0]) || 0;
79
+ const deletions = parseInt(parts[1]) || 0;
80
+ const file = parts.slice(2).join(' '); // handle spaces in filenames
81
+
82
+ commit.files.push({ file, additions, deletions });
83
+ commit.additions += additions;
84
+ commit.deletions += deletions;
85
+ }
86
+ }
87
+ }
88
+
89
+ commits.push(commit);
49
90
  }
50
- if (currentCommit) commits.push(currentCommit);
51
91
 
52
92
  return commits;
53
93
  } catch (error) {
@@ -1,10 +1,40 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
1
3
  const { getGitStats, calculateMetrics } = require('./insights');
2
4
  const logger = require('../utils/logger');
3
5
  const open = require('open');
6
+ const crypto = require('crypto');
4
7
 
5
8
  // Default API URL (can be overridden by config)
6
9
  const DEFAULT_API_URL = 'http://localhost:3000';
7
10
 
11
+ async function calculateFocusTime(repoPath) {
12
+ const logPath = path.join(repoPath, 'autopilot.log');
13
+ if (!await fs.pathExists(logPath)) return 0;
14
+
15
+ try {
16
+ const content = await fs.readFile(logPath, 'utf8');
17
+ const lines = content.split('\n').filter(l => l.trim());
18
+ let totalMs = 0;
19
+
20
+ for (const line of lines) {
21
+ try {
22
+ const entry = JSON.parse(line);
23
+ if (entry.type === 'FOCUS_SESSION_END' && entry.totalActiveMs) {
24
+ totalMs += entry.totalActiveMs;
25
+ }
26
+ } catch (e) {
27
+ // ignore bad lines
28
+ }
29
+ }
30
+
31
+ return Math.round(totalMs / 60000); // minutes
32
+ } catch (error) {
33
+ logger.warn(`Failed to parse autopilot.log: ${error.message}`);
34
+ return 0;
35
+ }
36
+ }
37
+
8
38
  async function leaderboard(options) {
9
39
  const apiUrl = process.env.AUTOPILOT_API_URL || DEFAULT_API_URL;
10
40
 
@@ -12,6 +42,7 @@ async function leaderboard(options) {
12
42
  await syncLeaderboard(apiUrl, options);
13
43
  } else {
14
44
  logger.info(`Opening leaderboard at ${apiUrl}/leaderboard...`);
45
+ const { default: open } = await import('open');
15
46
  await open(`${apiUrl}/leaderboard`);
16
47
  }
17
48
  }
@@ -34,19 +65,28 @@ async function syncLeaderboard(apiUrl, options) {
34
65
  const { stdout: username } = await git.runGit(repoPath, ['config', 'user.name']);
35
66
  const { stdout: email } = await git.runGit(repoPath, ['config', 'user.email']);
36
67
 
68
+ const userEmail = email.trim() || 'unknown';
69
+ const userName = username.trim() || 'Anonymous';
70
+
71
+ // Anonymize ID using hash
72
+ const userId = crypto.createHash('sha256').update(userEmail).digest('hex').substring(0, 12);
73
+
74
+ // Get focus time from logs (or fallback to git stats proxy)
75
+ const logFocusMinutes = await calculateFocusTime(repoPath);
76
+ const gitFocusMinutes = Math.round(metrics.totalAdditions / 10);
77
+ const focusMinutes = logFocusMinutes > 0 ? logFocusMinutes : gitFocusMinutes;
78
+
37
79
  const stats = {
38
- id: email.trim() || username.trim() || 'unknown_user', // Simple ID generation
39
- username: username.trim() || 'Anonymous',
80
+ id: userId,
81
+ username: userName, // Display name (can be public)
40
82
  score: metrics.quality.score * 100 + metrics.totalCommits * 10, // Example scoring
41
83
  commits: metrics.totalCommits,
42
- focusMinutes: Math.round(metrics.totalAdditions / 10), // Rough proxy if focus engine not linked
84
+ focusMinutes: focusMinutes,
43
85
  streak: metrics.streak.current
44
86
  };
45
87
 
46
- // If Focus Engine logs exist, use them for more accurate focus time
47
- // TODO: Read autopilot.log for accurate focus time
48
-
49
- logger.info(`Syncing stats for ${stats.username}...`);
88
+ logger.info(`Syncing stats for ${stats.username} (ID: ${userId})...`);
89
+ logger.info('Note: Only metrics are shared. No code or file contents are transmitted.');
50
90
 
51
91
  const response = await fetch(`${apiUrl}/api/leaderboard/sync`, {
52
92
  method: 'POST',
@@ -13,8 +13,11 @@ const DEFAULT_CONFIG = {
13
13
  commitMessageMode: 'smart', // smart | simple | ai
14
14
  ai: {
15
15
  enabled: false,
16
- apiKey: '', // Store in env var or secure config
17
- model: 'gemini-2.5-flash',
16
+ provider: 'gemini', // gemini | grok
17
+ apiKey: '',
18
+ grokApiKey: '',
19
+ model: 'gemini-2.5-flash', // default for gemini
20
+ grokModel: 'grok-beta', // default for grok
18
21
  interactive: true
19
22
  },
20
23
  // Phase 1: Team Mode
@@ -79,20 +79,20 @@ const createIgnoredFilter = (repoPath, userPatterns = []) => {
79
79
  ];
80
80
 
81
81
  return (absolutePath) => {
82
- // 1. Normalize paths
83
- const normalizedAbs = normalizePath(absolutePath);
82
+ // 1. Get relative path safely using path.relative
83
+ // This handles Windows casing and separators correctly
84
+ const relativeRaw = path.relative(repoPath, absolutePath);
84
85
 
85
- // 2. Get relative path
86
- let relativePath = normalizedAbs;
87
- if (normalizedAbs.startsWith(normalizedRepoPath)) {
88
- relativePath = normalizedAbs.slice(normalizedRepoPath.length);
89
- if (relativePath.startsWith('/')) {
90
- relativePath = relativePath.slice(1);
91
- }
86
+ // If outside repo, ignore (or handle differently? Chokidar usually stays inside)
87
+ if (relativeRaw.startsWith('..') || path.isAbsolute(relativeRaw)) {
88
+ return false;
92
89
  }
93
90
 
91
+ // Normalize to forward slashes for matching
92
+ const relativePath = normalizePath(relativeRaw);
93
+
94
94
  // Handle root path case
95
- if (!relativePath) return false;
95
+ if (!relativePath || relativePath === '.') return false;
96
96
 
97
97
  // 3. Check critical matches
98
98
  const parts = relativePath.split('/');