@andrebuzeli/git-mcp 13.6.0 → 14.0.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.
@@ -10,49 +10,87 @@ export function createGitIssuesTool(pm) {
10
10
  const inputSchema = {
11
11
  type: "object",
12
12
  properties: {
13
- projectPath: { type: "string" },
14
- action: { type: "string", enum: ["create", "list", "comment"] },
15
- title: { type: "string" },
16
- body: { type: "string" },
17
- number: { type: "number" }
13
+ projectPath: {
14
+ type: "string",
15
+ description: "Caminho absoluto do diretório do projeto"
16
+ },
17
+ action: {
18
+ type: "string",
19
+ enum: ["create", "list", "comment"],
20
+ description: `Ação a executar:
21
+ - create: Cria nova issue no GitHub E Gitea
22
+ - list: Lista issues existentes
23
+ - comment: Adiciona comentário a uma issue existente`
24
+ },
25
+ title: {
26
+ type: "string",
27
+ description: "Título da issue (obrigatório para action='create'). Ex: 'Bug: aplicação crasha ao clicar em salvar'"
28
+ },
29
+ body: {
30
+ type: "string",
31
+ description: "Descrição detalhada da issue ou texto do comentário"
32
+ },
33
+ number: {
34
+ type: "number",
35
+ description: "Número da issue para comentar (obrigatório para action='comment')"
36
+ }
18
37
  },
19
38
  required: ["projectPath", "action"],
20
- additionalProperties: true
39
+ additionalProperties: false
21
40
  };
22
41
 
42
+ const description = `Gerenciamento de Issues no GitHub e Gitea.
43
+
44
+ QUANDO USAR:
45
+ - Reportar bugs encontrados
46
+ - Sugerir novas funcionalidades
47
+ - Documentar tarefas a fazer
48
+ - Acompanhar progresso do projeto
49
+
50
+ AÇÕES:
51
+ - create: Criar nova issue (executa em AMBOS os providers)
52
+ - list: Ver issues existentes
53
+ - comment: Comentar em issue existente
54
+
55
+ BOAS PRÁTICAS:
56
+ - Use títulos descritivos
57
+ - Inclua passos para reproduzir bugs
58
+ - Use labels para categorizar (via git-remote label-create)`;
59
+
23
60
  async function handle(args) {
24
61
  const validate = ajv.compile(inputSchema);
25
62
  if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
26
63
  const repo = getRepoNameFromPath(args.projectPath);
27
64
  try {
28
65
  if (args.action === "create") {
66
+ if (!args.title) return asToolError("MISSING_PARAMETER", "title é obrigatório para criar issue", { parameter: "title" });
29
67
  const out = await runBoth(pm, {
30
- github: async (owner) => { const r = await pm.github.rest.issues.create({ owner, repo, title: args.title || "", body: args.body || "" }); return { ok: true, number: r.data.number }; },
31
- gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues`, { title: args.title || "", body: args.body || "" }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
68
+ github: async (owner) => { const r = await pm.github.rest.issues.create({ owner, repo, title: args.title, body: args.body || "" }); return { ok: true, number: r.data.number, url: r.data.html_url }; },
69
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues`, { title: args.title, body: args.body || "" }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
32
70
  });
33
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
71
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title: args.title, providers: out });
34
72
  }
35
73
  if (args.action === "list") {
36
74
  const out = await runBoth(pm, {
37
- github: async (owner) => { const r = await pm.github.rest.issues.listForRepo({ owner, repo, state: "all" }); return { ok: true, count: r.data.length }; },
38
- gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/issues`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
75
+ github: async (owner) => { const r = await pm.github.rest.issues.listForRepo({ owner, repo, state: "all", per_page: 30 }); return { ok: true, count: r.data.length, issues: r.data.slice(0, 10).map(i => ({ number: i.number, title: i.title, state: i.state })) }; },
76
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/issues?state=all`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, issues: (r.data||[]).slice(0, 10).map(i => ({ number: i.number, title: i.title, state: i.state })) }; }
39
77
  });
40
78
  return asToolResult({ providers: out });
41
79
  }
