@andrebuzeli/git-mcp 11.0.4 → 12.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.
Files changed (55) hide show
  1. package/README.md +39 -47
  2. package/package.json +24 -19
  3. package/src/index.js +84 -0
  4. package/src/providers/providerManager.js +107 -0
  5. package/src/tools/git-branches.js +63 -0
  6. package/src/tools/git-config.js +53 -0
  7. package/src/tools/git-files.js +41 -0
  8. package/src/tools/git-history.js +36 -0
  9. package/src/tools/git-ignore.js +48 -0
  10. package/src/tools/git-issues.js +58 -12
  11. package/src/tools/git-pulls.js +61 -0
  12. package/src/tools/git-remote.js +182 -29
  13. package/src/tools/git-reset.js +35 -0
  14. package/src/tools/git-stash.js +57 -0
  15. package/src/tools/git-sync.js +44 -40
  16. package/src/tools/git-tags.js +58 -0
  17. package/src/tools/git-workflow.js +85 -0
  18. package/src/utils/errors.js +22 -0
  19. package/src/utils/gitAdapter.js +116 -0
  20. package/src/utils/providerExec.js +13 -0
  21. package/src/utils/repoHelpers.js +25 -0
  22. package/src/utils/retry.js +12 -0
  23. package/bin/git-mcp.js +0 -21
  24. package/docs/TOOLS.md +0 -110
  25. package/mcp.json.template +0 -12
  26. package/src/local/git.js +0 -14
  27. package/src/providers/gitea.js +0 -13
  28. package/src/providers/github.js +0 -13
  29. package/src/server.js +0 -115
  30. package/src/tools/git-actions.js +0 -19
  31. package/src/tools/git-activity.js +0 -28
  32. package/src/tools/git-admin.js +0 -20
  33. package/src/tools/git-checks.js +0 -14
  34. package/src/tools/git-commits.js +0 -34
  35. package/src/tools/git-contents.js +0 -30
  36. package/src/tools/git-deployments.js +0 -21
  37. package/src/tools/git-gists.js +0 -15
  38. package/src/tools/git-gitdata.js +0 -19
  39. package/src/tools/git-issues-prs.js +0 -44
  40. package/src/tools/git-local.js +0 -66
  41. package/src/tools/git-meta.js +0 -19
  42. package/src/tools/git-misc.js +0 -21
  43. package/src/tools/git-orgs.js +0 -26
  44. package/src/tools/git-packages.js +0 -12
  45. package/src/tools/git-raw.js +0 -14
  46. package/src/tools/git-releases.js +0 -17
  47. package/src/tools/git-repos.js +0 -60
  48. package/src/tools/git-search.js +0 -18
  49. package/src/tools/git-user.js +0 -26
  50. package/src/tools/schema.js +0 -3
  51. package/src/utils/fs.js +0 -29
  52. package/src/utils/project.js +0 -7
  53. package/tests/errors.js +0 -26
  54. package/tests/full_suite.js +0 -98
  55. package/tests/run.js +0 -50
