@andrebuzeli/git-mcp 15.8.5 → 15.9.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrebuzeli/git-mcp",
3
- "version": "15.8.5",
3
+ "version": "15.9.0",
4
4
  "private": false,
5
5
  "description": "MCP server para Git com operações locais e sincronização paralela GitHub/Gitea",
6
6
  "license": "MIT",
@@ -23,6 +23,7 @@
23
23
  "@modelcontextprotocol/sdk": "^0.4.0",
24
24
  "@octokit/rest": "^20.0.0",
25
25
  "ajv": "^8.12.0",
26
+ "archiver": "^7.0.0",
26
27
  "axios": "^1.7.7"
27
28
  }
28
29
  }
package/src/index.js CHANGED
@@ -91,17 +91,18 @@ server.setRequestHandler(
91
91
  async (req) => {
92
92
  const name = req.params?.name || "";
93
93
  const args = req.params?.arguments || {};
94
- const progressToken = req.params?._meta?.progressToken;
94
+ // Progress reporting removed to prevent timeouts/unknown token errors on slow networks
95
+ // const progressToken = req.params?._meta?.progressToken;
96
+
95
97
  const tool = tools.find(t => t.name === name);
96
98
  if (!tool) return { content: [{ type: "text", text: `Tool não encontrada: ${name}` }], isError: true };
97
99
  try {
98
- if (progressToken) {
99
- await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0 } });
100
- }
100
+ // if (progressToken) { await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0 } }); }
101
+
101
102
  const result = await tool.handle(args);
102
- if (progressToken) {
103
- await server.notification({ method: "notifications/progress", params: { progressToken, progress: 100 } });
104
- }
103
+
104
+ // if (progressToken) { await server.notification({ method: "notifications/progress", params: { progressToken, progress: 100 } }); }
105
+
105
106
  return result;
106
107
  } catch (e) {
107
108
  return asToolError(e.code || "ERROR", e.message || String(e));
package/src/utils/env.js CHANGED
@@ -29,7 +29,7 @@ export function loadEnv(envPath = null) {
29
29
  // Ignora erros de leitura
30
30
  }
31
31
  }
32
-
32
+
33
33
  return false;
34
34
  }
35
35
 
