@grec0/memory-bank-mcp 0.0.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.
- package/.memoryignore.example +76 -0
- package/README.md +425 -0
- package/dist/common/chunker.js +407 -0
- package/dist/common/embeddingService.js +302 -0
- package/dist/common/errors.js +71 -0
- package/dist/common/fileScanner.js +261 -0
- package/dist/common/indexManager.js +332 -0
- package/dist/common/setup.js +49 -0
- package/dist/common/types.js +115 -0
- package/dist/common/utils.js +215 -0
- package/dist/common/vectorStore.js +332 -0
- package/dist/common/version.js +2 -0
- package/dist/index.js +274 -0
- 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 +316 -0
- 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/getStats.js +59 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/indexCode.js +53 -0
- package/dist/tools/readFile.js +69 -0
- package/dist/tools/searchMemory.js +60 -0
- package/dist/tools/workflow-actions.js +145 -0
- package/dist/tools/writeFile.js +66 -0
- package/package.json +58 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Board operations for the MCP Kanban server
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions for interacting with boards in the Planka Kanban system,
|
|
5
|
+
* including creating, retrieving, updating, and deleting boards. It also handles
|
|
6
|
+
* the creation of default lists and labels when a new board is created.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { plankaRequest } from "../common/utils.js";
|
|
10
|
+
import { PlankaBoardSchema } from "../common/types.js";
|
|
11
|
+
import { getAdminUserId } from "../common/setup.js";
|
|
12
|
+
import * as boardMemberships from "./boardMemberships.js";
|
|
13
|
+
import * as lists from "./lists.js";
|
|
14
|
+
import * as labels from "./labels.js";
|
|
15
|
+
// Schema definitions
|
|
16
|
+
/**
|
|
17
|
+
* Schema for creating a new board
|
|
18
|
+
* @property {string} projectId - The ID of the project to create the board in
|
|
19
|
+
* @property {string} name - The name of the board
|
|
20
|
+
* @property {number} [position] - The position of the board in the project (default: 65535)
|
|
21
|
+
*/
|
|
22
|
+
export const CreateBoardSchema = z.object({
|
|
23
|
+
projectId: z.string().describe("Project ID"),
|
|
24
|
+
name: z.string().describe("Board name"),
|
|
25
|
+
position: z.number().default(65535).describe("Board position (default: 65535)"),
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Schema for retrieving boards from a project
|
|
29
|
+
* @property {string} projectId - The ID of the project to get boards from
|
|
30
|
+
*/
|
|
31
|
+
export const GetBoardsSchema = z.object({
|
|
32
|
+
projectId: z.string().describe("Project ID"),
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Schema for retrieving a specific board
|
|
36
|
+
* @property {string} id - The ID of the board to retrieve
|
|
37
|
+
*/
|
|
38
|
+
export const GetBoardSchema = z.object({
|
|
39
|
+
id: z.string().describe("Board ID"),
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Schema for updating a board
|
|
43
|
+
* @property {string} id - The ID of the board to update
|
|
44
|
+
* @property {string} [name] - The new name for the board
|
|
45
|
+
* @property {number} [position] - The new position for the board
|
|
46
|
+
* @property {string} [type] - The type of the board
|
|
47
|
+
*/
|
|
48
|
+
export const UpdateBoardSchema = z.object({
|
|
49
|
+
id: z.string().describe("Board ID"),
|
|
50
|
+
name: z.string().optional().describe("Board name"),
|
|
51
|
+
position: z.number().optional().describe("Board position"),
|
|
52
|
+
type: z.string().optional().describe("Board type"),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Schema for deleting a board
|
|
56
|
+
* @property {string} id - The ID of the board to delete
|
|
57
|
+
*/
|
|
58
|
+
export const DeleteBoardSchema = z.object({
|
|
59
|
+
id: z.string().describe("Board ID"),
|
|
60
|
+
});
|
|
61
|
+
// Response schemas
|
|
62
|
+
const BoardsResponseSchema = z.object({
|
|
63
|
+
items: z.array(PlankaBoardSchema),
|
|
64
|
+
included: z.record(z.any()).optional(),
|
|
65
|
+
});
|
|
66
|
+
const BoardResponseSchema = z.object({
|
|
67
|
+
item: PlankaBoardSchema,
|
|
68
|
+
included: z.record(z.any()).optional(),
|
|
69
|
+
});
|
|
70
|
+
// Function implementations
|
|
71
|
+
/**
|
|
72
|
+
* Creates default lists for a new board
|
|
73
|
+
*
|
|
74
|
+
* @param {string} boardId - The ID of the board to create lists for
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
async function createDefaultLists(boardId) {
|
|
79
|
+
try {
|
|
80
|
+
const defaultLists = [
|
|
81
|
+
{ name: "Backlog", position: 65535 },
|
|
82
|
+
{ name: "To Do", position: 131070 },
|
|
83
|
+
{ name: "In Progress", position: 196605 },
|
|
84
|
+
{ name: "On Hold", position: 262140 },
|
|
85
|
+
{ name: "Review", position: 327675 },
|
|
86
|
+
{ name: "Done", position: 393210 },
|
|
87
|
+
];
|
|
88
|
+
for (const list of defaultLists) {
|
|
89
|
+
await lists.createList({
|
|
90
|
+
boardId,
|
|
91
|
+
name: list.name,
|
|
92
|
+
position: list.position,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error(`Error creating default lists for board ${boardId}:`, error instanceof Error ? error.message : String(error));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates default labels for a new board
|
|
102
|
+
* @param boardId The ID of the board to create labels for
|
|
103
|
+
*/
|
|
104
|
+
async function createDefaultLabels(boardId) {
|
|
105
|
+
try {
|
|
106
|
+
// Priority labels
|
|
107
|
+
const priorityLabels = [
|
|
108
|
+
{ name: "P0: Critical", color: "berry-red", position: 65535 },
|
|
109
|
+
{ name: "P1: High", color: "red-burgundy", position: 131070 },
|
|
110
|
+
{ name: "P2: Medium", color: "pumpkin-orange", position: 196605 },
|
|
111
|
+
{ name: "P3: Low", color: "sunny-grass", position: 262140 },
|
|
112
|
+
];
|
|
113
|
+
// Type labels
|
|
114
|
+
const typeLabels = [
|
|
115
|
+
{ name: "Bug", color: "coral-green", position: 327675 },
|
|
116
|
+
{ name: "Feature", color: "lagoon-blue", position: 393210 },
|
|
117
|
+
{ name: "Enhancement", color: "bright-moss", position: 458745 },
|
|
118
|
+
{ name: "Documentation", color: "light-orange", position: 524280 },
|
|
119
|
+
];
|
|
120
|
+
// Status labels
|
|
121
|
+
const statusLabels = [
|
|
122
|
+
{ name: "Blocked", color: "midnight-blue", position: 589815 },
|
|
123
|
+
{ name: "Needs Info", color: "desert-sand", position: 655350 },
|
|
124
|
+
{ name: "Ready", color: "egg-yellow", position: 720885 },
|
|
125
|
+
];
|
|
126
|
+
// Combine all labels
|
|
127
|
+
const defaultLabels = [
|
|
128
|
+
...priorityLabels,
|
|
129
|
+
...typeLabels,
|
|
130
|
+
...statusLabels,
|
|
131
|
+
];
|
|
132
|
+
for (const label of defaultLabels) {
|
|
133
|
+
await labels.createLabel({
|
|
134
|
+
boardId,
|
|
135
|
+
name: label.name,
|
|
136
|
+
color: label.color, // Type assertion needed due to enum constraints
|
|
137
|
+
position: label.position,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error(`Error creating default labels for board ${boardId}:`, error instanceof Error ? error.message : String(error));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Creates a new board in a project with default lists and labels
|
|
147
|
+
*
|
|
148
|
+
* @param {CreateBoardOptions} options - Options for creating the board
|
|
149
|
+
* @param {string} options.projectId - The ID of the project to create the board in
|
|
150
|
+
* @param {string} options.name - The name of the board
|
|
151
|
+
* @param {number} [options.position] - The position of the board in the project
|
|
152
|
+
* @returns {Promise<object>} The created board
|
|
153
|
+
* @throws {Error} If the board creation fails
|
|
154
|
+
*/
|
|
155
|
+
export async function createBoard(options) {
|
|
156
|
+
try {
|
|
157
|
+
const response = await plankaRequest(`/api/projects/${options.projectId}/boards`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: {
|
|
160
|
+
name: options.name,
|
|
161
|
+
position: options.position,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const parsedResponse = BoardResponseSchema.parse(response);
|
|
165
|
+
const board = parsedResponse.item;
|
|
166
|
+
// Add the admin user as a board member
|
|
167
|
+
try {
|
|
168
|
+
const adminUserId = await getAdminUserId();
|
|
169
|
+
if (adminUserId) {
|
|
170
|
+
await boardMemberships.createBoardMembership({
|
|
171
|
+
boardId: board.id,
|
|
172
|
+
userId: adminUserId,
|
|
173
|
+
role: "editor",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.error("Could not add admin user as board member: Admin user ID not found");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error("Error adding admin user as board member:", error);
|
|
182
|
+
}
|
|
183
|
+
// Create default lists and labels
|
|
184
|
+
await createDefaultLists(board.id);
|
|
185
|
+
await createDefaultLabels(board.id);
|
|
186
|
+
return board;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw new Error(`Failed to create board: ${error instanceof Error ? error.message : String(error)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Retrieves all boards for a specific project
|
|
194
|
+
*
|
|
195
|
+
* @param {string} projectId - The ID of the project to get boards from
|
|
196
|
+
* @returns {Promise<Array<object>>} Array of boards in the project
|
|
197
|
+
* @throws {Error} If retrieving boards fails
|
|
198
|
+
*/
|
|
199
|
+
export async function getBoards(projectId) {
|
|
200
|
+
try {
|
|
201
|
+
// Get all projects which includes boards in the response
|
|
202
|
+
const response = await plankaRequest(`/api/projects`);
|
|
203
|
+
// Check if the response has the expected structure
|
|
204
|
+
if (response &&
|
|
205
|
+
typeof response === "object" &&
|
|
206
|
+
"included" in response &&
|
|
207
|
+
response.included &&
|
|
208
|
+
typeof response.included === "object" &&
|
|
209
|
+
"boards" in response.included) {
|
|
210
|
+
// Filter boards by projectId
|
|
211
|
+
const allBoards = response.included.boards;
|
|
212
|
+
if (Array.isArray(allBoards)) {
|
|
213
|
+
const filteredBoards = allBoards.filter((board) => typeof board === "object" &&
|
|
214
|
+
board !== null &&
|
|
215
|
+
"projectId" in board &&
|
|
216
|
+
board.projectId === projectId);
|
|
217
|
+
return filteredBoards;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// If we can't find boards in the expected format, return an empty array
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
// If all else fails, return an empty array
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Retrieves a specific board by ID
|
|
230
|
+
*
|
|
231
|
+
* @param {string} id - The ID of the board to retrieve
|
|
232
|
+
* @returns {Promise<object>} The requested board
|
|
233
|
+
* @throws {Error} If retrieving the board fails
|
|
234
|
+
*/
|
|
235
|
+
export async function getBoard(id) {
|
|
236
|
+
const response = await plankaRequest(`/api/boards/${id}`);
|
|
237
|
+
const parsedResponse = BoardResponseSchema.parse(response);
|
|
238
|
+
return parsedResponse.item;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Updates a board's properties
|
|
242
|
+
*
|
|
243
|
+
* @param {string} id - The ID of the board to update
|
|
244
|
+
* @param {Partial<Omit<CreateBoardOptions, "projectId">>} options - The properties to update
|
|
245
|
+
* @returns {Promise<object>} The updated board
|
|
246
|
+
* @throws {Error} If updating the board fails
|
|
247
|
+
*/
|
|
248
|
+
export async function updateBoard(id, options) {
|
|
249
|
+
const response = await plankaRequest(`/api/boards/${id}`, {
|
|
250
|
+
method: "PATCH",
|
|
251
|
+
body: options,
|
|
252
|
+
});
|
|
253
|
+
const parsedResponse = BoardResponseSchema.parse(response);
|
|
254
|
+
return parsedResponse.item;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Deletes a board by ID
|
|
258
|
+
*
|
|
259
|
+
* @param {string} id - The ID of the board to delete
|
|
260
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
261
|
+
* @throws {Error} If deleting the board fails
|
|
262
|
+
*/
|
|
263
|
+
export async function deleteBoard(id) {
|
|
264
|
+
await plankaRequest(`/api/boards/${id}`, {
|
|
265
|
+
method: "DELETE",
|
|
266
|
+
});
|
|
267
|
+
return { success: true };
|
|
268
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Card operations for the MCP Kanban server
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions for interacting with cards in the Planka Kanban board,
|
|
5
|
+
* including creating, retrieving, updating, moving, duplicating, and deleting cards,
|
|
6
|
+
* as well as managing card stopwatches for time tracking.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { plankaRequest } from "../common/utils.js";
|
|
10
|
+
import { PlankaCardSchema } from "../common/types.js";
|
|
11
|
+
// Schema definitions
|
|
12
|
+
/**
|
|
13
|
+
* Schema for creating a new card
|
|
14
|
+
* @property {string} listId - The ID of the list to create the card in
|
|
15
|
+
* @property {string} name - The name of the card
|
|
16
|
+
* @property {string} [description] - The description of the card
|
|
17
|
+
* @property {number} [position] - The position of the card in the list (default: 65535)
|
|
18
|
+
*/
|
|
19
|
+
export const CreateCardSchema = z.object({
|
|
20
|
+
listId: z.string().describe("List ID"),
|
|
21
|
+
name: z.string().describe("Card name"),
|
|
22
|
+
description: z.string().optional().describe("Card description"),
|
|
23
|
+
position: z.number().optional().describe("Card position (default: 65535)"),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Schema for retrieving cards from a list
|
|
27
|
+
* @property {string} listId - The ID of the list to get cards from
|
|
28
|
+
*/
|
|
29
|
+
export const GetCardsSchema = z.object({
|
|
30
|
+
listId: z.string().describe("List ID"),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Schema for retrieving a specific card
|
|
34
|
+
* @property {string} id - The ID of the card to retrieve
|
|
35
|
+
*/
|
|
36
|
+
export const GetCardSchema = z.object({
|
|
37
|
+
id: z.string().describe("Card ID"),
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Schema for updating a card
|
|
41
|
+
* @property {string} id - The ID of the card to update
|
|
42
|
+
* @property {string} [name] - The new name for the card
|
|
43
|
+
* @property {string} [description] - The new description for the card
|
|
44
|
+
* @property {number} [position] - The new position for the card
|
|
45
|
+
* @property {string} [dueDate] - The due date for the card (ISO format)
|
|
46
|
+
* @property {boolean} [isCompleted] - Whether the card is completed
|
|
47
|
+
*/
|
|
48
|
+
export const UpdateCardSchema = z.object({
|
|
49
|
+
id: z.string().describe("Card ID"),
|
|
50
|
+
name: z.string().optional().describe("Card name"),
|
|
51
|
+
description: z.string().optional().describe("Card description"),
|
|
52
|
+
position: z.number().optional().describe("Card position"),
|
|
53
|
+
dueDate: z.string().optional().describe("Card due date (ISO format)"),
|
|
54
|
+
isCompleted: z.boolean().optional().describe("Whether the card is completed"),
|
|
55
|
+
});
|
|
56
|
+
export const MoveCardSchema = z.object({
|
|
57
|
+
id: z.string().describe("Card ID"),
|
|
58
|
+
listId: z.string().describe("Target list ID"),
|
|
59
|
+
position: z.number().optional().describe("Card position in the target list (default: 65535)"),
|
|
60
|
+
});
|
|
61
|
+
export const DuplicateCardSchema = z.object({
|
|
62
|
+
id: z.string().describe("Card ID to duplicate"),
|
|
63
|
+
position: z.number().optional().describe("Position for the duplicated card (default: 65535)"),
|
|
64
|
+
});
|
|
65
|
+
export const DeleteCardSchema = z.object({
|
|
66
|
+
id: z.string().describe("Card ID"),
|
|
67
|
+
});
|
|
68
|
+
// Stopwatch schemas
|
|
69
|
+
export const StartCardStopwatchSchema = z.object({
|
|
70
|
+
id: z.string().describe("Card ID"),
|
|
71
|
+
});
|
|
72
|
+
export const StopCardStopwatchSchema = z.object({
|
|
73
|
+
id: z.string().describe("Card ID"),
|
|
74
|
+
});
|
|
75
|
+
export const GetCardStopwatchSchema = z.object({
|
|
76
|
+
id: z.string().describe("Card ID"),
|
|
77
|
+
});
|
|
78
|
+
export const ResetCardStopwatchSchema = z.object({
|
|
79
|
+
id: z.string().describe("Card ID"),
|
|
80
|
+
});
|
|
81
|
+
// Response schemas
|
|
82
|
+
const CardsResponseSchema = z.object({
|
|
83
|
+
items: z.array(PlankaCardSchema),
|
|
84
|
+
included: z.record(z.any()).optional(),
|
|
85
|
+
});
|
|
86
|
+
const CardResponseSchema = z.object({
|
|
87
|
+
item: PlankaCardSchema,
|
|
88
|
+
included: z.record(z.any()).optional(),
|
|
89
|
+
});
|
|
90
|
+
// Function implementations
|
|
91
|
+
/**
|
|
92
|
+
* Creates a new card in a list
|
|
93
|
+
*
|
|
94
|
+
* @param {CreateCardOptions} options - Options for creating the card
|
|
95
|
+
* @param {string} options.listId - The ID of the list to create the card in
|
|
96
|
+
* @param {string} options.name - The name of the card
|
|
97
|
+
* @param {string} [options.description] - The description of the card
|
|
98
|
+
* @param {number} [options.position] - The position of the card in the list (default: 65535)
|
|
99
|
+
* @returns {Promise<object>} The created card
|
|
100
|
+
* @throws {Error} If the card creation fails
|
|
101
|
+
*/
|
|
102
|
+
export async function createCard(options) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await plankaRequest(`/api/lists/${options.listId}/cards`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: {
|
|
107
|
+
name: options.name,
|
|
108
|
+
description: options.description,
|
|
109
|
+
position: options.position,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
113
|
+
return parsedResponse.item;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new Error(`Failed to create card: ${error instanceof Error ? error.message : String(error)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Retrieves all cards for a specific list
|
|
121
|
+
*
|
|
122
|
+
* @param {string} listId - The ID of the list to get cards from
|
|
123
|
+
* @returns {Promise<Array<object>>} Array of cards in the list
|
|
124
|
+
*/
|
|
125
|
+
export async function getCards(listId) {
|
|
126
|
+
try {
|
|
127
|
+
// Get all projects which includes boards
|
|
128
|
+
const projectsResponse = await plankaRequest(`/api/projects`);
|
|
129
|
+
if (!projectsResponse ||
|
|
130
|
+
typeof projectsResponse !== "object" ||
|
|
131
|
+
!("included" in projectsResponse) ||
|
|
132
|
+
!projectsResponse.included ||
|
|
133
|
+
typeof projectsResponse.included !== "object") {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
const included = projectsResponse.included;
|
|
137
|
+
// Get all boards
|
|
138
|
+
if (!("boards" in included) || !Array.isArray(included.boards)) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const boards = included.boards;
|
|
142
|
+
// Check each board for cards with the matching list ID
|
|
143
|
+
for (const board of boards) {
|
|
144
|
+
if (typeof board !== "object" || board === null || !("id" in board)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const boardId = board.id;
|
|
148
|
+
// Get the board details which includes cards
|
|
149
|
+
const boardResponse = await plankaRequest(`/api/boards/${boardId}`);
|
|
150
|
+
if (!boardResponse ||
|
|
151
|
+
typeof boardResponse !== "object" ||
|
|
152
|
+
!("included" in boardResponse) ||
|
|
153
|
+
!boardResponse.included ||
|
|
154
|
+
typeof boardResponse.included !== "object") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const boardIncluded = boardResponse.included;
|
|
158
|
+
if (!("cards" in boardIncluded) ||
|
|
159
|
+
!Array.isArray(boardIncluded.cards)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const cards = boardIncluded.cards;
|
|
163
|
+
// Filter cards by list ID
|
|
164
|
+
const matchingCards = cards.filter((card) => typeof card === "object" &&
|
|
165
|
+
card !== null &&
|
|
166
|
+
"listId" in card &&
|
|
167
|
+
card.listId === listId);
|
|
168
|
+
if (matchingCards.length > 0) {
|
|
169
|
+
return matchingCards;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// If we couldn't find any cards for this list ID
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
// If all else fails, return an empty array
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Retrieves a specific card by ID
|
|
182
|
+
*
|
|
183
|
+
* @param {string} id - The ID of the card to retrieve
|
|
184
|
+
* @returns {Promise<object>} The requested card
|
|
185
|
+
*/
|
|
186
|
+
export async function getCard(id) {
|
|
187
|
+
const response = await plankaRequest(`/api/cards/${id}`);
|
|
188
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
189
|
+
return parsedResponse.item;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Updates a card's properties
|
|
193
|
+
*
|
|
194
|
+
* @param {string} id - The ID of the card to update
|
|
195
|
+
* @param {Partial<Omit<CreateCardOptions, "listId">>} options - The properties to update
|
|
196
|
+
* @returns {Promise<object>} The updated card
|
|
197
|
+
*/
|
|
198
|
+
export async function updateCard(id, options) {
|
|
199
|
+
const response = await plankaRequest(`/api/cards/${id}`, {
|
|
200
|
+
method: "PATCH",
|
|
201
|
+
body: options,
|
|
202
|
+
});
|
|
203
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
204
|
+
return parsedResponse.item;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Moves a card to a different list or position
|
|
208
|
+
*
|
|
209
|
+
* @param {string} cardId - The ID of the card to move
|
|
210
|
+
* @param {string} listId - The ID of the list to move the card to
|
|
211
|
+
* @param {number} [position=65535] - The position in the target list
|
|
212
|
+
* @param {string} [boardId] - The ID of the board (if moving between boards)
|
|
213
|
+
* @param {string} [projectId] - The ID of the project (if moving between projects)
|
|
214
|
+
* @returns {Promise<object>} The moved card
|
|
215
|
+
*/
|
|
216
|
+
export async function moveCard(cardId, listId, position = 65535, boardId, projectId) {
|
|
217
|
+
try {
|
|
218
|
+
// Use the PATCH endpoint to update the card with the new list ID and position
|
|
219
|
+
const response = await plankaRequest(`/api/cards/${cardId}`, {
|
|
220
|
+
method: "PATCH",
|
|
221
|
+
body: {
|
|
222
|
+
listId,
|
|
223
|
+
position,
|
|
224
|
+
boardId,
|
|
225
|
+
projectId,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
// Parse and return the updated card
|
|
229
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
230
|
+
return parsedResponse.item;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw new Error(`Failed to move card: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Duplicates a card in the same list
|
|
238
|
+
*
|
|
239
|
+
* @param {string} id - The ID of the card to duplicate
|
|
240
|
+
* @param {number} [position] - The position for the duplicated card
|
|
241
|
+
* @returns {Promise<object>} The duplicated card
|
|
242
|
+
*/
|
|
243
|
+
export async function duplicateCard(id, position) {
|
|
244
|
+
try {
|
|
245
|
+
// First, get the original card to access its name
|
|
246
|
+
const originalCard = await getCard(id);
|
|
247
|
+
// Create a new card with "Copy of" prefix
|
|
248
|
+
const cardName = originalCard ? `Copy of ${originalCard.name}` : "";
|
|
249
|
+
// Get the list ID from the original card
|
|
250
|
+
const listId = originalCard ? originalCard.listId : "";
|
|
251
|
+
if (!listId) {
|
|
252
|
+
throw new Error("Could not determine list ID for card duplication");
|
|
253
|
+
}
|
|
254
|
+
// Create a new card with the same properties but with "Copy of" prefix
|
|
255
|
+
const newCard = await createCard({
|
|
256
|
+
listId,
|
|
257
|
+
name: cardName,
|
|
258
|
+
description: originalCard.description || "",
|
|
259
|
+
position: position || 65535,
|
|
260
|
+
});
|
|
261
|
+
return newCard;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
throw new Error(`Failed to duplicate card: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Deletes a card by ID
|
|
269
|
+
*
|
|
270
|
+
* @param {string} id - The ID of the card to delete
|
|
271
|
+
* @returns {Promise<{success: boolean}>} Success indicator
|
|
272
|
+
*/
|
|
273
|
+
export async function deleteCard(id) {
|
|
274
|
+
await plankaRequest(`/api/cards/${id}`, {
|
|
275
|
+
method: "DELETE",
|
|
276
|
+
});
|
|
277
|
+
return { success: true };
|
|
278
|
+
}
|
|
279
|
+
// Stopwatch functions
|
|
280
|
+
/**
|
|
281
|
+
* Starts the stopwatch for a card to track time spent
|
|
282
|
+
*
|
|
283
|
+
* @param {string} id - The ID of the card to start the stopwatch for
|
|
284
|
+
* @returns {Promise<object>} The updated card with stopwatch information
|
|
285
|
+
*/
|
|
286
|
+
export async function startCardStopwatch(id) {
|
|
287
|
+
try {
|
|
288
|
+
// Get the current card to check if a stopwatch is already running
|
|
289
|
+
const card = await getCard(id);
|
|
290
|
+
// Calculate the stopwatch object
|
|
291
|
+
let stopwatch = {
|
|
292
|
+
startedAt: new Date().toISOString(),
|
|
293
|
+
total: 0,
|
|
294
|
+
};
|
|
295
|
+
// If there's an existing stopwatch, preserve the total time
|
|
296
|
+
if (card.stopwatch && card.stopwatch.total) {
|
|
297
|
+
stopwatch.total = card.stopwatch.total;
|
|
298
|
+
}
|
|
299
|
+
// Update the card with the new stopwatch
|
|
300
|
+
const response = await plankaRequest(`/api/cards/${id}`, {
|
|
301
|
+
method: "PATCH",
|
|
302
|
+
body: { stopwatch },
|
|
303
|
+
});
|
|
304
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
305
|
+
return parsedResponse.item;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
throw new Error(`Failed to start card stopwatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Stops the stopwatch for a card
|
|
313
|
+
*
|
|
314
|
+
* @param {string} id - The ID of the card to stop the stopwatch for
|
|
315
|
+
* @returns {Promise<object>} The updated card with stopwatch information
|
|
316
|
+
*/
|
|
317
|
+
export async function stopCardStopwatch(id) {
|
|
318
|
+
try {
|
|
319
|
+
// Get the current card to calculate elapsed time
|
|
320
|
+
const card = await getCard(id);
|
|
321
|
+
// If there's no stopwatch or it's not running, return the card as is
|
|
322
|
+
if (!card.stopwatch || !card.stopwatch.startedAt) {
|
|
323
|
+
return card;
|
|
324
|
+
}
|
|
325
|
+
// Calculate elapsed time
|
|
326
|
+
const startedAt = new Date(card.stopwatch.startedAt);
|
|
327
|
+
const now = new Date();
|
|
328
|
+
const elapsedSeconds = Math.floor((now.getTime() - startedAt.getTime()) / 1000);
|
|
329
|
+
// Calculate the new total time
|
|
330
|
+
const totalSeconds = (card.stopwatch.total || 0) + elapsedSeconds;
|
|
331
|
+
// Update the card with the stopped stopwatch (null startedAt but preserved total)
|
|
332
|
+
const stopwatch = {
|
|
333
|
+
startedAt: null,
|
|
334
|
+
total: totalSeconds,
|
|
335
|
+
};
|
|
336
|
+
const response = await plankaRequest(`/api/cards/${id}`, {
|
|
337
|
+
method: "PATCH",
|
|
338
|
+
body: { stopwatch },
|
|
339
|
+
});
|
|
340
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
341
|
+
return parsedResponse.item;
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
throw new Error(`Failed to stop card stopwatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Gets the current stopwatch time for a card
|
|
349
|
+
*
|
|
350
|
+
* @param {string} id - The ID of the card to get the stopwatch time for
|
|
351
|
+
* @returns {Promise<object>} The card's stopwatch information
|
|
352
|
+
*/
|
|
353
|
+
export async function getCardStopwatch(id) {
|
|
354
|
+
try {
|
|
355
|
+
const card = await getCard(id);
|
|
356
|
+
// If there's no stopwatch, return default values
|
|
357
|
+
if (!card.stopwatch) {
|
|
358
|
+
return {
|
|
359
|
+
isRunning: false,
|
|
360
|
+
total: 0,
|
|
361
|
+
current: 0,
|
|
362
|
+
formattedTotal: formatDuration(0),
|
|
363
|
+
formattedCurrent: formatDuration(0),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
// Calculate current elapsed time if stopwatch is running
|
|
367
|
+
let currentElapsed = 0;
|
|
368
|
+
const isRunning = !!card.stopwatch.startedAt;
|
|
369
|
+
if (isRunning && card.stopwatch.startedAt) {
|
|
370
|
+
const startedAt = new Date(card.stopwatch.startedAt);
|
|
371
|
+
const now = new Date();
|
|
372
|
+
currentElapsed = Math.floor((now.getTime() - startedAt.getTime()) / 1000);
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
isRunning,
|
|
376
|
+
total: card.stopwatch.total || 0,
|
|
377
|
+
current: currentElapsed,
|
|
378
|
+
startedAt: card.stopwatch.startedAt,
|
|
379
|
+
formattedTotal: formatDuration(card.stopwatch.total || 0),
|
|
380
|
+
formattedCurrent: formatDuration(currentElapsed),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
throw new Error(`Failed to get card stopwatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Resets the stopwatch for a card
|
|
389
|
+
*
|
|
390
|
+
* @param {string} id - The ID of the card to reset the stopwatch for
|
|
391
|
+
* @returns {Promise<object>} The updated card with reset stopwatch
|
|
392
|
+
*/
|
|
393
|
+
export async function resetCardStopwatch(id) {
|
|
394
|
+
try {
|
|
395
|
+
// Set stopwatch to null to clear it
|
|
396
|
+
const response = await plankaRequest(`/api/cards/${id}`, {
|
|
397
|
+
method: "PATCH",
|
|
398
|
+
body: { stopwatch: null },
|
|
399
|
+
});
|
|
400
|
+
const parsedResponse = CardResponseSchema.parse(response);
|
|
401
|
+
return parsedResponse.item;
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
throw new Error(`Failed to reset card stopwatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Formats a duration in seconds to a human-readable string
|
|
409
|
+
*
|
|
410
|
+
* @param {number} seconds - The duration in seconds
|
|
411
|
+
* @returns {string} Formatted duration string (e.g., "2h 30m 15s")
|
|
412
|
+
*/
|
|
413
|
+
function formatDuration(seconds) {
|
|
414
|
+
const hours = Math.floor(seconds / 3600);
|
|
415
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
416
|
+
const remainingSeconds = seconds % 60;
|
|
417
|
+
let result = "";
|
|
418
|
+
if (hours > 0) {
|
|
419
|
+
result += `${hours}h `;
|
|
420
|
+
}
|
|
421
|
+
if (minutes > 0 || hours > 0) {
|
|
422
|
+
result += `${minutes}m `;
|
|
423
|
+
}
|
|
424
|
+
result += `${remainingSeconds}s`;
|
|
425
|
+
return result.trim();
|
|
426
|
+
}
|