@andrebuzeli/git-mcp 13.5.0 → 13.7.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": "13.5.0",
3
+ "version": "13.7.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",
package/src/index.js CHANGED
@@ -19,6 +19,10 @@ import { createGitSyncTool } from "./tools/git-sync.js";
19
19
  import { createGitIssuesTool } from "./tools/git-issues.js";
20
20
  import { createGitPullsTool } from "./tools/git-pulls.js";
21
21
 
22
+ // #region agent log
23
+ const debugLog = (loc, msg, data) => { fetch('http://127.0.0.1:8242/ingest/e5799a4a-1a0d-4201-a6ce-42835e6f6fc7',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,message:msg,data,timestamp:Date.now(),sessionId:'debug-session'})}).catch(()=>{}); };
24
+ // #endregion
25
+
22
26
  const transport = new StdioServerTransport();
23
27
  const server = new Server({ name: "git-mcpv2", version: "0.0.0" });
24
28
  server.connect(transport);
@@ -66,6 +70,10 @@ server.setRequestHandler(
66
70
  const tool = tools.find(t => t.name === name);
67
71
  if (!tool) return { content: [{ type: "text", text: `Tool não encontrada: ${name}` }], isError: true };
68
72
  try {
73
+ // #region agent log
74
+ const startTime = Date.now();
75
+ debugLog('index.js:call', `START ${name}`, { action: args.action, projectPath: args.projectPath, hypothesisId: 'A' });
76
+ // #endregion
69
77
  if (progressToken) {
70
78
  await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0 } });
71
79
  }
@@ -73,6 +81,9 @@ server.setRequestHandler(
73
81
  if (progressToken) {
74
82
  await server.notification({ method: "notifications/progress", params: { progressToken, progress: 100 } });
75
83
  }
84
+ // #region agent log
85
+ debugLog('index.js:call', `END ${name}`, { duration: Date.now() - startTime, hypothesisId: 'A' });
86
+ // #endregion
76
87
  return result;
77
88
  } catch (e) {
78
89
  return asToolError(e.code || "ERROR", e.message || String(e));
@@ -2,6 +2,10 @@ import { Octokit } from "@octokit/rest";
2
2
  import axios from "axios";
3
3
  import { getProvidersEnv } from "../utils/repoHelpers.js";
4
4
 
5
+ // #region agent log
6
+ const debugLog = (loc, msg, data) => { fetch('http://127.0.0.1:8242/ingest/e5799a4a-1a0d-4201-a6ce-42835e6f6fc7',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,message:msg,data,timestamp:Date.now(),sessionId:'debug-session'})}).catch(()=>{}); };
7
+ // #endregion
8
+
5
9
  export class ProviderManager {
6
10
  constructor() {
7
11
  const { githubToken, giteaUrl, giteaToken } = getProvidersEnv();
@@ -18,12 +22,22 @@ export class ProviderManager {
18
22
  if (!this.github) return "";
19
23
  const now = Date.now();
20
24
  if (this._githubOwner && now - this._ownerFetchedAt < 5 * 60 * 1000) return this._githubOwner;
25
+ // #region agent log
26
+ const t0 = Date.now();
27
+ debugLog('providerManager.js:getGitHubOwner', 'START API call', { hypothesisId: 'B' });
28
+ // #endregion
21
29
  try {
22
30
  const me = await this.github.rest.users.getAuthenticated();
23
31
  this._githubOwner = me.data.login || "";
24
32
  this._ownerFetchedAt = now;
33
+ // #region agent log
34
+ debugLog('providerManager.js:getGitHubOwner', 'END API call', { duration: Date.now() - t0, owner: this._githubOwner, hypothesisId: 'B' });
35
+ // #endregion
25
36
  return this._githubOwner;
26
37
  } catch {
38
+ // #region agent log
39
+ debugLog('providerManager.js:getGitHubOwner', 'FAILED API call', { duration: Date.now() - t0, hypothesisId: 'B' });
40
+ // #endregion
27
41
  return "";
28
42
  }
29
43
  }
@@ -32,6 +46,10 @@ export class ProviderManager {
32
46
  if (!this.giteaUrl || !this.giteaToken) return "";
33
47
  const now = Date.now();
34
48
  if (this._giteaOwner && now - this._ownerFetchedAt < 5 * 60 * 1000) return this._giteaOwner;
49
+ // #region agent log
50
+ const t0 = Date.now();
51
+ debugLog('providerManager.js:getGiteaOwner', 'START API call', { hypothesisId: 'B' });
52
+ // #endregion
35
53
  try {
36
54
  const r = await axios.get(`${this.giteaUrl}/api/v1/user`, {
37
55
  headers: { Authorization: `token ${this.giteaToken}` },
@@ -40,8 +58,14 @@ export class ProviderManager {
40
58
  const d = r.data || {};
41
59
  this._giteaOwner = d.login || d.username || "";
42
60
  this._ownerFetchedAt = now;
61
+ // #region agent log
62
+ debugLog('providerManager.js:getGiteaOwner', 'END API call', { duration: Date.now() - t0, owner: this._giteaOwner, hypothesisId: 'B' });
63
+ // #endregion
43
64
  return this._giteaOwner;
44
65
  } catch {
66
+ // #region agent log
67
+ debugLog('providerManager.js:getGiteaOwner', 'FAILED API call', { duration: Date.now() - t0, hypothesisId: 'B' });
68
+ // #endregion
45
69
  return "";
46
70
  }
47
71
  }
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -29,32 +29,46 @@ export function createGitBranchesTool(git) {
29
29
  }
30
30
  if (action === "create") {
31
31
  const ref = args.branch;
32
- if (!ref) return asToolError("VALIDATION_ERROR", "branch é obrigatório");
32
+ if (!ref) return asToolError("MISSING_PARAMETER", "branch é obrigatório", { parameter: "branch" });
33
+ // Auto-correção: verificar se já existe
34
+ const existing = await git.listBranches(projectPath, false);
35
+ if (existing.includes(ref)) {
36
+ return asToolError("BRANCH_ALREADY_EXISTS", `Branch '${ref}' já existe`, { branch: ref, existingBranches: existing });
37
+ }
33
38
  await git.createBranch(projectPath, ref);
34
39
  return asToolResult({ success: true, branch: ref });
35
40
  }
36
41
  if (action === "delete") {
37
42
  const ref = args.branch;
38
- if (!ref) return asToolError("VALIDATION_ERROR", "branch é obrigatório");
43
+ if (!ref) return asToolError("MISSING_PARAMETER", "branch é obrigatório", { parameter: "branch" });
44
+ // Auto-correção: verificar se existe e não é a atual
45
+ const existing = await git.listBranches(projectPath, false);
46
+ if (!existing.includes(ref)) {
47
+ return asToolError("BRANCH_NOT_FOUND", `Branch '${ref}' não encontrada`, { branch: ref, availableBranches: existing });
48
+ }
49
+ const current = await git.getCurrentBranch(projectPath);
50
+ if (ref === current) {
51
+ return asToolError("CANNOT_DELETE_CURRENT", `Não pode deletar branch atual '${ref}'`, { branch: ref, suggestion: "Faça checkout para outra branch primeiro" });
52
+ }
39
53
  await git.deleteBranch(projectPath, ref, !!args.force);
40
54
  return asToolResult({ success: true, branch: ref });
41
55
  }
42
56
  if (action === "rename") {
43
57
  const oldName = args.branch;
44
58
  const newName = args.newBranch;
45
- if (!oldName || !newName) return asToolError("VALIDATION_ERROR", "branch e newBranch são obrigatórios");
59
+ if (!oldName || !newName) return asToolError("MISSING_PARAMETER", "branch e newBranch são obrigatórios", { parameters: ["branch", "newBranch"] });
46
60
  await git.renameBranch(projectPath, oldName, newName);
47
61
  return asToolResult({ success: true, from: oldName, to: newName });
48
62
  }
49
63
  if (action === "checkout") {
50
64
  const ref = args.branch;
51
- if (!ref) return asToolError("VALIDATION_ERROR", "branch é obrigatório");
65
+ if (!ref) return asToolError("MISSING_PARAMETER", "branch é obrigatório", { parameter: "branch" });
52
66
  await git.checkout(projectPath, ref);
53
67
  return asToolResult({ success: true, branch: ref });
54
68
  }
55
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
69
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "create", "delete", "rename", "checkout"] });
56
70
  } catch (e) {
57
- return asToolError(e.code || "ERROR", e.message || String(e));
71
+ return errorToResponse(e);
58
72
  }
59
73
  }
