@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.
- package/README.md +39 -125
- package/package.json +27 -44
- package/src/index.js +146 -139
- package/src/providers/providerManager.js +203 -217
- package/src/tools/git-diff.js +137 -126
- package/src/tools/git-help.js +285 -285
- package/src/tools/git-remote.js +472 -472
- package/src/tools/git-workflow.js +403 -403
- package/src/utils/env.js +104 -104
- package/src/utils/errors.js +431 -431
- package/src/utils/gitAdapter.js +932 -951
- package/src/utils/hooks.js +255 -255
- package/src/utils/metrics.js +198 -198
- package/src/utils/providerExec.js +58 -58
- package/src/utils/repoHelpers.js +160 -160
- package/src/utils/retry.js +123 -123
- package/install.sh +0 -68
package/src/utils/repoHelpers.js
CHANGED
|
@@ -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
|
+
|
package/src/utils/retry.js
CHANGED
|
@@ -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
|