@andrebuzeli/git-mcp 15.8.3 → 15.8.4

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.3",
3
+ "version": "15.8.4",
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",
@@ -11,6 +11,8 @@ export class ProviderManager {
11
11
  this.github = this.githubToken ? new Octokit({ auth: this.githubToken }) : null;
12
12
  this._githubOwner = "";
13
13
  this._giteaOwner = "";
14
+ this._githubEmail = "";
15
+ this._giteaEmail = "";
14
16
  this._ownerFetchedAt = 0;
15
17
  this._remoteUrlsCache = new Map(); // Cache para URLs de remotes
16
18
  this._cacheExpiry = 10 * 60 * 1000; // 10 minutos
@@ -23,6 +25,7 @@ export class ProviderManager {
23
25
  try {
24
26
  const me = await this.github.rest.users.getAuthenticated();
25
27
  this._githubOwner = me.data.login || "";
28
+ this._githubEmail = me.data.email || ""; // Capture email
26
29
  this._ownerFetchedAt = now;
27
30
  return this._githubOwner;
28
31
  } catch {
@@ -30,6 +33,11 @@ export class ProviderManager {
30
33
  }
31
34
  }
32
35
 
36
+ async getGitHubEmail() {
37
+ if (!this._githubEmail) await this.getGitHubOwner();
38
+ return this._githubEmail;
39
+ }
40
+
33
41
  async getGiteaOwner() {
34
42
  if (!this.giteaUrl || !this.giteaToken) return "";
35
43
  const now = Date.now();
@@ -41,6 +49,7 @@ export class ProviderManager {
41
49
  });
42
50
  const d = r.data || {};
43
51
  this._giteaOwner = d.login || d.username || "";
52
+ this._giteaEmail = d.email || ""; // Capture email
44
53
  this._ownerFetchedAt = now;
45
54
  return this._giteaOwner;
46
55
  } catch {
@@ -48,6 +57,11 @@ export class ProviderManager {
48
57
  }
49
58
  }
50
59
 
60
+ async getGiteaEmail() {
61
+ if (!this._giteaEmail) await this.getGiteaOwner();
62
+ return this._giteaEmail;
63
+ }
64
+
51
65
  async getRemoteUrls(repoName) {
52
66
  const cacheKey = `urls_${repoName}`;
53
67
  const now = Date.now();
@@ -156,16 +170,16 @@ export class ProviderManager {
156
170
  let page = 1;
157
171
  let hasMore = true;
158
172
  while (hasMore && page <= maxPages) {
159
- const { data } = await this.github.rest.repos.listForAuthenticatedUser({
160
- per_page: perPage,
173
+ const { data } = await this.github.rest.repos.listForAuthenticatedUser({
174
+ per_page: perPage,
161
175
  page,
162
- visibility: "all"
176
+ visibility: "all"
163
177
  });
164
- results.github.push(...data.map(r => ({
165
- name: r.name,
166
- fullName: r.full_name,
167
- url: r.html_url,
168
- isPrivate: r.private
178
+ results.github.push(...data.map(r => ({
179
+ name: r.name,
180
+ fullName: r.full_name,
181
+ url: r.html_url,
182
+ isPrivate: r.private
169
183
  })));
170
184
  hasMore = data.length === perPage;
171
185
  page++;
@@ -185,11 +199,11 @@ export class ProviderManager {
185
199
  const { data } = await axios.get(`${base}/api/v1/user/repos?limit=${perPage}&page=${page}`, {
186
200
  headers: { Authorization: `token ${this.giteaToken}` }
187
201
  });
188
- results.gitea.push(...data.map(r => ({
189
- name: r.name,
190
- fullName: r.full_name,
191
- url: r.html_url,
192
- isPrivate: r.private
202
+ results.gitea.push(...data.map(r => ({
203
+ name: r.name,
204
+ fullName: r.full_name,
205
+ url: r.html_url,
206
+ isPrivate: r.private
193
207
  })));
194
208
  hasMore = data.length === perPage;
195
209
  page++;
@@ -61,14 +61,19 @@ export class GitAdapter {
61
61
  return new Promise((resolve, reject) => {
62
62
  const cp = spawn(cmd, args, {
63
63
  cwd: dir,
64
- env: { ...this.gitEnv, ...env },
64
+ env: {
65
+ ...this.gitEnv,
66
+ ...env,
67
+ GIT_TERMINAL_PROMPT: "0", // Prevent interactive prompts (fixes "No such device or address")
68
+ GCM_INTERACTIVE: "never" // Disable Credential Manager prompts
69
+ },
65
70
  stdio: 'pipe'
66
71
  });
67
72
 
68
73
  let stdout = [];
69
74
  let stderr = [];
70
75
  let killed = false;
71
-
76
+
72
77
  // Timeout handler
73
78
  const timeoutId = setTimeout(() => {
74
79
  killed = true;
@@ -87,7 +92,7 @@ export class GitAdapter {
87
92
  cp.on("close", code => {
88
93
  clearTimeout(timeoutId);
89
94
  if (killed) return; // Already rejected by timeout
90
-
95
+
91
96
  const outStr = Buffer.concat(stdout).toString("utf8").trim();
92
97
  const errStr = Buffer.concat(stderr).toString("utf8").trim();
93
98
 
@@ -109,7 +114,7 @@ export class GitAdapter {
109
114
  return output;
110
115
  } catch (e) {
111
116
  const msg = e.message || "";
112
-
117
+
113
118
  // Auto-fix 1: dubious ownership error (common on network shares)
114
119
  if (msg.includes("dubious ownership") && !options._retried) {
115
120
  console.error("[GitAdapter] Auto-fix: dubious ownership, adding safe.directory...");
@@ -120,7 +125,7 @@ export class GitAdapter {
120
125
  console.error("[GitAdapter] Failed to configure safe.directory:", configError.message);
121
126
  }
122
127
  }
123
-
128
+
124
129
  // Auto-fix 2: Lock file presente (outro processo git pode ter crashado)
125
130
  if ((msg.includes(".git/index.lock") || msg.includes("Unable to create") && msg.includes("lock")) && !options._lockRetried) {
126
131
  const lockPath = path.join(dir, ".git", "index.lock");
@@ -134,7 +139,7 @@ export class GitAdapter {
134
139
  }
135
140
  }
136
141
  }
137
-
142
+
138
143
  // Auto-fix 3: CRLF warnings (just retry with autocrlf config)
139
144
  if (msg.includes("CRLF will be replaced") && !options._crlfRetried) {
140
145
  console.error("[GitAdapter] Auto-fix: configuring core.autocrlf...");
@@ -147,7 +152,7 @@ export class GitAdapter {
147
152
  }
148
153
 
149
154
  if (options.ignoreErrors) return "";
150
-
155
+
151
156
  // Enhance error message with context
152
157
  const cmdStr = `git ${args.join(" ")}`;
153
158
  const enhancedError = new Error(`Command failed: ${cmdStr}\n${msg}`);
@@ -182,12 +187,12 @@ export class GitAdapter {
182
187
  */
183
188
  async checkIntegrity(dir) {
184
189
  await this._ensureGitRepo(dir);
185
-
190
+
186
191
  try {
187
192
  // Verificação rápida de objetos
188
193
  const output = await this._exec(dir, ["fsck", "--connectivity-only"], { ignoreErrors: true });
189
194
  const hasErrors = output.includes("error") || output.includes("missing") || output.includes("broken");
190
-
195
+
191
196
  return {
192
197
  ok: !hasErrors,
193
198
  output: output || "Repository integrity OK",
@@ -211,7 +216,7 @@ export class GitAdapter {
211
216
  if (!url || typeof url !== "string") {
212
217
  return { valid: false, error: "URL vazia ou inválida" };
213
218
  }
214
-
219
+
215
220
  // Padrões válidos de URL git
216
221
  const patterns = [
217
222
  /^https?:\/\/[^\s]+\.git$/i, // https://github.com/user/repo.git
@@ -220,9 +225,9 @@ export class GitAdapter {
220
225
  /^git:\/\/[^\s]+\.git$/i, // git://github.com/user/repo.git
221
226
  /^ssh:\/\/[^\s]+\.git$/i, // ssh://git@github.com/user/repo.git
222
227
  ];
223
-
228
+
224
229
  const isValid = patterns.some(p => p.test(url));
225
-
230
+
226
231
  if (!isValid) {
227
232
  return {
228
233
  valid: false,
@@ -230,7 +235,7 @@ export class GitAdapter {
230
235
  suggestion: "Use: https://github.com/user/repo.git ou git@github.com:user/repo.git"
231
236
  };
232
237
  }
233
-
238
+
234
239
  return { valid: true, url };
235
240
  }
236
241
 
@@ -291,18 +296,18 @@ export class GitAdapter {
291
296
 
292
297
  const currentBranch = await this.getCurrentBranch(dir);
293
298
  return {
294
- modified,
295
- created,
296
- deleted,
297
- renamed: [],
299
+ modified,
300
+ created,
301
+ deleted,
302
+ renamed: [],
298
303
  notAdded,
299
304
  // Aliases para compatibilidade
300
305
  not_added: notAdded,
301
- staged,
306
+ staged,
302
307
  conflicted: [],
303
308
  currentBranch,
304
309
  current: currentBranch, // Alias para compatibilidade
305
- isClean: lines.length === 0,
310
+ isClean: lines.length === 0,
306
311
  files
307
312
  };
308
313
  }
@@ -330,8 +335,21 @@ export class GitAdapter {
330
335
  // Fallback logic
331
336
  const ownerGH = await this.pm.getGitHubOwner().catch(() => null);
332
337
  const ownerGE = await this.pm.getGiteaOwner().catch(() => null);
338
+
339
+ let email = "";
340
+
341
+ // Try to fetch real email
342
+ const emailGH = await this.pm.getGitHubEmail().catch(() => null);
343
+ const emailGE = await this.pm.getGiteaEmail().catch(() => null);
344
+
345
+ if (emailGE) email = emailGE;
346
+ else if (emailGH) email = emailGH;
347
+
333
348
  const who = ownerGH || ownerGE || "Git User";
334
- const email = `${who}@users.noreply`;
349
+
350
+ if (!email) {
351
+ email = `${who}@users.noreply`;
352
+ }
335
353
 
336
354
  await this.setConfig(dir, "user.name", who);
337
355
  await this.setConfig(dir, "user.email", email);
@@ -340,6 +358,7 @@ export class GitAdapter {
340
358
  }
341
359
 
342
360
  async commit(dir, message) {
361
+ // Ensure author is set
343
362
  await this.getAuthor(dir);
344
363
  await this._exec(dir, ["commit", "-m", message]);
345
364
  return await this._exec(dir, ["rev-parse", "HEAD"]);
@@ -413,19 +432,19 @@ export class GitAdapter {
413
432
  } catch (e) {
414
433
  const msg = e.message || "";
415
434
  const msgLower = msg.toLowerCase();
416
-
435
+
417
436
  // 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
-
437
+ const branchNotExists = msgLower.includes("has no upstream branch") ||
438
+ msgLower.includes("no upstream branch") ||
439
+ (msgLower.includes("remote branch") && msgLower.includes("does not exist")) ||
440
+ msgLower.includes("ref_not_found") ||
441
+ (msgLower.includes("fatal") && msgLower.includes("current branch") && msgLower.includes("has no upstream"));
442
+
424
443
  if (branchNotExists && !setUpstream && !force) {
425
444
  console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream (-u)...`);
426
445
  return await this.pushOne(dir, remote, branch, force, true);
427
446
  }
428
-
447
+
429
448
  if (msg.includes("rejected") || msgLower.includes("non-fast-forward")) {
430
449
  throw createError("PUSH_REJECTED", { message: msg, remote, branch });
431
450
  }
@@ -450,22 +469,22 @@ export class GitAdapter {
450
469
  } catch (error) {
451
470
  const errorMsg = error.message || String(error);
452
471
  const errorMsgLower = errorMsg.toLowerCase();
453
-
472
+
454
473
  // 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
-
474
+ const repoNotFound = errorMsgLower.includes("repository not found") ||
475
+ errorMsgLower.includes("repo not found") ||
476
+ (errorMsgLower.includes("fatal") && errorMsgLower.includes("repository") && errorMsgLower.includes("not found")) ||
477
+ (errorMsgLower.includes("not found") && (errorMsgLower.includes("remote") || errorMsgLower.includes("404")));
478
+
460
479
  if (repoNotFound && !force) {
461
480
  try {
462
481
  console.error(`[GitAdapter] Auto-fix: repositório não existe no remote '${remote}', criando automaticamente...`);
463
482
  const repoName = getRepoNameFromPath(dir);
464
483
  const ensured = await this.pm.ensureRepos({ repoName, createIfMissing: true, isPublic: false });
465
-
484
+
466
485
  // Atualiza remotes após criar repo
467
486
  await this.ensureRemotes(dir, {});
468
-
487
+
469
488
  // Tenta push novamente
470
489
  await this.pushOne(dir, remote, branch, force, true); // Usa -u para criar branch também
471
490
  return { remote, success: true, repoCreated: true, setUpstream: true };
@@ -473,13 +492,13 @@ export class GitAdapter {
473
492
  return { remote, success: false, error: `Falhou ao criar repo: ${repoCreateError.message}`, triedCreateRepo: true };
474
493
  }
475
494
  }
476
-
495
+
477
496
  // 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
-
497
+ const branchNotExists = errorMsgLower.includes("has no upstream branch") ||
498
+ errorMsgLower.includes("no upstream branch") ||
499
+ (errorMsgLower.includes("remote branch") && errorMsgLower.includes("does not exist")) ||
500
+ errorMsgLower.includes("ref_not_found");
501
+
483
502
  if (branchNotExists && !force) {
484
503
  try {
485
504
  console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream...`);
@@ -489,7 +508,7 @@ export class GitAdapter {
489
508
  return { remote, success: false, error: retryError.message, triedSetUpstream: true };
490
509
  }
491
510
  }
492
-
511
+
493
512
  return { remote, success: false, error: errorMsg };
494
513
  }
495
514
  });
@@ -499,17 +518,17 @@ export class GitAdapter {
499
518
  const failed = results.filter(r => r.status === "rejected" || !r.value.success);
500
519
 
501
520
  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")
521
+ throw createError("PUSH_REJECTED", {
522
+ message: "Push falhou para todos os remotes",
523
+ errors: failed.map(f => f.value?.error || f.reason?.message || "Erro desconhecido")
505
524
  });
506
525
  }
507
526
 
508
527
  return {
509
528
  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"
529
+ failed: failed.map(f => ({
530
+ remote: f.value?.remote || "unknown",
531
+ error: f.value?.error || f.reason?.message || "Erro desconhecido"
513
532
  }))
514
533
  };
515
534
  }
@@ -709,7 +728,7 @@ export class GitAdapter {
709
728
  async resetSoft(dir, ref) { await this._exec(dir, ["reset", "--soft", ref]); }
710
729
  async resetMixed(dir, ref) { await this._exec(dir, ["reset", "--mixed", ref]); }
711
730
  async resetHard(dir, ref) { await this._exec(dir, ["reset", "--hard", ref]); }
712
-
731
+
713
732
  async resetHardClean(dir, ref) {
714
733
  await this._exec(dir, ["reset", "--hard", ref]);
715
734
  const cleanResult = await this.cleanUntracked(dir);
@@ -719,19 +738,19 @@ export class GitAdapter {
719
738
  async getMergeStatus(dir) {
720
739
  const mergeHeadPath = path.join(dir, ".git", "MERGE_HEAD");
721
740
  const isMerging = fs.existsSync(mergeHeadPath);
722
-
741
+
723
742
  if (!isMerging) {
724
743
  return { merging: false, message: "Nenhum merge em andamento" };
725
744
  }
726
-
745
+
727
746
  // Get conflicted files
728
747
  try {
729
748
  const diff = await this._exec(dir, ["diff", "--name-only", "--diff-filter=U"]);
730
749
  const conflicts = diff.split("\n").filter(Boolean);
731
- return {
732
- merging: true,
750
+ return {
751
+ merging: true,
733
752
  conflicts,
734
- message: conflicts.length > 0
753
+ message: conflicts.length > 0
735
754
  ? `Merge em andamento com ${conflicts.length} conflito(s)`
736
755
  : "Merge em andamento sem conflitos"
737
756
  };
@@ -841,7 +860,7 @@ export class GitAdapter {
841
860
  }
842
861
 
843
862
  // ============ GIT LFS SUPPORT ============
844
-
863
+
845
864
  /**
846
865
  * Verifica se Git LFS está instalado
847
866
  */