60
74
 
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -24,30 +24,29 @@ export function createGitConfigTool(git) {
24
24
  const scope = args.scope || "local";
25
25
  try {
26
26
  if (action === "get") {
27
- if (!args.key) return asToolError("VALIDATION_ERROR", "key é obrigatório");
27
+ if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
28
28
  const val = await git.getConfig(projectPath, args.key, scope);
29
- return asToolResult({ key: args.key, value: val });
29
+ return asToolResult({ key: args.key, value: val, found: val !== undefined });
30
30
  }
31
31
  if (action === "set") {
32
- if (!args.key) return asToolError("VALIDATION_ERROR", "key é obrigatório");
32
+ if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
33
33
  await git.setConfig(projectPath, args.key, args.value ?? "", scope);
34
34
  return asToolResult({ success: true, key: args.key, value: args.value ?? "" });
35
35
  }
36
36
  if (action === "unset") {
37
- if (!args.key) return asToolError("VALIDATION_ERROR", "key é obrigatório");
37
+ if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
38
38
  await git.unsetConfig(projectPath, args.key, scope);
39
39
  return asToolResult({ success: true, key: args.key });
40
40
  }
41
41
  if (action === "list") {
42
42
  const items = await git.listConfig(projectPath, scope);
43
- return asToolResult({ scope, items });
43
+ return asToolResult({ scope, items, count: Object.keys(items).length });
44
44
  }
45
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
45
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["get", "set", "unset", "list"] });
46
46
  } catch (e) {
47
- return asToolError(e.code || "ERROR", e.message || String(e));
47
+ return errorToResponse(e);
48
48
  }
49
49
  }
50
50
 
51
51
  return { name: "git-config", description: "Gerencia configurações Git (local/global/system)", inputSchema, handle };
52
52
  }
53
-
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -23,19 +23,29 @@ export function createGitFilesTool(git) {
23
23
  try {
24
24
  if (action === "list") {
25
25
  const files = await git.listFiles(projectPath, args.ref || "HEAD");
26
- return asToolResult({ files });
26
+ return asToolResult({ files, count: files.length });
27
27
  }
28
28
  if (action === "read") {
29
- if (!args.filepath) return asToolError("VALIDATION_ERROR", "filepath é obrigatório");
30
- const text = await git.readFile(projectPath, args.filepath, args.ref || "HEAD");
31
- return { content: [{ type: "text", text }], isError: false };
29
+ if (!args.filepath) return asToolError("MISSING_PARAMETER", "filepath é obrigatório", { parameter: "filepath" });
30
+ try {
31
+ const text = await git.readFile(projectPath, args.filepath, args.ref || "HEAD");
32
+ return { content: [{ type: "text", text }], isError: false };
33
+ } catch (e) {
34
+ // Auto-diagnóstico: listar arquivos disponíveis se não encontrar
35
+ const files = await git.listFiles(projectPath, args.ref || "HEAD").catch(() => []);
36
+ return asToolError("FILE_NOT_FOUND", `Arquivo '${args.filepath}' não encontrado`, {
37
+ filepath: args.filepath,
38
+ ref: args.ref || "HEAD",
39
+ availableFiles: files.slice(0, 20),
40
+ totalFiles: files.length
41
+ });
42
+ }
32
43
  }
33
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
44
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "read"] });
34
45
  } catch (e) {
35
- return asToolError(e.code || "ERROR", e.message || String(e));
46
+ return errorToResponse(e);
36
47
  }
37
48
  }
38
49
 
39
50
  return { name: "git-files", description: "Lista e lê arquivos do repo", inputSchema, handle };
40
51
  }
41
-
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -23,14 +23,31 @@ export function createGitHistoryTool(git) {
23
23
  try {
24
24
  if (action === "log") {
25
25
  const items = await git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 });
26
- return asToolResult({ commits: items });
26
+ if (items.length === 0) {
27
+ return asToolResult({
28
+ commits: [],
29
+ count: 0,
30
+ message: "Nenhum commit encontrado. Use action='commit' para criar o primeiro commit."
31
+ });
32
+ }
33
+ return asToolResult({
34
+ commits: items.map(c => ({
35
+ sha: c.sha,
36
+ shortSha: c.sha.substring(0, 7),
37
+ message: c.message.split("\n")[0],
38
+ fullMessage: c.message,
39
+ author: c.author.name,
40
+ email: c.author.email,
41
+ date: c.date
42
+ })),
43
+ count: items.length
44
+ });
27
45
  }
28
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
46
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["log"] });
29
47
  } catch (e) {
30
- return asToolError(e.code || "ERROR", e.message || String(e));
48
+ return errorToResponse(e);
31
49
  }
32
50
  }
33
51
 
34
52
  return { name: "git-history", description: "Histórico de commits", inputSchema, handle };
35
53
  }
