@dami_deleon/rikudo 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,11 @@ rikudo pr -t 1234
90
90
  rikudo test
91
91
  ```
92
92
 
93
+ * **Ver Ticket:**
94
+ ```bash
95
+ rikudo ticket 1234
96
+ ```
97
+
93
98
  * **Configuración:**
94
99
  ```bash
95
100
  rikudo config
@@ -112,10 +117,11 @@ El servidor se comunica mediante **stdio**, permitiendo integrarse con cualquier
112
117
  | Herramienta | Descripción |
113
118
  |------------|-------------|
114
119
  | `validate_configuration` | Verifica que las variables de entorno necesarias (Redmine, Gitea) estén configuradas correctamente. |
115
- | `get_ticket_context` | Obtiene el contexto completo de un ticket de Redmine (título, descripción, estado, prioridad). |
120
+ | `get_ticket_context` | Obtiene el contexto completo de un ticket de Redmine (título, descripción, estado, prioridad, custom fields). |
116
121
  | `get_git_diff` | Retorna el diff de los archivos que están en staging (`git add`). |
117
122
  | `shift_left_start` | Flujo shift-left completo: checkout a rama objetivo, crea nueva rama, commit vacío y abre Draft PR en Gitea. Soporta parámetro `isWip` para usar prefijo "WIP:" en el título. |
118
123
  | `update_pm_redmine` | Agrega un comentario a un ticket de Redmine para notificar al Project Manager. |
124
+ | `update_custom_fields` | Actualiza los campos personalizados (Custom Fields) de un ticket de Redmine. |
119
125
 
120
126
  #### Prompts Disponibles
121
127
 
@@ -157,8 +163,8 @@ RIKUDO_REDMINE_API_KEY=tu_redmine_key
157
163
  # Gitea
158
164
  RIKUDO_GITEA_URL=https://gitea.tuempresa.com
159
165
  RIKUDO_GITEA_TOKEN=tu_gitea_token
160
- RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion
161
- RIKUDO_GITEA_REPO=nombre_del_repositorio
166
+ RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion # Opcional
167
+ RIKUDO_GITEA_REPO=nombre_del_repositorio # Opcional
162
168
  ```
163
169
 
164
170
  #### Formato de Ramas
@@ -197,8 +203,8 @@ RIKUDO_REDMINE_API_KEY=tu_redmine_key
197
203
  # --- Gitea (para servidor MCP) ---
198
204
  RIKUDO_GITEA_URL=https://gitea.tuempresa.com
199
205
  RIKUDO_GITEA_TOKEN=tu_gitea_token
200
- RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion
201
- RIKUDO_GITEA_REPO=nombre_del_repositorio
206
+ RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion # Opcional: se obtiene del remote si no se especifica
207
+ RIKUDO_GITEA_REPO=nombre_del_repositorio # Opcional: se obtiene del remote si no se especifica
202
208
  ```
203
209
 
204
210
  ### Plantillas Personalizadas
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import chalk from "chalk";
5
5
  import figlet from "figlet";
6
6
  import clipboardy from "clipboardy"; // <--- Nueva dependencia
7
7
  import { getStagedDiff, getStagedFiles, commitChanges, getRecentCommits, getCommitList, } from "./services/git.js";
8
- import { getIssueDetails, updateIssueComment, } from "./services/redmine.js";
8
+ import { getIssueDetails, updateIssueComment, printTicketDetails, } from "./services/redmine.js";
9
9
  import { showConfigMenu } from "./menus/configMenu.js";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import path from "node:path";
@@ -63,6 +63,23 @@ program
63
63
  .command("config")
64
64
  .description("Abrir menú de configuración")
65
65
  .action(() => showConfigMenu());
66
+ // Ticket
67
+ const ticketCmd = program
68
+ .command("ticket <ticketId>")
69
+ .description("Ver detalles de un ticket de Redmine");
70
+ ticketCmd.action(async (ticketId) => {
71
+ loadEnv();
72
+ if (!ticketId) {
73
+ console.log(chalk.red("❌ Debes especificar un ticket: rikudo ticket <id>"));
74
+ process.exit(1);
75
+ }
76
+ const ticket = await getIssueDetails(ticketId);
77
+ if (!ticket) {
78
+ console.log(chalk.red(`❌ No se encontró el ticket #${ticketId}`));
79
+ process.exit(1);
80
+ }
81
+ printTicketDetails(ticket);
82
+ });
66
83
  // MCP Server