42
80
  if (args.action === "comment") {
43
- const num = args.number;
44
- if (!num) return asToolError("VALIDATION_ERROR", "number é obrigatório");
81
+ if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para comentar", { parameter: "number" });
82
+ if (!args.body) return asToolError("MISSING_PARAMETER", "body é obrigatório para comentar", { parameter: "body" });
45
83
  const out = await runBoth(pm, {
46
- github: async (owner) => { await pm.github.rest.issues.createComment({ owner, repo, issue_number: num, body: args.body || "" }); return { ok: true }; },
47
- gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues/${num}/comments`, { body: args.body || "" }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
84
+ github: async (owner) => { await pm.github.rest.issues.createComment({ owner, repo, issue_number: args.number, body: args.body }); return { ok: true }; },
85
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues/${args.number}/comments`, { body: args.body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
48
86
  });
49
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
87
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), issueNumber: args.number, providers: out });
50
88
  }
51
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`, { availableActions: ["create", "list", "comment"] });
89
+ return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "comment"] });
52
90
  } catch (e) {
53
91
  return errorToResponse(e);
54
92
  }
55
93
  }
56
94
 
57
- return { name: "git-issues", description: "Issues em paralelo (GitHub + Gitea)", inputSchema, handle };
95
+ return { name: "git-issues", description, inputSchema, handle };
58
96
  }
@@ -10,52 +10,101 @@ export function createGitPullsTool(pm) {
10
10
  const inputSchema = {
11
11
  type: "object",
12
12
  properties: {
13
- projectPath: { type: "string" },
14
- action: { type: "string", enum: ["create", "list", "files"] },
15
- title: { type: "string" },
16
- head: { type: "string" },
17
- base: { type: "string" },
18
- number: { type: "number" }
13
+ projectPath: {
14
+ type: "string",
15
+ description: "Caminho absoluto do diretório do projeto"
16
+ },
17
+ action: {
18
+ type: "string",
19
+ enum: ["create", "list", "files"],
20
+ description: `Ação a executar:
21
+ - create: Cria Pull Request no GitHub E Gitea
22
+ - list: Lista Pull Requests existentes
23
+ - files: Lista arquivos modificados em um PR`
24
+ },
25
+ title: {
26
+ type: "string",
27
+ description: "Título do PR. Ex: 'feat: adiciona autenticação OAuth'"
28
+ },
29
+ head: {
30
+ type: "string",
31
+ description: "Branch de origem (com as mudanças). Ex: 'feature/oauth'"
32
+ },
33
+ base: {
34
+ type: "string",
35
+ description: "Branch de destino (onde será mergeado). Ex: 'main', 'develop'"
36
+ },
37
+ number: {
38
+ type: "number",
39
+ description: "Número do PR para action='files'"
40
+ },
41
+ body: {
42
+ type: "string",
43
+ description: "Descrição do PR com detalhes das mudanças"
44
+ }
19
45
  },
20
46
  required: ["projectPath", "action"],
21
- additionalProperties: true
47
+ additionalProperties: false
22
48
  };
23
49
 
50
+ const description = `Gerenciamento de Pull Requests no GitHub e Gitea.
51
+
52
+ QUANDO USAR:
53
+ - Propor mudanças de uma branch para outra
54
+ - Code review antes de merge
55
+ - Documentar mudanças significativas
56
+
57
+ FLUXO TÍPICO:
58
+ 1. Cria branch: git-branches create branch='feature/nova'
59
+ 2. Faz mudanças e commits
60
+ 3. Push: git-workflow push
61
+ 4. Cria PR: git-pulls create head='feature/nova' base='main' title='Nova feature'
62
+
63
+ AÇÕES:
64
+ - create: Criar novo PR
65
+ - list: Ver PRs existentes
66
+ - files: Ver arquivos modificados em um PR
67
+
68
+ NOTA: O PR é criado em AMBOS os providers simultaneamente.`;
69
+
24
70
  async function handle(args) {
25
71
  const validate = ajv.compile(inputSchema);
26
72
  if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
27
73
  const repo = getRepoNameFromPath(args.projectPath);
28
74
  try {
29
75
  if (args.action === "create") {
30
- const head = args.head || "feature";
31
- const base = args.base || "main";
76
+ if (!args.head) return asToolError("MISSING_PARAMETER", "head (branch de origem) é obrigatório", { parameter: "head", example: "feature/nova-funcionalidade" });
77
+ if (!args.base) return asToolError("MISSING_PARAMETER", "base (branch de destino) é obrigatório", { parameter: "base", example: "main" });
78
+ const head = args.head;
79
+ const base = args.base;
80
+ const title = args.title || `${head} -> ${base}`;
81
+ const body = args.body || "";
32
82
  const out = await runBoth(pm, {
33
- github: async (owner) => { const r = await pm.github.rest.pulls.create({ owner, repo, title: args.title || `${head} -> ${base}`, head, base }); return { ok: true, number: r.data.number }; },
34
- gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { title: args.title || `${head} -> ${base}`, head, base }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
83
+ github: async (owner) => { const r = await pm.github.rest.pulls.create({ owner, repo, title, head, base, body }); return { ok: true, number: r.data.number, url: r.data.html_url }; },
84
+ gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { title, head, base, body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
35
85
  });
36
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
86
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, head, base, providers: out });
37
87
  }
38
88
  if (args.action === "list") {
39
89
  const out = await runBoth(pm, {
40
- github: async (owner) => { const r = await pm.github.rest.pulls.list({ owner, repo, state: "all" }); return { ok: true, count: r.data.length }; },
41
- gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
90
+ github: async (owner) => { const r = await pm.github.rest.pulls.list({ owner, repo, state: "all", per_page: 30 }); return { ok: true, count: r.data.length, pulls: r.data.slice(0, 10).map(p => ({ number: p.number, title: p.title, state: p.state, head: p.head.ref, base: p.base.ref })) }; },
91
+ gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls?state=all`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, pulls: (r.data||[]).slice(0, 10).map(p => ({ number: p.number, title: p.title, state: p.state, head: p.head?.ref, base: p.base?.ref })) }; }
42
92
  });