36
-
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -23,26 +23,37 @@ export function createGitIgnoreTool(git) {
23
23
  try {
24
24
  if (action === "list") {
25
25
  const items = await git.listGitignore(projectPath);
26
- return asToolResult({ items });
26
+ return asToolResult({ items, count: items.length });
27
27
  }
28
28
  if (action === "create") {
29
+ if (patterns.length === 0) {
30
+ return asToolError("MISSING_PARAMETER", "patterns é obrigatório para criar .gitignore", {
31
+ parameter: "patterns",
32
+ suggestion: "Forneça um array de padrões, ex: ['node_modules/', '*.log', '.env']"
33
+ });
34
+ }
29
35
  await git.createGitignore(projectPath, patterns);
30
- return asToolResult({ success: true });
36
+ return asToolResult({ success: true, patterns });
31
37
  }
32
38
  if (action === "add") {
39
+ if (patterns.length === 0) {
40
+ return asToolError("MISSING_PARAMETER", "patterns é obrigatório para adicionar ao .gitignore", { parameter: "patterns" });
41
+ }
33
42
  await git.addToGitignore(projectPath, patterns);
34
- return asToolResult({ success: true });
43
+ return asToolResult({ success: true, added: patterns });
35
44
  }
36
45
  if (action === "remove") {
46
+ if (patterns.length === 0) {
47
+ return asToolError("MISSING_PARAMETER", "patterns é obrigatório para remover do .gitignore", { parameter: "patterns" });
48
+ }
37
49
  await git.removeFromGitignore(projectPath, patterns);
38
- return asToolResult({ success: true });
50
+ return asToolResult({ success: true, removed: patterns });
39
51
  }
40
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
52
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "create", "add", "remove"] });
41
53
  } catch (e) {
42
- return asToolError(e.code || "ERROR", e.message || String(e));
54
+ return errorToResponse(e);
43
55
  }
44
56
  }
45
57
 
46
58
  return { name: "git-ignore", description: "Gerencia .gitignore", inputSchema, handle };
47
59
  }
48
-
@@ -1,6 +1,6 @@
1
1
  import Ajv from "ajv";
2
2
  import axios from "axios";
3
- import { asToolError, asToolResult } from "../utils/errors.js";
3
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
4
4
  import { getRepoNameFromPath } from "../utils/repoHelpers.js";
5
5
  import { runBoth } from "../utils/providerExec.js";
6
6
 
@@ -48,9 +48,9 @@ export function createGitIssuesTool(pm) {
48
48
  });
49
49
  return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
50
50
  }
51
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`);
51
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`, { availableActions: ["create", "list", "comment"] });
52
52
  } catch (e) {
53
- return asToolError(e.code || "ERROR", e.message || String(e));
53
+ return errorToResponse(e);
54
54
  }
55
55
  }
56
56
 
@@ -1,6 +1,6 @@
1
1
  import Ajv from "ajv";
2
2
  import axios from "axios";
3
- import { asToolError, asToolResult } from "../utils/errors.js";
3
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
4
4
  import { getRepoNameFromPath } from "../utils/repoHelpers.js";
5
5
  import { runBoth } from "../utils/providerExec.js";
6
6
 
@@ -51,9 +51,9 @@ export function createGitPullsTool(pm) {
51
51
  });
52
52
  return asToolResult({ providers: out });
53
53
  }
54
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`);
54
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`, { availableActions: ["create", "list", "files"] });
55
55
  } catch (e) {
56
- return asToolError(e.code || "ERROR", e.message || String(e));
56
+ return errorToResponse(e);
57
57
  }
58
58
  }
59
59
 
@@ -1,6 +1,6 @@
1
1
  import Ajv from "ajv";
2
2
  import axios from "axios";
3
- import { asToolError, asToolResult } from "../utils/errors.js";
3
+ import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
4
4
  import { getRepoNameFromPath } from "../utils/repoHelpers.js";
5
5
  import { runBoth } from "../utils/providerExec.js";
6
6
 
@@ -167,9 +167,11 @@ export function createGitRemoteTool(pm, git) {
167
167
  });
168
168
  return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, providers: out });
169
169
  }
170
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
170
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, {
171
+ availableActions: ["list", "ensure", "set-url", "repo-delete", "release-create", "topics-set", "milestone-create", "label-create", "fork-create", "fork-list", "star-set", "star-unset", "star-check", "subscription-set", "subscription-unset", "contents-create"]
172
+ });
171
173
  } catch (e) {
172
- return asToolError(e.code || "ERROR", e.message || String(e));
174
+ return errorToResponse(e);
173
175
  }
174
176
  }
175
177
 
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -20,16 +20,31 @@ export function createGitResetTool(git) {
20
20
  if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
21
21
  const { projectPath, action, ref } = args;
22
22
  try {
23
+ // Auto-verificação: checar se há commits suficientes para HEAD~N
24
+ if (ref.match(/HEAD~(\d+)/)) {
25
+ const match = ref.match(/HEAD~(\d+)/);
26
+ const steps = parseInt(match[1], 10);
27
+ const log = await git.log(projectPath, { maxCount: steps + 2 });
28
+ if (log.length <= steps) {
29
+ return asToolError("INSUFFICIENT_HISTORY", `Histórico insuficiente para ${ref}`, {
30
+ requestedSteps: steps,
31
+ availableCommits: log.length,
32
+ suggestion: `Use HEAD~${log.length - 1} no máximo, ou um SHA específico`,
33
+ recentCommits: log.slice(0, 5).map(c => ({ sha: c.sha.substring(0, 7), message: c.message.split("\n")[0] }))
34
+ });
35
+ }
36
+ }
37
+
23
38
  if (action === "soft") await git.resetSoft(projectPath, ref);
24
39
  else if (action === "mixed") await git.resetMixed(projectPath, ref);
25
40
  else if (action === "hard") await git.resetHard(projectPath, ref);
26
- else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
27
- return asToolResult({ success: true, action, ref });
41
+ else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["soft", "mixed", "hard"] });
42
+
43
+ return asToolResult({ success: true, action, ref, message: `Reset ${action} para ${ref} realizado` });
28
44
  } catch (e) {
29
- return asToolError(e.code || "ERROR", e.message || String(e));
45
+ return errorToResponse(e);
30
46
  }
31
47
  }
32
48
 
33
49
  return { name: "git-reset", description: "Reset soft/mixed/hard", inputSchema, handle };
34
50
  }