@@ -39,24 +39,24 @@ export function loadEnv(envPath = null) {
39
39
  */
40
40
  function parseEnvFile(content) {
41
41
  const lines = content.split("\n");
42
-
42
+
43
43
  for (const line of lines) {
44
44
  // Ignora linhas vazias e comentários
45
45
  const trimmed = line.trim();
46
46
  if (!trimmed || trimmed.startsWith("#")) continue;
47
-
47
+
48
48
  // Parse KEY=VALUE
49
49
  const match = trimmed.match(/^([^=]+)=(.*)$/);
50
50
  if (match) {
51
51
  const key = match[1].trim();
52
52
  let value = match[2].trim();
53
-
53
+
54
54
  // Remove aspas ao redor do valor
55
55
  if ((value.startsWith('"') && value.endsWith('"')) ||
56
- (value.startsWith("'") && value.endsWith("'"))) {
56
+ (value.startsWith("'") && value.endsWith("'"))) {
57
57
  value = value.slice(1, -1);
58
58
  }
59
-
59
+
60
60
  // Só define se não existir (variáveis de ambiente têm precedência)
61
61
  if (!(key in process.env)) {
62
62
  process.env[key] = value;
@@ -95,10 +95,10 @@ export const ENV_CONFIG = {
95
95
  GITHUB_TOKEN: "Token de autenticação do GitHub",
96
96
  GITEA_URL: "URL do servidor Gitea",
97
97
  GITEA_TOKEN: "Token de autenticação do Gitea",
98
-
98
+
99
99
  // Performance
100
- GIT_TIMEOUT_MS: "Timeout para operações Git em ms (default: 60000)",
101
-
100
+ GIT_TIMEOUT_MS: "Timeout para operações Git em ms (default: 120000)",
101
+
102
102
  // Debug
103
103
  DEBUG_AGENT_LOG: "Habilita logging de debug"
104
104
  };
@@ -21,8 +21,8 @@ const GIT_CANDIDATES = [
21
21
  "git"
22
22
  ];
23
23
 
24
- // Timeout configurável via variável de ambiente (default: 60 segundos)
25
- const GIT_TIMEOUT = parseInt(process.env.GIT_TIMEOUT_MS || "60000", 10);
24
+ // Timeout configurável via variável de ambiente (default: 120 segundos para suportar drives de rede)
25
+ const GIT_TIMEOUT = parseInt(process.env.GIT_TIMEOUT_MS || "120000", 10);
26
26
 
27
27
  export class GitAdapter {
28
28
  constructor(providerManager) {
@@ -68,7 +68,7 @@ export class GitAdapter {
68
68
  let stdout = [];
69
69
  let stderr = [];
70
70
  let killed = false;
71
-
71
+
72
72
  // Timeout handler
73
73
  const timeoutId = setTimeout(() => {
74
74
  killed = true;
@@ -87,7 +87,7 @@ export class GitAdapter {
87
87
  cp.on("close", code => {
88
88
  clearTimeout(timeoutId);
89
89
  if (killed) return; // Already rejected by timeout
90
-
90
+
91
91
  const outStr = Buffer.concat(stdout).toString("utf8").trim();
92
92
  const errStr = Buffer.concat(stderr).toString("utf8").trim();
93
93
 
@@ -109,7 +109,7 @@ export class GitAdapter {
109
109
  return output;
110
110
  } catch (e) {
111
111
  const msg = e.message || "";
112
-
112
+
113
113
  // Auto-fix 1: dubious ownership error (common on network shares)
114
114
  if (msg.includes("dubious ownership") && !options._retried) {
115
115
  console.error("[GitAdapter] Auto-fix: dubious ownership, adding safe.directory...");
@@ -120,21 +120,43 @@ export class GitAdapter {
120
120
  console.error("[GitAdapter] Failed to configure safe.directory:", configError.message);
121
121
  }
122
122
  }
123
-
123
+
124
124
  // Auto-fix 2: Lock file presente (outro processo git pode ter crashado)
125
- if ((msg.includes(".git/index.lock") || msg.includes("Unable to create") && msg.includes("lock")) && !options._lockRetried) {
125
+ if ((msg.includes(".git/index.lock") || (msg.includes("Unable to create") && msg.includes("lock"))) && !options._lockRetried) {
126
126
  const lockPath = path.join(dir, ".git", "index.lock");
127
+ let shouldDelete = false;
128
+
127
129
  if (fs.existsSync(lockPath)) {
128
- console.error("[GitAdapter] Auto-fix: removing stale .git/index.lock...");
129
130
  try {
130
- fs.unlinkSync(lockPath);
131
- return await this._exec(dir, args, { ...options, _lockRetried: true });
131
+ const stats = fs.statSync(lockPath);
132
+ const now = Date.now();
133
+ const ageMs = now - stats.mtimeMs;
134
+ // Se arquivo tem mais de 5 minutos ou se o erro já persiste (implicado pelo fato de estarmos aqui) -> Deletar
135
+ // Para redes lentas, 5 min é seguro.
136
+ if (ageMs > 5 * 60 * 1000) {
137
+ console.error(`[GitAdapter] Lock file is old (${Math.round(ageMs / 1000)}s), deleting...`);
138
+ shouldDelete = true;
139
+ } else {
140
+ // Se é recente, pode ser um processo ativo. Mas como falhou o comando, assumimos que bloqueou.
141
+ // Em execuções agenticas sequenciais, dificilmente tem outro processo legítimo rodando ao mesmo tempo.
142
+ console.error(`[GitAdapter] Lock file found. Agentic force cleanup activated.`);
143
+ shouldDelete = true;
144
+ }
145
+
146
+ if (shouldDelete) {
147
+ console.error("[GitAdapter] Auto-fix: removing stale .git/index.lock...");
148
+ fs.unlinkSync(lockPath);
149
+ // Espera um pouco para o sistema de arquivos (especialmente SMB) registrar
150
+ await new Promise(r => setTimeout(r, 1000));
151
+ return await this._exec(dir, args, { ...options, _lockRetried: true });
152
+ }
132
153
  } catch (unlinkError) {
133
- console.error("[GitAdapter] Failed to remove lock file:", unlinkError.message);
154
+ console.error("[GitAdapter] Failed to check/remove lock file:", unlinkError.message);
155
+ // Continua para lançar o erro original
134
156
  }
135
157
  }
136
158
  }
137
-
159
+
138
160
  // Auto-fix 3: CRLF warnings (just retry with autocrlf config)
139
161
  if (msg.includes("CRLF will be replaced") && !options._crlfRetried) {
140
162
  console.error("[GitAdapter] Auto-fix: configuring core.autocrlf...");
@@ -147,7 +169,7 @@ export class GitAdapter {
147
169
  }
148
170
 
149
171
  if (options.ignoreErrors) return "";
150
-
172
+
151
173
  // Enhance error message with context
152
174
  const cmdStr = `git ${args.join(" ")}`;
153
175
  const enhancedError = new Error(`Command failed: ${cmdStr}\n${msg}`);
@@ -182,12 +204,12 @@ export class GitAdapter {
182
204
  */
183
205
  async checkIntegrity(dir) {
184
206
  await this._ensureGitRepo(dir);
185
-
207
+
186
208
  try {
187
209
  // Verificação rápida de objetos
188
210
  const output = await this._exec(dir, ["fsck", "--connectivity-only"], { ignoreErrors: true });
189
211
  const hasErrors = output.includes("error") || output.includes("missing") || output.includes("broken");
190
-
212
+
191
213
  return {
192
214
  ok: !hasErrors,
193
215
  output: output || "Repository integrity OK",
@@ -211,7 +233,7 @@ export class GitAdapter {
211
233
  if (!url || typeof url !== "string") {
212
234
  return { valid: false, error: "URL vazia ou inválida" };
213
235
  }
214
-
236
+
215
237
  // Padrões válidos de URL git
216
238
  const patterns = [
217
239
  /^https?:\/\/[^\s]+\.git$/i, // https://github.com/user/repo.git
@@ -220,9 +242,9 @@ export class GitAdapter {
220
242
  /^git:\/\/[^\s]+\.git$/i, // git://github.com/user/repo.git
221
243
  /^ssh:\/\/[^\s]+\.git$/i, // ssh://git@github.com/user/repo.git
222
244
  ];
223
-
245
+
224
246
  const isValid = patterns.some(p => p.test(url));
225
-
247
+
226
248
  if (!isValid) {
227
249
  return {
228
250
  valid: false,
@@ -230,7 +252,7 @@ export class GitAdapter {
230
252
  suggestion: "Use: https://github.com/user/repo.git ou git@github.com:user/repo.git"
231
253
  };
232
254
  }
233
-
255
+
234
256
  return { valid: true, url };
235
257
  }
236
258
 
@@ -291,18 +313,18 @@ export class GitAdapter {
291
313
 
292
314
  const currentBranch = await this.getCurrentBranch(dir);
293
315
  return {
294
- modified,
295
- created,
296
- deleted,
297
- renamed: [],
316
+ modified,
317
+ created,
318
+ deleted,
319
+ renamed: [],
298
320
  notAdded,
299
321
  // Aliases para compatibilidade
300
322
  not_added: notAdded,
301
- staged,
323
+ staged,
302
324
  conflicted: [],
303
325
  currentBranch,
304
326
  current: currentBranch, // Alias para compatibilidade
305
- isClean: lines.length === 0,
327
+ isClean: lines.length === 0,
306
328
  files
307
329
  };
308
330
  }
@@ -413,19 +435,19 @@ export class GitAdapter {
413
435
  } catch (e) {
414
436
  const msg = e.message || "";
415
437
  const msgLower = msg.toLowerCase();
416
-
438
+
417
439
  // Auto-correção: Se branch não existe no remote, tenta com -u (set-upstream)
418
- const branchNotExists = msgLower.includes("has no upstream branch") ||
419
- msgLower.includes("no upstream branch") ||
420
- (msgLower.includes("remote branch") && msgLower.includes("does not exist")) ||
421
- msgLower.includes("ref_not_found") ||
422
- (msgLower.includes("fatal") && msgLower.includes("current branch") && msgLower.includes("has no upstream"));
423
-
440
+ const branchNotExists = msgLower.includes("has no upstream branch") ||
441
+ msgLower.includes("no upstream branch") ||
442
+ (msgLower.includes("remote branch") && msgLower.includes("does not exist")) ||
443
+ msgLower.includes("ref_not_found") ||
444
+ (msgLower.includes("fatal") && msgLower.includes("current branch") && msgLower.includes("has no upstream"));
445
+
424
446
  if (branchNotExists && !setUpstream && !force) {
425
447
  console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream (-u)...`);
426
448
  return await this.pushOne(dir, remote, branch, force, true);
427
449
  }
428
-
450
+
429
451
  if (msg.includes("rejected") || msgLower.includes("non-fast-forward")) {
430
452
  throw createError("PUSH_REJECTED", { message: msg, remote, branch });
431
453
  }
@@ -450,22 +472,22 @@ export class GitAdapter {
450
472
  } catch (error) {
451
473
  const errorMsg = error.message || String(error);
452
474
  const errorMsgLower = errorMsg.toLowerCase();
453
-
475
+
454
476
  // Auto-correção 1: Se repositório não existe, cria automaticamente
455
- const repoNotFound = errorMsgLower.includes("repository not found") ||
456
- errorMsgLower.includes("repo not found") ||
457
- (errorMsgLower.includes("fatal") && errorMsgLower.includes("repository") && errorMsgLower.includes("not found")) ||
458
- (errorMsgLower.includes("not found") && (errorMsgLower.includes("remote") || errorMsgLower.includes("404")));
459
-
477
+ const repoNotFound = errorMsgLower.includes("repository not found") ||
478
+ errorMsgLower.includes("repo not found") ||
479
+ (errorMsgLower.includes("fatal") && errorMsgLower.includes("repository") && errorMsgLower.includes("not found")) ||
480
+ (errorMsgLower.includes("not found") && (errorMsgLower.includes("remote") || errorMsgLower.includes("404")));
481
+
460
482
  if (repoNotFound && !force) {
461
483
  try {
462
484
  console.error(`[GitAdapter] Auto-fix: repositório não existe no remote '${remote}', criando automaticamente...`);
463
485
  const repoName = getRepoNameFromPath(dir);
464
486
  const ensured = await this.pm.ensureRepos({ repoName, createIfMissing: true, isPublic: false });
465
-
487
+
466
488
  // Atualiza remotes após criar repo
467
489
  await this.ensureRemotes(dir, {});
468
-
490
+
469
491
  // Tenta push novamente
470
492
  await this.pushOne(dir, remote, branch, force, true); // Usa -u para criar branch também
471
493
  return { remote, success: true, repoCreated: true, setUpstream: true };
@@ -473,13 +495,13 @@ export class GitAdapter {
473
495
  return { remote, success: false, error: `Falhou ao criar repo: ${repoCreateError.message}`, triedCreateRepo: true };
474
496
  }
475
497
  }
476
-
498
+
477
499
  // Auto-correção 2: Se branch não existe no remote, tenta com --set-upstream
478
- const branchNotExists = errorMsgLower.includes("has no upstream branch") ||
479
- errorMsgLower.includes("no upstream branch") ||
480
- (errorMsgLower.includes("remote branch") && errorMsgLower.includes("does not exist")) ||
481
- errorMsgLower.includes("ref_not_found");
482
-
500
+ const branchNotExists = errorMsgLower.includes("has no upstream branch") ||
501
+ errorMsgLower.includes("no upstream branch") ||
502
+ (errorMsgLower.includes("remote branch") && errorMsgLower.includes("does not exist")) ||
503
+ errorMsgLower.includes("ref_not_found");
504
+
483
505
  if (branchNotExists && !force) {
484
506
  try {
485
507
  console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream...`);
@@ -489,7 +511,7 @@ export class GitAdapter {
489
511
  return { remote, success: false, error: retryError.message, triedSetUpstream: true };
490
512
  }
491
513
  }
492
-
514
+
493
515
  return { remote, success: false, error: errorMsg };
494
516
  }
495
517
  });
@@ -499,17 +521,17 @@ export class GitAdapter {
499
521
  const failed = results.filter(r => r.status === "rejected" || !r.value.success);
500
522
 
501
523
  if (successful.length === 0) {
502
- throw createError("PUSH_REJECTED", {
503
- message: "Push falhou para todos os remotes",
504
- errors: failed.map(f => f.value?.error || f.reason?.message || "Erro desconhecido")
524
+ throw createError("PUSH_REJECTED", {
525
+ message: "Push falhou para todos os remotes",
526
+ errors: failed.map(f => f.value?.error || f.reason?.message || "Erro desconhecido")
505
527
  });
506
528
  }
507
529
 
508
530
  return {
509
531
  pushed: successful.map(s => s.value.remote),
510
- failed: failed.map(f => ({
511
- remote: f.value?.remote || "unknown",
512
- error: f.value?.error || f.reason?.message || "Erro desconhecido"
532
+ failed: failed.map(f => ({
533
+ remote: f.value?.remote || "unknown",
534
+ error: f.value?.error || f.reason?.message || "Erro desconhecido"
513
535
  }))
514
536
  };
515
537
  }
@@ -709,7 +731,7 @@ export class GitAdapter {
709
731
  async resetSoft(dir, ref) { await this._exec(dir, ["reset", "--soft", ref]); }
710
732
  async resetMixed(dir, ref) { await this._exec(dir, ["reset", "--mixed", ref]); }
711
733
  async resetHard(dir, ref) { await this._exec(dir, ["reset", "--hard", ref]); }
712
-
734
+
713
735
  async resetHardClean(dir, ref) {
714
736
  await this._exec(dir, ["reset", "--hard", ref]);
715
737
  const cleanResult = await this.cleanUntracked(dir);
@@ -719,19 +741,19 @@ export class GitAdapter {
719
741
  async getMergeStatus(dir) {
720
742
  const mergeHeadPath = path.join(dir, ".git", "MERGE_HEAD");
721
743
  const isMerging = fs.existsSync(mergeHeadPath);
722
-
744
+
723
745
  if (!isMerging) {
724
746
  return { merging: false, message: "Nenhum merge em andamento" };
725
747
  }
726
-
748
+
727
749
  // Get conflicted files
728
750
  try {
729
751
  const diff = await this._exec(dir, ["diff", "--name-only", "--diff-filter=U"]);
730
752
  const conflicts = diff.split("\n").filter(Boolean);
731
- return {
732
- merging: true,
753
+ return {
754
+ merging: true,
733
755
  conflicts,
734
- message: conflicts.length > 0
756
+ message: conflicts.length > 0
735
757
  ? `Merge em andamento com ${conflicts.length} conflito(s)`
736
758
  : "Merge em andamento sem conflitos"
737
759
  };
@@ -841,7 +863,7 @@ export class GitAdapter {
841
863
  }
842
864
 
843
865
  // ============ GIT LFS SUPPORT ============
844
-
866
+
845
867
  /**
846
868
  * Verifica se Git LFS está instalado
847
869
  */