@andre.buzeli/git-mcp 16.0.6 → 16.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,492 +1,503 @@
1
- import Ajv from "ajv";
2
- import axios from "axios";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import archiver from "archiver";
6
- import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
7
- import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
8
- import { runBoth } from "../utils/providerExec.js";
9
-
10
- /**
11
- * Compacta uma pasta em ZIP e retorna o caminho do arquivo temporário
12
- * @param {string} folderPath - Caminho da pasta a compactar
13
- * @param {string} zipName - Nome do arquivo ZIP (sem extensão)
14
- * @returns {Promise<string>} Caminho do arquivo ZIP criado
15
- */
16
- async function compactFolder(folderPath, zipName) {
17
- return new Promise((resolve, reject) => {
18
- const outputPath = path.join(process.cwd(), 'temp_scripts', `${zipName}.zip`);
19
- const output = fs.createWriteStream(outputPath);
20
- const archive = archiver('zip', { zlib: { level: 9 } });
21
-
22
- output.on('close', () => resolve(outputPath));
23
- output.on('error', reject);
24
- archive.on('error', reject);
25
-
26
- archive.pipe(output);
27
- archive.directory(folderPath, false);
28
- archive.finalize();
29
- });
30
- }
31
-
32
- /**
33
- * Faz upload de um asset para um release
34
- * @param {Object} pm - Provider Manager
35
- * @param {string} owner - Dono do repo
36
- * @param {string} repo - Nome do repo
37
- * @param {number} releaseId - ID do release
38
- * @param {string} assetPath - Caminho do arquivo a fazer upload
39
- * @param {string} assetName - Nome do asset (com extensão)
40
- * @returns {Promise<Object>} Resultado do upload
41
- */
42
- async function uploadAsset(pm, owner, repo, releaseId, assetPath, assetName) {
43
- const fileBuffer = fs.readFileSync(assetPath);
44
- const fileSize = fs.statSync(assetPath).size;
45
-
46
- const out = await runBoth(pm, {
47
- github: async () => {
48
- const response = await pm.github.request("POST /repos/{owner}/{repo}/releases/{release_id}/assets", {
49
- owner,
50
- repo,
51
- release_id: releaseId,
52
- name: assetName,
53
- data: fileBuffer,
54
- headers: {
55
- 'content-type': 'application/octet-stream',
56
- 'content-length': fileSize
57
- }
58
- });
59
- return { ok: true, id: response.data.id, url: response.data.browser_download_url };
60
- },
61
- gitea: async () => {
62
- const base = pm.giteaUrl.replace(/\/$/, "");
63
-
64
- const response = await axios.post(
65
- `${base}/api/v1/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`,
66
- fileBuffer,
67
- {
68
- headers: {
69
- 'Authorization': `token ${pm.giteaToken}`,
70
- 'Content-Type': 'application/octet-stream',
71
- 'Content-Length': fileSize
72
- }
73
- }
74
- );
75
- return { ok: true, id: response.data.id, url: response.data.browser_download_url };
76
- }
77
- });
78
-
79
- return out;
80
- }
81
-
82
- const ajv = new Ajv({ allErrors: true });
83
-
84
- export function createGitRemoteTool(pm, git) {
85
- const inputSchema = {
86
- type: "object",
87
- properties: {
88
- projectPath: {
89
- type: "string",
90
- description: "Caminho absoluto do diretório do projeto"
91
- },
92
- action: {
93
- type: "string",
94
- enum: [
95
- "list", "list-all", "list-orgs", "ensure", "repo-delete",
96
- "release-create", "topics-set", "milestone-create", "label-create",
97
- "fork-create", "fork-list", "star-set", "star-unset",
98
- "subscription-set", "subscription-unset", "contents-create"
99
- ],
100
- description: `Ação a executar:
101
-
102
- CONFIGURAÇÃO:
103
- CONFIGURAÇÃO:
104
- - list: Lista remotes configurados (github, gitea, origin)
105
- - list-all: Lista TODOS os repositórios da conta ou organização (GitHub + Gitea)
106
- - list-orgs: Lista organizações disponíveis no GitHub e Gitea
107
- - ensure: Cria repos no GitHub/Gitea e configura remotes (USE ESTE se push falhar)
108
-
109
- REPOSITÓRIO:
110
- - repo-delete: Deleta repositório no GitHub E Gitea (⚠️ PERIGOSO)
111
- - release-create: Cria release/versão no GitHub e Gitea
112
- - topics-set: Define tópicos/tags do repositório
113
- - milestone-create: Cria milestone para organização
114
- - label-create: Cria label para issues
115
-
116
- SOCIAL:
117
- - star-set: Adiciona estrela ao repo
118
- - star-unset: Remove estrela
119
- - fork-create: Cria fork do repositório
120
- - fork-list: Lista forks existentes
121
-
122
- NOTIFICAÇÕES:
123
- - subscription-set: Ativa notificações do repo
124
- - subscription-unset: Desativa notificações
125
-
126
- ARQUIVOS VIA API:
127
- - contents-create: Cria arquivo diretamente via API (sem git local)`
128
- },
129
- tag: {
130
- type: "string",
131
- description: "Tag para release-create. Ex: 'v1.0.0'"
132
- },
133
- name: {
134
- type: "string",
135
- description: "Nome da release, label, etc."
136
- },
137
- body: {
138
- type: "string",
139
- description: "Descrição/corpo da release"
140
- },
141
- assets: {
142
- type: "array",
143
- items: {
144
- oneOf: [
145
- { type: "string" }, // path simples: "dist/app.exe"
146
- {
147
- type: "object",
148
- properties: {
149
- path: { type: "string" }, // "dist/build"
150
- name: { type: "string" } // "MeuApp-Windows" (sem extensao)
151
- },
152
- required: ["path"]
153
- }
154
- ]
155
- },
156
- description: "Assets para upload. String = nome automatico, Objeto = nome custom. Pastas são compactadas automaticamente"
157
- },
158
- topics: {
159
- type: "array",
160
- items: { type: "string" },
161
- description: "Lista de tópicos para topics-set. Ex: ['javascript', 'nodejs', 'mcp']"
162
- },
163
- path: {
164
- type: "string",
165
- description: "Caminho do arquivo para contents-create"
166
- },
167
- content: {
168
- type: "string",
169
- description: "Conteúdo do arquivo para contents-create"
170
- },
171
- branch: {
172
- type: "string",
173
- description: "Branch alvo para contents-create. Default: main"
174
- },
175
- color: {
176
- type: "string",
177
- description: "Cor do label em hex (sem #). Ex: 'ff0000' para vermelho"
178
- },
179
- title: {
180
- type: "string",
181
- description: "Título do milestone"
182
- },
183
- isPublic: {
184
- type: "boolean",
185
- description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='ensure'"
186
- },
187
- organization: {
188
- type: "string",
189
- description: "Opcional. Nome da organização no GitHub/Gitea. Se informado, operações são feitas na org em vez da conta pessoal. Ex: 'automacao-casa', 'cliente-joao'"
190
- }
191
- },
192
- required: ["projectPath", "action"],
193
- additionalProperties: false
194
- };
195
-
196
- const description = `Operações em repositórios remotos GitHub e Gitea.
197
-
198
- AÇÕES MAIS USADAS:
199
- - ensure: Configurar remotes e criar repos (ESSENCIAL antes de push)
200
- - list: Ver remotes configurados
201
- - release-create: Publicar nova versão
202
-
203
- EXECUÇÃO PARALELA:
204
- Todas as operações são executadas em AMBOS os providers (GitHub + Gitea) simultaneamente.
205
-
206
- QUANDO USAR:
207
- - Se git-workflow push falhar: use action='ensure'
208
- - Para publicar release: use action='release-create'
209
- - Para configurar tópicos: use action='topics-set'`;
210
-
211
- async function handle(args) {
212
- const validate = ajv.compile(inputSchema);
213
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
214
- const { projectPath, action } = args;
215
- try {
216
- validateProjectPath(projectPath);
217
- if (action === "list") {
218
- const remotes = await git.listRemotes(projectPath);
219
-
220
- // Debug info: calculate what URLs should be
221
- let repoName = getRepoNameFromPath(projectPath);
222
- if (repoName === "GIT_MCP") repoName = "git-mcp";
223
- const calculated = await pm.getRemoteUrls(repoName, args.organization);
224
-
225
- return asToolResult({
226
- remotes,
227
- configured: remotes.length > 0,
228
- hasGithub: remotes.some(r => r.remote === "github"),
229
- hasGitea: remotes.some(r => r.remote === "gitea"),
230
- debug: {
231
- repoName,
232
- calculatedUrls: calculated,
233
- env: {
234
- hasGithubToken: !!pm.githubToken,
235
- hasGiteaToken: !!pm.giteaToken,
236
- giteaUrl: pm.giteaUrl
237
- }
238
- },
239
- message: remotes.length === 0 ? "Nenhum remote configurado. Use action='ensure' para configurar." : undefined
240
- });
241
- }
242
-
243
- if (action === "list-all") {
244
- const repos = await pm.listAllRepos({ organization: args.organization });
245
- const summary = [];
246
- if (repos.github?.length) summary.push(`${repos.github.length} GitHub repos`);
247
- if (repos.gitea?.length) summary.push(`${repos.gitea.length} Gitea repos`);
248
-
249
- return asToolResult({
250
- success: true,
251
- organization: args.organization || undefined,
252
- summary: summary.join(", "),
253
- github: repos.github,
254
- gitea: repos.gitea
255
- });
256
- }
257
- if (action === "list-orgs") {
258
- const orgs = await pm.listOrgs();
259
- const summary = [];
260
- if (orgs.github?.length) summary.push(`${orgs.github.length} GitHub orgs`);
261
- if (orgs.gitea?.length) summary.push(`${orgs.gitea.length} Gitea orgs`);
262
-
263
- return asToolResult({
264
- success: true,
265
- summary: summary.length > 0 ? summary.join(", ") : "Nenhuma organização encontrada",
266
- github: orgs.github,
267
- gitea: orgs.gitea,
268
- _aiContext: {
269
- hint: "Use o nome da organização no parâmetro 'organization' das outras actions para criar/gerenciar repos dentro da org"
270
- }
271
- });
272
- }
273
- if (action === "ensure") {
274
- const repo = getRepoNameFromPath(projectPath);
275
- const isPublic = args.isPublic === true; // Default: privado
276
- const organization = args.organization || undefined;
277
- const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
278
- const urls = await pm.getRemoteUrls(repo, organization);
279
- await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
280
- const remotes = await git.listRemotes(projectPath);
281
- return asToolResult({
282
- success: true,
283
- isPrivate: !isPublic,
284
- organization: organization || undefined,
285
- ensured,
286
- remotes,
287
- urls,
288
- message: "Remotes configurados. Agora pode usar git-workflow push." + (organization ? ` [org: ${organization}]` : "")
289
- });
290
- }
291
- if (action === "repo-delete") {
292
- const repo = getRepoNameFromPath(projectPath);
293
- const out = await runBoth(pm, {
294
- github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
295
- 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 }; }
296
- }, { organization: args.organization });
297
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), deleted: repo, providers: out, warning: "Repositório deletado permanentemente!" });
298
- }
299
- if (action === "release-create") {
300
- const repo = getRepoNameFromPath(projectPath);
301
- if (!args.tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório para criar release", { parameter: "tag", example: "v1.0.0" });
302
- const tag = args.tag;
303
- const name = args.name || tag;
304
- const body = args.body || "";
305
-
306
- // Criar release primeiro
307
- const out = await runBoth(pm, {
308
- 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 }; },
309
- 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 }; }
310
- }, { organization: args.organization });
311
-
312
- const releaseCreated = !!(out.github?.ok || out.gitea?.ok);
313
- if (!releaseCreated) {
314
- return asToolResult({ success: false, tag, name, providers: out, error: "Falha ao criar release" });
315
- }
316
-
317
- // Processar assets se fornecidos
318
- let assetsUploaded = [];
319
- if (args.assets && args.assets.length > 0) {
320
- const tempFiles = []; // Arquivos temporários a serem deletados
321
- const [ghOwner, geOwner] = await Promise.all([
322
- pm.getGitHubOwner().catch(() => ""),
323
- pm.getGiteaOwner().catch(() => "")
324
- ]);
325
-
326
- for (const asset of args.assets) {
327
- try {
328
- const assetPath = typeof asset === 'string' ? asset : asset.path;
329
- const customName = typeof asset === 'object' ? asset.name : null;
330
-
331
- // Resolver caminho absoluto
332
- const absolutePath = path.resolve(projectPath, assetPath);
333
-
334
- // Verificar se existe
335
- if (!fs.existsSync(absolutePath)) {
336
- assetsUploaded.push({ path: assetPath, error: "Arquivo/pasta não encontrado" });
337
- continue;
338
- }
339
-
340
- const stats = fs.statSync(absolutePath);
341
- let uploadPath = absolutePath;
342
- let uploadName = customName;
343
- let isTempFile = false;
344
-
345
- if (stats.isDirectory()) {
346
- // Compactar pasta
347
- const zipName = customName || path.basename(absolutePath);
348
- const zipPath = await compactFolder(absolutePath, zipName);
349
- uploadPath = zipPath;
350
- uploadName = `${zipName}.zip`;
351
- tempFiles.push(zipPath);
352
- isTempFile = true;
353
- } else {
354
- // Arquivo direto
355
- if (!uploadName) {
356
- uploadName = path.basename(absolutePath);
357
- } else {
358
- // Adicionar extensão se não tiver
359
- const ext = path.extname(absolutePath);
360
- if (!uploadName.includes('.')) {
361
- uploadName += ext;
362
- }
363
- }
364
- }
365
-
366
- // Upload para GitHub e Gitea
367
- const uploadResult = await uploadAsset(pm, ghOwner || geOwner, repo, out.github?.id || out.gitea?.id, uploadPath, uploadName);
368
-
369
- assetsUploaded.push({
370
- path: assetPath,
371
- name: uploadName,
372
- uploaded: !!(uploadResult.github?.ok || uploadResult.gitea?.ok),
373
- providers: uploadResult
374
- });
375
-
376
- // Deletar arquivo temporário se foi criado
377
- if (isTempFile && tempFiles.includes(uploadPath)) {
378
- try {
379
- fs.unlinkSync(uploadPath);
380
- tempFiles.splice(tempFiles.indexOf(uploadPath), 1);
381
- } catch (e) {
382
- console.warn(`Falha ao deletar arquivo temporário: ${uploadPath}`);
383
- }
384
- }
385
-
386
- } catch (error) {
387
- assetsUploaded.push({ path: typeof asset === 'string' ? asset : asset.path, error: String(error.message || error) });
388
- }
389
- }
390
- }
391
-
392
- return asToolResult({
393
- success: releaseCreated,
394
- tag,
395
- name,
396
- providers: out,
397
- assets: assetsUploaded.length > 0 ? assetsUploaded : undefined
398
- });
399
- }
400
- if (action === "topics-set") {
401
- const repo = getRepoNameFromPath(projectPath);
402
- const topics = Array.isArray(args.topics) ? args.topics : [];
403
- if (topics.length === 0) return asToolError("MISSING_PARAMETER", "topics é obrigatório", { parameter: "topics", example: ["javascript", "nodejs"] });
404
- const out = await runBoth(pm, {
405
- 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 }; },
406
- 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 }; }
407
- }, { organization: args.organization });
408
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), topics, providers: out });
409
- }
410
- if (action === "milestone-create") {
411
- const repo = getRepoNameFromPath(projectPath);
412
- const title = args.title || args.name || "v1.0";
413
- const out = await runBoth(pm, {
414
- github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
415
- 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 }; }
416
- }, { organization: args.organization });
417
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, providers: out });
418
- }
419
- if (action === "label-create") {
420
- const repo = getRepoNameFromPath(projectPath);
421
- const name = args.name || "bug";
422
- const color = String(args.color || "ff0000").replace(/^#/, "");
423
- const out = await runBoth(pm, {
424
- github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
425
- 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 }; }
426
- }, { organization: args.organization });
427
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), label: { name, color }, providers: out });
428
- }
429
- if (action === "fork-create") {
430
- const repo = getRepoNameFromPath(projectPath);
431
- const out = await runBoth(pm, {
432
- github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
433
- gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/forks`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, full_name: r.data?.full_name }; }
434
- }, { organization: args.organization });
435
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
436
- }
437
- if (action === "fork-list") {
438
- const repo = getRepoNameFromPath(projectPath);
439
- const out = await runBoth(pm, {
440
- github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
441
- gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/forks`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data || []).length }; }
442
- }, { organization: args.organization });
443
- return asToolResult({ providers: out });
444
- }
445
- if (action === "star-set" || action === "star-unset") {
446
- const repo = getRepoNameFromPath(projectPath);
447
- const out = await runBoth(pm, {
448
- github: async (owner) => {
449
- if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
450
- await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true };
451
- },
452
- gitea: async (owner) => {
453
- const base = pm.giteaUrl.replace(/\/$/, "");
454
- if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
455
- await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true };
456
- }
457
- }, { organization: args.organization });
458
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
459
- }
460
- if (action === "subscription-set" || action === "subscription-unset") {
461
- const repo = getRepoNameFromPath(projectPath);
462
- const out = await runBoth(pm, {
463
- 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 }; },
464
- 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 }; }
465
- }, { organization: args.organization });
466
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
467
- }
468
- if (action === "contents-create") {
469
- const repo = getRepoNameFromPath(projectPath);
470
- if (!args.path) return asToolError("MISSING_PARAMETER", "path é obrigatório", { parameter: "path" });
471
- if (!args.content) return asToolError("MISSING_PARAMETER", "content é obrigatório", { parameter: "content" });
472
- const filePath = args.path;
473
- const message = args.message || `Add ${filePath}`;
474
- const data = args.content;
475
- const branch = args.branch || "main";
476
- const b64 = Buffer.from(data, "utf8").toString("base64");
477
- const out = await runBoth(pm, {
478
- github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
479
- 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 }; }
480
- }, { organization: args.organization });
481
- return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, branch, providers: out });
482
- }
483
- return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
484
- availableActions: ["list", "list-all", "list-orgs", "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"]
485
- });
486
- } catch (e) {
487
- return errorToResponse(e);
488
- }
489
- }
490
-
491
- return { name: "git-remote", description, inputSchema, handle };
492
- }
1
+ import Ajv from "ajv";
2
+ import axios from "axios";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import archiver from "archiver";
6
+ import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
7
+ import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
8
+ import { runBoth } from "../utils/providerExec.js";
9
+ import { sendLog, requestConfirmation } from "../utils/mcpNotify.js";
10
+
11
+ /**
12
+ * Compacta uma pasta em ZIP e retorna o caminho do arquivo temporário
13
+ * @param {string} folderPath - Caminho da pasta a compactar
14
+ * @param {string} zipName - Nome do arquivo ZIP (sem extensão)
15
+ * @returns {Promise<string>} Caminho do arquivo ZIP criado
16
+ */
17
+ async function compactFolder(folderPath, zipName) {
18
+ return new Promise((resolve, reject) => {
19
+ const outputPath = path.join(process.cwd(), 'temp_scripts', `${zipName}.zip`);
20
+ const output = fs.createWriteStream(outputPath);
21
+ const archive = archiver('zip', { zlib: { level: 9 } });
22
+
23
+ output.on('close', () => resolve(outputPath));
24
+ output.on('error', reject);
25
+ archive.on('error', reject);
26
+
27
+ archive.pipe(output);
28
+ archive.directory(folderPath, false);
29
+ archive.finalize();
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Faz upload de um asset para um release
35
+ * @param {Object} pm - Provider Manager
36
+ * @param {string} owner - Dono do repo
37
+ * @param {string} repo - Nome do repo
38
+ * @param {number} releaseId - ID do release
39
+ * @param {string} assetPath - Caminho do arquivo a fazer upload
40
+ * @param {string} assetName - Nome do asset (com extensão)
41
+ * @returns {Promise<Object>} Resultado do upload
42
+ */
43
+ async function uploadAsset(pm, owner, repo, releaseId, assetPath, assetName) {
44
+ const fileBuffer = fs.readFileSync(assetPath);
45
+ const fileSize = fs.statSync(assetPath).size;
46
+
47
+ const out = await runBoth(pm, {
48
+ github: async () => {
49
+ const response = await pm.github.request("POST /repos/{owner}/{repo}/releases/{release_id}/assets", {
50
+ owner,
51
+ repo,
52
+ release_id: releaseId,
53
+ name: assetName,
54
+ data: fileBuffer,
55
+ headers: {
56
+ 'content-type': 'application/octet-stream',
57
+ 'content-length': fileSize
58
+ }
59
+ });
60
+ return { ok: true, id: response.data.id, url: response.data.browser_download_url };
61
+ },
62
+ gitea: async () => {
63
+ const base = pm.giteaUrl.replace(/\/$/, "");
64
+
65
+ const response = await axios.post(
66
+ `${base}/api/v1/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`,
67
+ fileBuffer,
68
+ {
69
+ headers: {
70
+ 'Authorization': `token ${pm.giteaToken}`,
71
+ 'Content-Type': 'application/octet-stream',
72
+ 'Content-Length': fileSize
73
+ }
74
+ }
75
+ );
76
+ return { ok: true, id: response.data.id, url: response.data.browser_download_url };
77
+ }
78
+ });
79
+
80
+ return out;
81
+ }
82
+
83
+ const ajv = new Ajv({ allErrors: true });
84
+
85
+ export function createGitRemoteTool(pm, git, server) {
86
+ const inputSchema = {
87
+ type: "object",
88
+ properties: {
89
+ projectPath: {
90
+ type: "string",
91
+ description: "Caminho absoluto do diretório do projeto no IDE (ex: '/home/user/meu-projeto' ou 'C:/Users/user/meu-projeto'). IMPORTANTE: este valor não pode ser inferido automaticamente pelo servidor — o agente deve fornecer o path real do projeto sendo trabalhado, não o diretório home do usuário."
92
+ },
93
+ action: {
94
+ type: "string",
95
+ enum: [
96
+ "list", "list-all", "list-orgs", "ensure", "repo-delete",
97
+ "release-create", "topics-set", "milestone-create", "label-create",
98
+ "fork-create", "fork-list", "star-set", "star-unset",
99
+ "subscription-set", "subscription-unset", "contents-create"
100
+ ],
101
+ description: `Ação a executar:
102
+
103
+ CONFIGURAÇÃO:
104
+ CONFIGURAÇÃO:
105
+ - list: Lista remotes configurados (github, gitea, origin)
106
+ - list-all: Lista TODOS os repositórios da conta ou organização (GitHub + Gitea)
107
+ - list-orgs: Lista organizações disponíveis no GitHub e Gitea
108
+ - ensure: Cria repos no GitHub/Gitea e configura remotes (USE ESTE se push falhar)
109
+
110
+ REPOSITÓRIO:
111
+ - repo-delete: Deleta repositório no GitHub E Gitea (⚠️ PERIGOSO)
112
+ - release-create: Cria release/versão no GitHub e Gitea
113
+ - topics-set: Define tópicos/tags do repositório
114
+ - milestone-create: Cria milestone para organização
115
+ - label-create: Cria label para issues
116
+
117
+ SOCIAL:
118
+ - star-set: Adiciona estrela ao repo
119
+ - star-unset: Remove estrela
120
+ - fork-create: Cria fork do repositório
121
+ - fork-list: Lista forks existentes
122
+
123
+ NOTIFICAÇÕES:
124
+ - subscription-set: Ativa notificações do repo
125
+ - subscription-unset: Desativa notificações
126
+
127
+ ARQUIVOS VIA API:
128
+ - contents-create: Cria arquivo diretamente via API (sem git local)`
129
+ },
130
+ tag: {
131
+ type: "string",
132
+ description: "Tag para release-create. Ex: 'v1.0.0'"
133
+ },
134
+ name: {
135
+ type: "string",
136
+ description: "Nome da release, label, etc."
137
+ },
138
+ body: {
139
+ type: "string",
140
+ description: "Descrição/corpo da release"
141
+ },
142
+ assets: {
143
+ type: "array",
144
+ items: {
145
+ oneOf: [
146
+ { type: "string" }, // path simples: "dist/app.exe"
147
+ {
148
+ type: "object",
149
+ properties: {
150
+ path: { type: "string" }, // "dist/build"
151
+ name: { type: "string" } // "MeuApp-Windows" (sem extensao)
152
+ },
153
+ required: ["path"]
154
+ }
155
+ ]
156
+ },
157
+ description: "Assets para upload. String = nome automatico, Objeto = nome custom. Pastas são compactadas automaticamente"
158
+ },
159
+ topics: {
160
+ type: "array",
161
+ items: { type: "string" },
162
+ description: "Lista de tópicos para topics-set. Ex: ['javascript', 'nodejs', 'mcp']"
163
+ },
164
+ path: {
165
+ type: "string",
166
+ description: "Caminho do arquivo para contents-create"
167
+ },
168
+ content: {
169
+ type: "string",
170
+ description: "Conteúdo do arquivo para contents-create"
171
+ },
172
+ branch: {
173
+ type: "string",
174
+ description: "Branch alvo para contents-create. Default: main"
175
+ },
176
+ color: {
177
+ type: "string",
178
+ description: "Cor do label em hex (sem #). Ex: 'ff0000' para vermelho"
179
+ },
180
+ title: {
181
+ type: "string",
182
+ description: "Título do milestone"
183
+ },
184
+ isPublic: {
185
+ type: "boolean",
186
+ description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='ensure'"
187
+ },
188
+ organization: {
189
+ type: "string",
190
+ description: "Opcional. Nome da organização no GitHub/Gitea. Se informado, operações são feitas na org em vez da conta pessoal. Ex: 'automacao-casa', 'cliente-joao'"
191
+ }
192
+ },
193
+ required: ["projectPath", "action"],
194
+ additionalProperties: false
195
+ };
196
+
197
+ const description = `IMPORTANTE — projectPath:
198
+ Informe o caminho absoluto do projeto aberto no IDE. O servidor MCP não tem acesso ao
199
+ contexto do IDE e não consegue detectar automaticamente qual projeto está sendo trabalhado.
200
+
201
+ Operações em repositórios remotos GitHub e Gitea.
202
+
203
+ AÇÕES MAIS USADAS:
204
+ - ensure: Configurar remotes e criar repos (ESSENCIAL antes de push)
205
+ - list: Ver remotes configurados
206
+ - release-create: Publicar nova versão
207
+
208
+ EXECUÇÃO PARALELA:
209
+ Todas as operações são executadas em AMBOS os providers (GitHub + Gitea) simultaneamente.
210
+
211
+ QUANDO USAR:
212
+ - Se git-workflow push falhar: use action='ensure'
213
+ - Para publicar release: use action='release-create'
214
+ - Para configurar tópicos: use action='topics-set'`;
215
+
216
+ async function handle(args) {
217
+ const validate = ajv.compile(inputSchema);
218
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
219
+ const { projectPath, action } = args;
220
+ try {
221
+ validateProjectPath(projectPath);
222
+ if (action === "list") {
223
+ const remotes = await git.listRemotes(projectPath);
224
+
225
+ // Debug info: calculate what URLs should be
226
+ let repoName = getRepoNameFromPath(projectPath);
227
+ if (repoName === "GIT_MCP") repoName = "git-mcp";
228
+ const calculated = await pm.getRemoteUrls(repoName, args.organization);
229
+
230
+ return asToolResult({
231
+ remotes,
232
+ configured: remotes.length > 0,
233
+ hasGithub: remotes.some(r => r.remote === "github"),
234
+ hasGitea: remotes.some(r => r.remote === "gitea"),
235
+ debug: {
236
+ repoName,
237
+ calculatedUrls: calculated,
238
+ env: {
239
+ hasGithubToken: !!pm.githubToken,
240
+ hasGiteaToken: !!pm.giteaToken,
241
+ giteaUrl: pm.giteaUrl
242
+ }
243
+ },
244
+ message: remotes.length === 0 ? "Nenhum remote configurado. Use action='ensure' para configurar." : undefined
245
+ });
246
+ }
247
+
248
+ if (action === "list-all") {
249
+ const repos = await pm.listAllRepos({ organization: args.organization });
250
+ const summary = [];
251
+ if (repos.github?.length) summary.push(`${repos.github.length} GitHub repos`);
252
+ if (repos.gitea?.length) summary.push(`${repos.gitea.length} Gitea repos`);
253
+
254
+ return asToolResult({
255
+ success: true,
256
+ organization: args.organization || undefined,
257
+ summary: summary.join(", "),
258
+ github: repos.github,
259
+ gitea: repos.gitea
260
+ });
261
+ }
262
+ if (action === "list-orgs") {
263
+ const orgs = await pm.listOrgs();
264
+ const summary = [];
265
+ if (orgs.github?.length) summary.push(`${orgs.github.length} GitHub orgs`);
266
+ if (orgs.gitea?.length) summary.push(`${orgs.gitea.length} Gitea orgs`);
267
+
268
+ return asToolResult({
269
+ success: true,
270
+ summary: summary.length > 0 ? summary.join(", ") : "Nenhuma organização encontrada",
271
+ github: orgs.github,
272
+ gitea: orgs.gitea,
273
+ _aiContext: {
274
+ hint: "Use o nome da organização no parâmetro 'organization' das outras actions para criar/gerenciar repos dentro da org"
275
+ }
276
+ });
277
+ }
278
+ if (action === "ensure") {
279
+ const repo = getRepoNameFromPath(projectPath);
280
+ const isPublic = args.isPublic === true; // Default: privado
281
+ const organization = args.organization || undefined;
282
+ const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
283
+ const urls = await pm.getRemoteUrls(repo, organization);
284
+ await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
285
+ const remotes = await git.listRemotes(projectPath);
286
+ return asToolResult({
287
+ success: true,
288
+ isPrivate: !isPublic,
289
+ organization: organization || undefined,
290
+ ensured,
291
+ remotes,
292
+ urls,
293
+ message: "Remotes configurados. Agora pode usar git-workflow push." + (organization ? ` [org: ${organization}]` : "")
294
+ });
295
+ }
296
+ if (action === "repo-delete") {
297
+ const repo = getRepoNameFromPath(projectPath);
298
+
299
+ // Elicitation
300
+ await sendLog(server, "warning", "repo-delete solicitado", { repo });
301
+ const confirmed = await requestConfirmation(server, `⚠️ Vai DELETAR permanentemente o repositório '${repo}' do GitHub e Gitea. Confirmar?`);
302
+ if (!confirmed) return asToolError("CANCELLED", "Operação cancelada pelo usuário");
303
+
304
+ const out = await runBoth(pm, {
305
+ github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
306
+ 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 }; }
307
+ }, { organization: args.organization });
308
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), deleted: repo, providers: out, warning: "Repositório deletado permanentemente!" });
309
+ }
310
+ if (action === "release-create") {
311
+ const repo = getRepoNameFromPath(projectPath);
312
+ if (!args.tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório para criar release", { parameter: "tag", example: "v1.0.0" });
313
+ const tag = args.tag;
314
+ const name = args.name || tag;
315
+ const body = args.body || "";
316
+
317
+ // Criar release primeiro
318
+ const out = await runBoth(pm, {
319
+ 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 }; },
320
+ 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 }; }
321
+ }, { organization: args.organization });
322
+
323
+ const releaseCreated = !!(out.github?.ok || out.gitea?.ok);
324
+ if (!releaseCreated) {
325
+ return asToolResult({ success: false, tag, name, providers: out, error: "Falha ao criar release" });
326
+ }
327
+
328
+ // Processar assets se fornecidos
329
+ let assetsUploaded = [];
330
+ if (args.assets && args.assets.length > 0) {
331
+ const tempFiles = []; // Arquivos temporários a serem deletados
332
+ const [ghOwner, geOwner] = await Promise.all([
333
+ pm.getGitHubOwner().catch(() => ""),
334
+ pm.getGiteaOwner().catch(() => "")
335
+ ]);
336
+
337
+ for (const asset of args.assets) {
338
+ try {
339
+ const assetPath = typeof asset === 'string' ? asset : asset.path;
340
+ const customName = typeof asset === 'object' ? asset.name : null;
341
+
342
+ // Resolver caminho absoluto
343
+ const absolutePath = path.resolve(projectPath, assetPath);
344
+
345
+ // Verificar se existe
346
+ if (!fs.existsSync(absolutePath)) {
347
+ assetsUploaded.push({ path: assetPath, error: "Arquivo/pasta não encontrado" });
348
+ continue;
349
+ }
350
+
351
+ const stats = fs.statSync(absolutePath);
352
+ let uploadPath = absolutePath;
353
+ let uploadName = customName;
354
+ let isTempFile = false;
355
+
356
+ if (stats.isDirectory()) {
357
+ // Compactar pasta
358
+ const zipName = customName || path.basename(absolutePath);
359
+ const zipPath = await compactFolder(absolutePath, zipName);
360
+ uploadPath = zipPath;
361
+ uploadName = `${zipName}.zip`;
362
+ tempFiles.push(zipPath);
363
+ isTempFile = true;
364
+ } else {
365
+ // Arquivo direto
366
+ if (!uploadName) {
367
+ uploadName = path.basename(absolutePath);
368
+ } else {
369
+ // Adicionar extensão se não tiver
370
+ const ext = path.extname(absolutePath);
371
+ if (!uploadName.includes('.')) {
372
+ uploadName += ext;
373
+ }
374
+ }
375
+ }
376
+
377
+ // Upload para GitHub e Gitea
378
+ const uploadResult = await uploadAsset(pm, ghOwner || geOwner, repo, out.github?.id || out.gitea?.id, uploadPath, uploadName);
379
+
380
+ assetsUploaded.push({
381
+ path: assetPath,
382
+ name: uploadName,
383
+ uploaded: !!(uploadResult.github?.ok || uploadResult.gitea?.ok),
384
+ providers: uploadResult
385
+ });
386
+
387
+ // Deletar arquivo temporário se foi criado
388
+ if (isTempFile && tempFiles.includes(uploadPath)) {
389
+ try {
390
+ fs.unlinkSync(uploadPath);
391
+ tempFiles.splice(tempFiles.indexOf(uploadPath), 1);
392
+ } catch (e) {
393
+ console.warn(`Falha ao deletar arquivo temporário: ${uploadPath}`);
394
+ }
395
+ }
396
+
397
+ } catch (error) {
398
+ assetsUploaded.push({ path: typeof asset === 'string' ? asset : asset.path, error: String(error.message || error) });
399
+ }
400
+ }
401
+ }
402
+
403
+ return asToolResult({
404
+ success: releaseCreated,
405
+ tag,
406
+ name,
407
+ providers: out,
408
+ assets: assetsUploaded.length > 0 ? assetsUploaded : undefined
409
+ });
410
+ }
411
+ if (action === "topics-set") {
412
+ const repo = getRepoNameFromPath(projectPath);
413
+ const topics = Array.isArray(args.topics) ? args.topics : [];
414
+ if (topics.length === 0) return asToolError("MISSING_PARAMETER", "topics é obrigatório", { parameter: "topics", example: ["javascript", "nodejs"] });
415
+ const out = await runBoth(pm, {
416
+ 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 }; },
417
+ 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 }; }
418
+ }, { organization: args.organization });
419
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), topics, providers: out });
420
+ }
421
+ if (action === "milestone-create") {
422
+ const repo = getRepoNameFromPath(projectPath);
423
+ const title = args.title || args.name || "v1.0";
424
+ const out = await runBoth(pm, {
425
+ github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
426
+ 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 }; }
427
+ }, { organization: args.organization });
428
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, providers: out });
429
+ }
430
+ if (action === "label-create") {
431
+ const repo = getRepoNameFromPath(projectPath);
432
+ const name = args.name || "bug";
433
+ const color = String(args.color || "ff0000").replace(/^#/, "");
434
+ const out = await runBoth(pm, {
435
+ github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
436
+ 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 }; }
437
+ }, { organization: args.organization });
438
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), label: { name, color }, providers: out });
439
+ }
440
+ if (action === "fork-create") {
441
+ const repo = getRepoNameFromPath(projectPath);
442
+ const out = await runBoth(pm, {
443
+ github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
444
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/forks`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, full_name: r.data?.full_name }; }
445
+ }, { organization: args.organization });
446
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
447
+ }
448
+ if (action === "fork-list") {
449
+ const repo = getRepoNameFromPath(projectPath);
450
+ const out = await runBoth(pm, {
451
+ github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
452
+ gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/forks`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data || []).length }; }
453
+ }, { organization: args.organization });
454
+ return asToolResult({ providers: out });
455
+ }
456
+ if (action === "star-set" || action === "star-unset") {
457
+ const repo = getRepoNameFromPath(projectPath);
458
+ const out = await runBoth(pm, {
459
+ github: async (owner) => {
460
+ if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
461
+ await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true };
462
+ },
463
+ gitea: async (owner) => {
464
+ const base = pm.giteaUrl.replace(/\/$/, "");
465
+ if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
466
+ await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true };
467
+ }
468
+ }, { organization: args.organization });
469
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
470
+ }
471
+ if (action === "subscription-set" || action === "subscription-unset") {
472
+ const repo = getRepoNameFromPath(projectPath);
473
+ const out = await runBoth(pm, {
474
+ 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 }; },
475
+ 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 }; }
476
+ }, { organization: args.organization });
477
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
478
+ }
479
+ if (action === "contents-create") {
480
+ const repo = getRepoNameFromPath(projectPath);
481
+ if (!args.path) return asToolError("MISSING_PARAMETER", "path é obrigatório", { parameter: "path" });
482
+ if (!args.content) return asToolError("MISSING_PARAMETER", "content é obrigatório", { parameter: "content" });
483
+ const filePath = args.path;
484
+ const message = args.message || `Add ${filePath}`;
485
+ const data = args.content;
486
+ const branch = args.branch || "main";
487
+ const b64 = Buffer.from(data, "utf8").toString("base64");
488
+ const out = await runBoth(pm, {
489
+ github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
490
+ 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 }; }
491
+ }, { organization: args.organization });
492
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, branch, providers: out });
493
+ }
494
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
495
+ availableActions: ["list", "list-all", "list-orgs", "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"]
496
+ });
497
+ } catch (e) {
498
+ return errorToResponse(e);
499
+ }
500
+ }
501
+
502
+ return { name: "git-remote", description, inputSchema, handle, annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false } };
503
+ }