35
-
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -24,34 +24,55 @@ export function createGitStashTool(git) {
24
24
  try {
25
25
  if (action === "list") {
26
26
  const items = await git.listStash(projectPath);
27
- return asToolResult({ items });
27
+ return asToolResult({
28
+ items: items.map((s, i) => ({ index: i, ref: `stash@{${i}}`, message: s.message, timestamp: s.timestamp })),
29
+ count: items.length
30
+ });
28
31
  }
29
32
  if (action === "save") {
33
+ // Auto-verificação: checar se há mudanças
34
+ const status = await git.status(projectPath);
35
+ if (status.isClean && !args.includeUntracked) {
36
+ return asToolError("NOTHING_TO_STASH", "Working tree limpa, nada para stash", { status });
37
+ }
30
38
  await git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked);
31
- return asToolResult({ success: true });
39
+ return asToolResult({ success: true, message: args.message || "WIP" });
32
40
  }
33
41
  if (action === "apply") {
42
+ // Auto-verificação: checar se stash existe
43
+ const items = await git.listStash(projectPath);
44
+ if (items.length === 0) {
45
+ return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
46
+ }
34
47
  await git.applyStash(projectPath, args.ref);
35
- return asToolResult({ success: true });
48
+ return asToolResult({ success: true, ref: args.ref || "stash@{0}" });
36
49
  }
37
50
  if (action === "pop") {
51
+ // Auto-verificação: checar se stash existe
52
+ const items = await git.listStash(projectPath);
53
+ if (items.length === 0) {
54
+ return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
55
+ }
38
56
  await git.popStash(projectPath, args.ref);
39
- return asToolResult({ success: true });
57
+ return asToolResult({ success: true, ref: args.ref || "stash@{0}" });
40
58
  }
41
59
  if (action === "drop") {
60
+ const items = await git.listStash(projectPath);
61
+ if (items.length === 0) {
62
+ return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
63
+ }
42
64
  await git.dropStash(projectPath, args.ref);
43
- return asToolResult({ success: true });
65
+ return asToolResult({ success: true, ref: args.ref || "stash@{0}" });
44
66
  }
45
67
  if (action === "clear") {
46
68
  await git.clearStash(projectPath);
47
- return asToolResult({ success: true });
69
+ return asToolResult({ success: true, message: "Todos os stashes removidos" });
48
70
  }
49
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
71
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "save", "apply", "pop", "drop", "clear"] });
50
72
  } catch (e) {
51
- return asToolError(e.code || "ERROR", e.message || String(e));
73
+ return errorToResponse(e);
52
74
  }
53
75
  }
54
76
 
55
77
  return { name: "git-stash", description: "Gerencia stash", inputSchema, handle };
56
78
  }
57
-
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -26,17 +26,33 @@ export function createGitSyncTool(git) {
26
26
  const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
27
27
  const promises = remotes.map(r => git.fetch(projectPath, r, branch).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
28
28
  const results = await Promise.all(promises);
29
- return asToolResult({ success: results.some(x => x.ok), branch, results });
29
+ const successful = results.filter(x => x.ok);
30
+ const failed = results.filter(x => !x.ok);
31
+ return asToolResult({
32
+ success: successful.length > 0,
33
+ branch,
34
+ fetched: successful.map(x => x.remote),
35
+ failed: failed.map(x => ({ remote: x.remote, error: x.error })),
36
+ message: successful.length === 0 ? "Nenhum remote alcançado. Verifique conexão e configuração." : undefined
37
+ });
30
38
  }
31
39
  if (action === "pull") {
32
40
  const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
33
41
  const promises = remotes.map(r => git.pull(projectPath, r, branch).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
34
42
  const results = await Promise.all(promises);
35
- return asToolResult({ success: results.some(x => x.ok), branch, results });
43
+ const successful = results.filter(x => x.ok);
44
+ const failed = results.filter(x => !x.ok);
45
+ return asToolResult({
46
+ success: successful.length > 0,
47
+ branch,
48
+ pulled: successful.map(x => x.remote),
49
+ failed: failed.map(x => ({ remote: x.remote, error: x.error })),
50
+ message: successful.length === 0 ? "Nenhum remote alcançado. Verifique conexão e configuração." : undefined
51
+ });
36
52
  }
37
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
53
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["pull", "fetch"] });
38
54
  } catch (e) {
39
- return asToolError(e.code || "ERROR", e.message || String(e));
55
+ return errorToResponse(e);
40
56
  }
41
57
  }
42
58
 
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true });
5
5
 
@@ -24,35 +24,56 @@ export function createGitTagsTool(git) {
24
24
  try {
25
25
  if (action === "list") {
26
26
  const tags = await git.listTags(projectPath);
27
- return asToolResult({ tags });
27
+ return asToolResult({ tags, count: tags.length });
28
28
  }
29
29
  if (action === "create") {
30
30
  const tag = args.tag;
31
- if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
31
+ if (!tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório", { parameter: "tag" });
32
+ // Auto-verificação: checar se já existe
33
+ const existing = await git.listTags(projectPath);
34
+ if (existing.includes(tag)) {
35
+ return asToolError("TAG_ALREADY_EXISTS", `Tag '${tag}' já existe`, { tag, existingTags: existing });
36
+ }
32
37
  await git.createTag(projectPath, tag, args.ref || "HEAD", args.message);
33
- return asToolResult({ success: true, tag });
38
+ return asToolResult({ success: true, tag, ref: args.ref || "HEAD" });
34
39
  }
35
40
  if (action === "delete") {
36
41
  const tag = args.tag;
37
- if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
42
+ if (!tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório", { parameter: "tag" });
43
+ // Auto-verificação: checar se existe
44
+ const existing = await git.listTags(projectPath);
45
+ if (!existing.includes(tag)) {
46
+ return asToolError("TAG_NOT_FOUND", `Tag '${tag}' não encontrada`, { tag, existingTags: existing });
47
+ }
38
48
  await git.deleteTag(projectPath, tag);
39
49
  return asToolResult({ success: true, tag });
40
50
  }
41
51
  if (action === "push") {
42
52
  const tag = args.tag;
43
- if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
44
- await Promise.all([
45
- git.pushTag(projectPath, "github", tag).catch(() => {}),
46
- git.pushTag(projectPath, "gitea", tag).catch(() => {})
53
+ if (!tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório", { parameter: "tag" });
54
+ // Auto-verificação: checar se existe
55
+ const existing = await git.listTags(projectPath);
56
+ if (!existing.includes(tag)) {
57
+ return asToolError("TAG_NOT_FOUND", `Tag '${tag}' não existe localmente. Crie primeiro com action='create'`, { tag, existingTags: existing });
58
+ }
59
+ const results = await Promise.allSettled([
60
+ git.pushTag(projectPath, "github", tag),
61
+ git.pushTag(projectPath, "gitea", tag)
47
62
  ]);
48
- return asToolResult({ success: true, tag, remotes: ["github", "gitea"] });
63
+ const pushed = [];
64
+ const failed = [];
65
+ if (results[0].status === "fulfilled") pushed.push("github");
66
+ else failed.push({ remote: "github", error: results[0].reason?.message });
67
+ if (results[1].status === "fulfilled") pushed.push("gitea");
68
+ else failed.push({ remote: "gitea", error: results[1].reason?.message });
69
+
70
+ return asToolResult({ success: pushed.length > 0, tag, pushed, failed });
49
71
  }
50
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
72
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "create", "delete", "push"] });
51
73
  } catch (e) {
52
- return asToolError(e.code || "ERROR", e.message || String(e));
74
+ return errorToResponse(e);
53
75
  }
54
76
  }
55
77
 
56
78
  return { name: "git-tags", description: "Gerencia tags e push paralelo", inputSchema, handle };
57
79
  }
58
-
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { MCPError, asToolError, asToolResult } from "../utils/errors.js";
2
+ import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
3
3
  import { getRepoNameFromPath } from "../utils/repoHelpers.js";
4
4
 
5
5
  const ajv = new Ajv({ allErrors: true });
@@ -71,9 +71,11 @@ export function createGitWorkflowTool(pm, git) {
71
71
  // Basic implementation: fetch + status; full merge/pull can be added
72
72
  return asToolResult({ success: true, message: "Operação de pull/sync não-op realizada" });
73
73
  }
74
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
74
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, {
75
+ availableActions: ["init", "status", "add", "remove", "commit", "push", "pull", "sync", "ensure-remotes"]
76
+ });
75
77
  } catch (e) {
76
- return asToolError(e.code || "ERROR", e.message || String(e));
78
+ return errorToResponse(e);
77
79
  }
