@grec0/memory-bank-mcp 0.0.3 → 0.0.4

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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @fileoverview Task operations for the MCP Kanban server
3
+ *
4
+ * This module provides functions for interacting with tasks in the Planka Kanban board,
5
+ * including creating, retrieving, updating, and deleting tasks, as well as batch operations.
6
+ */
7
+ import { z } from "zod";
8
+ import { plankaRequest } from "../common/utils.js";
9
+ import { PlankaTaskSchema } from "../common/types.js";
10
+ // Schema definitions
11
+ /**
12
+ * Schema for creating a new task
13
+ * @property {string} cardId - The ID of the card to create the task in
14
+ * @property {string} name - The name of the task
15
+ * @property {number} [position] - The position of the task in the card (default: 65535)
16
+ */
17
+ export const CreateTaskSchema = z.object({
18
+ cardId: z.string().describe("Card ID"),
19
+ name: z.string().describe("Task name"),
20
+ position: z.number().optional().describe("Task position (default: 65535)"),
21
+ });
22
+ /**
23
+ * Schema for batch creating multiple tasks
24
+ * @property {Array<CreateTaskSchema>} tasks - Array of tasks to create
25
+ */
26
+ export const BatchCreateTasksSchema = z.object({
27
+ tasks: z.array(CreateTaskSchema).describe("Array of tasks to create"),
28
+ });
29
+ /**
30
+ * Schema for retrieving tasks from a card
31
+ * @property {string} cardId - The ID of the card to get tasks from
32
+ */
33
+ export const GetTasksSchema = z.object({
34
+ cardId: z.string().describe("Card ID"),
35
+ });
36
+ /**
37
+ * Schema for retrieving a specific task
38
+ * @property {string} id - The ID of the task to retrieve
39
+ * @property {string} [cardId] - The ID of the card containing the task
40
+ */
41
+ export const GetTaskSchema = z.object({
42
+ id: z.string().describe("Task ID"),
43
+ cardId: z.string().optional().describe("Card ID containing the task"),
44
+ });
45
+ /**
46
+ * Schema for updating a task
47
+ * @property {string} id - The ID of the task to update
48
+ * @property {string} [name] - The new name for the task
49
+ * @property {boolean} [isCompleted] - Whether the task is completed
50
+ * @property {number} [position] - The new position for the task
51
+ */
52
+ export const UpdateTaskSchema = z.object({
53
+ id: z.string().describe("Task ID"),
54
+ name: z.string().optional().describe("Task name"),
55
+ isCompleted: z.boolean().optional().describe("Whether the task is completed"),
56
+ position: z.number().optional().describe("Task position"),
57
+ });
58
+ /**
59
+ * Schema for deleting a task
60
+ * @property {string} id - The ID of the task to delete
61
+ */
62
+ export const DeleteTaskSchema = z.object({
63
+ id: z.string().describe("Task ID"),
64
+ });
65
+ // Response schemas
66
+ const TasksResponseSchema = z.object({
67
+ items: z.array(PlankaTaskSchema),
68
+ included: z.record(z.any()).optional(),
69
+ });
70
+ const TaskResponseSchema = z.object({
71
+ item: PlankaTaskSchema,
72
+ included: z.record(z.any()).optional(),
73
+ });
74
+ // Map to store task ID to card ID mapping
75
+ const taskCardIdMap = {};
76
+ // Function implementations
77
+ /**
78
+ * Creates a new task for a card
79
+ *
80
+ * @param {object} params - The task creation parameters
81
+ * @param {string} params.cardId - The ID of the card to create the task in
82
+ * @param {string} params.name - The name of the new task
83
+ * @param {number} params.position - The position of the task in the card
84
+ * @returns {Promise<object>} The created task
85
+ */
86
+ export async function createTask(params) {
87
+ try {
88
+ const { cardId, name, position = 65535 } = params;
89
+ const response = await plankaRequest(`/api/cards/${cardId}/tasks`, {
90
+ method: "POST",
91
+ body: { name, position },
92
+ });
93
+ // Store the task ID to card ID mapping for getTask
94
+ if (response.item && response.item.id) {
95
+ taskCardIdMap[response.item.id] = cardId;
96
+ }
97
+ return response.item;
98
+ }
99
+ catch (error) {
100
+ console.error("Error creating task:", error);
101
+ throw new Error(`Failed to create task: ${error instanceof Error ? error.message : String(error)}`);
102
+ }
103
+ }
104
+ /**
105
+ * Creates multiple tasks for cards in a single operation
106
+ *
107
+ * @param {BatchCreateTasksOptions} options - The batch create tasks options
108
+ * @returns {Promise<{results: any[], successes: any[], failures: TaskError[]}>} The results of the batch operation
109
+ * @throws {Error} If the batch operation fails completely
110
+ */
111
+ export async function batchCreateTasks(options) {
112
+ try {
113
+ const results = [];
114
+ const successes = [];
115
+ const failures = [];
116
+ // Process each task in sequence
117
+ for (let i = 0; i < options.tasks.length; i++) {
118
+ const task = options.tasks[i];
119
+ // Ensure position is set if not provided
120
+ if (!task.position) {
121
+ task.position = 65535 * (i + 1);
122
+ }
123
+ try {
124
+ const result = await createTask(task);
125
+ results.push({
126
+ success: true,
127
+ result,
128
+ });
129
+ successes.push(result);
130
+ }
131
+ catch (error) {
132
+ const errorMessage = error instanceof Error
133
+ ? error.message
134
+ : String(error);
135
+ results.push({
136
+ success: false,
137
+ error: { message: errorMessage },
138
+ });
139
+ failures.push({
140
+ index: i,
141
+ task,
142
+ error: errorMessage,
143
+ });
144
+ }
145
+ }
146
+ return {
147
+ results,
148
+ successes,
149
+ failures,
150
+ };
151
+ }
152
+ catch (error) {
153
+ throw new Error(`Failed to batch create tasks: ${error instanceof Error ? error.message : String(error)}`);
154
+ }
155
+ }
156
+ /**
157
+ * Retrieves all tasks for a specific card
158
+ *
159
+ * @param {string} cardId - The ID of the card to get tasks from
160
+ * @returns {Promise<Array<object>>} Array of tasks in the card
161
+ */
162
+ export async function getTasks(cardId) {
163
+ try {
164
+ // Instead of using the tasks endpoint which returns HTML,
165
+ // we'll get the card details which includes tasks
166
+ const response = await plankaRequest(`/api/cards/${cardId}`);
167
+ // Extract tasks from the card response
168
+ if (response?.included?.tasks && Array.isArray(response.included.tasks)) {
169
+ const tasks = response.included.tasks;
170
+ return tasks;
171
+ }
172
+ return [];
173
+ }
174
+ catch (error) {
175
+ console.error(`Error getting tasks for card ${cardId}:`, error);
176
+ // If there's an error, return an empty array
177
+ return [];
178
+ }
179
+ }
180
+ /**
181
+ * Retrieves a specific task by ID
182
+ *
183
+ * @param {string} id - The ID of the task to retrieve
184
+ * @param {string} [cardId] - Optional card ID to help find the task
185
+ * @returns {Promise<object>} The requested task
186
+ */
187
+ export async function getTask(id, cardId) {
188
+ try {
189
+ // Tasks in Planka are always part of a card, so we need the card ID
190
+ const taskCardId = cardId || taskCardIdMap[id];
191
+ if (!taskCardId) {
192
+ throw new Error("Card ID is required to get a task. Either provide it directly or create the task first.");
193
+ }
194
+ // Get the card details which includes tasks
195
+ const response = await plankaRequest(`/api/cards/${taskCardId}`);
196
+ if (!response?.included?.tasks ||
197
+ !Array.isArray(response.included.tasks)) {
198
+ throw new Error(`Failed to get tasks for card ${taskCardId}`);
199
+ }
200
+ // Find the task with the matching ID
201
+ const task = response.included.tasks.find((task) => task.id === id);
202
+ if (!task) {
203
+ throw new Error(`Task with ID ${id} not found in card ${taskCardId}`);
204
+ }
205
+ return task;
206
+ }
207
+ catch (error) {
208
+ console.error(`Error getting task with ID ${id}:`, error);
209
+ throw new Error(`Failed to get task: ${error instanceof Error ? error.message : String(error)}`);
210
+ }
211
+ }
212
+ /**
213
+ * Updates a task's properties
214
+ *
215
+ * @param {string} id - The ID of the task to update
216
+ * @param {Partial<Omit<CreateTaskOptions, "cardId">>} options - The properties to update
217
+ * @returns {Promise<object>} The updated task
218
+ */
219
+ export async function updateTask(id, options) {
220
+ const response = await plankaRequest(`/api/tasks/${id}`, {
221
+ method: "PATCH",
222
+ body: options,
223
+ });
224
+ const parsedResponse = TaskResponseSchema.parse(response);
225
+ return parsedResponse.item;
226
+ }
227
+ /**
228
+ * Deletes a task by ID
229
+ *
230
+ * @param {string} id - The ID of the task to delete
231
+ * @returns {Promise<{success: boolean}>} Success indicator
232
+ */
233
+ export async function deleteTask(id) {
234
+ await plankaRequest(`/api/tasks/${id}`, {
235
+ method: "DELETE",
236
+ });
237
+ return { success: true };
238
+ }
@@ -21,22 +21,18 @@ function buildDirectoryTree(files, indexedFiles, pendingFiles, rootPath) {
21
21
  // Build tree structure
22
22
  const dirMap = new Map();
23
23
  dirMap.set("", root);
24
- // Helper to normalize paths to forward slashes
25
- const normalize = (p) => p.replace(/\\/g, "/");
26
24
  // Sort files by path for consistent tree building
27
- const sortedFiles = [...files].sort((a, b) => normalize(a.path).localeCompare(normalize(b.path)));
25
+ const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
28
26
  for (const file of sortedFiles) {
29
- const normalizedPath = normalize(file.path);
30
- const parts = normalizedPath.split("/");
27
+ const parts = file.path.split(path.sep);
31
28
  let currentPath = "";
32
29
  // Create directory nodes
33
30
  for (let i = 0; i < parts.length - 1; i++) {
34
- const part = parts[i];
35
31
  const parentPath = currentPath;
36
- currentPath = currentPath ? `${currentPath}/${part}` : part;
32
+ currentPath = currentPath ? path.join(currentPath, parts[i]) : parts[i];
37
33
  if (!dirMap.has(currentPath)) {
38
34
  const dirNode = {
39
- name: part,
35
+ name: parts[i],
40
36
  path: currentPath,
41
37
  type: "directory",
42
38
  status: "indexed",
@@ -54,10 +50,10 @@ function buildDirectoryTree(files, indexedFiles, pendingFiles, rootPath) {
54
50
  }
55
51
  // Add file node
56
52
  const fileName = parts[parts.length - 1];
57
- const fileDir = parts.length > 1 ? parts.slice(0, -1).join("/") : "";
53
+ const fileDir = parts.length > 1 ? path.dirname(file.path) : "";
58
54
  const parentDir = dirMap.get(fileDir);
59
55
  if (parentDir && parentDir.children) {
60
- const indexed = indexedFiles.has(normalize(file.path)); // Use normalized path for lookup
56
+ const indexed = indexedFiles.has(file.path);
61
57
  const pending = pendingFiles.has(file.path);
62
58
  const fileNode = {
63
59
  name: fileName,
@@ -70,43 +66,33 @@ function buildDirectoryTree(files, indexedFiles, pendingFiles, rootPath) {
70
66
  chunkCount: indexed ? indexedFiles.get(file.path).chunks : 0,
71
67
  };
72
68
  parentDir.children.push(fileNode);
69
+ // Update parent stats
70
+ let current = parentDir;
71
+ while (current) {
72
+ current.fileCount = (current.fileCount || 0) + 1;
73
+ if (indexed)
74
+ current.indexedCount = (current.indexedCount || 0) + 1;
75
+ if (pending)
76
+ current.pendingCount = (current.pendingCount || 0) + 1;
77
+ // Find parent
78
+ const parentPath = path.dirname(current.path);
79
+ current = parentPath !== current.path ? dirMap.get(parentPath === "." ? "" : parentPath) : null;
80
+ }
73
81
  }
74
82
  }
75
- // Calculate stats bottom-up
76
- const calculateNodeStats = (node) => {
77
- if (node.type === "file") {
78
- return;
79
- }
80
- let fileCount = 0;
81
- let indexedCount = 0;
82
- let pendingCount = 0;
83
+ // Sort children (directories first, then files)
84
+ const sortChildren = (node) => {
83
85
  if (node.children) {
84
86
  node.children.sort((a, b) => {
85
- if (a.type !== b.type)
87
+ if (a.type !== b.type) {
86
88
  return a.type === "directory" ? -1 : 1;
89
+ }
87
90
  return a.name.localeCompare(b.name);
88
91
  });
89
- for (const child of node.children) {
90
- if (child.type === "directory") {
91
- calculateNodeStats(child);
92
- fileCount += child.fileCount || 0;
93
- indexedCount += child.indexedCount || 0;
94
- pendingCount += child.pendingCount || 0;
95
- }
96
- else {
97
- fileCount++;
98
- if (child.status === "indexed")
99
- indexedCount++;
100
- if (child.status === "pending_reindex")
101
- pendingCount++;
102
- }
103
- }
92
+ node.children.forEach(sortChildren);
104
93
  }
105
- node.fileCount = fileCount;
106
- node.indexedCount = indexedCount;
107
- node.pendingCount = pendingCount;
108
94
  };
109
- calculateNodeStats(root);
95
+ sortChildren(root);
110
96
  return root;
111
97
  }
112
98
  /**
@@ -126,11 +112,9 @@ function calculateStats(files, indexedFiles, pendingFiles, totalChunks) {
126
112
  languageBreakdown: {},
127
113
  directoryBreakdown: {},
128
114
  };
129
- // Helper to normalize paths
130
- const normalize = (p) => p.replace(/\\/g, "/");
131
115
  for (const file of files) {
132
116
  stats.totalSize += file.size;
133
- const indexed = indexedFiles.has(normalize(file.path));
117
+ const indexed = indexedFiles.has(file.path);
134
118
  const pending = pendingFiles.has(file.path);
135
119
  if (pending) {
136
120
  stats.pendingReindexFiles++;
@@ -153,7 +137,7 @@ function calculateStats(files, indexedFiles, pendingFiles, totalChunks) {
153
137
  stats.languageBreakdown[file.language].total++;
154
138
  if (indexed) {
155
139
  stats.languageBreakdown[file.language].indexed++;
156
- stats.languageBreakdown[file.language].chunks += indexedFiles.get(normalize(file.path)).chunks;
140
+ stats.languageBreakdown[file.language].chunks += indexedFiles.get(file.path).chunks;
157
141
  }
158
142
  // Directory breakdown
159
143
  const dir = path.dirname(file.path);
@@ -230,7 +214,7 @@ export async function analyzeCoverage(indexManager, vectorStore, workspaceRoot)
230
214
  const maxScanTime = 10000; // 10 seconds max
231
215
  let allFiles = [];
232
216
  try {
233
- allFiles = await scanFiles({
217
+ allFiles = scanFiles({
234
218
  rootPath: workspaceRoot,
235
219
  recursive: true
236
220
  });
@@ -247,37 +231,33 @@ export async function analyzeCoverage(indexManager, vectorStore, workspaceRoot)
247
231
  console.error(`Error escaneando archivos: ${error}`);
248
232
  throw error;
249
233
  }
250
- // 2. Get indexed files stats in ONE batch query (Optimized)
251
- console.error("Obteniendo estadísticas de archivos indexados...");
234
+ // 2. Get indexed files from vector store
235
+ console.error("Obteniendo archivos indexados...");
252
236
  await vectorStore.initialize();
253
- // This single call replaces thousands of potential DB queries
254
- // It returns Map<filePath, { lastIndexed, chunkCount, fileHash }>
255
- const indexedFileStats = await vectorStore.getIndexedFileStats();
237
+ const fileHashes = await vectorStore.getFileHashes();
256
238
  // 3. Get index metadata
257
239
  const indexStats = await indexManager.getStats();
258
- // 4. Adapt to expected format for efficient loopups
259
- // Helper to normalize paths to forward slashes
260
- const normalize = (p) => p.replace(/\\/g, "/");
261
- // 4. Adapt to expected format for efficient loopups
240
+ // 4. Build indexed files map with chunk counts
262
241
  const indexedFiles = new Map();
263
- const normalizedStats = new Map();
264
- for (const [path, stats] of indexedFileStats) {
265
- const normPath = normalize(path);
266
- normalizedStats.set(normPath, stats);
267
- indexedFiles.set(normPath, {
268
- lastIndexed: stats.lastIndexed,
269
- chunks: stats.chunkCount
270
- });
242
+ // Get chunks grouped by file from vector store
243
+ for (const [filePath, hash] of fileHashes) {
244
+ const chunks = await vectorStore.getChunksByFile(filePath);
245
+ if (chunks.length > 0) {
246
+ indexedFiles.set(filePath, {
247
+ lastIndexed: chunks[0].timestamp,
248
+ chunks: chunks.length,
249
+ });
250
+ }
271
251
  }
272
252
  // 5. Identify pending files (files that changed)
273
253
  const pendingFiles = new Set();
274
254
  for (const file of allFiles) {
275
- const normPath = normalize(file.path);
276
- const stats = normalizedStats.get(normPath);
277
- if (stats) {
278
- // Check if file hash matches the one in DB
279
- if (stats.fileHash !== file.hash) {
280
- pendingFiles.add(file.path); // keep original path for file system ops if needed
255
+ const indexed = indexedFiles.get(file.path);
256
+ if (indexed) {
257
+ // Check if file hash matches
258
+ const chunks = await vectorStore.getChunksByFile(file.path);
259
+ if (chunks.length > 0 && chunks[0].file_hash !== file.hash) {
260
+ pendingFiles.add(file.path);
281
261
  }
282
262
  }
283
263
  }
@@ -0,0 +1,151 @@
1
+ import { z } from "zod";
2
+ // Import functions from operations directory
3
+ import { getBoard } from "../operations/boards.js";
4
+ import { getLists } from "../operations/lists.js";
5
+ import { getCards } from "../operations/cards.js";
6
+ import { getTasks } from "../operations/tasks.js";
7
+ import { getLabels } from "../operations/labels.js";
8
+ import { getComments } from "../operations/comments.js";
9
+ /**
10
+ * Zod schema for the getBoardSummary function parameters
11
+ * @property {string} boardId - The ID of the board to get a summary for
12
+ * @property {boolean} [includeTaskDetails=false] - Whether to include detailed task information for each card
13
+ * @property {boolean} [includeComments=false] - Whether to include comments for each card
14
+ */
15
+ export const getBoardSummarySchema = z.object({
16
+ boardId: z.string().describe("The ID of the board to get a summary for"),
17
+ includeTaskDetails: z.boolean().optional().default(false).describe("Whether to include detailed task information for each card"),
18
+ includeComments: z.boolean().optional().default(false).describe("Whether to include comments for each card"),
19
+ });
20
+ /**
21
+ * Retrieves a comprehensive summary of a board including lists, cards, tasks, and statistics
22
+ *
23
+ * This function aggregates data from multiple sources to provide a complete view of a board,
24
+ * including its lists, cards, tasks, and labels. It also calculates various statistics and
25
+ * provides workflow state analysis.
26
+ *
27
+ * @param {GetBoardSummaryParams} params - Parameters for retrieving board summary
28
+ * @param {string} params.boardId - The ID of the board to get a summary for
29
+ * @param {boolean} [params.includeTaskDetails=false] - Whether to include detailed task information for each card
30
+ * @param {boolean} [params.includeComments=false] - Whether to include comments for each card
31
+ * @returns {Promise<object>} Comprehensive board summary including lists, cards, tasks, statistics, and workflow state
32
+ * @throws {Error} If the board is not found
33
+ */
34
+ export async function getBoardSummary(params) {
35
+ const { boardId, includeTaskDetails, includeComments } = params;
36
+ try {
37
+ // Get the board details
38
+ const board = await getBoard(boardId);
39
+ if (!board) {
40
+ throw new Error(`Board with ID ${boardId} not found`);
41
+ }
42
+ // Get all lists on the board
43
+ const allLists = await getLists(boardId);
44
+ // Get all cards for each list
45
+ const listsWithCards = await Promise.all(allLists.map(async (list) => {
46
+ const listCards = await getCards(list.id);
47
+ // Get tasks for each card if requested
48
+ const cardsWithDetails = await Promise.all(listCards.map(async (card) => {
49
+ let taskDetails = [];
50
+ if (includeTaskDetails) {
51
+ taskDetails = await getTasks(card.id);
52
+ }
53
+ // Get comments if requested
54
+ let cardComments = [];
55
+ if (includeComments) {
56
+ cardComments = await getComments(card.id);
57
+ }
58
+ // Calculate task completion percentage
59
+ const completedTasks = taskDetails.filter((task) => task.isCompleted).length;
60
+ const totalTasks = taskDetails.length;
61
+ const completionPercentage = totalTasks > 0
62
+ ? Math.round((completedTasks / totalTasks) * 100)
63
+ : 0;
64
+ return {
65
+ ...card,
66
+ tasks: includeTaskDetails
67
+ ? {
68
+ items: taskDetails,
69
+ total: totalTasks,
70
+ completed: completedTasks,
71
+ completionPercentage,
72
+ }
73
+ : undefined,
74
+ comments: includeComments
75
+ ? cardComments
76
+ : undefined,
77
+ };
78
+ }));
79
+ return {
80
+ ...list,
81
+ cards: cardsWithDetails,
82
+ cardCount: cardsWithDetails.length,
83
+ };
84
+ }));
85
+ // Get all labels for the board
86
+ const boardLabels = await getLabels(boardId);
87
+ // Calculate overall statistics
88
+ const totalCards = listsWithCards.reduce((sum, list) => sum + list.cardCount, 0);
89
+ // Find specific lists by name
90
+ const backlogList = listsWithCards.find((list) => list.name.toLowerCase() === "backlog");
91
+ const inProgressList = listsWithCards.find((list) => list.name.toLowerCase() === "in progress");
92
+ const testingList = listsWithCards.find((list) => list.name.toLowerCase() === "testing");
93
+ const doneList = listsWithCards.find((list) => list.name.toLowerCase() === "done");
94
+ // Count cards with specific labels
95
+ const urgentCards = listsWithCards.flatMap((list) => list.cards)
96
+ .filter((card) => card.labelIds?.some((labelId) => boardLabels.find((label) => label.id === labelId &&
97
+ label.name.toLowerCase() === "urgent"))).length;
98
+ const bugCards = listsWithCards.flatMap((list) => list.cards)
99
+ .filter((card) => card.labelIds?.some((labelId) => boardLabels.find((label) => label.id === labelId &&
100
+ label.name.toLowerCase() === "bug"))).length;
101
+ return {
102
+ board,
103
+ lists: listsWithCards,
104
+ labels: boardLabels,
105
+ stats: {
106
+ totalCards,
107
+ backlogCount: backlogList?.cardCount || 0,
108
+ inProgressCount: inProgressList?.cardCount || 0,
109
+ testingCount: testingList?.cardCount || 0,
110
+ doneCount: doneList?.cardCount || 0,
111
+ urgentCount: urgentCards,
112
+ bugCount: bugCards,
113
+ completionPercentage: totalCards > 0
114
+ ? Math.round((doneList?.cardCount || 0) / totalCards * 100)
115
+ : 0,
116
+ },
117
+ workflowState: {
118
+ hasCardsInBacklog: (backlogList?.cardCount || 0) > 0,
119
+ hasCardsInProgress: (inProgressList?.cardCount || 0) > 0,
120
+ hasCardsInTesting: (testingList?.cardCount || 0) > 0,
121
+ nextActionSuggestion: getNextActionSuggestion(backlogList?.cardCount || 0, inProgressList?.cardCount || 0, testingList?.cardCount || 0),
122
+ },
123
+ };
124
+ }
125
+ catch (error) {
126
+ console.error("Error in getBoardSummary:", error);
127
+ throw error;
128
+ }
129
+ }
130
+ /**
131
+ * Helper function to suggest the next action based on board state
132
+ *
133
+ * @param {number} backlogCount - Number of cards in the Backlog list
134
+ * @param {number} inProgressCount - Number of cards in the In Progress list
135
+ * @param {number} testingCount - Number of cards in the Testing list
136
+ * @returns {string} A suggestion for the next action to take
137
+ */
138
+ function getNextActionSuggestion(backlogCount, inProgressCount, testingCount) {
139
+ if (testingCount > 0) {
140
+ return "Review cards in Testing that need feedback";
141
+ }
142
+ else if (inProgressCount > 0) {
143
+ return "Continue working on cards in In Progress";
144
+ }
145
+ else if (backlogCount > 0) {
146
+ return "Start working on a card from Backlog";
147
+ }
148
+ else {
149
+ return "All tasks complete! Create new cards or projects";
150
+ }
151
+ }
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ import { getCard } from "../operations/cards.js";
3
+ import { getTasks } from "../operations/tasks.js";
4
+ import { getComments } from "../operations/comments.js";
5
+ import { getLabels } from "../operations/labels.js";
6
+ import { getProjects } from "../operations/projects.js";
7
+ import { getBoards } from "../operations/boards.js";
8
+ import { getLists } from "../operations/lists.js";
9
+ /**
10
+ * Zod schema for the getCardDetails function parameters
11
+ * @property {string} cardId - The ID of the card to get details for
12
+ */
13
+ export const getCardDetailsSchema = z.object({
14
+ cardId: z.string().describe("The ID of the card to get details for"),
15
+ });
16
+ /**
17
+ * Retrieves comprehensive details about a card including tasks, comments, labels, and analysis
18
+ *
19
+ * This function aggregates data from multiple sources to provide a complete view of a card,
20
+ * including its tasks, comments, and labels. It also calculates task completion percentage
21
+ * and performs analysis on the card's status.
22
+ *
23
+ * @param {GetCardDetailsParams} params - Parameters for retrieving card details
24
+ * @param {string} params.cardId - The ID of the card to get details for
25
+ * @returns {Promise<object>} Comprehensive card details including tasks, comments, labels, and analysis
26
+ * @throws {Error} If the card is not found or if the board ID cannot be determined
27
+ */
28
+ export async function getCardDetails(params) {
29
+ const { cardId } = params;
30
+ try {
31
+ // Get the card details
32
+ const card = await getCard(cardId);
33
+ if (!card) {
34
+ throw new Error(`Card with ID ${cardId} not found`);
35
+ }
36
+ // Get tasks for the card
37
+ const tasks = await getTasks(card.id);
38
+ // Get comments for the card
39
+ const comments = await getComments(card.id);
40
+ // Find the board ID by searching through all projects and boards
41
+ let boardId = null;
42
+ // Get all projects
43
+ const projectsResponse = await getProjects(1, 100);
44
+ const projects = projectsResponse.items;
45
+ // For each project, get its boards
46
+ for (const project of projects) {
47
+ if (boardId)
48
+ break; // Stop if we already found the board ID
49
+ const boards = await getBoards(project.id);
50
+ // For each board, get its lists
51
+ for (const board of boards) {
52
+ if (boardId)
53
+ break; // Stop if we already found the board ID
54
+ const lists = await getLists(board.id);
55
+ // Check if the card's list ID is in this board
56
+ const matchingList = lists.find((list) => list.id === card.listId);
57
+ if (matchingList) {
58
+ boardId = board.id;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ if (!boardId) {
64
+ throw new Error(`Could not determine board ID for card ${cardId}`);
65
+ }
66
+ const labels = await getLabels(boardId);
67
+ // Filter to just the labels assigned to this card
68
+ // Note: We need to get the labelIds from the card's data
69
+ // This might require additional API calls or data structure knowledge
70
+ // For now, we'll return all labels for the board
71
+ // Calculate task completion percentage
72
+ const completedTasks = tasks.filter((task) => task.isCompleted).length;
73
+ const totalTasks = tasks.length;
74
+ const completionPercentage = totalTasks > 0
75
+ ? Math.round((completedTasks / totalTasks) * 100)
76
+ : 0;
77
+ // Sort comments by date (newest first)
78
+ const sortedComments = comments.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
79
+ // Check if the most recent comment is likely from a human (not the LLM)
80
+ // This is a heuristic and might need adjustment
81
+ const hasRecentHumanFeedback = sortedComments.length > 0 &&
82
+ sortedComments[0].data &&
83
+ !sortedComments[0].data.text.includes("Implemented feature") &&
84
+ !sortedComments[0].data.text.includes("Awaiting human review");
85
+ return {
86
+ card,
87
+ taskItems: tasks,
88
+ taskStats: {
89
+ total: totalTasks,
90
+ completed: completedTasks,
91
+ completionPercentage,
92
+ },
93
+ comments: sortedComments,
94
+ labels,
95
+ analysis: {
96
+ hasRecentHumanFeedback,
97
+ isComplete: completionPercentage === 100,
98
+ needsAttention: hasRecentHumanFeedback || completedTasks === 0,
99
+ },
100
+ };
101
+ }
102
+ catch (error) {
103
+ console.error("Error in getCardDetails:", error);
104
+ throw error;
105
+ }
106
+ }