@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.
- package/README.md +494 -420
- package/dist/common/chunker.js +166 -534
- package/dist/common/embeddingService.js +39 -51
- package/dist/common/fileScanner.js +123 -58
- package/dist/common/indexManager.js +135 -102
- package/dist/common/projectKnowledgeService.js +627 -0
- package/dist/common/setup.js +49 -0
- package/dist/common/utils.js +215 -0
- package/dist/common/vectorStore.js +80 -67
- package/dist/index.js +77 -9
- package/dist/operations/boardMemberships.js +186 -0
- package/dist/operations/boards.js +268 -0
- package/dist/operations/cards.js +426 -0
- package/dist/operations/comments.js +249 -0
- package/dist/operations/labels.js +258 -0
- package/dist/operations/lists.js +157 -0
- package/dist/operations/projects.js +102 -0
- package/dist/operations/tasks.js +238 -0
- package/dist/tools/analyzeCoverage.js +46 -66
- package/dist/tools/board-summary.js +151 -0
- package/dist/tools/card-details.js +106 -0
- package/dist/tools/create-card-with-tasks.js +81 -0
- package/dist/tools/generateProjectDocs.js +133 -0
- package/dist/tools/getProjectDocs.js +126 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/indexCode.js +0 -1
- package/dist/tools/searchMemory.js +2 -2
- package/dist/tools/workflow-actions.js +145 -0
- package/package.json +2 -2
|
@@ -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) =>
|
|
25
|
+
const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
28
26
|
for (const file of sortedFiles) {
|
|
29
|
-
const
|
|
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 ?
|
|
32
|
+
currentPath = currentPath ? path.join(currentPath, parts[i]) : parts[i];
|
|
37
33
|
if (!dirMap.has(currentPath)) {
|
|
38
34
|
const dirNode = {
|
|
39
|
-
name:
|
|
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 ?
|
|
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(
|
|
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
|
-
//
|
|
76
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
251
|
-
console.error("Obteniendo
|
|
234
|
+
// 2. Get indexed files from vector store
|
|
235
|
+
console.error("Obteniendo archivos indexados...");
|
|
252
236
|
await vectorStore.initialize();
|
|
253
|
-
|
|
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.
|
|
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
|
-
|
|
264
|
-
for (const [
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
pendingFiles.add(file.path);
|
|
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
|
+
}
|