78
80
  }
79
81
 
@@ -1,3 +1,5 @@
1
+ // Sistema de Erros Melhorado para AI Agents
2
+
1
3
  export class MCPError extends Error {
2
4
  constructor(code, message, data) {
3
5
  super(message);
@@ -6,6 +8,262 @@ export class MCPError extends Error {
6
8
  }
7
9
  }
8
10
 
11
+ // Códigos de erro padronizados com sugestões para AI
12
+ export const ERROR_CODES = {
13
+ // Auth
14
+ AUTH_GITHUB_INVALID: {
15
+ code: "AUTH_GITHUB_INVALID",
16
+ suggestion: "Verifique se GITHUB_TOKEN está configurado e é válido. Gere um novo token em github.com/settings/tokens"
17
+ },
18
+ AUTH_GITEA_INVALID: {
19
+ code: "AUTH_GITEA_INVALID",
20
+ suggestion: "Verifique se GITEA_TOKEN e GITEA_URL estão configurados corretamente"
21
+ },
22
+ AUTH_NO_PERMISSION: {
23
+ code: "AUTH_NO_PERMISSION",
24
+ suggestion: "Token não tem permissão para esta operação. Verifique os scopes do token"
25
+ },
26
+
27
+ // Repo
28
+ REPO_NOT_FOUND: {
29
+ code: "REPO_NOT_FOUND",
30
+ suggestion: "Repositório não existe. Use action='init' com createIfMissing=true para criar"
31
+ },
32
+ REPO_NO_WRITE: {
33
+ code: "REPO_NO_WRITE",
34
+ suggestion: "Sem permissão de escrita. Verifique se você é owner ou colaborador do repositório"
35
+ },
36
+ REPO_ALREADY_EXISTS: {
37
+ code: "REPO_ALREADY_EXISTS",
38
+ suggestion: "Repositório já existe. Use action='ensure' para verificar ou escolha outro nome"
39
+ },
40
+
41
+ // Git Local
42
+ NOT_A_GIT_REPO: {
43
+ code: "NOT_A_GIT_REPO",
44
+ suggestion: "Diretório não é um repositório git. Use action='init' primeiro para inicializar"
45
+ },
46
+ NO_COMMITS: {
47
+ code: "NO_COMMITS",
48
+ suggestion: "Repositório não tem commits. Use action='add' e depois action='commit' primeiro"
49
+ },
50
+ NOTHING_TO_COMMIT: {
51
+ code: "NOTHING_TO_COMMIT",
52
+ suggestion: "Working tree limpa, nada para commitar. Modifique arquivos primeiro"
53
+ },
54
+ NOTHING_TO_PUSH: {
55
+ code: "NOTHING_TO_PUSH",
56
+ suggestion: "Nenhuma mudança para push. Faça commits primeiro"
57
+ },
58
+
59
+ // Branches
60
+ BRANCH_NOT_FOUND: {
61
+ code: "BRANCH_NOT_FOUND",
62
+ suggestion: "Branch não existe. Use action='list' para ver branches disponíveis"
63
+ },
64
+ BRANCH_ALREADY_EXISTS: {
65
+ code: "BRANCH_ALREADY_EXISTS",
66
+ suggestion: "Branch já existe. Use outro nome ou delete a existente primeiro"
67
+ },
68
+ CANNOT_DELETE_CURRENT: {
69
+ code: "CANNOT_DELETE_CURRENT",
70
+ suggestion: "Não pode deletar branch atual. Faça checkout para outra branch primeiro"
71
+ },
72
+
73
+ // Tags
74
+ TAG_NOT_FOUND: {
75
+ code: "TAG_NOT_FOUND",
76
+ suggestion: "Tag não existe. Use action='list' para ver tags disponíveis"
77
+ },
78
+ TAG_ALREADY_EXISTS: {
79
+ code: "TAG_ALREADY_EXISTS",
80
+ suggestion: "Tag já existe. Use outro nome ou delete a existente primeiro"
81
+ },
82
+
83
+ // Refs
84
+ REF_NOT_FOUND: {
85
+ code: "REF_NOT_FOUND",
86
+ suggestion: "Referência não encontrada. Verifique se o commit/branch/tag existe. Use action='log' para ver histórico"
87
+ },
88
+ INSUFFICIENT_HISTORY: {
89
+ code: "INSUFFICIENT_HISTORY",
90
+ suggestion: "Histórico insuficiente para HEAD~N. Use action='log' para verificar quantos commits existem"
91
+ },
92
+
93
+ // Stash
94
+ NOTHING_TO_STASH: {
95
+ code: "NOTHING_TO_STASH",
96
+ suggestion: "Working tree limpa, nada para stash. Modifique arquivos primeiro"
97
+ },
98
+ STASH_NOT_FOUND: {
99
+ code: "STASH_NOT_FOUND",
100
+ suggestion: "Stash não encontrado. Use action='list' para ver stashes disponíveis"
101
+ },
102
+
103
+ // Remote
104
+ REMOTE_NOT_FOUND: {
105
+ code: "REMOTE_NOT_FOUND",
106
+ suggestion: "Remote não configurado. Use action='ensure-remotes' para configurar"
107
+ },
108
+ PUSH_REJECTED: {
109
+ code: "PUSH_REJECTED",
110
+ suggestion: "Push rejeitado (histórico divergente). Use force=true para forçar, ou faça pull primeiro"
111
+ },
112
+ MERGE_CONFLICT: {
113
+ code: "MERGE_CONFLICT",
114
+ suggestion: "Conflito de merge. Resolva conflitos manualmente e faça novo commit"
115
+ },
116
+
117
+ // Network
118
+ NETWORK_TIMEOUT: {
119
+ code: "NETWORK_TIMEOUT",
120
+ suggestion: "Timeout de conexão. Verifique sua internet e tente novamente"
121
+ },
122
+ RATE_LIMIT: {
123
+ code: "RATE_LIMIT",
124
+ suggestion: "Rate limit excedido. Aguarde alguns minutos antes de tentar novamente"
125
+ },
126
+
127
+ // Validation
128
+ VALIDATION_ERROR: {
129
+ code: "VALIDATION_ERROR",
130
+ suggestion: "Parâmetros inválidos. Verifique os parâmetros obrigatórios da action"
131
+ },
132
+ MISSING_PARAMETER: {
133
+ code: "MISSING_PARAMETER",
134
+ suggestion: "Parâmetro obrigatório faltando"
135
+ },
136
+
137
+ // Files
138
+ FILE_NOT_FOUND: {
139
+ code: "FILE_NOT_FOUND",
140
+ suggestion: "Arquivo não encontrado no repositório. Use action='list' para ver arquivos disponíveis"
141
+ },
142
+
143
+ // Issues/PRs
144
+ ISSUE_NOT_FOUND: {
145
+ code: "ISSUE_NOT_FOUND",
146
+ suggestion: "Issue não encontrada. Use action='list' para ver issues disponíveis"
147
+ },
148
+ PR_NOT_FOUND: {
149
+ code: "PR_NOT_FOUND",
150
+ suggestion: "Pull Request não encontrado. Use action='list' para ver PRs disponíveis"
151
+ },
152
+
153
+ // Generic
154
+ UNKNOWN_ERROR: {
155
+ code: "UNKNOWN_ERROR",
156
+ suggestion: "Erro desconhecido. Verifique os logs para mais detalhes"
157
+ }
158
+ };
159
+
160
+ // Função para criar erro com sugestão
161
+ export function createError(errorCode, details = {}) {
162
+ const errorInfo = ERROR_CODES[errorCode] || ERROR_CODES.UNKNOWN_ERROR;
163
+ return new MCPError(errorInfo.code, details.message || errorInfo.code, {
164
+ suggestion: errorInfo.suggestion,
165
+ ...details
166
+ });
167
+ }
168
+
169
+ // Função para mapear erros externos para códigos internos
170
+ export function mapExternalError(error, context = {}) {
171
+ const msg = error?.message?.toLowerCase() || String(error).toLowerCase();
172
+
173
+ // GitHub/Gitea API errors
174
+ if (msg.includes("bad credentials") || msg.includes("401")) {
175
+ return context.provider === "gitea"
176
+ ? createError("AUTH_GITEA_INVALID", { originalError: msg })
177
+ : createError("AUTH_GITHUB_INVALID", { originalError: msg });
178
+ }
179
+
180
+ if (msg.includes("not found") || msg.includes("404")) {
181
+ if (context.type === "repo") return createError("REPO_NOT_FOUND", { repo: context.repo, originalError: msg });
182
+ if (context.type === "branch") return createError("BRANCH_NOT_FOUND", { branch: context.branch, originalError: msg });
183
+ if (context.type === "tag") return createError("TAG_NOT_FOUND", { tag: context.tag, originalError: msg });
184
+ if (context.type === "file") return createError("FILE_NOT_FOUND", { file: context.file, originalError: msg });
185
+ if (context.type === "issue") return createError("ISSUE_NOT_FOUND", { number: context.number, originalError: msg });
186
+ if (context.type === "pr") return createError("PR_NOT_FOUND", { number: context.number, originalError: msg });
187
+ return createError("REF_NOT_FOUND", { ref: context.ref, originalError: msg });
188
+ }
189
+
190
+ if (msg.includes("permission") || msg.includes("403") || msg.includes("forbidden")) {
191
+ return createError("AUTH_NO_PERMISSION", { originalError: msg });
192
+ }
193
+
194
+ if (msg.includes("rate limit") || msg.includes("429")) {
195
+ return createError("RATE_LIMIT", { originalError: msg });
196
+ }
197
+
198
+ if (msg.includes("timeout") || msg.includes("etimedout") || msg.includes("econnrefused")) {
199
+ return createError("NETWORK_TIMEOUT", { originalError: msg });
200
+ }
201
+
202
+ if (msg.includes("push rejected") || msg.includes("non-fast-forward")) {
203
+ return createError("PUSH_REJECTED", { originalError: msg });
204
+ }
205
+
206
+ if (msg.includes("conflict")) {
207
+ return createError("MERGE_CONFLICT", { originalError: msg });
208
+ }
209
+
210
+ if (msg.includes("already exists")) {
211
+ if (context.type === "repo") return createError("REPO_ALREADY_EXISTS", { repo: context.repo, originalError: msg });
212
+ if (context.type === "branch") return createError("BRANCH_ALREADY_EXISTS", { branch: context.branch, originalError: msg });
213
+ if (context.type === "tag") return createError("TAG_ALREADY_EXISTS", { tag: context.tag, originalError: msg });
214
+ return createError("BRANCH_ALREADY_EXISTS", { originalError: msg });
215
+ }
216
+
217
+ // isomorphic-git specific errors
218
+ if (msg.includes("could not find") && msg.includes("head~")) {
219
+ return createError("INSUFFICIENT_HISTORY", { originalError: msg });
220
+ }
221
+
222
+ if (msg.includes("is not a git repository") || msg.includes("could not find .git")) {
223
+ return createError("NOT_A_GIT_REPO", { path: context.path, originalError: msg });
224
+ }
225
+
226
+ if (msg.includes("nothing to commit") || msg.includes("working tree clean")) {
227
+ return createError("NOTHING_TO_COMMIT", { originalError: msg });
228
+ }
229
+
230
+ if (msg.includes("could not resolve ref") || msg.includes("invalid reference")) {
231
+ return createError("REF_NOT_FOUND", { originalError: msg });
232
+ }
233
+
234
+ if (msg.includes("branch") && (msg.includes("does not exist") || msg.includes("not found"))) {
235
+ return createError("BRANCH_NOT_FOUND", { originalError: msg });
236
+ }
237
+
238
+ if (msg.includes("tag") && (msg.includes("does not exist") || msg.includes("not found"))) {
239
+ return createError("TAG_NOT_FOUND", { originalError: msg });
240
+ }
241
+
242
+ if (msg.includes("no commits") || msg.includes("unknown revision")) {
243
+ return createError("NO_COMMITS", { originalError: msg });
244
+ }
245
+
246
+ if (msg.includes("nada para stash") || msg.includes("nothing to stash") || msg.includes("no local changes")) {
247
+ return createError("NOTHING_TO_STASH", { originalError: msg });
248
+ }
249
+
250
+ if (msg.includes("stash") && (msg.includes("not found") || msg.includes("não encontrado"))) {
251
+ return createError("STASH_NOT_FOUND", { originalError: msg });
252
+ }
253
+
254
+ if (msg.includes("author") || msg.includes("committer")) {
255
+ return createError("VALIDATION_ERROR", {
256
+ message: "Configure user.name e user.email",
257
+ suggestion: "Use git-config para configurar ou forneça tokens de provider",
258
+ originalError: msg
259
+ });
260
+ }
261
+
262
+ // Generic fallback
263
+ return createError("UNKNOWN_ERROR", { originalError: msg, context });
264
+ }
265
+
266
+ // Resultado de sucesso
9
267
  export function asToolResult(result) {
10
268
  return {
11
269
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -13,10 +271,27 @@ export function asToolResult(result) {
13
271
  };
14
272
  }
15
273
 
274
+ // Resultado de erro (formato melhorado para AI agents)
16
275
  export function asToolError(code, message, data) {
276
+ const errorInfo = ERROR_CODES[code] || {};
277
+ const response = {
278
+ error: true,
279
+ code: code,
280
+ message: message,
281
+ suggestion: errorInfo.suggestion || "Verifique os parâmetros e tente novamente",
282
+ ...data
283
+ };
17
284
  return {
18
- content: [{ type: "text", text: JSON.stringify({ code, message, data }) }],
285
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
19
286
  isError: true,
20
287
  };
21
288
  }
22
289
 
290
+ // Helper para converter MCPError em resposta
291
+ export function errorToResponse(error) {
292
+ if (error instanceof MCPError) {
293
+ return asToolError(error.code, error.message, error.data);
294
+ }
295
+ const mapped = mapExternalError(error);
296
+ return asToolError(mapped.code, mapped.message, mapped.data);
297
+ }
@@ -2,8 +2,13 @@ import * as git from "isomorphic-git";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import http from "isomorphic-git/http/node";
5
- import { MCPError } from "./errors.js";
5
+ import { MCPError, createError, mapExternalError } from "./errors.js";
6
6
  import { getProvidersEnv } from "./repoHelpers.js";
7
+ import { withRetry } from "./retry.js";
8
+
9
+ // #region agent log
10
+ const debugLog = (loc, msg, data) => { fetch('http://127.0.0.1:8242/ingest/e5799a4a-1a0d-4201-a6ce-42835e6f6fc7',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,message:msg,data,timestamp:Date.now(),sessionId:'debug-session'})}).catch(()=>{}); };
11
+ // #endregion
7
12
 
