@andrebuzeli/git-mcp 15.9.0 → 15.9.2

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.9.0",
3
+ "version": "15.9.2",
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",
package/src/index.js CHANGED
@@ -47,7 +47,7 @@ if (!hasGitHub && !hasGitea) {
47
47
 
48
48
  const transport = new StdioServerTransport();
49
49
  const server = new Server(
50
- { name: "git-mcpv2", version: "15.8.0" },
50
+ { name: "git-mcpv2", version: "15.9.2" },
51
51
  { capabilities: { tools: {}, resources: {}, prompts: {} } }
52
52
  );
53
53
  server.connect(transport);
@@ -8,13 +8,13 @@ export function createGitWorkflowTool(pm, git) {
8
8
  const inputSchema = {
9
9
  type: "object",
10
10
  properties: {
11
- projectPath: {
12
- type: "string",
13
- description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
11
+ projectPath: {
12
+ type: "string",
13
+ description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
14
14
  },
15
- action: {
16
- type: "string",
17
- enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
15
+ action: {
16
+ type: "string",
17
+ enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
18
18
  description: `Ação a executar:
19
19
  - init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
20
20
  - status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
@@ -23,20 +23,19 @@ export function createGitWorkflowTool(pm, git) {
23
23
  - commit: Cria commit com os arquivos staged (use DEPOIS de add)
24
24
  - push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
25
25
  - ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
26
- - clean: Remove arquivos não rastreados do working directory
27
- - update: Atualiza projeto completo: init (se necessário), add, commit, ensure-remotes e push`
26
+ - clean: Remove arquivos não rastreados do working directory`
28
27
  },
29
- files: {
30
- type: "array",
28
+ files: {
29
+ type: "array",
31
30
  items: { type: "string" },
32
31
  description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
33
32
  },
34
- message: {
33
+ message: {
35
34
  type: "string",
36
- description: "Mensagem do commit. Obrigatório para action='commit'. Opcional para action='update' (se não fornecido, usa mensagem padrão). Ex: 'feat: adiciona nova funcionalidade'"
35
+ description: "Mensagem do commit. Obrigatório para action='commit'. Ex: 'feat: adiciona nova funcionalidade'"
37
36
  },
38
- force: {
39
- type: "boolean",
37
+ force: {
38
+ type: "boolean",
40
39
  description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
41
40
  },
42
41
  createGitignore: {
@@ -48,7 +47,7 @@ export function createGitWorkflowTool(pm, git) {
48
47
  description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
49
48
  },
50
49
  dryRun: {
51
- type: "boolean",
50
+ type: "boolean",
52
51
  description: "Se true, simula a operação sem executar (útil para testes). Default: false"
53
52
  }
54
53
  },
@@ -72,7 +71,6 @@ QUANDO USAR CADA ACTION:
72
71
  - init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
73
72
  - ensure-remotes: Se push falhar por falta de configuração
74
73
  - clean: Limpar arquivos não rastreados
75
- - update: Para atualizar projeto completo (init + add + commit + push) - mais rápido que fazer cada ação separadamente
76
74
 
77
75
  EXEMPLOS DE USO:
78
76
  • Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
@@ -90,29 +88,29 @@ EXEMPLOS DE USO:
90
88
  try {
91
89
  // #region agent log
92
90
  if (process.env.DEBUG_AGENT_LOG) {
93
- fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-entry',message:'handle start',data:{action,projectPath},timestamp:Date.now()})}).catch(()=>{});
91
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H1', location: 'git-workflow.js:handle-entry', message: 'handle start', data: { action, projectPath }, timestamp: Date.now() }) }).catch(() => { });
94
92
  }
95
93
  // #endregion
96
94
 
97
95
  validateProjectPath(projectPath);
98
96
  // #region agent log
99
97
  if (process.env.DEBUG_AGENT_LOG) {
100
- fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-validated',message:'projectPath validated',data:{action},timestamp:Date.now()})}).catch(()=>{});
98
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H1', location: 'git-workflow.js:handle-validated', message: 'projectPath validated', data: { action }, timestamp: Date.now() }) }).catch(() => { });
101
99
  }
102
100
  // #endregion
103
101
  if (action === "init") {
104
102
  if (args.dryRun) {
105
- return asToolResult({
106
- success: true,
103
+ return asToolResult({
104
+ success: true,
107
105
  dryRun: true,
108
106
  message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
109
107
  repoName: getRepoNameFromPath(projectPath),
110
108
  gitignoreCreated: shouldCreateGitignore
111
109
  });
112
110
  }
113
-
111
+
114
112
  await git.init(projectPath);
115
-
113
+
116
114
  // Criar .gitignore baseado no tipo de projeto
117
115
  const shouldCreateGitignore = args.createGitignore !== false;
118
116
  let gitignoreCreated = false;
@@ -122,12 +120,12 @@ EXEMPLOS DE USO:
122
120
  await git.createGitignore(projectPath, patterns);
123
121
  gitignoreCreated = true;
124
122
  }
125
-
123
+
126
124
  const repo = getRepoNameFromPath(projectPath);
127
125
  const isPublic = args.isPublic === true; // Default: privado
128
126
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
129
- return asToolResult({
130
- success: true,
127
+ return asToolResult({
128
+ success: true,
131
129
  ensured,
132
130
  isPrivate: !isPublic,
133
131
  gitignoreCreated,
@@ -138,24 +136,24 @@ EXEMPLOS DE USO:
138
136
  const st = await git.status(projectPath);
139
137
  // #region agent log
140
138
  if (process.env.DEBUG_AGENT_LOG) {
141
- fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H2',location:'git-workflow.js:status',message:'status result',data:{modified:st.modified.length,created:st.created.length,deleted:st.deleted.length,notAdded:st.not_added.length,isClean:st.isClean},timestamp:Date.now()})}).catch(()=>{});
139
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H2', location: 'git-workflow.js:status', message: 'status result', data: { modified: st.modified.length, created: st.created.length, deleted: st.deleted.length, notAdded: st.not_added.length, isClean: st.isClean }, timestamp: Date.now() }) }).catch(() => { });
142
140
  }
143
141
  // #endregion
144
-
142
+
145
143
  if (args.dryRun) {
146
- return asToolResult({
147
- success: true,
144
+ return asToolResult({
145
+ success: true,
148
146
  dryRun: true,
149
147
  message: "DRY RUN: Status seria verificado",
150
148
  ...st
151
149
  });
152
150
  }
153
-
151
+
154
152
  // Adicionar contexto para AI Agent decidir próximo passo
155
153
  const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
156
154
  const hasStaged = st.staged?.length > 0;
157
155
  const hasConflicts = st.conflicted?.length > 0;
158
-
156
+
159
157
  let _aiContext = {
160
158
  needsAdd: hasUnstaged && !hasStaged,
161
159
  readyToCommit: hasStaged && !hasConflicts,
@@ -163,7 +161,7 @@ EXEMPLOS DE USO:
163
161
  isClean: st.isClean,
164
162
  suggestedAction: null
165
163
  };
166
-
164
+
167
165
  if (hasConflicts) {
168
166
  _aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
169
167
  } else if (hasStaged) {
@@ -173,21 +171,21 @@ EXEMPLOS DE USO:
173
171
  } else {
174
172
  _aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
175
173
  }
176
-
174
+
177
175
  return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
178
176
  }
179
177
  if (action === "add") {
180
178
  const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
181
-
179
+
182
180
  if (args.dryRun) {
183
- return asToolResult({
184
- success: true,
181
+ return asToolResult({
182
+ success: true,
185
183
  dryRun: true,
186
184
  message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
187
185
  files
188
186
  });
189
187
  }
190
-
188
+
191
189
  await git.add(projectPath, files);
192
190
  return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
193
191
  }
@@ -200,19 +198,19 @@ EXEMPLOS DE USO:
200
198
  if (!args.message) {
201
199
  return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
202
200
  }
203
-
201
+
204
202
  if (args.dryRun) {
205
- return asToolResult({
206
- success: true,
203
+ return asToolResult({
204
+ success: true,
207
205
  dryRun: true,
208
206
  message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
209
207
  });
210
208
  }
211
-
209
+
212
210
  const sha = await git.commit(projectPath, args.message);
213
211
  // #region agent log
214
212
  if (process.env.DEBUG_AGENT_LOG) {
215
- fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H3',location:'git-workflow.js:commit',message:'commit created',data:{sha,message:args.message},timestamp:Date.now()})}).catch(()=>{});
213
+ fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H3', location: 'git-workflow.js:commit', message: 'commit created', data: { sha, message: args.message }, timestamp: Date.now() }) }).catch(() => { });
216
214
  }
217
215
  // #endregion
218
216
  return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
@@ -220,19 +218,19 @@ EXEMPLOS DE USO:
220
218
  if (action === "clean") {
221
219
  if (args.dryRun) {
222
220
  const result = await git.cleanUntracked(projectPath);
223
- return asToolResult({
224
- success: true,
221
+ return asToolResult({
222
+ success: true,
225
223
  dryRun: true,
226
224
  message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
227
225
  wouldClean: result.cleaned
228
226
  });
229
227
  }
230
-
228
+
231
229
  const result = await git.cleanUntracked(projectPath);
232
- return asToolResult({
233
- success: true,
230
+ return asToolResult({
231
+ success: true,
234
232
  ...result,
235
- message: result.cleaned.length > 0
233
+ message: result.cleaned.length > 0
236
234
  ? `${result.cleaned.length} arquivo(s) não rastreados removidos`
237
235
  : "Nenhum arquivo para limpar"
238
236
  });
@@ -240,7 +238,7 @@ EXEMPLOS DE USO:
240
238
  if (action === "ensure-remotes") {
241
239
  const repo = getRepoNameFromPath(projectPath);
242
240
  const isPublic = args.isPublic === true; // Default: privado
243
-
241
+
244
242
  if (args.dryRun) {
245
243
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic }); // Don't create for dry run
246
244
  const ghOwner = await pm.getGitHubOwner();
@@ -248,9 +246,9 @@ EXEMPLOS DE USO:
248
246
  const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
249
247
  const base = pm.giteaUrl?.replace(/\/$/, "") || "";
250
248
  const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
251
-
252
- return asToolResult({
253
- success: true,
249
+
250
+ return asToolResult({
251
+ success: true,
254
252
  dryRun: true,
255
253
  message: "DRY RUN: Remotes seriam configurados",
256
254
  repo,
@@ -259,7 +257,7 @@ EXEMPLOS DE USO:
259
257
  ensured
260
258
  });
261
259
  }
262
-
260
+
263
261
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
264
262
  const ghOwner = await pm.getGitHubOwner();
265
263
  const geOwner = await pm.getGiteaOwner();
@@ -273,120 +271,22 @@ EXEMPLOS DE USO:
273
271
  if (action === "push") {
274
272
  const branch = await git.getCurrentBranch(projectPath);
275
273
  const force = !!args.force;
276
-
274
+
277
275
  if (args.dryRun) {
278
- return asToolResult({
279
- success: true,
276
+ return asToolResult({
277
+ success: true,
280
278
  dryRun: true,
281
279
  message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
282
280
  branch,
283
281
  force
284
282
  });
285
283
  }
286
-
284
+
287
285
  const result = await git.pushParallel(projectPath, branch, force);
288
286
  return asToolResult({ success: true, branch, ...result });
289
287
  }
290
- if (action === "update") {
291
- if (args.dryRun) {
292
- return asToolResult({
293
- success: true,
294
- dryRun: true,
295
- message: "DRY RUN: Update completo seria executado (init se necessário, add, commit, push)"
296
- });
297
- }
298
-
299
- const results = {
300
- init: null,
301
- ensureRemotes: null,
302
- add: null,
303
- commit: null,
304
- push: null
305
- };
306
-
307
- const repo = getRepoNameFromPath(projectPath);
308
- const isPublic = args.isPublic === true;
309
-
310
- // 1. Verificar se é repo Git, se não for, fazer init
311
- const isRepo = await git.isRepo(projectPath).catch(() => false);
312
- if (!isRepo) {
313
- await git.init(projectPath);
314
-
315
- // Criar .gitignore baseado no tipo de projeto
316
- const shouldCreateGitignore = args.createGitignore !== false;
317
- if (shouldCreateGitignore) {
318
- const projectType = detectProjectType(projectPath);
319
- const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
320
- await git.createGitignore(projectPath, patterns);
321
- }
322
-
323
- const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
324
- results.init = { success: true, ensured, isPrivate: !isPublic, gitignoreCreated: shouldCreateGitignore };
325
- } else {
326
- results.init = { success: true, skipped: true, message: "Repositório já existe" };
327
- }
328
-
329
- // 2. Garantir remotes configurados (sempre verifica/configura)
330
- const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
331
- const ghOwner = await pm.getGitHubOwner();
332
- const geOwner = await pm.getGiteaOwner();
333
- const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
334
- const base = pm.giteaUrl?.replace(/\/$/, "") || "";
335
- const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
336
- await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
337
- const remotes = await git.listRemotes(projectPath);
338
- results.ensureRemotes = { success: true, ensured, remotes };
339
-
340
- // 3. Verificar status e fazer add se necessário
341
- const status = await git.status(projectPath);
342
- if (!status.isClean || status.modified?.length > 0 || status.created?.length > 0 || status.notAdded?.length > 0) {
343
- await git.add(projectPath, ["."]);
344
- results.add = { success: true, files: ["."] };
345
- } else {
346
- results.add = { success: true, skipped: true, message: "Nenhum arquivo para adicionar" };
347
- }
348
-
349
- // 4. Verificar se há algo staged e fazer commit
350
- const statusAfterAdd = await git.status(projectPath);
351
- if (statusAfterAdd.staged?.length > 0) {
352
- // Gerar mensagem padrão se não fornecida
353
- const commitMessage = args.message || `Update: ${new Date().toISOString().split('T')[0]} - ${statusAfterAdd.staged.length} arquivo(s) modificado(s)`;
354
- const sha = await git.commit(projectPath, commitMessage);
355
- results.commit = { success: true, sha, message: commitMessage };
356
- } else {
357
- results.commit = { success: true, skipped: true, message: "Nenhum arquivo staged para commit" };
358
- }
359
-
360
- // 5. Fazer push (só se houver commits para enviar)
361
- const branch = await git.getCurrentBranch(projectPath);
362
- const force = !!args.force;
363
- try {
364
- const pushResult = await git.pushParallel(projectPath, branch, force);
365
- results.push = { success: true, branch, ...pushResult };
366
- } catch (pushError) {
367
- // Se push falhar mas não houver commits, não é erro crítico
368
- if (results.commit?.skipped) {
369
- results.push = { success: true, skipped: true, message: "Nenhum commit para enviar" };
370
- } else {
371
- throw pushError;
372
- }
373
- }
374
-
375
- // Resumo final
376
- const allSuccess = Object.values(results).every(r => r?.success !== false);
377
- const stepsExecuted = Object.entries(results)
378
- .filter(([_, r]) => r && !r.skipped)
379
- .map(([step, _]) => step);
380
-
381
- return asToolResult({
382
- success: allSuccess,
383
- message: `Update completo executado: ${stepsExecuted.join(" → ")}`,
384
- results,
385
- stepsExecuted
386
- }, { tool: 'workflow', action: 'update' });
387
- }
388
- return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
389
- availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
288
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
289
+ availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
390
290
  suggestion: "Use uma das actions disponíveis"
391
291
  });
392
292
  } catch (e) {
@@ -16,14 +16,14 @@ export const ERROR_CODES = {
16
16
  suggestion: "Verifique se GITHUB_TOKEN está configurado e é válido. Gere um novo token em github.com/settings/tokens"
17
17
  },
18
18
  AUTH_GITEA_INVALID: {
19
- code: "AUTH_GITEA_INVALID",
19
+ code: "AUTH_GITEA_INVALID",
20
20
  suggestion: "Verifique se GITEA_TOKEN e GITEA_URL estão configurados corretamente"
21
21
  },
22
22
  AUTH_NO_PERMISSION: {
23
23
  code: "AUTH_NO_PERMISSION",
24
24
  suggestion: "Token não tem permissão para esta operação. Verifique os scopes do token"
25
25
  },
26
-
26
+
27
27
  // Repo
28
28
  REPO_NOT_FOUND: {
29
29
  code: "REPO_NOT_FOUND",
@@ -37,7 +37,7 @@ export const ERROR_CODES = {
37
37
  code: "REPO_ALREADY_EXISTS",
38
38
  suggestion: "Repositório já existe. Use action='ensure' para verificar ou escolha outro nome"
39
39
  },
40
-
40
+
41
41
  // Git Local
42
42
  NOT_A_GIT_REPO: {
43
43
  code: "NOT_A_GIT_REPO",
@@ -55,7 +55,7 @@ export const ERROR_CODES = {
55
55
  code: "NOTHING_TO_PUSH",
56
56
  suggestion: "Nenhuma mudança para push. Faça commits primeiro"
57
57
  },
58
-
58
+
59
59
  // Branches
60
60
  BRANCH_NOT_FOUND: {
61
61
  code: "BRANCH_NOT_FOUND",
@@ -69,7 +69,7 @@ export const ERROR_CODES = {
69
69
  code: "CANNOT_DELETE_CURRENT",
70
70
  suggestion: "Não pode deletar branch atual. Faça checkout para outra branch primeiro"
71
71
  },
72
-
72
+
73
73
  // Tags
74
74
  TAG_NOT_FOUND: {
75
75
  code: "TAG_NOT_FOUND",
@@ -79,7 +79,7 @@ export const ERROR_CODES = {
79
79
  code: "TAG_ALREADY_EXISTS",
80
80
  suggestion: "Tag já existe. Use outro nome ou delete a existente primeiro"
81
81
  },
82
-
82
+
83
83
  // Refs
84
84
  REF_NOT_FOUND: {
85
85
  code: "REF_NOT_FOUND",
@@ -89,7 +89,7 @@ export const ERROR_CODES = {
89
89
  code: "INSUFFICIENT_HISTORY",
90
90
  suggestion: "Histórico insuficiente para HEAD~N. Use action='log' para verificar quantos commits existem"
91
91
  },
92
-
92
+
93
93
  // Stash
94
94
  NOTHING_TO_STASH: {
95
95
  code: "NOTHING_TO_STASH",
@@ -99,7 +99,7 @@ export const ERROR_CODES = {
99
99
  code: "STASH_NOT_FOUND",
100
100
  suggestion: "Stash não encontrado. Use action='list' para ver stashes disponíveis"
101
101
  },
102
-
102
+
103
103
  // Remote
104
104
  REMOTE_NOT_FOUND: {
105
105
  code: "REMOTE_NOT_FOUND",
@@ -113,7 +113,7 @@ export const ERROR_CODES = {
113
113
  code: "MERGE_CONFLICT",
114
114
  suggestion: "Conflito de merge. Resolva conflitos manualmente e faça novo commit"
115
115
  },
116
-
116
+
117
117
  // Network
118
118
  NETWORK_TIMEOUT: {
119
119
  code: "NETWORK_TIMEOUT",
@@ -123,7 +123,7 @@ export const ERROR_CODES = {
123
123
  code: "RATE_LIMIT",
124
124
  suggestion: "Rate limit excedido. Aguarde alguns minutos antes de tentar novamente"
125
125
  },
126
-
126
+
127
127
  // Validation
128
128
  VALIDATION_ERROR: {
129
129
  code: "VALIDATION_ERROR",
@@ -133,13 +133,13 @@ export const ERROR_CODES = {
133
133
  code: "MISSING_PARAMETER",
134
134
  suggestion: "Parâmetro obrigatório faltando"
135
135
  },
136
-
136
+
137
137
  // Files
138
138
  FILE_NOT_FOUND: {
139
139
  code: "FILE_NOT_FOUND",
140
140
  suggestion: "Arquivo não encontrado no repositório. Use action='list' para ver arquivos disponíveis"
141
141
  },
142
-
142
+
143
143
  // Issues/PRs
144
144
  ISSUE_NOT_FOUND: {
145
145
  code: "ISSUE_NOT_FOUND",
@@ -149,7 +149,7 @@ export const ERROR_CODES = {
149
149
  code: "PR_NOT_FOUND",
150
150
  suggestion: "Pull Request não encontrado. Use action='list' para ver PRs disponíveis"
151
151
  },
152
-
152
+
153
153
  // Filesystem
154
154
  DISK_FULL: {
155
155
  code: "DISK_FULL",
@@ -163,7 +163,7 @@ export const ERROR_CODES = {
163
163
  code: "PERMISSION_DENIED_FS",
164
164
  suggestion: "Sem permissão para acessar arquivo/diretório. Verifique permissões do sistema de arquivos"
165
165
  },
166
-
166
+
167
167
  // Git Corruption
168
168
  GIT_CORRUPTED: {
169
169
  code: "GIT_CORRUPTED",
@@ -171,9 +171,9 @@ export const ERROR_CODES = {
171
171
  },
172
172
  GIT_LOCK_FILE: {
173
173
  code: "GIT_LOCK_FILE",
174
- suggestion: "Arquivo de lock presente (.git/index.lock). Outro processo git pode estar rodando. Se não, delete o arquivo de lock manualmente"
174
+ suggestion: "Arquivo de lock presente (.git/index.lock). Outro processo git pode estar rodando (comum em drives de rede). O sistema tentou auto-corrigir, mas se persistir, delete o arquivo de lock manualmente."
175
175
  },
176
-
176
+
177
177
  // Remote/URL
178
178
  INVALID_REMOTE_URL: {
179
179
  code: "INVALID_REMOTE_URL",
@@ -187,7 +187,7 @@ export const ERROR_CODES = {
187
187
  code: "SSL_ERROR",
188
188
  suggestion: "Erro de SSL/TLS. Verifique certificados ou use http.sslVerify=false (não recomendado)"
189
189
  },
190
-
190
+
191
191
  // Generic
192
192
  UNKNOWN_ERROR: {
193
193
  code: "UNKNOWN_ERROR",
@@ -207,14 +207,14 @@ export function createError(errorCode, details = {}) {
207
207
  // Função para mapear erros externos para códigos internos
208
208
  export function mapExternalError(error, context = {}) {
209
209
  const msg = error?.message?.toLowerCase() || String(error).toLowerCase();
210
-
210
+
211
211
  // GitHub/Gitea API errors
212
212
  if (msg.includes("bad credentials") || msg.includes("401")) {
213
- return context.provider === "gitea"
213
+ return context.provider === "gitea"
214
214
  ? createError("AUTH_GITEA_INVALID", { originalError: msg })
215
215
  : createError("AUTH_GITHUB_INVALID", { originalError: msg });
216
216
  }
217
-
217
+
218
218
  if (msg.includes("not found") || msg.includes("404")) {
219
219
  if (context.type === "repo") return createError("REPO_NOT_FOUND", { repo: context.repo, originalError: msg });
220
220
  if (context.type === "branch") return createError("BRANCH_NOT_FOUND", { branch: context.branch, originalError: msg });
@@ -224,116 +224,116 @@ export function mapExternalError(error, context = {}) {
224
224
  if (context.type === "pr") return createError("PR_NOT_FOUND", { number: context.number, originalError: msg });
225
225
  return createError("REF_NOT_FOUND", { ref: context.ref, originalError: msg });
226
226
  }
227
-
227
+
228
228
  if (msg.includes("permission") || msg.includes("403") || msg.includes("forbidden")) {
229
229
  return createError("AUTH_NO_PERMISSION", { originalError: msg });
230
230
  }
231
-
231
+
232
232
  if (msg.includes("rate limit") || msg.includes("429")) {
233
233
  return createError("RATE_LIMIT", { originalError: msg });
234
234
  }
235
-
235
+
236
236
  if (msg.includes("timeout") || msg.includes("etimedout") || msg.includes("econnrefused")) {
237
237
  return createError("NETWORK_TIMEOUT", { originalError: msg });
238
238
  }
239
-
239
+
240
240
  if (msg.includes("push rejected") || msg.includes("non-fast-forward")) {
241
241
  return createError("PUSH_REJECTED", { originalError: msg });
242
242
  }
243
-
243
+
244
244
  if (msg.includes("conflict")) {
245
245
  return createError("MERGE_CONFLICT", { originalError: msg });
246
246
  }
247
-
247
+
248
248
  if (msg.includes("already exists")) {
249
249
  if (context.type === "repo") return createError("REPO_ALREADY_EXISTS", { repo: context.repo, originalError: msg });
250
250
  if (context.type === "branch") return createError("BRANCH_ALREADY_EXISTS", { branch: context.branch, originalError: msg });
251
251
  if (context.type === "tag") return createError("TAG_ALREADY_EXISTS", { tag: context.tag, originalError: msg });
252
252
  return createError("BRANCH_ALREADY_EXISTS", { originalError: msg });
253
253
  }
254
-
254
+
255
255
  // isomorphic-git specific errors
256
256
  if (msg.includes("could not find") && msg.includes("head~")) {
257
257
  return createError("INSUFFICIENT_HISTORY", { originalError: msg });
258
258
  }
259
-
259
+
260
260
  if (msg.includes("is not a git repository") || msg.includes("could not find .git")) {
261
261
  return createError("NOT_A_GIT_REPO", { path: context.path, originalError: msg });
262
262
  }
263
-
263
+
264
264
  if (msg.includes("nothing to commit") || msg.includes("working tree clean")) {
265
265
  return createError("NOTHING_TO_COMMIT", { originalError: msg });
266
266
  }
267
-
267
+
268
268
  if (msg.includes("could not resolve ref") || msg.includes("invalid reference")) {
269
269
  return createError("REF_NOT_FOUND", { originalError: msg });
270
270
  }
271
-
271
+
272
272
  if (msg.includes("branch") && (msg.includes("does not exist") || msg.includes("not found"))) {
273
273
  return createError("BRANCH_NOT_FOUND", { originalError: msg });
274
274
  }
275
-
275
+
276
276
  if (msg.includes("tag") && (msg.includes("does not exist") || msg.includes("not found"))) {
277
277
  return createError("TAG_NOT_FOUND", { originalError: msg });
278
278
  }
279
-
279
+
280
280
  if (msg.includes("no commits") || msg.includes("unknown revision")) {
281
281
  return createError("NO_COMMITS", { originalError: msg });
282
282
  }
283
-
283
+
284
284
  if (msg.includes("nada para stash") || msg.includes("nothing to stash") || msg.includes("no local changes")) {
285
285
  return createError("NOTHING_TO_STASH", { originalError: msg });
286
286
  }
287
-
287
+
288
288
  if (msg.includes("stash") && (msg.includes("not found") || msg.includes("não encontrado"))) {
289
289
  return createError("STASH_NOT_FOUND", { originalError: msg });
290
290
  }
291
-
291
+
292
292
  if (msg.includes("author") || msg.includes("committer")) {
293
- return createError("VALIDATION_ERROR", {
294
- message: "Configure user.name e user.email",
293
+ return createError("VALIDATION_ERROR", {
294
+ message: "Configure user.name e user.email",
295
295
  suggestion: "Use git-config para configurar ou forneça tokens de provider",
296
- originalError: msg
296
+ originalError: msg
297
297
  });
298
298
  }
299
-
299
+
300
300
  // Filesystem errors
301
301
  if (msg.includes("enospc") || msg.includes("no space left") || msg.includes("disk full")) {
302
302
  return createError("DISK_FULL", { originalError: msg });
303
303
  }
304
-
304
+
305
305
  if (msg.includes("file too large") || msg.includes("exceeds") || msg.includes("size limit")) {
306
306
  return createError("FILE_TOO_LARGE", { originalError: msg });
307
307
  }
308
-
308
+
309
309
  if (msg.includes("eacces") || msg.includes("eperm") || (msg.includes("permission") && msg.includes("denied"))) {
310
310
  return createError("PERMISSION_DENIED_FS", { originalError: msg });
311
311
  }
312
-
312
+
313
313
  // Git corruption
314
- if (msg.includes("corrupt") || msg.includes("bad object") || msg.includes("broken") ||
315
- msg.includes("missing object") || msg.includes("invalid sha") || msg.includes("fatal: packed")) {
314
+ if (msg.includes("corrupt") || msg.includes("bad object") || msg.includes("broken") ||
315
+ msg.includes("missing object") || msg.includes("invalid sha") || msg.includes("fatal: packed")) {
316
316
  return createError("GIT_CORRUPTED", { originalError: msg });
317
317
  }
318
-
318
+
319
319
  if (msg.includes(".git/index.lock") || msg.includes("unable to create") && msg.includes("lock")) {
320
320
  return createError("GIT_LOCK_FILE", { originalError: msg });
321
321
  }
322
-
322
+
323
323
  // Remote/URL errors
324
324
  if (msg.includes("invalid url") || msg.includes("malformed") || msg.includes("not a valid")) {
325
325
  return createError("INVALID_REMOTE_URL", { originalError: msg });
326
326
  }
327
-
328
- if (msg.includes("could not resolve host") || msg.includes("name resolution") ||
329
- msg.includes("no address") || msg.includes("unreachable")) {
327
+
328
+ if (msg.includes("could not resolve host") || msg.includes("name resolution") ||
329
+ msg.includes("no address") || msg.includes("unreachable")) {
330
330
  return createError("REMOTE_UNREACHABLE", { originalError: msg });
331
331
  }
332
-
332
+
333
333
  if (msg.includes("ssl") || msg.includes("certificate") || msg.includes("tls")) {
334
334
  return createError("SSL_ERROR", { originalError: msg });
335
335
  }
336
-
336
+
337
337
  // Generic fallback
338
338
  return createError("UNKNOWN_ERROR", { originalError: msg, context });
339
339
  }
@@ -347,27 +347,27 @@ export const NEXT_ACTIONS = {
347
347
  "workflow:add": { tool: "git-workflow", action: "commit", hint: "Crie um commit com uma mensagem descritiva" },
348
348
  "workflow:commit": { tool: "git-workflow", action: "push", hint: "Envie as mudanças para GitHub/Gitea" },
349
349
  "workflow:push": null, // Fluxo completo
350
-
350
+
351
351
  // git-branches
352
352
  "branches:create": { tool: "git-branches", action: "checkout", hint: "Mude para a nova branch criada" },
353
353
  "branches:checkout": { tool: "git-workflow", action: "status", hint: "Verifique o estado da branch" },
354
354
  "branches:delete": null,
355
-
355
+
356
356
  // git-stash
357
357
  "stash:save": { tool: "git-branches", action: "checkout", hint: "Agora pode trocar de branch com segurança" },
358
358
  "stash:pop": { tool: "git-workflow", action: "status", hint: "Verifique as mudanças restauradas" },
359
-
359
+
360
360
  // git-merge
361
361
  "merge:merge": { tool: "git-workflow", action: "push", hint: "Envie o merge para os remotes" },
362
-
362
+
363
363
  // git-tags
364
364
  "tags:create": { tool: "git-tags", action: "push", hint: "Envie a tag para os remotes" },
365
365
  "tags:push": null,
366
-
366
+
367
367
  // git-remote
368
368
  "remote:ensure": { tool: "git-workflow", action: "push", hint: "Agora pode fazer push" },
369
369
  "remote:release-create": null,
370
-
370
+
371
371
  // git-sync
372
372
  "sync:pull": { tool: "git-workflow", action: "status", hint: "Verifique mudanças após pull" },
373
373
  "sync:fetch": { tool: "git-sync", action: "pull", hint: "Aplique as mudanças baixadas" },
@@ -388,7 +388,7 @@ export function getNextAction(tool, action) {
388
388
  export function asToolResult(result, options = {}) {
389
389
  const { tool, action } = options;
390
390
  const response = { ...result };
391
-
391
+
392
392
  // Adicionar próxima ação sugerida se disponível
393
393
  if (tool && action) {
394
394
  const next = getNextAction(tool, action);
@@ -398,7 +398,7 @@ export function asToolResult(result, options = {}) {
398
398
  response._flowComplete = true;
399
399
  }
400
400
  }
401
-
401
+
402
402
  return {
403
403
  content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
404
404
  isError: false,
@@ -28,7 +28,11 @@ export class GitAdapter {
28
28
  constructor(providerManager) {
29
29
  this.pm = providerManager;
30
30
  this.gitPath = "git"; // Initial default
31
- this.gitEnv = { ...process.env, LANG: "en_US.UTF-8" }; // Base env
31
+ this.gitEnv = {
32
+ ...process.env,
33
+ LANG: "en_US.UTF-8",
34
+ GIT_OPTIONAL_LOCKS: "0", // Evita operações de background que seguram lock (importante para drives de rede)
35
+ };
32
36
  this.timeout = GIT_TIMEOUT;
33
37
  this.resolvePromise = this._resolveGit();
34
38
  }
@@ -145,13 +149,22 @@ export class GitAdapter {
145
149
 
146
150
  if (shouldDelete) {
147
151
  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
+ try {
153
+ fs.unlinkSync(lockPath);
154
+ } catch (unlinkErr) {
155
+ console.error("[GitAdapter] Failed to delete lock file:", unlinkErr.message);
156
+ // Set shouldDelete to false so we don't try to retry if deletion failed
157
+ shouldDelete = false;
158
+ }
159
+
160
+ if (shouldDelete) {
161
+ // Espera um pouco para o sistema de arquivos (especialmente SMB) registrar
162
+ await new Promise(r => setTimeout(r, 2000));
163
+ return await this._exec(dir, args, { ...options, _lockRetried: true });
164
+ }
152
165
  }
153
- } catch (unlinkError) {
154
- console.error("[GitAdapter] Failed to check/remove lock file:", unlinkError.message);
166
+ } catch (fsError) {
167
+ console.error("[GitAdapter] Error handling lock file:", fsError.message);
155
168
  // Continua para lançar o erro original
156
169
  }
157
170
  }