43
93
  return asToolResult({ providers: out });
44
94
  }
45
95
  if (args.action === "files") {
46
- const num = args.number;
47
- if (!num) return asToolError("VALIDATION_ERROR", "number é obrigatório");
96
+ if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para ver arquivos do PR", { parameter: "number" });
48
97
  const out = await runBoth(pm, {
49
- github: async (owner) => { const r = await pm.github.rest.pulls.listFiles({ owner, repo, pull_number: num }); return { ok: true, count: r.data.length }; },
50
- gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls/${num}/files`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
98
+ github: async (owner) => { const r = await pm.github.rest.pulls.listFiles({ owner, repo, pull_number: args.number }); return { ok: true, count: r.data.length, files: r.data.map(f => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions })) }; },
99
+ gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls/${args.number}/files`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, files: (r.data||[]).map(f => ({ filename: f.filename, status: f.status })) }; }
51
100
  });
52
- return asToolResult({ providers: out });
101
+ return asToolResult({ prNumber: args.number, providers: out });
53
102
  }
54
- return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`, { availableActions: ["create", "list", "files"] });
103
+ return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "files"] });
55
104
  } catch (e) {
56
105
  return errorToResponse(e);
57
106
  }
58
107
  }
59
108
 
60
- return { name: "git-pulls", description: "Pull Requests em paralelo (GitHub + Gitea)", inputSchema, handle };
109
+ return { name: "git-pulls", description, inputSchema, handle };
61
110
  }
@@ -10,37 +10,101 @@ export function createGitRemoteTool(pm, git) {
10
10
  const inputSchema = {
11
11
  type: "object",
12
12
  properties: {
13
- projectPath: { type: "string" },
14
- action: { type: "string", enum: [
15
- "list",
16
- "ensure",
17
- "set-url",
18
- "repo-delete",
19
- "release-create",
20
- "topics-set",
21
- "milestone-create",
22
- "label-create",
23
- "fork-create",
24
- "fork-list",
25
- "star-set",
26
- "star-unset",
27
- "star-check",
28
- "subscription-set",
29
- "subscription-unset",
30
- "contents-create"
31
- ] },
32
- tag: { type: "string" },
33
- name: { type: "string" },
34
- body: { type: "string" },
35
- topics: { type: "array", items: { type: "string" } },
36
- path: { type: "string" },
37
- content: { type: "string" },
38
- branch: { type: "string" }
13
+ projectPath: {
14
+ type: "string",
15
+ description: "Caminho absoluto do diretório do projeto"
16
+ },
17
+ action: {
18
+ type: "string",
19
+ enum: [
20
+ "list", "ensure", "repo-delete",
21
+ "release-create", "topics-set", "milestone-create", "label-create",
22
+ "fork-create", "fork-list", "star-set", "star-unset",
23
+ "subscription-set", "subscription-unset", "contents-create"
24
+ ],
25
+ description: `Ação a executar:
26
+
27
+ CONFIGURAÇÃO:
28
+ - list: Lista remotes configurados (github, gitea, origin)
29
+ - ensure: Cria repos no GitHub/Gitea e configura remotes (USE ESTE se push falhar)
30
+
31
+ REPOSITÓRIO:
32
+ - repo-delete: Deleta repositório no GitHub E Gitea (⚠️ PERIGOSO)
33
+ - release-create: Cria release/versão no GitHub e Gitea
34
+ - topics-set: Define tópicos/tags do repositório
35
+ - milestone-create: Cria milestone para organização
36
+ - label-create: Cria label para issues
37
+
38
+ SOCIAL:
39
+ - star-set: Adiciona estrela ao repo
40
+ - star-unset: Remove estrela
41
+ - fork-create: Cria fork do repositório
42
+ - fork-list: Lista forks existentes
43
+
44
+ NOTIFICAÇÕES:
45
+ - subscription-set: Ativa notificações do repo
46
+ - subscription-unset: Desativa notificações
47
+
48
+ ARQUIVOS VIA API:
49
+ - contents-create: Cria arquivo diretamente via API (sem git local)`
50
+ },
51
+ tag: {
52
+ type: "string",
53
+ description: "Tag para release-create. Ex: 'v1.0.0'"
54
+ },
55
+ name: {
56
+ type: "string",
57
+ description: "Nome da release, label, etc."
58
+ },
59
+ body: {
60
+ type: "string",
61
+ description: "Descrição/corpo da release"
62
+ },
63
+ topics: {
64
+ type: "array",
65
+ items: { type: "string" },
66
+ description: "Lista de tópicos para topics-set. Ex: ['javascript', 'nodejs', 'mcp']"
67
+ },
68
+ path: {
69
+ type: "string",
70
+ description: "Caminho do arquivo para contents-create"
71
+ },
72
+ content: {
73
+ type: "string",
74
+ description: "Conteúdo do arquivo para contents-create"
75
+ },
76
+ branch: {
77
+ type: "string",
78
+ description: "Branch alvo para contents-create. Default: main"
79
+ },
80
+ color: {
81
+ type: "string",
82
+ description: "Cor do label em hex (sem #). Ex: 'ff0000' para vermelho"
83
+ },
84
+ title: {
85
+ type: "string",
86
+ description: "Título do milestone"
87
+ }
39
88
  },
40
89
  required: ["projectPath", "action"],
41
- additionalProperties: true
90
+ additionalProperties: false
42
91
  };
43
92
 
93
+ const description = `Operações em repositórios remotos GitHub e Gitea.
94
+
95
+ AÇÕES MAIS USADAS:
96
+ - ensure: Configurar remotes e criar repos (ESSENCIAL antes de push)
97
+ - list: Ver remotes configurados
98
+ - release-create: Publicar nova versão
99
+
100
+ EXECUÇÃO PARALELA:
101
+ Todas as operações são executadas em AMBOS os providers (GitHub + Gitea) simultaneamente.
102
+
103
+ QUANDO USAR:
104
+ - Se git-workflow push falhar: use action='ensure'
105
+ - Para publicar release: use action='release-create'
106
+ - Para configurar tópicos: use action='topics-set'`;
107
+
44
108
  async function handle(args) {
45
109
  const validate = ajv.compile(inputSchema);
46
110
  if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
@@ -48,7 +112,13 @@ export function createGitRemoteTool(pm, git) {
48
112
  try {
49
113
  if (action === "list") {
50
114
  const remotes = await git.listRemotes(projectPath);
51
- return asToolResult({ remotes });
115
+ return asToolResult({
116
+ remotes,
117
+ configured: remotes.length > 0,
118
+ hasGithub: remotes.some(r => r.remote === "github"),
119
+ hasGitea: remotes.some(r => r.remote === "gitea"),
120
+ message: remotes.length === 0 ? "Nenhum remote configurado. Use action='ensure' para configurar." : undefined
121
+ });
52
122
  }
53
123
  if (action === "ensure") {
54
124
  const repo = getRepoNameFromPath(projectPath);
@@ -60,11 +130,13 @@ export function createGitRemoteTool(pm, git) {
60
130
  const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
61
131
  await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
62
132
  const remotes = await git.listRemotes(projectPath);
63
- return asToolResult({ success: true, ensured, remotes });
64
- }
65
- if (action === "set-url") {
66
- // handled via ensure (auto)
67
- return asToolResult({ success: true, message: "Use action=ensure para set-url automático" });
133
+ return asToolResult({
134
+ success: true,
135
+ ensured,
136
+ remotes,
137
+ urls: { github: githubUrl, gitea: giteaUrl },
138
+ message: "Remotes configurados. Agora pode usar git-workflow push."
139
+ });
68
140
  }
69
141
  if (action === "repo-delete") {
70
142
  const repo = getRepoNameFromPath(projectPath);
@@ -72,36 +144,38 @@ export function createGitRemoteTool(pm, git) {
72
144
  github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
73
145
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.delete(`${base}/api/v1/repos/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
74
146
  });