8
13
  export class GitAdapter {
9
14
  constructor(providerManager) {
@@ -15,7 +20,14 @@ export class GitAdapter {
15
20
  }
16
21
 
17
22
  async status(dir) {
23
+ // #region agent log
24
+ const t0 = Date.now();
25
+ debugLog('gitAdapter.js:status', 'START statusMatrix', { dir, hypothesisId: 'A' });
26
+ // #endregion
18
27
  const matrix = await git.statusMatrix({ fs, dir });
28
+ // #region agent log
29
+ debugLog('gitAdapter.js:status', 'END statusMatrix', { duration: Date.now() - t0, fileCount: matrix.length, hypothesisId: 'A' });
30
+ // #endregion
19
31
  const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3;
20
32
  const modified = [], created = [], deleted = [], not_added = [], files = [];
21
33
  for (const row of matrix) {
@@ -90,27 +102,58 @@ export class GitAdapter {
90
102
  getAuth(remoteUrl) {
91
103
  const { githubToken, giteaToken } = getProvidersEnv();
92
104
  if (remoteUrl.includes("github.com")) {
93
- if (!githubToken) throw new MCPError("AUTH", "GITHUB_TOKEN ausente");
105
+ if (!githubToken) throw createError("AUTH_GITHUB_INVALID", { message: "GITHUB_TOKEN não configurado" });
94
106
  return () => ({ username: githubToken, password: "x-oauth-basic" });
95
107
  }
96
108
  if (giteaToken) return () => ({ username: "git", password: giteaToken });
97
- throw new MCPError("AUTH", "GITEA_TOKEN ausente");
109
+ throw createError("AUTH_GITEA_INVALID", { message: "GITEA_TOKEN não configurado" });
98
110
  }
99
111
 
100
112
  async pushOne(dir, remote, branch, force = false) {
101
113
  const remotes = await git.listRemotes({ fs, dir });
102
114
  const info = remotes.find(r => r.remote === remote);
103
- if (!info) throw new MCPError("REMOTE", `Remote '${remote}' não encontrado`);
115
+ if (!info) {
116
+ const available = remotes.map(r => r.remote).join(", ");
117
+ throw createError("REMOTE_NOT_FOUND", {
118
+ message: `Remote '${remote}' não encontrado`,
119
+ remote,
120
+ availableRemotes: available || "nenhum"
121
+ });
122
+ }
104
123
  const onAuth = this.getAuth(info.url);
105
124
  const ref = branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
106
- await git.push({ fs, http, dir, remote, ref, remoteRef: ref, onAuth, force });
125
+ try {
126
+ await withRetry(() => git.push({ fs, http, dir, remote, ref, remoteRef: ref, onAuth, force }));
127
+ } catch (e) {
128
+ if (e.message?.includes("non-fast-forward") || e.message?.includes("rejected")) {
129
+ throw createError("PUSH_REJECTED", { message: e.message, remote, branch });
130
+ }
131
+ throw mapExternalError(e, { type: "push", remote, branch });
132
+ }
107
133
  }
108
134
 
109
135
  async pushParallel(dir, branch, force = false) {
110
136
  const remotes = await git.listRemotes({ fs, dir });
111
137
  const targets = remotes.filter(r => ["github", "gitea"].includes(r.remote));
112
- if (targets.length === 0) throw new MCPError("REMOTE", "Nenhum remote github/gitea configurado");
113
- await Promise.all(targets.map(t => this.pushOne(dir, t.remote, branch, force)));
138
+ if (targets.length === 0) {
139
+ throw createError("REMOTE_NOT_FOUND", {
140
+ message: "Nenhum remote github/gitea configurado",
141
+ availableRemotes: remotes.map(r => r.remote).join(", ") || "nenhum",
142
+ suggestion: "Use action='ensure-remotes' para configurar os remotes"
143
+ });
144
+ }
145
+ const results = await Promise.allSettled(targets.map(t => this.pushOne(dir, t.remote, branch, force)));
146
+ const errors = results.filter(r => r.status === "rejected");
147
+ if (errors.length === targets.length) {
148
+ throw createError("PUSH_REJECTED", {
149
+ message: "Push falhou para todos os remotes",
150
+ errors: errors.map(e => e.reason?.message || String(e.reason))
151
+ });
152
+ }
153
+ return {
154
+ pushed: targets.filter((t, i) => results[i].status === "fulfilled").map(t => t.remote),
155
+ failed: errors.map((e, i) => ({ remote: targets[i]?.remote, error: e.reason?.message }))
156
+ };
114
157
  }
115
158
 
116
159
  // ============ BRANCHES ============
@@ -188,7 +231,12 @@ export class GitAdapter {
188
231
 
189
232
  async saveStash(dir, message = "WIP", includeUntracked = false) {
190
233
  const st = await this.status(dir);
191
- if (st.isClean && !includeUntracked) throw new MCPError("STASH", "Nada para stash");
234
+ if (st.isClean && !includeUntracked) {
235
+ throw createError("NOTHING_TO_STASH", {
236
+ message: "Working tree limpa, nada para stash",
237
+ status: st
238
+ });
239
+ }
192
240
 
193
241
  // Salva arquivos modificados/adicionados
194
242
  const filesToSave = [...st.not_added, ...st.modified, ...st.created];
@@ -223,7 +271,14 @@ export class GitAdapter {
223
271
  const stashes = await this.listStash(dir);
224
272
  const idx = Number(ref.match(/\{(\d+)\}/)?.[1] || 0);
225
273
  const stash = stashes[idx];
226
- if (!stash) throw new MCPError("STASH", "Stash não encontrado");
274
+ if (!stash) {
275
+ throw createError("STASH_NOT_FOUND", {
276
+ message: `Stash '${ref}' não encontrado`,
277
+ requestedIndex: idx,
278
+ availableStashes: stashes.length,
279
+ stashList: stashes.map((s, i) => `stash@{${i}}: ${s.message}`).slice(0, 5)
280
+ });
281
+ }
227
282
 
228
283
  // Restaura arquivos do stash
229
284
  for (const [file, content] of Object.entries(stash.files)) {
@@ -330,17 +385,39 @@ export class GitAdapter {
330
385
  async fetch(dir, remote, branch) {
331
386
  const remotes = await git.listRemotes({ fs, dir });
332
387
  const info = remotes.find(r => r.remote === remote);
333
- if (!info) throw new MCPError("REMOTE", `Remote '${remote}' não encontrado`);
388
+ if (!info) {
389
+ throw createError("REMOTE_NOT_FOUND", {
390
+ message: `Remote '${remote}' não encontrado`,
391
+ availableRemotes: remotes.map(r => r.remote)
392
+ });
393
+ }
334
394
  const onAuth = this.getAuth(info.url);
335
- await git.fetch({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth });
395
+ try {
396
+ await withRetry(() => git.fetch({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth }));
397
+ } catch (e) {
398
+ throw mapExternalError(e, { type: "fetch", remote, branch, provider: info.url.includes("github") ? "github" : "gitea" });
399
+ }
336
400
  }
337
401
 
338
402
  async pull(dir, remote, branch) {
339
403
  const remotes = await git.listRemotes({ fs, dir });
340
404
  const info = remotes.find(r => r.remote === remote);
341
- if (!info) throw new MCPError("REMOTE", `Remote '${remote}' não encontrado`);
405
+ if (!info) {
406
+ throw createError("REMOTE_NOT_FOUND", {
407
+ message: `Remote '${remote}' não encontrado`,
408
+ availableRemotes: remotes.map(r => r.remote)
409
+ });
410
+ }
342
411
  const onAuth = this.getAuth(info.url);
343
- await git.pull({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth, author: await this.getAuthor(dir) });
412
+ const author = await this.getAuthor(dir);
413
+ try {
414
+ await withRetry(async () => git.pull({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth, author }));
415
+ } catch (e) {
416
+ if (e.message?.includes("conflict")) {
417
+ throw createError("MERGE_CONFLICT", { message: e.message, remote, branch });
418
+ }
419
+ throw mapExternalError(e, { type: "pull", remote, branch, provider: info.url.includes("github") ? "github" : "gitea" });
420
+ }
344
421
  }
345
422
 
346
423
  // ============ RESET ============
@@ -352,10 +429,26 @@ export class GitAdapter {
352
429
  const steps = parseInt(match[2], 10);
353
430
  const commits = await git.log({ fs, dir, ref: baseRef, depth: steps + 1 });
354
431
  if (commits.length > steps) return commits[steps].oid;
355
- throw new MCPError("REF", `Não foi possível resolver ${ref}`);
432
+ throw createError("INSUFFICIENT_HISTORY", {
433
+ message: `Não foi possível resolver ${ref}`,
434
+ requestedSteps: steps,
435
+ availableCommits: commits.length,
436
+ suggestion: `Histórico tem apenas ${commits.length} commits. Use HEAD~${commits.length - 1} no máximo.`
437
+ });
356
438
  }
357
439
  // Refs normais
358
- return await git.resolveRef({ fs, dir, ref });
440
+ try {
441
+ return await git.resolveRef({ fs, dir, ref });
442
+ } catch (e) {
443
+ const branches = await git.listBranches({ fs, dir }).catch(() => []);
444
+ const tags = await git.listTags({ fs, dir }).catch(() => []);
445
+ throw createError("REF_NOT_FOUND", {
446
+ message: `Ref '${ref}' não encontrada`,
447
+ ref,
448
+ availableBranches: branches.slice(0, 10),
449
+ availableTags: tags.slice(0, 10)
450
+ });
451
+ }
359
452
  }
360
453
 
361
454
  async resetSoft(dir, ref) {
@@ -1,12 +1,61 @@
1
- export async function withRetry(fn, { retries = 3, baseDelayMs = 300 } = {}) {
2
- let attempt = 0;
3
- let lastErr;
4
- while (attempt < retries) {
5
- try { return await fn(); } catch (e) { lastErr = e; }
6
- const delay = baseDelayMs * Math.pow(2, attempt);
7
- await new Promise(r => setTimeout(r, delay));
8
- attempt++;
1
+ // Sistema de Retry com Backoff Exponencial
2
+
3
+ const DEFAULT_OPTIONS = {
4
+ maxRetries: 3,
5
+ initialDelay: 1000,
6
+ maxDelay: 10000,
7
+ backoffFactor: 2,
8
+ retryableErrors: ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "timeout", "rate limit", "429", "503", "502"]
9
+ };
10
+
11
+ function shouldRetry(error, options) {
12
+ const msg = (error?.message || String(error)).toLowerCase();
13
+ const code = error?.code?.toLowerCase() || "";
14
+
15
+ return options.retryableErrors.some(e =>
16
+ msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
17
+ );
18
+ }
19
+
20
+ function sleep(ms) {
21
+ return new Promise(resolve => setTimeout(resolve, ms));
22
+ }
23
+
24
+ export async function withRetry(fn, options = {}) {
25
+ const opts = { ...DEFAULT_OPTIONS, ...options };
26
+ let lastError;
27
+ let delay = opts.initialDelay;
28
+
29
+ for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
30
+ try {
31
+ return await fn();
32
+ } catch (error) {
33
+ lastError = error;
34
+
35
+ if (attempt === opts.maxRetries || !shouldRetry(error, opts)) {
36
+ throw error;
37
+ }
38
+
39
+ // Log retry (silencioso)
40
+ console.error(`[Retry] Attempt ${attempt}/${opts.maxRetries} failed, retrying in ${delay}ms...`);
41
+
42
+ await sleep(delay);
43
+ delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
44
+ }
9
45
  }
10
- throw lastErr;
46
+
47
+ throw lastError;
48
+ }
49
+
50
+ // Wrapper para axios com retry
51
+ export async function axiosWithRetry(axiosInstance, config, options = {}) {
52
+ return withRetry(() => axiosInstance(config), options);
11
53
  }
12
54
 
55
+ // Wrapper para operações git com retry
56
+ export async function gitWithRetry(fn, options = {}) {
57
+ return withRetry(fn, {
58
+ ...options,
59
+ retryableErrors: [...DEFAULT_OPTIONS.retryableErrors, "ENOENT", "lock"]
60
+ });
61
+ }