67
84
  program
68
85
  .command("mcp")
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
4
  import { simpleGit } from "simple-git";
5
- import { getIssueDetails, updateIssueComment } from "../services/redmine.js";
5
+ import { getIssueDetails, updateIssueComment, updateIssueCustomFields } from "../services/redmine.js";
6
6
  import { getStagedDiff } from "../services/git.js";
7
7
  import { createPullRequest } from "../services/gitea.js";
8
8
  import { loadEnv } from "../utils/env.js";
@@ -46,6 +46,7 @@ server.registerTool("get_ticket_context", {
46
46
  priority: ticket.priority,
47
47
  author: ticket.author,
48
48
  url: ticket.url,
49
+ customFields: ticket.customFields,
49
50
  }, null, 2),
50
51
  },
51
52
  ],
@@ -190,6 +191,28 @@ server.registerTool("update_pm_redmine", {
190
191
  return { content: [{ type: "text", text: `Error actualizando Redmine: ${errorMessage}` }], isError: true };
191
192
  }
192
193
  });
194
+ server.registerTool("update_custom_fields", {
195
+ title: "Update Custom Fields",
196
+ description: "Actualiza los campos personalizados (Custom Fields) de un ticket de Redmine. Usa get_ticket_context primero para ver los campos disponibles y sus IDs.",
197
+ inputSchema: z.object({
198
+ ticketId: z.string().describe("ID del ticket de Redmine (ej: '1234')"),
199
+ customFields: z.array(z.object({
200
+ id: z.number().describe("ID del custom field"),
201
+ value: z.string().describe("Nuevo valor para el campo"),
202
+ })).describe("Array de custom fields a actualizar"),
203
+ }),
204
+ }, async ({ ticketId, customFields }) => {
205
+ try {
206
+ await updateIssueCustomFields(ticketId, customFields);
207
+ return {
208
+ content: [{ type: "text", text: `Custom fields actualizados exitosamente en el ticket #${ticketId}` }],
209
+ };
210
+ }
211
+ catch (error) {
212
+ const errorMessage = error instanceof Error ? error.message : "Error desconocido";
213
+ return { content: [{ type: "text", text: `Error actualizando custom fields: ${errorMessage}` }], isError: true };
214
+ }
215
+ });
193
216
  server.registerPrompt("start_feature", {
194
217
  title: "Start Feature",
195
218
  description: "Inicia el flujo de trabajo para una nueva feature. Obtiene contexto del ticket y prepara el entorno en Gitea.",
@@ -0,0 +1,59 @@
1
+ import axios from "axios";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { getConfig } from "../utils/config.js";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const CUSTOM_FIELDS_CACHE_FILE = path.resolve(__dirname, "../../.rikudo_custom_fields.json");
9
+ let cachedFields = null;
10
+ export function getCustomFieldsCache() {
11
+ if (cachedFields) {
12
+ return cachedFields;
13
+ }
14
+ try {
15
+ if (fs.existsSync(CUSTOM_FIELDS_CACHE_FILE)) {
16
+ const data = JSON.parse(fs.readFileSync(CUSTOM_FIELDS_CACHE_FILE, "utf-8"));
17
+ cachedFields = new Map(data.fields.map((f) => [f.id, f]));
18
+ return cachedFields;
19
+ }
20
+ }
21
+ catch (error) {
22
+ console.error("Error reading custom fields cache:", error);
23
+ }
24
+ return new Map();
25
+ }
26
+ export async function initCustomFieldsCache() {
27
+ const config = getConfig();
28
+ if (!config.REDMINE_URL || !config.REDMINE_API_KEY) {
29
+ return;
30
+ }
31
+ try {
32
+ const client = axios.create({
33
+ baseURL: config.REDMINE_URL,
34
+ headers: { "X-Redmine-API-Key": config.REDMINE_API_KEY },
35
+ });
36
+ const response = await client.get("/custom_fields.json");
37
+ const fields = response.data.custom_fields;
38
+ const cache = {
39
+ fields,
40
+ updatedAt: Date.now(),
41
+ };
42
+ fs.writeFileSync(CUSTOM_FIELDS_CACHE_FILE, JSON.stringify(cache, null, 2));
43
+ cachedFields = new Map(fields.map((f) => [f.id, f]));
44
+ }
45
+ catch (error) {
46
+ console.error("Error initializing custom fields cache:", error);
47
+ }
48
+ }
49
+ export function enrichCustomFields(customFields) {
50
+ const cache = getCustomFieldsCache();
51
+ return customFields.map((field) => {
52
+ const definition = cache.get(field.id);
53
+ return {
54
+ id: field.id,
55
+ name: definition?.name || `Field ${field.id}`,
56
+ value: field.value,
57
+ };
58
+ });
59
+ }
@@ -1,4 +1,5 @@
1
1
  import axios, { AxiosError } from "axios";
2
+ import chalk from "chalk";
2
3
  import { getConfig } from "../utils/config.js";
3
4
  import z from "zod";
4
5
  function cleanRedmineMarkdown(text) {
@@ -112,6 +113,7 @@ const issueSchema = z
112
113
  updated: issue.updated_on,
113
114
  estimatedHours: issue.estimated_hours,
114
115
  },
116
+ customFields: issue.custom_fields,
115
117
  };
116
118
  });