75
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
147
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), deleted: repo, providers: out, warning: "Repositório deletado permanentemente!" });
76
148
  }
77
149
  if (action === "release-create") {
78
150
  const repo = getRepoNameFromPath(projectPath);
79
- const tag = args.tag || "v1.0.0";
151
+ if (!args.tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório para criar release", { parameter: "tag", example: "v1.0.0" });
152
+ const tag = args.tag;
80
153
  const name = args.name || tag;
81
154
  const body = args.body || "";
82
155
  const out = await runBoth(pm, {
83
- github: async (owner) => { const r = await pm.github.rest.repos.createRelease({ owner, repo, tag_name: tag, name, body, draft: false, prerelease: false }); return { ok: true, id: r.data.id }; },
156
+ github: async (owner) => { const r = await pm.github.rest.repos.createRelease({ owner, repo, tag_name: tag, name, body, draft: false, prerelease: false }); return { ok: true, id: r.data.id, url: r.data.html_url }; },
84
157
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/releases`, { tag_name: tag, name, body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, id: r.data?.id }; }
85
158
  });
86
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
159
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), tag, name, providers: out });
87
160
  }
88
161
  if (action === "topics-set") {
89
162
  const repo = getRepoNameFromPath(projectPath);
90
163
  const topics = Array.isArray(args.topics) ? args.topics : [];
164
+ if (topics.length === 0) return asToolError("MISSING_PARAMETER", "topics é obrigatório", { parameter: "topics", example: ["javascript", "nodejs"] });
91
165
  const out = await runBoth(pm, {
92
166
  github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/topics", { owner, repo, names: topics, headers: { accept: "application/vnd.github.mercy-preview+json" } }); return { ok: true }; },
93
167
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.put(`${base}/api/v1/repos/${owner}/${repo}/topics`, { topics }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
94
168
  });
95
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
169
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), topics, providers: out });
96
170
  }
97
171
  if (action === "milestone-create") {
98
172
  const repo = getRepoNameFromPath(projectPath);
99
- const title = args.title || "v1.0";
173
+ const title = args.title || args.name || "v1.0";
100
174
  const out = await runBoth(pm, {
101
175
  github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
102
176
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/milestones`, { title }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, id: r.data?.id }; }
103
177
  });