package/README.md CHANGED
@@ -1,47 +1,39 @@
1
- # @andrebuzeli/git-mcp
2
-
3
- - Pacote MCP para manipulação Git com Gitea e GitHub em paralelo
4
- - Configuração via NPX gerando `mcp.json` com `GITEA_URL`, `GITEA_TOKEN`, `GITHUB_TOKEN`
5
- - Independente de Git instalado: usa `isomorphic-git` puro em Node
6
-
7
- ## Instalação
8
-
9
- - `npm i -g @andrebuzeli/git-mcp@11.0.0` ou use via npx
10
-
11
- ## Configuração
12
-
13
- - `npx @andrebuzeli/git-mcp@11.0.0 init --path .`
14
- - Edite `mcp.json` preenchendo os valores de ambiente
15
- - Garanta que o processo/IDE exporte as variáveis de ambiente
16
-
17
- ## Uso rápido
18
-
19
- - Criar repositórios remotos em ambos provedores
20
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-remote --action createRepo --projectPath "C:\\Projetos\\Meu Projeto"`
21
- - Inicializar e enviar para ambos remotos
22
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-local --action init --projectPath "..."`
23
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-remote --action ensureRemotes --projectPath "..."`
24
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-local --action add --projectPath "..." --args '{"patterns":["."]}'`
25
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-local --action commit --projectPath "..." --args '{"message":"Initial commit"}'`
26
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-local --action push --projectPath "..." --args '{"remote":"origin-gitea","branch":"main"}'`
27
- - `npx @andrebuzeli/git-mcp@11.0.0 call --tool git-local --action push --projectPath "..." --args '{"remote":"origin-github","branch":"main"}'`
28
-
29
- ## Integração em IDEs
30
-
31
- - VSCode/Cursor/Trae: apontar o executor MCP para `npx @andrebuzeli/git-mcp@11.0.0 start` e usar `mcp.json` para descoberta das tools
32
- - Todas as tools exigem `projectPath` e derivam o nome do projeto do último segmento, substituindo espaços por underscore
33
- - Consulte `docs/TOOLS.md` para descrições, schema e exemplos de uso por tool/action
34
-
35
- ## Tools
36
-
37
- - `git-local`: status, init, add, commit, push, pull, branch, tag
38
- - Implementado com `isomorphic-git` (sem binário git)
39
- - `git-remote`: createRepo, deleteRepo, ensureRemotes
40
- - `git-sync`: mirror (push simultâneo para Gitea e GitHub)
41
- - `git-issues`: create
42
- - `git-releases`: create
43
-
44
- ## Notas
45
-
46
- - Tokens nunca são persistidos, somente lidos de variáveis de ambiente
47
- - Operações remotas usam chamadas paralelas com relatório agregado
1
+ # @andrebuzeli/git-mcp
2
+
3
+ Servidor MCP (Model Context Protocol) para operações Git locais sem git instalado, com sincronização paralela para GitHub e Gitea.
4
+
5
+ ## Configuração MCP
6
+
7
+ ```json
8
+ {
9
+ "mcpServers": {
10
+ "git-mcp": {
11
+ "command": "npx",
12
+ "args": ["@andrebuzeli/git-mcp@latest"],
13
+ "env": {
14
+ "GITEA_URL": "https://seu-gitea",
15
+ "GITEA_TOKEN": "...",
16
+ "GITHUB_TOKEN": "..."
17
+ }
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Tools
24
+
25
+ - git-workflow: init, status, add, remove, commit, ensure-remotes, push
26
+ - git-remote: list, ensure
27
+ - git-branches: list, create, delete, rename, checkout
28
+ - git-tags: list, create, delete, push
29
+ - git-stash: list, save, apply, pop, drop, clear
30
+ - git-reset: soft, mixed, hard
31
+ - git-config: get, set, unset, list
32
+ - git-ignore: list, create, add, remove
33
+ - git-files: list, read
34
+ - git-history: log
35
+ - git-sync: fetch, pull
36
+ - git-issues: create, list, comment
37
+ - git-pulls: create, list, files
38
+
39
+ Todas as tools exigem `projectPath` e operam com derivação automática do nome do repositório a partir do caminho.
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
- {
2
- "name": "@andrebuzeli/git-mcp",
3
- "version": "11.0.4",
4
- "type": "module",
5
- "bin": {
6
- "git-mcp": "bin/git-mcp.js"
7
- },
8
- "scripts": {
9
- "start": "node bin/git-mcp.js start",
10
- "init": "node bin/git-mcp.js init",
11
- "test": "node tests/run.js"
12
- },
13
- "dependencies": {
14
- "axios": "^1.7.7",
15
- "kleur": "^4.1.5",
16
- "isomorphic-git": "^1.25.4",
17
- "yargs": "^17.7.2"
18
- }
19
- }
1
+ {
2
+ "name": "@andrebuzeli/git-mcp",
3
+ "version": "12.0.0",
4
+ "private": false,
5
+ "description": "MCP server para Git com operações locais e sincronização paralela GitHub/Gitea",
6
+ "license": "MIT",
7
+ "author": "andrebuzeli",
8
+ "type": "module",
9
+ "main": "src/index.js",
10
+ "bin": {
11
+ "git-mcpv2": "src/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "echo 'no build'",
15
+ "start": "node src/index.js"
16
+ },
17
+ "dependencies": {
18
+ "ajv": "^8.12.0",
19
+ "axios": "^1.7.7",
20
+ "@octokit/rest": "^20.0.0",
21
+ "isomorphic-git": "^1.27.5",
22
+ "@modelcontextprotocol/sdk": "^0.4.0"
23
+ }
24
+ }
package/src/index.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/dist/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/dist/server/stdio.js";
4
+ import Ajv from "ajv";
5
+ import { asToolError } from "./utils/errors.js";
6
+ import { ProviderManager } from "./providers/providerManager.js";
7
+ import { GitAdapter } from "./utils/gitAdapter.js";
8
+ import { createGitWorkflowTool } from "./tools/git-workflow.js";
9
+ import { createGitRemoteTool } from "./tools/git-remote.js";
10
+ import { createGitBranchesTool } from "./tools/git-branches.js";
11
+ import { createGitTagsTool } from "./tools/git-tags.js";
12
+ import { createGitStashTool } from "./tools/git-stash.js";
13
+ import { createGitResetTool } from "./tools/git-reset.js";
14
+ import { createGitConfigTool } from "./tools/git-config.js";
15
+ import { createGitIgnoreTool } from "./tools/git-ignore.js";
16
+ import { createGitFilesTool } from "./tools/git-files.js";
17
+ import { createGitHistoryTool } from "./tools/git-history.js";
18
+ import { createGitSyncTool } from "./tools/git-sync.js";
19
+ import { createGitIssuesTool } from "./tools/git-issues.js";
20
+ import { createGitPullsTool } from "./tools/git-pulls.js";
21
+
22
+ const transport = new StdioServerTransport();
23
+ const server = new Server({ name: "git-mcpv2", version: "0.0.0" });
24
+ server.connect(transport);
25
+
26
+ const pm = new ProviderManager();
27
+ const git = new GitAdapter(pm);
28
+ const ajv = new Ajv({ allErrors: true });
29
+
30
+ const tools = [
31
+ createGitWorkflowTool(pm, git),
32
+ createGitRemoteTool(pm, git),
33
+ createGitBranchesTool(git),
34
+ createGitTagsTool(git),
35
+ createGitStashTool(git),
36
+ createGitResetTool(git),
37
+ createGitConfigTool(git),
38
+ createGitIgnoreTool(git),
39
+ createGitFilesTool(git),
40
+ createGitHistoryTool(git),
41
+ createGitSyncTool(git),
42
+ createGitIssuesTool(pm),
43
+ createGitPullsTool(pm),
44
+ ];
45
+
46
+ function listToolsResult() {
47
+ return {
48
+ nextCursor: undefined,
49
+ tools: tools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
50
+ };
51
+ }
52
+
53
+ server.setRequestHandler(
54
+ // list tools
55
+ (await import("@modelcontextprotocol/sdk/dist/types.js")).ListToolsRequestSchema,
56
+ async () => listToolsResult()
57
+ );
58
+
59
+ server.setRequestHandler(
60
+ // call tool
61
+ (await import("@modelcontextprotocol/sdk/dist/types.js")).CallToolRequestSchema,
62
+ async (req) => {
63
+ const name = req.params?.name || "";
64
+ const args = req.params?.arguments || {};
65
+ const progressToken = req.params?._meta?.progressToken;
66
+ const tool = tools.find(t => t.name === name);
67
+ if (!tool) return { content: [{ type: "text", text: `Tool não encontrada: ${name}` }], isError: true };
68
+ try {
69
+ if (progressToken) {
70
+ await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0 } });
71
+ }
72
+ const result = await tool.handle(args);
73
+ if (progressToken) {
74
+ await server.notification({ method: "notifications/progress", params: { progressToken, progress: 100 } });
75
+ }
76
+ return result;
77
+ } catch (e) {
78
+ return asToolError(e.code || "ERROR", e.message || String(e));
79
+ }
80
+ }
81
+ );
82
+
83
+ // Keep process alive
84
+ process.stdin.resume();
@@ -0,0 +1,107 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import axios from "axios";
3
+ import { getProvidersEnv } from "../utils/repoHelpers.js";
4
+
5
+ export class ProviderManager {
6
+ constructor() {
7
+ const { githubToken, giteaUrl, giteaToken } = getProvidersEnv();
8
+ this.githubToken = githubToken || "";
9
+ this.giteaUrl = giteaUrl || "";
10
+ this.giteaToken = giteaToken || "";
11
+ this.github = this.githubToken ? new Octokit({ auth: this.githubToken }) : null;
12
+ this._githubOwner = "";
13
+ this._giteaOwner = "";
14
+ this._ownerFetchedAt = 0;
15
+ }
16
+
17
+ async getGitHubOwner() {
18
+ if (!this.github) return "";
19
+ const now = Date.now();
20
+ if (this._githubOwner && now - this._ownerFetchedAt < 5 * 60 * 1000) return this._githubOwner;
21
+ try {
22
+ const me = await this.github.rest.users.getAuthenticated();
23
+ this._githubOwner = me.data.login || "";
24
+ this._ownerFetchedAt = now;
25
+ return this._githubOwner;
26
+ } catch {
27
+ return "";
28
+ }
29
+ }
30
+
31
+ async getGiteaOwner() {
32
+ if (!this.giteaUrl || !this.giteaToken) return "";
33
+ const now = Date.now();
34
+ if (this._giteaOwner && now - this._ownerFetchedAt < 5 * 60 * 1000) return this._giteaOwner;
35
+ try {
36
+ const r = await axios.get(`${this.giteaUrl}/api/v1/user`, {
37
+ headers: { Authorization: `token ${this.giteaToken}` },
38
+ timeout: 8000,
39
+ });
40
+ const d = r.data || {};
41
+ this._giteaOwner = d.login || d.username || "";
42
+ this._ownerFetchedAt = now;
43
+ return this._giteaOwner;
44
+ } catch {
45
+ return "";
46
+ }
47
+ }
48
+
49
+ async ensureRepos({ repoName, createIfMissing = true, description = "Managed by git-mcpv2" }) {
50
+ const results = { github: null, gitea: null };
51
+ // GitHub
52
+ if (this.github) {
53
+ const owner = await this.getGitHubOwner();
54
+ if (owner) {
55
+ try {
56
+ const full = `${owner}/${repoName}`;
57
+ try {
58
+ await this.github.rest.repos.get({ owner, repo: repoName });
59
+ results.github = { ok: true, repo: full, created: false };
60
+ } catch {
61
+ if (createIfMissing) {
62
+ const cr = await this.github.rest.repos.createForAuthenticatedUser({
63
+ name: repoName,
64
+ description,
65
+ private: false,
66
+ auto_init: true,
67
+ });
68
+ results.github = { ok: true, repo: cr.data.full_name, created: true };
69
+ } else {
70
+ results.github = { ok: false, error: "missing" };
71
+ }
72
+ }
73
+ } catch (e) {
74
+ results.github = { ok: false, error: String(e?.message || e) };
75
+ }
76
+ }
77
+ }
78
+ // Gitea
79
+ if (this.giteaUrl && this.giteaToken) {
80
+ const owner = await this.getGiteaOwner();
81
+ if (owner) {
82
+ try {
83
+ const base = this.giteaUrl.replace(/\/$/, "");
84
+ const getRepo = await axios.get(`${base}/api/v1/repos/${owner}/${repoName}`,
85
+ { headers: { Authorization: `token ${this.giteaToken}` }, timeout: 8000 });
86
+ if (getRepo.status === 200) {
87
+ results.gitea = { ok: true, repo: `${owner}/${repoName}`, created: false };
88
+ }
89
+ } catch (e) {
90
+ if (createIfMissing) {
91
+ try {
92
+ const cr = await axios.post(`${this.giteaUrl}/api/v1/user/repos`,
93
+ { name: repoName, description, private: false, auto_init: true },
94
+ { headers: { Authorization: `token ${this.giteaToken}` }, timeout: 8000 });
95
+ results.gitea = { ok: true, repo: `${cr.data?.owner?.login || owner}/${repoName}`, created: true };
96
+ } catch (err) {
97
+ results.gitea = { ok: false, error: String(err?.message || err) };
98
+ }
99
+ } else {
100
+ results.gitea = { ok: false, error: "missing" };
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return results;
106
+ }
107
+ }
@@ -0,0 +1,63 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitBranchesTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["list", "create", "delete", "rename", "checkout"] },
12
+ branch: { type: "string" },
13
+ newBranch: { type: "string" },
14
+ force: { type: "boolean" }
15
+ },
16
+ required: ["projectPath", "action"],
17
+ additionalProperties: true
18
+ };
19
+
20
+ async function handle(args) {
21
+ const validate = ajv.compile(inputSchema);
22
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
23
+ const { projectPath, action } = args;
24
+ try {
25
+ if (action === "list") {
26
+ const local = await git.listBranches(projectPath, false);
27
+ const remote = await git.listBranches(projectPath, true).catch(() => []);
28
+ return asToolResult({ local, remote });
29
+ }
30
+ if (action === "create") {
31
+ const ref = args.branch;
32
+ if (!ref) return asToolError("VALIDATION_ERROR", "branch é obrigatório");
33
+ await git.createBranch(projectPath, ref);
34
+ return asToolResult({ success: true, branch: ref });
35
+ }
36
+ if (action === "delete") {
37
+ const ref = args.branch;
38
+ if (!ref) return asToolError("VALIDATION_ERROR", "branch é obrigatório");
39
+ await git.deleteBranch(projectPath, ref, !!args.force);
40
+ return asToolResult({ success: true, branch: ref });
41
+ }
42
+ if (action === "rename") {
43
+ const oldName = args.branch;
44
+ const newName = args.newBranch;
45
+ if (!oldName || !newName) return asToolError("VALIDATION_ERROR", "branch e newBranch são obrigatórios");
46
+ await git.renameBranch(projectPath, oldName, newName);
47
+ return asToolResult({ success: true, from: oldName, to: newName });
48
+ }
49
+ if (action === "checkout") {
50
+ const ref = args.branch;
51
+ if (!ref) return asToolError("VALIDATION_ERROR", "branch é obrigatório");
52
+ await git.checkout(projectPath, ref);
53
+ return asToolResult({ success: true, branch: ref });
54
+ }
55
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
56
+ } catch (e) {
57
+ return asToolError(e.code || "ERROR", e.message || String(e));
58
+ }
59
+ }
60
+
61
+ return { name: "git-branches", description: "Gerencia branches", inputSchema, handle };
62
+ }
63
+
@@ -0,0 +1,53 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitConfigTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["get", "set", "unset", "list"] },
12
+ key: { type: "string" },
13
+ value: { type: "string" },
14
+ scope: { type: "string", enum: ["local", "global", "system"], default: "local" }
15
+ },
16
+ required: ["projectPath", "action"],
17
+ additionalProperties: true
18
+ };
19
+
20
+ async function handle(args) {
21
+ const validate = ajv.compile(inputSchema);
22
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
23
+ const { projectPath, action } = args;
24
+ const scope = args.scope || "local";
25
+ try {
26
+ if (action === "get") {
27
+ if (!args.key) return asToolError("VALIDATION_ERROR", "key é obrigatório");
28
+ const val = await git.getConfig(projectPath, args.key, scope);
29
+ return asToolResult({ key: args.key, value: val });
30
+ }
31
+ if (action === "set") {
32
+ if (!args.key) return asToolError("VALIDATION_ERROR", "key é obrigatório");
33
+ await git.setConfig(projectPath, args.key, args.value ?? "", scope);
34
+ return asToolResult({ success: true, key: args.key, value: args.value ?? "" });
35
+ }
36
+ if (action === "unset") {
37
+ if (!args.key) return asToolError("VALIDATION_ERROR", "key é obrigatório");
38
+ await git.unsetConfig(projectPath, args.key, scope);
39
+ return asToolResult({ success: true, key: args.key });
40
+ }
41
+ if (action === "list") {
42
+ const items = await git.listConfig(projectPath, scope);
43
+ return asToolResult({ scope, items });
44
+ }
45
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
46
+ } catch (e) {
47
+ return asToolError(e.code || "ERROR", e.message || String(e));
48
+ }
49
+ }
50
+
51
+ return { name: "git-config", description: "Gerencia configurações Git (local/global/system)", inputSchema, handle };
52
+ }
53
+
@@ -0,0 +1,41 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitFilesTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["list", "read"] },
12
+ filepath: { type: "string" },
13
+ ref: { type: "string" }
14
+ },
15
+ required: ["projectPath", "action"],
16
+ additionalProperties: true
17
+ };
18
+
19
+ async function handle(args) {
20
+ const validate = ajv.compile(inputSchema);
21
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
22
+ const { projectPath, action } = args;
23
+ try {
24
+ if (action === "list") {
25
+ const files = await git.listFiles(projectPath, args.ref || "HEAD");
26
+ return asToolResult({ files });
27
+ }
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 };
32
+ }
33
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
34
+ } catch (e) {
35
+ return asToolError(e.code || "ERROR", e.message || String(e));
36
+ }
37
+ }
38
+
39
+ return { name: "git-files", description: "Lista e lê arquivos do repo", inputSchema, handle };
40
+ }
41
+
@@ -0,0 +1,36 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitHistoryTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["log"] },
12
+ ref: { type: "string" },
13
+ maxCount: { type: "number" }
14
+ },
15
+ required: ["projectPath", "action"],
16
+ additionalProperties: true
17
+ };
18
+
19
+ async function handle(args) {
20
+ const validate = ajv.compile(inputSchema);
21
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
22
+ const { projectPath, action } = args;
23
+ try {
24
+ if (action === "log") {
25
+ const items = await git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 });
26
+ return asToolResult({ commits: items });
27
+ }
28
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
29
+ } catch (e) {
30
+ return asToolError(e.code || "ERROR", e.message || String(e));
31
+ }
32
+ }
33
+
34
+ return { name: "git-history", description: "Histórico de commits", inputSchema, handle };
35
+ }
36
+
@@ -0,0 +1,48 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult } from "../utils/errors.js";
3
+
4
+ const ajv = new Ajv({ allErrors: true });
5
+
6
+ export function createGitIgnoreTool(git) {
7
+ const inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ projectPath: { type: "string" },
11
+ action: { type: "string", enum: ["list", "create", "add", "remove"] },
12
+ patterns: { type: "array", items: { type: "string" } }
13
+ },
14
+ required: ["projectPath", "action"],
15
+ additionalProperties: true
16
+ };
17
+
18
+ async function handle(args) {
19
+ const validate = ajv.compile(inputSchema);
20
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
21
+ const { projectPath, action } = args;
22
+ const patterns = Array.isArray(args.patterns) ? args.patterns : [];
23
+ try {
24
+ if (action === "list") {
25
+ const items = await git.listGitignore(projectPath);
26
+ return asToolResult({ items });
27
+ }
28
+ if (action === "create") {
29
+ await git.createGitignore(projectPath, patterns);
30
+ return asToolResult({ success: true });
31
+ }
32
+ if (action === "add") {
33
+ await git.addToGitignore(projectPath, patterns);
34
+ return asToolResult({ success: true });
35
+ }
36
+ if (action === "remove") {
37
+ await git.removeFromGitignore(projectPath, patterns);
38
+ return asToolResult({ success: true });
39
+ }
40
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
41
+ } catch (e) {
42
+ return asToolError(e.code || "ERROR", e.message || String(e));
43
+ }
44
+ }
45
+
46
+ return { name: "git-ignore", description: "Gerencia .gitignore", inputSchema, handle };
47
+ }
48
+
@@ -1,12 +1,58 @@
1
- import { Gitea } from '../providers/gitea.js'
2
- import { Github } from '../providers/github.js'
3
- import { deriveProjectName } from '../utils/project.js'
4
-
5
- export class GitIssuesTool {
6
- constructor(env) { this.providers = { gitea: new Gitea(env.GITEA_URL, env.GITEA_TOKEN), github: new Github(env.GITHUB_TOKEN) } }
7
- async handle(action, projectPath, args) {
8
- const name = deriveProjectName(projectPath)
9
- if (action === 'create') { const r = await Promise.allSettled([ this.providers.gitea.createIssue(name, args?.title || 'Issue', args?.body || ''), this.providers.github.createIssue(name, args?.title || 'Issue', args?.body || '' ) ]); return r.map(x => x.status === 'fulfilled' ? x.value : { error: x.reason?.message }) }
10
- return { error: 'ação inválida' }
11
- }
12
- }
1
+ import Ajv from "ajv";
2
+ import axios from "axios";
3
+ import { asToolError, asToolResult } from "../utils/errors.js";
4
+ import { getRepoNameFromPath } from "../utils/repoHelpers.js";
5
+ import { runBoth } from "../utils/providerExec.js";
6
+
7
+ const ajv = new Ajv({ allErrors: true });
8
+
9
+ export function createGitIssuesTool(pm) {
10
+ const inputSchema = {
11
+ type: "object",
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" }
18
+ },
19
+ required: ["projectPath", "action"],
20
+ additionalProperties: true
21
+ };
22
+
23
+ async function handle(args) {
24
+ const validate = ajv.compile(inputSchema);
25
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
26
+ const repo = getRepoNameFromPath(args.projectPath);
27
+ try {
28
+ if (args.action === "create") {
29
+ 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 }; }
32
+ });
33
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
34
+ }
35
+ if (args.action === "list") {
36
+ 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 }; }
39
+ });
40
+ return asToolResult({ providers: out });
41
+ }
42
+ if (args.action === "comment") {
43
+ const num = args.number;
44
+ if (!num) return asToolError("VALIDATION_ERROR", "number é obrigatório");
45
+ 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 }; }
48
+ });
49
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
50
+ }
51
+ return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`);
52
+ } catch (e) {
53
+ return asToolError(e.code || "ERROR", e.message || String(e));
54
+ }
55
+ }
56
+
57
+ return { name: "git-issues", description: "Issues em paralelo (GitHub + Gitea)", inputSchema, handle };
58
+ }