117
119
  const getHttpRedmineClient = () => {
@@ -157,6 +159,12 @@ export const updateIssueComment = async (issueId, comment) => {
157
159
  console.error("❌ Error actualizando Redmine.");
158
160
  }
159
161
  };
162
+ export const updateIssueCustomFields = async (ticketId, customFields) => {
163
+ const client = getHttpRedmineClient();
164
+ await client.put(`/issues/${ticketId}.json`, {
165
+ issue: { custom_fields: customFields },
166
+ });
167
+ };
160
168
  const userSchema = z
161
169
  .object({
162
170
  id: z.number(),
@@ -196,9 +204,37 @@ export const checkRedmineConnection = async () => {
196
204
  return false;
197
205
  }
198
206
  };
207
+ export function printTicketDetails(ticket) {
208
+ console.log(chalk.bold.cyan("\n━━━ Ticket #" + ticket.id + " ━━━"));
209
+ console.log(chalk.bold.white(ticket.subject));
210
+ console.log(chalk.dim("─".repeat(40)));
211
+ console.log(chalk.gray("Estado: ") + chalk.green(ticket.status));
212
+ console.log(chalk.gray("Prioridad: ") + chalk.yellow(ticket.priority));
213
+ console.log(chalk.gray("Tracker: ") + ticket.tracker);
214
+ console.log(chalk.gray("Proyecto: ") + ticket.project);
215
+ console.log(chalk.gray("Autor: ") + ticket.author);
216
+ console.log(chalk.gray("URL: ") + chalk.blue.underline(ticket.url));
217
+ if (ticket.time.end) {
218
+ console.log(chalk.gray("Vencimiento: ") + chalk.red(ticket.time.end));
219
+ }
220
+ console.log(chalk.dim("─".repeat(40)));
221
+ console.log(chalk.bold("Descripción:"));
222
+ console.log(chalk.white(ticket.description));
223
+ if (ticket.customFields && ticket.customFields.length > 0) {
224
+ console.log(chalk.dim("─".repeat(40)));
225
+ console.log(chalk.bold("Custom Fields:"));
226
+ ticket.customFields.forEach((field) => {
227
+ const value = field.value && String(field.value).length > 0 ? String(field.value) : chalk.dim("(vacío)");
228
+ console.log(chalk.gray(` ${field.name}: `) + chalk.white(value));
229
+ });
230
+ }
231
+ console.log(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━\n"));
232
+ }
199
233
  export const Redmine = {
200
234
  getIssueDetails,
201
235
  updateIssueComment,
236
+ updateIssueCustomFields,
202
237
  getCurrentUser,
203
238
  checkRedmineConnection,
239
+ printTicketDetails,
204
240
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dami_deleon/rikudo",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "El Sabio de los Commits: AI Assistant con integración a Redmine",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",