104
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
178
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, providers: out });
105
179
  }
106
180
  if (action === "label-create") {
107
181
  const repo = getRepoNameFromPath(projectPath);
@@ -111,7 +185,7 @@ export function createGitRemoteTool(pm, git) {
111
185
  github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
112
186
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/labels`, { name, color }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
113
187
  });
114
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
188
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), label: { name, color }, providers: out });
115
189
  }
116
190
  if (action === "fork-create") {
117
191
  const repo = getRepoNameFromPath(projectPath);
@@ -129,22 +203,20 @@ export function createGitRemoteTool(pm, git) {
129
203
  });
130
204
  return asToolResult({ providers: out });
131
205
  }
132
- if (action === "star-set" || action === "star-unset" || action === "star-check") {
206
+ if (action === "star-set" || action === "star-unset") {
133
207
  const repo = getRepoNameFromPath(projectPath);
134
208
  const out = await runBoth(pm, {
135
209
  github: async (owner) => {
136
210
  if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
137
- if (action === "star-unset") { await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
138
- const r = await pm.github.request("GET /user/starred/{owner}/{repo}", { owner, repo }); return { ok: r.status === 204 || r.status === 200 };
211
+ await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true };
139
212
  },
140
213
  gitea: async (owner) => {
141
214
  const base = pm.giteaUrl.replace(/\/$/, "");
142
215
  if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
143
- if (action === "star-unset") { await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
144
- const r = await axios.get(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` }, validateStatus: () => true }); return { ok: r.status === 204 || r.status === 200 };
216
+ await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true };
145
217
  }
