@andrebuzeli/git-mcp 15.8.4 → 15.8.5

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.
@@ -1,160 +1,160 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
-
4
- export function getRepoNameFromPath(projectPath) {
5
- const base = path.basename(projectPath).trim();
6
- return base.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_\-]/g, "");
7
- }
8
-
9
- export function normalizeProjectFilePath(projectPath, relative) {
10
- const joined = path.resolve(projectPath, relative || ".");
11
- return joined;
12
- }
13
-
14
- /**
15
- * Valida se um caminho é seguro (não contém path traversal)
16
- * @param {string} projectPath - Caminho do projeto
17
- * @throws {Error} Se o caminho contiver path traversal ou for inválido
18
- */
19
- export function validateProjectPath(projectPath) {
20
- if (!projectPath || typeof projectPath !== "string") {
21
- throw new Error("projectPath é obrigatório e deve ser uma string");
22
- }
23
-
24
- // Verifica se é um caminho absoluto
25
- if (!path.isAbsolute(projectPath)) {
26
- throw new Error("projectPath deve ser um caminho absoluto");
27
- }
28
-
29
- // Normaliza o caminho
30
- const normalized = path.resolve(projectPath);
31
-
32
- // Verifica path traversal patterns
33
- if (projectPath.includes("..") || projectPath.includes("./..")) {
34
- throw new Error("Path traversal detectado: caminho não pode conter '..'");
35
- }
36
-
37
- // Verifica se o diretório pai existe
38
- const parentDir = path.dirname(normalized);
39
- if (!fs.existsSync(parentDir)) {
40
- throw new Error("Diretório pai não existe");
41
- }
42
-
43
- // Verifica se o caminho não é muito longo (limite do Windows é 260 chars)
44
- if (normalized.length > 260) {
45
- throw new Error("Caminho muito longo (máximo 260 caracteres)");
46
- }
47
-
48
- return normalized;
49
- }
50
-
51
- /**
52
- * Valida um caminho de arquivo relativo ao projeto
53
- * @param {string} projectPath - Caminho do projeto
54
- * @param {string} filePath - Caminho do arquivo relativo
55
- * @returns {string} Caminho absoluto validado
56
- */
57
- export function validateFilePath(projectPath, filePath) {
58
- const normalizedProject = validateProjectPath(projectPath);
59
- const absoluteFile = path.resolve(normalizedProject, filePath);
60
-
61
- // Garante que o arquivo está dentro do projeto
62
- if (!absoluteFile.startsWith(normalizedProject)) {
63
- throw new Error(`Path traversal detectado: '${filePath}' tenta acessar fora do projeto`);
64
- }
65
-
66
- return absoluteFile;
67
- }
68
-
69
- export function getEnv(key) {
70
- const v = process.env[key];
71
- return v === undefined ? "" : String(v);
72
- }
73
-
74
- export function getProvidersEnv() {
75
- return {
76
- githubToken: getEnv("GITHUB_TOKEN"),
77
- giteaUrl: getEnv("GITEA_URL"),
78
- giteaToken: getEnv("GITEA_TOKEN"),
79
- };
80
- }
81
-
82
- /**
83
- * Padrões comuns de .gitignore para diferentes tipos de projeto
84
- */
85
- export const GITIGNORE_TEMPLATES = {
86
- node: [
87
- "node_modules/",
88
- "npm-debug.log*",
89
- "yarn-debug.log*",
90
- "yarn-error.log*",
91
- ".npm",
92
- ".yarn",
93
- "dist/",
94
- "build/",
95
- ".env",
96
- ".env.local",
97
- ".env.*.local",
98
- "*.log",
99
- ".DS_Store",
100
- "Thumbs.db",
101
- "*.swp",
102
- "*.swo",
103
- ".idea/",
104
- ".vscode/",
105
- "*.sublime-*",
106
- "coverage/",
107
- ".nyc_output/"
108
- ],
109
- python: [
110
- "__pycache__/",
111
- "*.py[cod]",
112
- "*$py.class",
113
- "*.so",
114
- ".Python",
115
- "env/",
116
- "venv/",
117
- ".venv/",
118
- "pip-log.txt",
119
- "pip-delete-this-directory.txt",
120
- ".tox/",
121
- ".coverage",
122
- ".cache",
123
- "*.egg-info/",
124
- ".installed.cfg",
125
- "*.egg",
126
- ".env",
127
- ".vscode/",
128
- ".idea/"
129
- ],
130
- general: [
131
- ".DS_Store",
132
- "Thumbs.db",
133
- "*.log",
134
- "*.tmp",
135
- "*.temp",
136
- "*.swp",
137
- "*.swo",
138
- "*~",
139
- ".env",
140
- ".env.local",
141
- ".vscode/",
142
- ".idea/"
143
- ]
144
- };
145
-
146
- /**
147
- * Detecta o tipo de projeto baseado nos arquivos existentes
148
- */
149
- export function detectProjectType(projectPath) {
150
- if (fs.existsSync(path.join(projectPath, "package.json"))) {
151
- return "node";
152
- }
153
- if (fs.existsSync(path.join(projectPath, "requirements.txt")) ||
154
- fs.existsSync(path.join(projectPath, "setup.py")) ||
155
- fs.existsSync(path.join(projectPath, "pyproject.toml"))) {
156
- return "python";
157
- }
158
- return "general";
159
- }
160
-
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+
4
+ export function getRepoNameFromPath(projectPath) {
5
+ const base = path.basename(projectPath).trim();
6
+ return base.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_\-]/g, "");
7
+ }
8
+
9
+ export function normalizeProjectFilePath(projectPath, relative) {
10
+ const joined = path.resolve(projectPath, relative || ".");
11
+ return joined;
12
+ }
13
+
14
+ /**
15
+ * Valida se um caminho é seguro (não contém path traversal)
16
+ * @param {string} projectPath - Caminho do projeto
17
+ * @throws {Error} Se o caminho contiver path traversal ou for inválido
18
+ */
19
+ export function validateProjectPath(projectPath) {
20
+ if (!projectPath || typeof projectPath !== "string") {
21
+ throw new Error("projectPath é obrigatório e deve ser uma string");
22
+ }
23
+
24
+ // Verifica se é um caminho absoluto
25
+ if (!path.isAbsolute(projectPath)) {
26
+ throw new Error("projectPath deve ser um caminho absoluto");
27
+ }
28
+
29
+ // Normaliza o caminho
30
+ const normalized = path.resolve(projectPath);
31
+
32
+ // Verifica path traversal patterns
33
+ if (projectPath.includes("..") || projectPath.includes("./..")) {
34
+ throw new Error("Path traversal detectado: caminho não pode conter '..'");
35
+ }
36
+
37
+ // Verifica se o diretório pai existe
38
+ const parentDir = path.dirname(normalized);
39
+ if (!fs.existsSync(parentDir)) {
40
+ throw new Error("Diretório pai não existe");
41
+ }
42
+
43
+ // Verifica se o caminho não é muito longo (limite do Windows é 260 chars)
44
+ if (normalized.length > 260) {
45
+ throw new Error("Caminho muito longo (máximo 260 caracteres)");
46
+ }
47
+
48
+ return normalized;
49
+ }
50
+
51
+ /**
52
+ * Valida um caminho de arquivo relativo ao projeto
53
+ * @param {string} projectPath - Caminho do projeto
54
+ * @param {string} filePath - Caminho do arquivo relativo
55
+ * @returns {string} Caminho absoluto validado
56
+ */
57
+ export function validateFilePath(projectPath, filePath) {
58
+ const normalizedProject = validateProjectPath(projectPath);
59
+ const absoluteFile = path.resolve(normalizedProject, filePath);
60
+
61
+ // Garante que o arquivo está dentro do projeto
62
+ if (!absoluteFile.startsWith(normalizedProject)) {
63
+ throw new Error(`Path traversal detectado: '${filePath}' tenta acessar fora do projeto`);
64
+ }
65
+
66
+ return absoluteFile;
67
+ }
68
+
69
+ export function getEnv(key) {
70
+ const v = process.env[key];
71
+ return v === undefined ? "" : String(v);
72
+ }
73
+
74
+ export function getProvidersEnv() {
75
+ return {
76
+ githubToken: getEnv("GITHUB_TOKEN"),
77
+ giteaUrl: getEnv("GITEA_URL"),
78
+ giteaToken: getEnv("GITEA_TOKEN"),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Padrões comuns de .gitignore para diferentes tipos de projeto
84
+ */
85
+ export const GITIGNORE_TEMPLATES = {
86
+ node: [
87
+ "node_modules/",
88
+ "npm-debug.log*",
89
+ "yarn-debug.log*",
90
+ "yarn-error.log*",
91
+ ".npm",
92
+ ".yarn",
93
+ "dist/",
94
+ "build/",
95
+ ".env",
96
+ ".env.local",
97
+ ".env.*.local",
98
+ "*.log",
99
+ ".DS_Store",
100
+ "Thumbs.db",
101
+ "*.swp",
102
+ "*.swo",
103
+ ".idea/",
104
+ ".vscode/",
105
+ "*.sublime-*",
106
+ "coverage/",
107
+ ".nyc_output/"
108
+ ],
109
+ python: [
110
+ "__pycache__/",
111
+ "*.py[cod]",
112
+ "*$py.class",
113
+ "*.so",
114
+ ".Python",
115
+ "env/",
116
+ "venv/",
117
+ ".venv/",
118
+ "pip-log.txt",
119
+ "pip-delete-this-directory.txt",
120
+ ".tox/",
121
+ ".coverage",
122
+ ".cache",
123
+ "*.egg-info/",
124
+ ".installed.cfg",
125
+ "*.egg",
126
+ ".env",
127
+ ".vscode/",
128
+ ".idea/"
129
+ ],
130
+ general: [
131
+ ".DS_Store",
132
+ "Thumbs.db",
133
+ "*.log",
134
+ "*.tmp",
135
+ "*.temp",
136
+ "*.swp",
137
+ "*.swo",
138
+ "*~",
139
+ ".env",
140
+ ".env.local",
141
+ ".vscode/",
142
+ ".idea/"
143
+ ]
144
+ };
145
+
146
+ /**
147
+ * Detecta o tipo de projeto baseado nos arquivos existentes
148
+ */
149
+ export function detectProjectType(projectPath) {
150
+ if (fs.existsSync(path.join(projectPath, "package.json"))) {
151
+ return "node";
152
+ }
153
+ if (fs.existsSync(path.join(projectPath, "requirements.txt")) ||
154
+ fs.existsSync(path.join(projectPath, "setup.py")) ||
155
+ fs.existsSync(path.join(projectPath, "pyproject.toml"))) {
156
+ return "python";
157
+ }
158
+ return "general";
159
+ }
160
+
@@ -1,123 +1,123 @@
1
- // Sistema de Retry com Backoff Exponencial e Rate Limit Handling
2
-
3
- const DEFAULT_OPTIONS = {
4
- maxRetries: 3,
5
- initialDelay: 1000,
6
- maxDelay: 10000,
7
- backoffFactor: 2,
8
- retryableErrors: [
9
- // Network errors
10
- "ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH",
11
- // HTTP status codes
12
- "timeout", "rate limit", "429", "503", "502", "504",
13
- // SSL/TLS (pode ser temporário)
14
- "EPROTO", "UNABLE_TO_GET_ISSUER_CERT",
15
- // Outros
16
- "socket hang up", "network error"
17
- ]
18
- };
19
-
20
- function shouldRetry(error, options) {
21
- const msg = (error?.message || String(error)).toLowerCase();
22
- const code = error?.code?.toLowerCase() || "";
23
-
24
- return options.retryableErrors.some(e =>
25
- msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
26
- );
27
- }
28
-
29
- function sleep(ms) {
30
- return new Promise(resolve => setTimeout(resolve, ms));
31
- }
32
-
33
- /**
34
- * Extrai o tempo de espera do header Retry-After
35
- * @param {Error} error - Erro com possível informação de rate limit
36
- * @returns {number|null} - Tempo em ms para esperar, ou null se não encontrado
37
- */
38
- function getRetryAfterMs(error) {
39
- // Tenta extrair de headers (axios errors)
40
- const headers = error?.response?.headers;
41
- if (headers) {
42
- const retryAfter = headers['retry-after'] || headers['x-ratelimit-reset'];
43
- if (retryAfter) {
44
- // Se for número de segundos
45
- const seconds = parseInt(retryAfter, 10);
46
- if (!isNaN(seconds)) {
47
- // Se for timestamp unix (maior que ano 2000 em segundos)
48
- if (seconds > 946684800) {
49
- return Math.max(0, (seconds * 1000) - Date.now());
50
- }
51
- return seconds * 1000;
52
- }
53
- }
54
- }
55
-
56
- // Tenta extrair de GitHub API rate limit
57
- const rateLimitReset = error?.response?.headers?.['x-ratelimit-reset'];
58
- if (rateLimitReset) {
59
- const resetTime = parseInt(rateLimitReset, 10) * 1000;
60
- return Math.max(0, resetTime - Date.now());
61
- }
62
-
63
- return null;
64
- }
65
-
66
- export async function withRetry(fn, options = {}) {
67
- const opts = { ...DEFAULT_OPTIONS, ...options };
68
- let lastError;
69
- let delay = opts.initialDelay;
70
-
71
- for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
72
- try {
73
- return await fn();
74
- } catch (error) {
75
- lastError = error;
76
-
77
- if (attempt === opts.maxRetries || !shouldRetry(error, opts)) {
78
- throw error;
79
- }
80
-
81
- // Verificar se há Retry-After header
82
- const retryAfterMs = getRetryAfterMs(error);
83
- const waitTime = retryAfterMs !== null
84
- ? Math.min(retryAfterMs, opts.maxDelay)
85
- : delay;
86
-
87
- if (process.env.DEBUG_AGENT_LOG) {
88
- console.error(`[Retry] Attempt ${attempt}/${opts.maxRetries} failed, waiting ${waitTime}ms...`);
89
- }
90
-
91
- await sleep(waitTime);
92
- delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
93
- }
94
- }
95
-
96
- throw lastError;
97
- }
98
-
99
- // Wrapper para axios com retry
100
- export async function axiosWithRetry(axiosInstance, config, options = {}) {
101
- return withRetry(() => axiosInstance(config), options);
102
- }
103
-
104
- // Wrapper para operações git com retry
105
- export async function gitWithRetry(fn, options = {}) {
106
- return withRetry(fn, {
107
- ...options,
108
- retryableErrors: [...DEFAULT_OPTIONS.retryableErrors, "ENOENT", "lock"]
109
- });
110
- }
111
-
112
- /**
113
- * Wrapper específico para APIs com rate limit
114
- */
115
- export async function apiWithRateLimitRetry(fn, options = {}) {
116
- return withRetry(fn, {
117
- maxRetries: 5,
118
- initialDelay: 2000,
119
- maxDelay: 60000,
120
- ...options,
121
- retryableErrors: ["rate limit", "429", "403", "too many requests"]
122
- });
123
- }
1
+ // Sistema de Retry com Backoff Exponencial e Rate Limit Handling
2
+
3
+ const DEFAULT_OPTIONS = {
4
+ maxRetries: 3,
5
+ initialDelay: 1000,
6
+ maxDelay: 10000,
7
+ backoffFactor: 2,
8
+ retryableErrors: [
9
+ // Network errors
10
+ "ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH",
11
+ // HTTP status codes
12
+ "timeout", "rate limit", "429", "503", "502", "504",
13
+ // SSL/TLS (pode ser temporário)
14
+ "EPROTO", "UNABLE_TO_GET_ISSUER_CERT",
15
+ // Outros
16
+ "socket hang up", "network error"
17
+ ]
18
+ };
19
+
20
+ function shouldRetry(error, options) {
21
+ const msg = (error?.message || String(error)).toLowerCase();
22
+ const code = error?.code?.toLowerCase() || "";
23
+
24
+ return options.retryableErrors.some(e =>
25
+ msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
26
+ );
27
+ }
28
+
29
+ function sleep(ms) {
30
+ return new Promise(resolve => setTimeout(resolve, ms));
31
+ }
32
+
33
+ /**
34
+ * Extrai o tempo de espera do header Retry-After
35
+ * @param {Error} error - Erro com possível informação de rate limit
36
+ * @returns {number|null} - Tempo em ms para esperar, ou null se não encontrado
37
+ */
38
+ function getRetryAfterMs(error) {
39
+ // Tenta extrair de headers (axios errors)
40
+ const headers = error?.response?.headers;
41
+ if (headers) {
42
+ const retryAfter = headers['retry-after'] || headers['x-ratelimit-reset'];
43
+ if (retryAfter) {
44
+ // Se for número de segundos
45
+ const seconds = parseInt(retryAfter, 10);
46
+ if (!isNaN(seconds)) {
47
+ // Se for timestamp unix (maior que ano 2000 em segundos)
48
+ if (seconds > 946684800) {
49
+ return Math.max(0, (seconds * 1000) - Date.now());
50
+ }
51
+ return seconds * 1000;
52
+ }
53
+ }
54
+ }
55
+
56
+ // Tenta extrair de GitHub API rate limit
57
+ const rateLimitReset = error?.response?.headers?.['x-ratelimit-reset'];
58
+ if (rateLimitReset) {
59
+ const resetTime = parseInt(rateLimitReset, 10) * 1000;
60
+ return Math.max(0, resetTime - Date.now());
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ export async function withRetry(fn, options = {}) {
67
+ const opts = { ...DEFAULT_OPTIONS, ...options };
68
+ let lastError;
69
+ let delay = opts.initialDelay;
70
+
71
+ for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
72
+ try {
73
+ return await fn();
74
+ } catch (error) {
75
+ lastError = error;
76
+
77
+ if (attempt === opts.maxRetries || !shouldRetry(error, opts)) {
78
+ throw error;
79
+ }
80
+
81
+ // Verificar se há Retry-After header
82
+ const retryAfterMs = getRetryAfterMs(error);
83
+ const waitTime = retryAfterMs !== null
84
+ ? Math.min(retryAfterMs, opts.maxDelay)
85
+ : delay;
86
+
87
+ if (process.env.DEBUG_AGENT_LOG) {
88
+ console.error(`[Retry] Attempt ${attempt}/${opts.maxRetries} failed, waiting ${waitTime}ms...`);
89
+ }
90
+
91
+ await sleep(waitTime);
92
+ delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
93
+ }
94
+ }
95
+
96
+ throw lastError;
97
+ }
98
+
99
+ // Wrapper para axios com retry
100
+ export async function axiosWithRetry(axiosInstance, config, options = {}) {
101
+ return withRetry(() => axiosInstance(config), options);
102
+ }
103
+
104
+ // Wrapper para operações git com retry
105
+ export async function gitWithRetry(fn, options = {}) {
106
+ return withRetry(fn, {
107
+ ...options,
108
+ retryableErrors: [...DEFAULT_OPTIONS.retryableErrors, "ENOENT", "lock"]
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Wrapper específico para APIs com rate limit
114
+ */
115
+ export async function apiWithRateLimitRetry(fn, options = {}) {
116
+ return withRetry(fn, {
117
+ maxRetries: 5,
118
+ initialDelay: 2000,
119
+ maxDelay: 60000,
120
+ ...options,
121
+ retryableErrors: ["rate limit", "429", "403", "too many requests"]
122
+ });
123
+ }
package/install.sh DELETED
@@ -1,68 +0,0 @@
1
- #!/bin/bash
2
- # =============================================================================
3
- # git-mcp Install Script
4
- # =============================================================================
5
- # Quick install for Linux servers (especially SSH remote environments)
6
- # This avoids the npx timeout issues on slow/unstable networks
7
- # =============================================================================
8
-
9
- set -e
10
-
11
- GREEN='\033[0;32m'
12
- YELLOW='\033[1;33m'
13
- RED='\033[0;31m'
14
- NC='\033[0m' # No Color
15
-
16
- echo -e "${GREEN}🚀 Installing @andrebuzeli/git-mcp...${NC}"
17
-
18
- # Check for Node.js
19
- if ! command -v node &> /dev/null; then
20
- echo -e "${RED}❌ Node.js not found!${NC}"
21
- echo "Please install Node.js first: https://nodejs.org/"
22
- exit 1
23
- fi
24
-
25
- # Check Node version (need 18+)
26
- NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
27
- if [ "$NODE_VERSION" -lt 18 ]; then
28
- echo -e "${YELLOW}⚠️ Node.js version 18+ recommended (found: $(node -v))${NC}"
29
- fi
30
-
31
- # Check for npm
32
- if ! command -v npm &> /dev/null; then
33
- echo -e "${RED}❌ npm not found!${NC}"
34
- exit 1
35
- fi
36
-
37
- echo "📦 Installing globally..."
38
- npm install -g @andrebuzeli/git-mcp@latest
39
-
40
- # Verify installation
41
- if command -v git-mcp &> /dev/null; then
42
- echo -e "${GREEN}✅ git-mcp installed successfully!${NC}"
43
- echo ""
44
- echo -e "${YELLOW}📝 Next steps:${NC}"
45
- echo "1. Configure your IDE to use 'git-mcp' as the command"
46
- echo ""
47
- echo " For Cursor/VS Code (settings.json or mcp.json):"
48
- echo ' {'
49
- echo ' "mcpServers": {'
50
- echo ' "git-mcp": {'
51
- echo ' "command": "git-mcp",'
52
- echo ' "env": {'
53
- echo ' "GITHUB_TOKEN": "your-token",'
54
- echo ' "GITEA_URL": "https://your-gitea",'
55
- echo ' "GITEA_TOKEN": "your-token"'
56
- echo ' }'
57
- echo ' }'
58
- echo ' }'
59
- echo ' }'
60
- echo ""
61
- echo "2. Restart your IDE or reconnect SSH"
62
- echo ""
63
- echo -e "${GREEN}Done! 🎉${NC}"
64
- else
65
- echo -e "${RED}❌ Installation failed. Check npm permissions.${NC}"
66
- echo "Try: sudo npm install -g @andrebuzeli/git-mcp@latest"
67
- exit 1
68
- fi