146
218
  });
147
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
219
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
148
220
  }
149
221
  if (action === "subscription-set" || action === "subscription-unset") {
150
222
  const repo = getRepoNameFromPath(projectPath);
@@ -152,33 +224,30 @@ export function createGitRemoteTool(pm, git) {
152
224
  github: async (owner) => { if (action === "subscription-set") { await pm.github.request("PUT /repos/{owner}/{repo}/subscription", { owner, repo, subscribed: true }); } else { await pm.github.request("DELETE /repos/{owner}/{repo}/subscription", { owner, repo }); } return { ok: true }; },
153
225
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); if (action === "subscription-set") { await axios.put(`${base}/api/v1/repos/${owner}/${repo}/subscription`, { subscribed: true }, { headers: { Authorization: `token ${pm.giteaToken}` } }); } else { await axios.delete(`${base}/api/v1/repos/${owner}/${repo}/subscription`, { headers: { Authorization: `token ${pm.giteaToken}` } }); } return { ok: true }; }
154
226
  });
155
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
227
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
156
228
  }
157
229
  if (action === "contents-create") {
158
230
  const repo = getRepoNameFromPath(projectPath);
159
- const filePath = args.path || "test.txt";
160
- const message = args.message || "Add file";
161
- const data = typeof args.content === "string" ? args.content : "";
231
+ if (!args.path) return asToolError("MISSING_PARAMETER", "path é obrigatório", { parameter: "path" });
232
+ if (!args.content) return asToolError("MISSING_PARAMETER", "content é obrigatório", { parameter: "content" });
233
+ const filePath = args.path;
234
+ const message = args.message || `Add ${filePath}`;
235
+ const data = args.content;
162
236
  const branch = args.branch || "main";
163
237
  const b64 = Buffer.from(data, "utf8").toString("base64");
164
238
  const out = await runBoth(pm, {
165
239
  github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
166
240
  gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(filePath)}`, { content: b64, message, branch }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
167
241
  });
168
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, providers: out });
242
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, branch, providers: out });
169
243
  }
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"]
244
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
245
+ availableActions: ["list", "ensure", "repo-delete", "release-create", "topics-set", "milestone-create", "label-create", "fork-create", "fork-list", "star-set", "star-unset", "subscription-set", "subscription-unset", "contents-create"]
172
246
  });
173
247
  } catch (e) {
174
248
  return errorToResponse(e);
175
249
  }
176
250
  }
177
251
 
178
- return {
179
- name: "git-remote",
180
- description: "Gerencia remotes e garante GitHub + Gitea",
181
- inputSchema,
182
- handle
183
- };
252
+ return { name: "git-remote", description, inputSchema, handle };
184
253
  }
@@ -7,20 +7,52 @@ export function createGitResetTool(git) {
7
7
  const inputSchema = {
8
8
  type: "object",
9
9
  properties: {
10
- projectPath: { type: "string" },
11
- action: { type: "string", enum: ["soft", "mixed", "hard"] },
12
- ref: { type: "string" }
10
+ projectPath: {
11
+ type: "string",
12
+ description: "Caminho absoluto do diretório do projeto"
13
+ },
14
+ action: {
15
+ type: "string",
16
+ enum: ["soft", "mixed", "hard"],
17
+ description: `Tipo de reset a executar:
18
+ - soft: Desfaz commits mas MANTÉM mudanças staged (pronto para re-commit)
19
+ - mixed: Desfaz commits e unstage, mas MANTÉM mudanças no working directory
20
+ - hard: Desfaz commits e DESCARTA todas as mudanças (PERIGOSO - dados perdidos)`
21
+ },
22
+ ref: {
23
+ type: "string",
24
+ description: "Referência para reset. Ex: 'HEAD~1' (volta 1 commit), 'HEAD~2' (volta 2), ou SHA específico"
25
+ }
13
26
  },
14
27
  required: ["projectPath", "action", "ref"],
15
- additionalProperties: true
28
+ additionalProperties: false
16
29
  };
17
30
 
31
+ const description = `Reset Git - desfaz commits e/ou mudanças.
32
+
33
+ ⚠️ CUIDADO: Reset pode causar perda de dados!
34
+
35
+ TIPOS DE RESET:
36
+ - soft: Seguro - mantém tudo, apenas move HEAD
37
+ - mixed: Moderado - remove do staging mas mantém arquivos
38
+ - hard: PERIGOSO - descarta todas as mudanças
39
+
40
+ EXEMPLOS COMUNS:
41
+ - Desfazer último commit (manter mudanças): action='soft' ref='HEAD~1'
42
+ - Desfazer último commit (unstage): action='mixed' ref='HEAD~1'
43
+ - Descartar tudo e voltar ao commit anterior: action='hard' ref='HEAD~1'
44
+
45
+ REFERÊNCIAS:
46
+ - HEAD~1: Um commit atrás
47
+ - HEAD~2: Dois commits atrás
48
+ - abc1234: SHA específico do commit`;
49
+
18
50
  async function handle(args) {
19
51
  const validate = ajv.compile(inputSchema);
20
52
  if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
21
53
  const { projectPath, action, ref } = args;
22
54
  try {
23
- // Auto-verificação: checar se há commits suficientes para HEAD~N
55
+ // Verificar se há commits suficientes para HEAD~N
24
56
  if (ref.match(/HEAD~(\d+)/)) {
25
57
  const match = ref.match(/HEAD~(\d+)/);
26
58
  const steps = parseInt(match[1], 10);
@@ -35,16 +67,24 @@ export function createGitResetTool(git) {
35
67
  }
36
68
  }
37
69
 
38
- if (action === "soft") await git.resetSoft(projectPath, ref);
39
- else if (action === "mixed") await git.resetMixed(projectPath, ref);
40
- else if (action === "hard") await git.resetHard(projectPath, ref);
41
- else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["soft", "mixed", "hard"] });
70
+ if (action === "soft") {
71
+ await git.resetSoft(projectPath, ref);
72
+ return asToolResult({ success: true, action: "soft", ref, message: "Commits desfeitos, mudanças mantidas staged" });
73
+ }
74
+ if (action === "mixed") {
75
+ await git.resetMixed(projectPath, ref);
76
+ return asToolResult({ success: true, action: "mixed", ref, message: "Commits desfeitos, mudanças mantidas no working directory" });
77
+ }
78
+ if (action === "hard") {
79
+ await git.resetHard(projectPath, ref);
80
+ return asToolResult({ success: true, action: "hard", ref, message: "⚠️ Reset hard executado - mudanças descartadas" });
81
+ }
42
82
 
43
- return asToolResult({ success: true, action, ref, message: `Reset ${action} para ${ref} realizado` });
83
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["soft", "mixed", "hard"] });
44
84
  } catch (e) {
45
85
  return errorToResponse(e);
46
86
  }
47
87
  }
48
88
 
49
- return { name: "git-reset", description: "Reset soft/mixed/hard", inputSchema, handle };
89
+ return { name: "git-reset", description, inputSchema, handle };
50
90
  }