motionmcp 1.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/LICENSE +202 -0
- package/README.md +241 -450
- package/dist/handlers/CommentHandler.d.ts +9 -0
- package/dist/handlers/CommentHandler.d.ts.map +1 -0
- package/dist/handlers/CommentHandler.js +66 -0
- package/dist/handlers/CommentHandler.js.map +1 -0
- package/dist/handlers/CustomFieldHandler.d.ts +14 -0
- package/dist/handlers/CustomFieldHandler.d.ts.map +1 -0
- package/dist/handlers/CustomFieldHandler.js +95 -0
- package/dist/handlers/CustomFieldHandler.js.map +1 -0
- package/dist/handlers/HandlerFactory.d.ts +15 -0
- package/dist/handlers/HandlerFactory.d.ts.map +1 -0
- package/dist/handlers/HandlerFactory.js +58 -0
- package/dist/handlers/HandlerFactory.js.map +1 -0
- package/dist/handlers/ProjectHandler.d.ts +10 -0
- package/dist/handlers/ProjectHandler.d.ts.map +1 -0
- package/dist/handlers/ProjectHandler.js +63 -0
- package/dist/handlers/ProjectHandler.js.map +1 -0
- package/dist/handlers/RecurringTaskHandler.d.ts +10 -0
- package/dist/handlers/RecurringTaskHandler.d.ts.map +1 -0
- package/dist/handlers/RecurringTaskHandler.js +68 -0
- package/dist/handlers/RecurringTaskHandler.js.map +1 -0
- package/dist/handlers/ScheduleHandler.d.ts +8 -0
- package/dist/handlers/ScheduleHandler.d.ts.map +1 -0
- package/dist/handlers/ScheduleHandler.js +43 -0
- package/dist/handlers/ScheduleHandler.js.map +1 -0
- package/dist/handlers/SearchHandler.d.ts +10 -0
- package/dist/handlers/SearchHandler.d.ts.map +1 -0
- package/dist/handlers/SearchHandler.js +116 -0
- package/dist/handlers/SearchHandler.js.map +1 -0
- package/dist/handlers/StatusHandler.d.ts +8 -0
- package/dist/handlers/StatusHandler.d.ts.map +1 -0
- package/dist/handlers/StatusHandler.js +22 -0
- package/dist/handlers/StatusHandler.js.map +1 -0
- package/dist/handlers/TaskHandler.d.ts +22 -0
- package/dist/handlers/TaskHandler.d.ts.map +1 -0
- package/dist/handlers/TaskHandler.js +324 -0
- package/dist/handlers/TaskHandler.js.map +1 -0
- package/dist/handlers/UserHandler.d.ts +9 -0
- package/dist/handlers/UserHandler.d.ts.map +1 -0
- package/dist/handlers/UserHandler.js +36 -0
- package/dist/handlers/UserHandler.js.map +1 -0
- package/dist/handlers/WorkspaceHandler.d.ts +10 -0
- package/dist/handlers/WorkspaceHandler.d.ts.map +1 -0
- package/dist/handlers/WorkspaceHandler.js +49 -0
- package/dist/handlers/WorkspaceHandler.js.map +1 -0
- package/dist/handlers/base/BaseHandler.d.ts +16 -0
- package/dist/handlers/base/BaseHandler.d.ts.map +1 -0
- package/dist/handlers/base/BaseHandler.js +31 -0
- package/dist/handlers/base/BaseHandler.js.map +1 -0
- package/dist/handlers/base/HandlerInterface.d.ts +18 -0
- package/dist/handlers/base/HandlerInterface.d.ts.map +1 -0
- package/dist/handlers/base/HandlerInterface.js +3 -0
- package/dist/handlers/base/HandlerInterface.js.map +1 -0
- package/dist/handlers/index.d.ts +14 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +31 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/mcp-server.d.ts +15 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +145 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/schemas/motion.d.ts +4971 -0
- package/dist/schemas/motion.d.ts.map +1 -0
- package/dist/schemas/motion.js +328 -0
- package/dist/schemas/motion.js.map +1 -0
- package/dist/services/motionApi.d.ts +199 -0
- package/dist/services/motionApi.d.ts.map +1 -0
- package/dist/services/motionApi.js +1950 -0
- package/dist/services/motionApi.js.map +1 -0
- package/dist/tools/ToolConfigurator.d.ts +19 -0
- package/dist/tools/ToolConfigurator.d.ts.map +1 -0
- package/dist/tools/ToolConfigurator.js +89 -0
- package/dist/tools/ToolConfigurator.js.map +1 -0
- package/dist/tools/ToolDefinitions.d.ts +25 -0
- package/dist/tools/ToolDefinitions.d.ts.map +1 -0
- package/dist/tools/ToolDefinitions.js +508 -0
- package/dist/tools/ToolDefinitions.js.map +1 -0
- package/dist/tools/ToolRegistry.d.ts +16 -0
- package/dist/tools/ToolRegistry.d.ts.map +1 -0
- package/dist/tools/ToolRegistry.js +89 -0
- package/dist/tools/ToolRegistry.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +21 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/mcp-tool-args.d.ts +123 -0
- package/dist/types/mcp-tool-args.d.ts.map +1 -0
- package/dist/types/mcp-tool-args.js +7 -0
- package/dist/types/mcp-tool-args.js.map +1 -0
- package/dist/types/mcp.d.ts +32 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +3 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/motion.d.ts +304 -0
- package/dist/types/motion.d.ts.map +1 -0
- package/dist/types/motion.js +3 -0
- package/dist/types/motion.js.map +1 -0
- package/dist/utils/cache.d.ts +25 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +135 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/constants.d.ts +88 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +188 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/errorHandling.d.ts +50 -0
- package/dist/utils/errorHandling.d.ts.map +1 -0
- package/dist/utils/errorHandling.js +86 -0
- package/dist/utils/errorHandling.js.map +1 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +38 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +47 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/pagination.d.ts +61 -0
- package/dist/utils/pagination.d.ts.map +1 -0
- package/dist/utils/pagination.js +168 -0
- package/dist/utils/pagination.js.map +1 -0
- package/dist/utils/paginationNew.d.ts +44 -0
- package/dist/utils/paginationNew.d.ts.map +1 -0
- package/dist/utils/paginationNew.js +149 -0
- package/dist/utils/paginationNew.js.map +1 -0
- package/dist/utils/parameterUtils.d.ts +79 -0
- package/dist/utils/parameterUtils.d.ts.map +1 -0
- package/dist/utils/parameterUtils.js +189 -0
- package/dist/utils/parameterUtils.js.map +1 -0
- package/dist/utils/responseFormatters.d.ts +95 -0
- package/dist/utils/responseFormatters.d.ts.map +1 -0
- package/dist/utils/responseFormatters.js +342 -0
- package/dist/utils/responseFormatters.js.map +1 -0
- package/dist/utils/responseWrapper.d.ts +38 -0
- package/dist/utils/responseWrapper.d.ts.map +1 -0
- package/dist/utils/responseWrapper.js +201 -0
- package/dist/utils/responseWrapper.js.map +1 -0
- package/dist/utils/sanitize.d.ts +51 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +138 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/validator.d.ts +37 -0
- package/dist/utils/validator.d.ts.map +1 -0
- package/dist/utils/validator.js +74 -0
- package/dist/utils/validator.js.map +1 -0
- package/dist/utils/workspaceResolver.d.ts +40 -0
- package/dist/utils/workspaceResolver.d.ts.map +1 -0
- package/dist/utils/workspaceResolver.js +207 -0
- package/dist/utils/workspaceResolver.js.map +1 -0
- package/package.json +41 -17
- package/.claude/settings.local.json +0 -15
- package/.env.example +0 -3
- package/sample.png +0 -0
- package/src/index.js +0 -179
- package/src/mcp-server.js +0 -1137
- package/src/routes/motion.js +0 -152
- package/src/services/motionApi.js +0 -1177
- package/src/worker.js +0 -248
|
@@ -0,0 +1,1950 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MotionApiService = void 0;
|
|
37
|
+
const axios_1 = __importStar(require("axios"));
|
|
38
|
+
const constants_1 = require("../utils/constants");
|
|
39
|
+
const logger_1 = require("../utils/logger");
|
|
40
|
+
const cache_1 = require("../utils/cache");
|
|
41
|
+
const paginationNew_1 = require("../utils/paginationNew");
|
|
42
|
+
const responseWrapper_1 = require("../utils/responseWrapper");
|
|
43
|
+
const zod_1 = require("zod");
|
|
44
|
+
const motion_1 = require("../schemas/motion");
|
|
45
|
+
// Note: Using native axios.isAxiosError instead of custom implementation
|
|
46
|
+
// Helper to get error message
|
|
47
|
+
function getErrorMessage(error) {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
return error.message;
|
|
50
|
+
}
|
|
51
|
+
return String(error);
|
|
52
|
+
}
|
|
53
|
+
class MotionApiService {
|
|
54
|
+
/**
|
|
55
|
+
* Validate API response against schema
|
|
56
|
+
* Handles strict/lenient/off modes based on configuration
|
|
57
|
+
*/
|
|
58
|
+
validateResponse(data, schema, context) {
|
|
59
|
+
if (motion_1.VALIDATION_CONFIG.mode === 'off') {
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return schema.parse(data);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof zod_1.z.ZodError) {
|
|
67
|
+
const errorDetails = {
|
|
68
|
+
context,
|
|
69
|
+
validationErrors: error.errors,
|
|
70
|
+
...(motion_1.VALIDATION_CONFIG.includeDataInLogs ? { receivedData: data } : {})
|
|
71
|
+
};
|
|
72
|
+
if (motion_1.VALIDATION_CONFIG.logErrors) {
|
|
73
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `API response validation failed for ${context}`, errorDetails);
|
|
74
|
+
}
|
|
75
|
+
if (motion_1.VALIDATION_CONFIG.mode === 'strict') {
|
|
76
|
+
throw new Error(`Invalid API response structure for ${context}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
// Lenient mode: return original data and hope for the best
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
constructor() {
|
|
85
|
+
const apiKey = process.env.MOTION_API_KEY;
|
|
86
|
+
if (!apiKey) {
|
|
87
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API key not found in environment variables', {
|
|
88
|
+
component: 'MotionApiService',
|
|
89
|
+
method: 'constructor'
|
|
90
|
+
});
|
|
91
|
+
throw new Error('MOTION_API_KEY environment variable is required');
|
|
92
|
+
}
|
|
93
|
+
this.apiKey = apiKey;
|
|
94
|
+
this.baseUrl = 'https://api.usemotion.com/v1';
|
|
95
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Initializing Motion API service', {
|
|
96
|
+
component: 'MotionApiService',
|
|
97
|
+
baseUrl: this.baseUrl
|
|
98
|
+
});
|
|
99
|
+
this.client = axios_1.default.create({
|
|
100
|
+
baseURL: this.baseUrl,
|
|
101
|
+
headers: {
|
|
102
|
+
'X-API-Key': this.apiKey,
|
|
103
|
+
'Content-Type': 'application/json'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// Initialize cache instances with TTL from constants (converted to ms)
|
|
107
|
+
this.workspaceCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES);
|
|
108
|
+
this.userCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.USERS);
|
|
109
|
+
this.projectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
|
|
110
|
+
this.singleProjectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
|
|
111
|
+
this.commentCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.COMMENTS);
|
|
112
|
+
this.customFieldCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.CUSTOM_FIELDS);
|
|
113
|
+
this.recurringTaskCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.RECURRING_TASKS);
|
|
114
|
+
this.scheduleCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.SCHEDULES);
|
|
115
|
+
this.statusCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES); // 10 minutes, like workspaces
|
|
116
|
+
this.client.interceptors.response.use((response) => {
|
|
117
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Motion API response successful', {
|
|
118
|
+
url: response.config?.url,
|
|
119
|
+
method: response.config?.method?.toUpperCase(),
|
|
120
|
+
status: response.status,
|
|
121
|
+
component: 'MotionApiService'
|
|
122
|
+
});
|
|
123
|
+
return response;
|
|
124
|
+
}, (error) => {
|
|
125
|
+
const errorData = error.response?.data;
|
|
126
|
+
const errorDetails = {
|
|
127
|
+
url: error.config?.url,
|
|
128
|
+
method: error.config?.method?.toUpperCase(),
|
|
129
|
+
status: error.response?.status,
|
|
130
|
+
statusText: error.response?.statusText,
|
|
131
|
+
apiMessage: errorData?.message,
|
|
132
|
+
apiCode: errorData?.code,
|
|
133
|
+
errorMessage: error.message,
|
|
134
|
+
component: 'MotionApiService'
|
|
135
|
+
};
|
|
136
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API request failed', errorDetails);
|
|
137
|
+
// Create typed error for better handling
|
|
138
|
+
const typedError = new Error(errorData?.message || error.message);
|
|
139
|
+
typedError.response = error.response;
|
|
140
|
+
throw typedError;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Formats API errors consistently across all methods
|
|
145
|
+
* @param error - The error that occurred
|
|
146
|
+
* @param action - Description of the action that failed (e.g., 'fetch projects')
|
|
147
|
+
* @returns Formatted Error object
|
|
148
|
+
*/
|
|
149
|
+
formatApiError(error, action) {
|
|
150
|
+
const baseMessage = `Failed to ${action}`;
|
|
151
|
+
const apiMessage = (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined;
|
|
152
|
+
const errorMessage = getErrorMessage(error);
|
|
153
|
+
return new Error(`${baseMessage}: ${apiMessage || errorMessage}`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Wraps an axios request with a retry mechanism featuring exponential backoff.
|
|
157
|
+
* Only retries on 5xx server errors or 429 rate-limiting errors.
|
|
158
|
+
*/
|
|
159
|
+
async requestWithRetry(request) {
|
|
160
|
+
for (let attempt = 1; attempt <= constants_1.RETRY_CONFIG.MAX_RETRIES; attempt++) {
|
|
161
|
+
try {
|
|
162
|
+
return await request();
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (!axios_1.default.isAxiosError(error)) {
|
|
166
|
+
throw error; // Not a network error, re-throw immediately
|
|
167
|
+
}
|
|
168
|
+
const status = error.response?.status;
|
|
169
|
+
const isRetryable = (status && status >= 500) || status === 429;
|
|
170
|
+
if (!isRetryable || attempt === constants_1.RETRY_CONFIG.MAX_RETRIES) {
|
|
171
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `Request failed and will not be retried`, {
|
|
172
|
+
status,
|
|
173
|
+
attempt,
|
|
174
|
+
maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
|
|
175
|
+
isRetryable,
|
|
176
|
+
component: 'MotionApiService',
|
|
177
|
+
method: 'requestWithRetry'
|
|
178
|
+
});
|
|
179
|
+
throw error; // Final attempt failed or error is not retryable
|
|
180
|
+
}
|
|
181
|
+
// Handle Retry-After header for 429
|
|
182
|
+
const retryAfterHeader = error.response?.headers['retry-after'];
|
|
183
|
+
let delay = 0;
|
|
184
|
+
if (status === 429 && retryAfterHeader) {
|
|
185
|
+
const retryAfterSeconds = parseInt(retryAfterHeader, 10);
|
|
186
|
+
if (!isNaN(retryAfterSeconds)) {
|
|
187
|
+
delay = retryAfterSeconds * 1000;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// If no Retry-After header, use exponential backoff with jitter
|
|
191
|
+
if (delay === 0) {
|
|
192
|
+
const backoff = constants_1.RETRY_CONFIG.INITIAL_BACKOFF_MS * Math.pow(constants_1.RETRY_CONFIG.BACKOFF_MULTIPLIER, attempt - 1);
|
|
193
|
+
const jitter = backoff * constants_1.RETRY_CONFIG.JITTER_FACTOR * Math.random();
|
|
194
|
+
delay = Math.min(backoff + jitter, constants_1.RETRY_CONFIG.MAX_BACKOFF_MS);
|
|
195
|
+
}
|
|
196
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, `Request failed, retrying`, {
|
|
197
|
+
attempt,
|
|
198
|
+
maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
|
|
199
|
+
delayMs: Math.round(delay),
|
|
200
|
+
error: error.message,
|
|
201
|
+
status,
|
|
202
|
+
component: 'MotionApiService',
|
|
203
|
+
method: 'requestWithRetry'
|
|
204
|
+
});
|
|
205
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Should never reach here, but TypeScript requires a return or throw
|
|
209
|
+
throw new Error('Max retries exceeded');
|
|
210
|
+
}
|
|
211
|
+
async getProjects(workspaceId, maxPages = 5) {
|
|
212
|
+
const cacheKey = `projects:workspace:${workspaceId}`;
|
|
213
|
+
return this.projectCache.withCache(cacheKey, async () => {
|
|
214
|
+
try {
|
|
215
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
|
|
216
|
+
method: 'getProjects',
|
|
217
|
+
workspaceId,
|
|
218
|
+
maxPages
|
|
219
|
+
});
|
|
220
|
+
// Create a fetch function for potential pagination
|
|
221
|
+
const fetchPage = async (cursor) => {
|
|
222
|
+
const params = new URLSearchParams();
|
|
223
|
+
params.append('workspaceId', workspaceId);
|
|
224
|
+
if (cursor) {
|
|
225
|
+
params.append('cursor', cursor);
|
|
226
|
+
}
|
|
227
|
+
const queryString = params.toString();
|
|
228
|
+
const url = `/projects?${queryString}`;
|
|
229
|
+
return this.requestWithRetry(() => this.client.get(url));
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
// Attempt pagination-aware fetch with new response wrapper
|
|
233
|
+
const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
|
|
234
|
+
maxPages,
|
|
235
|
+
logProgress: false
|
|
236
|
+
});
|
|
237
|
+
if (paginatedResult.totalFetched > 0) {
|
|
238
|
+
let projects = paginatedResult.items;
|
|
239
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully with pagination', {
|
|
240
|
+
method: 'getProjects',
|
|
241
|
+
totalCount: projects.length,
|
|
242
|
+
hasMore: paginatedResult.hasMore,
|
|
243
|
+
workspaceId
|
|
244
|
+
});
|
|
245
|
+
return projects;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (paginationError) {
|
|
249
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
|
|
250
|
+
method: 'getProjects',
|
|
251
|
+
error: paginationError instanceof Error ? paginationError.message : String(paginationError)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// Use new response wrapper for single page fallback
|
|
255
|
+
const response = await fetchPage();
|
|
256
|
+
const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'projects');
|
|
257
|
+
let projects = unwrapped.data;
|
|
258
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully (single page)', {
|
|
259
|
+
method: 'getProjects',
|
|
260
|
+
count: projects.length,
|
|
261
|
+
workspaceId
|
|
262
|
+
});
|
|
263
|
+
return projects;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects', {
|
|
267
|
+
method: 'getProjects',
|
|
268
|
+
error: getErrorMessage(error),
|
|
269
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
270
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
271
|
+
});
|
|
272
|
+
throw this.formatApiError(error, 'fetch projects');
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async getAllProjects() {
|
|
277
|
+
try {
|
|
278
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from all workspaces', {
|
|
279
|
+
method: 'getAllProjects'
|
|
280
|
+
});
|
|
281
|
+
const allWorkspaces = await this.getWorkspaces();
|
|
282
|
+
const allProjects = [];
|
|
283
|
+
for (const workspace of allWorkspaces) {
|
|
284
|
+
try {
|
|
285
|
+
const workspaceProjects = await this.getProjects(workspace.id);
|
|
286
|
+
allProjects.push(...workspaceProjects);
|
|
287
|
+
}
|
|
288
|
+
catch (workspaceError) {
|
|
289
|
+
// Log error but continue with other workspaces
|
|
290
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch projects from workspace', {
|
|
291
|
+
method: 'getAllProjects',
|
|
292
|
+
workspaceId: workspace.id,
|
|
293
|
+
workspaceName: workspace.name,
|
|
294
|
+
error: getErrorMessage(workspaceError)
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All projects fetched successfully', {
|
|
299
|
+
method: 'getAllProjects',
|
|
300
|
+
totalProjects: allProjects.length,
|
|
301
|
+
workspaceCount: allWorkspaces.length
|
|
302
|
+
});
|
|
303
|
+
return allProjects;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects from all workspaces', {
|
|
307
|
+
method: 'getAllProjects',
|
|
308
|
+
error: getErrorMessage(error)
|
|
309
|
+
});
|
|
310
|
+
throw this.formatApiError(error, 'fetch all projects');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async getProject(projectId) {
|
|
314
|
+
const cacheKey = `project:${projectId}`;
|
|
315
|
+
return this.singleProjectCache.withCache(cacheKey, async () => {
|
|
316
|
+
try {
|
|
317
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single project from Motion API', {
|
|
318
|
+
method: 'getProject',
|
|
319
|
+
projectId
|
|
320
|
+
});
|
|
321
|
+
const response = await this.requestWithRetry(() => this.client.get(`/projects/${projectId}`));
|
|
322
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched project', {
|
|
323
|
+
method: 'getProject',
|
|
324
|
+
projectId,
|
|
325
|
+
projectName: response.data.name
|
|
326
|
+
});
|
|
327
|
+
return response.data;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch project', {
|
|
331
|
+
method: 'getProject',
|
|
332
|
+
projectId,
|
|
333
|
+
error: getErrorMessage(error),
|
|
334
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
335
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
336
|
+
});
|
|
337
|
+
throw this.formatApiError(error, 'fetch project');
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async createProject(projectData) {
|
|
342
|
+
try {
|
|
343
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating project in Motion API', {
|
|
344
|
+
method: 'createProject',
|
|
345
|
+
projectName: projectData.name,
|
|
346
|
+
workspaceId: projectData.workspaceId
|
|
347
|
+
});
|
|
348
|
+
if (!projectData.workspaceId) {
|
|
349
|
+
throw new Error('Workspace ID is required to create a project');
|
|
350
|
+
}
|
|
351
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
352
|
+
const minimalPayload = (0, constants_1.createMinimalPayload)(projectData);
|
|
353
|
+
// Debug logging: log the exact payload being sent to API
|
|
354
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for project creation', {
|
|
355
|
+
method: 'createProject',
|
|
356
|
+
payload: JSON.stringify(minimalPayload, null, 2)
|
|
357
|
+
});
|
|
358
|
+
const response = await this.requestWithRetry(() => this.client.post('/projects', minimalPayload));
|
|
359
|
+
// Invalidate cache after successful creation
|
|
360
|
+
this.projectCache.invalidate(`projects:workspace:${projectData.workspaceId}`);
|
|
361
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project created successfully', {
|
|
362
|
+
method: 'createProject',
|
|
363
|
+
projectId: response.data.id,
|
|
364
|
+
projectName: response.data.name
|
|
365
|
+
});
|
|
366
|
+
return response.data;
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create project', {
|
|
370
|
+
method: 'createProject',
|
|
371
|
+
error: getErrorMessage(error),
|
|
372
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
373
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
374
|
+
fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
|
|
375
|
+
});
|
|
376
|
+
throw this.formatApiError(error, 'create project');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async updateProject(projectId, updates) {
|
|
380
|
+
try {
|
|
381
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating project in Motion API', {
|
|
382
|
+
method: 'updateProject',
|
|
383
|
+
projectId,
|
|
384
|
+
updates: Object.keys(updates)
|
|
385
|
+
});
|
|
386
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
387
|
+
const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
|
|
388
|
+
const response = await this.requestWithRetry(() => this.client.patch(`/projects/${projectId}`, minimalUpdates));
|
|
389
|
+
// Invalidate cache after successful update
|
|
390
|
+
this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
|
|
391
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project updated successfully', {
|
|
392
|
+
method: 'updateProject',
|
|
393
|
+
projectId,
|
|
394
|
+
projectName: response.data.name
|
|
395
|
+
});
|
|
396
|
+
return response.data;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update project', {
|
|
400
|
+
method: 'updateProject',
|
|
401
|
+
projectId,
|
|
402
|
+
error: getErrorMessage(error),
|
|
403
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
404
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
405
|
+
});
|
|
406
|
+
throw this.formatApiError(error, 'update project');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async deleteProject(projectId) {
|
|
410
|
+
try {
|
|
411
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting project from Motion API', {
|
|
412
|
+
method: 'deleteProject',
|
|
413
|
+
projectId
|
|
414
|
+
});
|
|
415
|
+
await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}`));
|
|
416
|
+
// Invalidate all project caches since we don't know the workspace ID
|
|
417
|
+
this.projectCache.invalidate();
|
|
418
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project deleted successfully', {
|
|
419
|
+
method: 'deleteProject',
|
|
420
|
+
projectId
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete project', {
|
|
425
|
+
method: 'deleteProject',
|
|
426
|
+
projectId,
|
|
427
|
+
error: getErrorMessage(error),
|
|
428
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
429
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
430
|
+
});
|
|
431
|
+
throw this.formatApiError(error, 'delete project');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async getTasks(options) {
|
|
435
|
+
const { workspaceId, projectId, status, assigneeId, priority, dueDate, labels, limit, maxPages = 5 } = options;
|
|
436
|
+
try {
|
|
437
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching tasks from Motion API', {
|
|
438
|
+
method: 'getTasks',
|
|
439
|
+
workspaceId,
|
|
440
|
+
projectId,
|
|
441
|
+
status,
|
|
442
|
+
assigneeId,
|
|
443
|
+
priority,
|
|
444
|
+
dueDate,
|
|
445
|
+
labelsCount: labels?.length,
|
|
446
|
+
maxPages
|
|
447
|
+
});
|
|
448
|
+
// Create a fetch function for potential pagination
|
|
449
|
+
const fetchPage = async (cursor) => {
|
|
450
|
+
const params = new URLSearchParams();
|
|
451
|
+
params.append('workspaceId', workspaceId);
|
|
452
|
+
if (projectId) {
|
|
453
|
+
params.append('projectId', projectId);
|
|
454
|
+
}
|
|
455
|
+
if (status) {
|
|
456
|
+
params.append('status', status);
|
|
457
|
+
}
|
|
458
|
+
if (assigneeId) {
|
|
459
|
+
params.append('assigneeId', assigneeId);
|
|
460
|
+
}
|
|
461
|
+
if (priority) {
|
|
462
|
+
params.append('priority', priority);
|
|
463
|
+
}
|
|
464
|
+
if (dueDate) {
|
|
465
|
+
params.append('dueDate', dueDate);
|
|
466
|
+
}
|
|
467
|
+
if (labels && labels.length > 0) {
|
|
468
|
+
for (const label of labels) {
|
|
469
|
+
if (label) {
|
|
470
|
+
params.append('labels', label);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (cursor) {
|
|
475
|
+
params.append('cursor', cursor);
|
|
476
|
+
}
|
|
477
|
+
const queryString = params.toString();
|
|
478
|
+
const url = queryString ? `/tasks?${queryString}` : '/tasks';
|
|
479
|
+
return this.requestWithRetry(() => this.client.get(url));
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
// Attempt pagination-aware fetch with new response wrapper
|
|
483
|
+
const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'tasks', {
|
|
484
|
+
maxPages,
|
|
485
|
+
logProgress: false, // Less verbose for tasks
|
|
486
|
+
...(limit ? { maxItems: limit } : {})
|
|
487
|
+
});
|
|
488
|
+
if (paginatedResult.totalFetched > 0) {
|
|
489
|
+
let tasks = paginatedResult.items;
|
|
490
|
+
// Apply limit if specified
|
|
491
|
+
if (limit && limit > 0) {
|
|
492
|
+
tasks = tasks.slice(0, limit);
|
|
493
|
+
}
|
|
494
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully with pagination', {
|
|
495
|
+
method: 'getTasks',
|
|
496
|
+
totalCount: paginatedResult.totalFetched,
|
|
497
|
+
returnedCount: tasks.length,
|
|
498
|
+
hasMore: paginatedResult.hasMore,
|
|
499
|
+
workspaceId,
|
|
500
|
+
projectId,
|
|
501
|
+
limitApplied: limit
|
|
502
|
+
});
|
|
503
|
+
return tasks;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (paginationError) {
|
|
507
|
+
// Fallback to simple fetch if pagination fails
|
|
508
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
|
|
509
|
+
method: 'getTasks',
|
|
510
|
+
error: paginationError instanceof Error ? paginationError.message : String(paginationError)
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
// Use new response wrapper for single page fallback
|
|
514
|
+
const response = await fetchPage();
|
|
515
|
+
const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'tasks');
|
|
516
|
+
let tasks = unwrapped.data;
|
|
517
|
+
// Apply limit if specified
|
|
518
|
+
if (limit && limit > 0) {
|
|
519
|
+
tasks = tasks.slice(0, limit);
|
|
520
|
+
}
|
|
521
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully (single page)', {
|
|
522
|
+
method: 'getTasks',
|
|
523
|
+
count: tasks.length,
|
|
524
|
+
workspaceId,
|
|
525
|
+
projectId,
|
|
526
|
+
limitApplied: limit
|
|
527
|
+
});
|
|
528
|
+
return tasks;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch tasks', {
|
|
532
|
+
method: 'getTasks',
|
|
533
|
+
error: getErrorMessage(error),
|
|
534
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
535
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
536
|
+
});
|
|
537
|
+
throw this.formatApiError(error, 'fetch tasks');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async getTask(taskId) {
|
|
541
|
+
try {
|
|
542
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single task from Motion API', {
|
|
543
|
+
method: 'getTask',
|
|
544
|
+
taskId
|
|
545
|
+
});
|
|
546
|
+
const response = await this.requestWithRetry(() => this.client.get(`/tasks/${taskId}`));
|
|
547
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched task', {
|
|
548
|
+
method: 'getTask',
|
|
549
|
+
taskId,
|
|
550
|
+
taskName: response.data.name
|
|
551
|
+
});
|
|
552
|
+
return response.data;
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch task', {
|
|
556
|
+
method: 'getTask',
|
|
557
|
+
taskId,
|
|
558
|
+
error: getErrorMessage(error),
|
|
559
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
560
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
561
|
+
});
|
|
562
|
+
throw this.formatApiError(error, 'fetch task');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async createTask(taskData) {
|
|
566
|
+
try {
|
|
567
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating task in Motion API', {
|
|
568
|
+
method: 'createTask',
|
|
569
|
+
taskName: taskData.name,
|
|
570
|
+
workspaceId: taskData.workspaceId,
|
|
571
|
+
projectId: taskData.projectId
|
|
572
|
+
});
|
|
573
|
+
if (!taskData.workspaceId) {
|
|
574
|
+
throw new Error('Workspace ID is required to create a task');
|
|
575
|
+
}
|
|
576
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
577
|
+
const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
|
|
578
|
+
// Debug logging: log the exact payload being sent to API
|
|
579
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for task creation', {
|
|
580
|
+
method: 'createTask',
|
|
581
|
+
payload: JSON.stringify(minimalPayload, null, 2)
|
|
582
|
+
});
|
|
583
|
+
const response = await this.requestWithRetry(() => this.client.post('/tasks', minimalPayload));
|
|
584
|
+
// Invalidate task-related caches after successful creation
|
|
585
|
+
// Note: Task cache would need to be implemented separately if needed
|
|
586
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task created successfully', {
|
|
587
|
+
method: 'createTask',
|
|
588
|
+
taskId: response.data.id,
|
|
589
|
+
taskName: response.data.name
|
|
590
|
+
});
|
|
591
|
+
return response.data;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create task', {
|
|
595
|
+
method: 'createTask',
|
|
596
|
+
error: getErrorMessage(error),
|
|
597
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
598
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
599
|
+
fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
|
|
600
|
+
});
|
|
601
|
+
throw this.formatApiError(error, 'create task');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async updateTask(taskId, updates) {
|
|
605
|
+
try {
|
|
606
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating task in Motion API', {
|
|
607
|
+
method: 'updateTask',
|
|
608
|
+
taskId,
|
|
609
|
+
updates: Object.keys(updates)
|
|
610
|
+
});
|
|
611
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
612
|
+
const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
|
|
613
|
+
const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}`, minimalUpdates));
|
|
614
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task updated successfully', {
|
|
615
|
+
method: 'updateTask',
|
|
616
|
+
taskId,
|
|
617
|
+
taskName: response.data.name
|
|
618
|
+
});
|
|
619
|
+
return response.data;
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update task', {
|
|
623
|
+
method: 'updateTask',
|
|
624
|
+
taskId,
|
|
625
|
+
error: getErrorMessage(error),
|
|
626
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
627
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
628
|
+
});
|
|
629
|
+
throw this.formatApiError(error, 'update task');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async deleteTask(taskId) {
|
|
633
|
+
try {
|
|
634
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting task from Motion API', {
|
|
635
|
+
method: 'deleteTask',
|
|
636
|
+
taskId
|
|
637
|
+
});
|
|
638
|
+
await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}`));
|
|
639
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task deleted successfully', {
|
|
640
|
+
method: 'deleteTask',
|
|
641
|
+
taskId
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete task', {
|
|
646
|
+
method: 'deleteTask',
|
|
647
|
+
taskId,
|
|
648
|
+
error: getErrorMessage(error),
|
|
649
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
650
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
651
|
+
});
|
|
652
|
+
throw this.formatApiError(error, 'delete task');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async moveTask(taskId, targetProjectId, targetWorkspaceId) {
|
|
656
|
+
try {
|
|
657
|
+
if (!targetProjectId && !targetWorkspaceId) {
|
|
658
|
+
throw new Error('Either targetProjectId or targetWorkspaceId must be provided');
|
|
659
|
+
}
|
|
660
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Moving task in Motion API', {
|
|
661
|
+
method: 'moveTask',
|
|
662
|
+
taskId,
|
|
663
|
+
targetProjectId,
|
|
664
|
+
targetWorkspaceId
|
|
665
|
+
});
|
|
666
|
+
const moveData = {};
|
|
667
|
+
if (targetProjectId)
|
|
668
|
+
moveData.projectId = targetProjectId;
|
|
669
|
+
if (targetWorkspaceId)
|
|
670
|
+
moveData.workspaceId = targetWorkspaceId;
|
|
671
|
+
const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/move`, moveData));
|
|
672
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task moved successfully', {
|
|
673
|
+
method: 'moveTask',
|
|
674
|
+
taskId,
|
|
675
|
+
targetProjectId,
|
|
676
|
+
targetWorkspaceId
|
|
677
|
+
});
|
|
678
|
+
// TODO: Invalidate task cache for source and destination projects/workspaces when implemented
|
|
679
|
+
return response.data;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to move task', {
|
|
683
|
+
method: 'moveTask',
|
|
684
|
+
taskId,
|
|
685
|
+
targetProjectId,
|
|
686
|
+
targetWorkspaceId,
|
|
687
|
+
error: getErrorMessage(error),
|
|
688
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
689
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
690
|
+
});
|
|
691
|
+
throw this.formatApiError(error, 'move task');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async unassignTask(taskId) {
|
|
695
|
+
try {
|
|
696
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Unassigning task in Motion API', {
|
|
697
|
+
method: 'unassignTask',
|
|
698
|
+
taskId
|
|
699
|
+
});
|
|
700
|
+
const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/unassign`));
|
|
701
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task unassigned successfully', {
|
|
702
|
+
method: 'unassignTask',
|
|
703
|
+
taskId
|
|
704
|
+
});
|
|
705
|
+
// TODO: Invalidate task cache for this task and any assignee-related caches when implemented
|
|
706
|
+
return response.data;
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to unassign task', {
|
|
710
|
+
method: 'unassignTask',
|
|
711
|
+
taskId,
|
|
712
|
+
error: getErrorMessage(error),
|
|
713
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
714
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
715
|
+
});
|
|
716
|
+
throw this.formatApiError(error, 'unassign task');
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async getWorkspaces() {
|
|
720
|
+
return this.workspaceCache.withCache('workspaces', async () => {
|
|
721
|
+
try {
|
|
722
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching workspaces from Motion API', {
|
|
723
|
+
method: 'getWorkspaces'
|
|
724
|
+
});
|
|
725
|
+
const response = await this.requestWithRetry(() => this.client.get('/workspaces'));
|
|
726
|
+
// Validate the response structure
|
|
727
|
+
const validatedResponse = this.validateResponse(response.data, motion_1.WorkspacesListResponseSchema, 'getWorkspaces');
|
|
728
|
+
// Extract workspaces array (handle both wrapped and unwrapped responses)
|
|
729
|
+
const workspaces = Array.isArray(validatedResponse)
|
|
730
|
+
? validatedResponse
|
|
731
|
+
: validatedResponse.workspaces;
|
|
732
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Workspaces fetched and cached successfully', {
|
|
733
|
+
method: 'getWorkspaces',
|
|
734
|
+
count: workspaces.length,
|
|
735
|
+
workspaceNames: workspaces.map((w) => w.name)
|
|
736
|
+
});
|
|
737
|
+
return workspaces;
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch workspaces', {
|
|
741
|
+
method: 'getWorkspaces',
|
|
742
|
+
error: getErrorMessage(error),
|
|
743
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
744
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
745
|
+
});
|
|
746
|
+
throw this.formatApiError(error, 'fetch workspaces');
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async getUsers(workspaceId) {
|
|
751
|
+
const cacheKey = workspaceId ? `users:workspace:${workspaceId}` : 'users:all';
|
|
752
|
+
return this.userCache.withCache(cacheKey, async () => {
|
|
753
|
+
try {
|
|
754
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching users from Motion API', {
|
|
755
|
+
method: 'getUsers',
|
|
756
|
+
workspaceId
|
|
757
|
+
});
|
|
758
|
+
const params = new URLSearchParams();
|
|
759
|
+
if (workspaceId) {
|
|
760
|
+
params.append('workspaceId', workspaceId);
|
|
761
|
+
}
|
|
762
|
+
const queryString = params.toString();
|
|
763
|
+
const url = queryString ? `/users?${queryString}` : '/users';
|
|
764
|
+
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
765
|
+
// The Motion API might wrap the users in a 'users' array
|
|
766
|
+
const usersData = response.data?.users || response.data || [];
|
|
767
|
+
const users = Array.isArray(usersData) ? usersData : [];
|
|
768
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Users fetched successfully', {
|
|
769
|
+
method: 'getUsers',
|
|
770
|
+
count: users.length,
|
|
771
|
+
workspaceId
|
|
772
|
+
});
|
|
773
|
+
return users;
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch users', {
|
|
777
|
+
method: 'getUsers',
|
|
778
|
+
error: getErrorMessage(error),
|
|
779
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
780
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
781
|
+
});
|
|
782
|
+
throw this.formatApiError(error, 'fetch users');
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
async getCurrentUser() {
|
|
787
|
+
const cacheKey = 'currentUser';
|
|
788
|
+
// Use userCache but with a special single-user wrapper
|
|
789
|
+
const cachedUsers = await this.userCache.withCache(cacheKey, async () => {
|
|
790
|
+
try {
|
|
791
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching current user from Motion API', {
|
|
792
|
+
method: 'getCurrentUser'
|
|
793
|
+
});
|
|
794
|
+
const response = await this.requestWithRetry(() => this.client.get('/users/me'));
|
|
795
|
+
const user = response.data;
|
|
796
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Current user fetched successfully', {
|
|
797
|
+
method: 'getCurrentUser',
|
|
798
|
+
userId: user.id,
|
|
799
|
+
email: user.email
|
|
800
|
+
});
|
|
801
|
+
return [user]; // Wrap in array for cache compatibility
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch current user', {
|
|
805
|
+
method: 'getCurrentUser',
|
|
806
|
+
error: getErrorMessage(error),
|
|
807
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
808
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
809
|
+
});
|
|
810
|
+
throw this.formatApiError(error, 'fetch current user');
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
return cachedUsers[0]; // Return just the user object
|
|
814
|
+
}
|
|
815
|
+
// Additional methods for intelligent features
|
|
816
|
+
/**
|
|
817
|
+
* Resolves a project identifier (either projectId or projectName) to a MotionProject
|
|
818
|
+
* Searches across all workspaces if not found in the specified workspace
|
|
819
|
+
* @param identifier Object containing either projectId or projectName
|
|
820
|
+
* @param workspaceId Workspace to start searching in
|
|
821
|
+
* @returns Resolved MotionProject with workspace info, or undefined if not found
|
|
822
|
+
*/
|
|
823
|
+
async resolveProjectIdentifier(identifier, workspaceId) {
|
|
824
|
+
try {
|
|
825
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving project identifier', {
|
|
826
|
+
method: 'resolveProjectIdentifier',
|
|
827
|
+
projectId: identifier.projectId,
|
|
828
|
+
projectName: identifier.projectName,
|
|
829
|
+
workspaceId
|
|
830
|
+
});
|
|
831
|
+
// If projectId is provided, try to get it directly
|
|
832
|
+
if (identifier.projectId) {
|
|
833
|
+
try {
|
|
834
|
+
const project = await this.getProject(identifier.projectId);
|
|
835
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by ID', {
|
|
836
|
+
method: 'resolveProjectIdentifier',
|
|
837
|
+
projectId: identifier.projectId,
|
|
838
|
+
projectName: project.name,
|
|
839
|
+
workspaceId: project.workspaceId
|
|
840
|
+
});
|
|
841
|
+
return project;
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project by ID', {
|
|
845
|
+
method: 'resolveProjectIdentifier',
|
|
846
|
+
projectId: identifier.projectId,
|
|
847
|
+
error: getErrorMessage(error)
|
|
848
|
+
});
|
|
849
|
+
// Fall through to projectName resolution if projectId fails
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// If projectName is provided (or projectId failed), resolve by name across workspaces
|
|
853
|
+
if (identifier.projectName) {
|
|
854
|
+
const project = await this.getProjectByName(identifier.projectName, workspaceId);
|
|
855
|
+
if (project) {
|
|
856
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by name across workspaces', {
|
|
857
|
+
method: 'resolveProjectIdentifier',
|
|
858
|
+
projectName: identifier.projectName,
|
|
859
|
+
projectId: project.id,
|
|
860
|
+
foundInWorkspaceId: project.workspaceId
|
|
861
|
+
});
|
|
862
|
+
return project;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project identifier', {
|
|
866
|
+
method: 'resolveProjectIdentifier',
|
|
867
|
+
projectId: identifier.projectId,
|
|
868
|
+
projectName: identifier.projectName,
|
|
869
|
+
workspaceId
|
|
870
|
+
});
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving project identifier', {
|
|
875
|
+
method: 'resolveProjectIdentifier',
|
|
876
|
+
projectId: identifier.projectId,
|
|
877
|
+
projectName: identifier.projectName,
|
|
878
|
+
error: getErrorMessage(error)
|
|
879
|
+
});
|
|
880
|
+
throw error;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Resolves a user identifier (either userId or userName/email) to a MotionUser
|
|
885
|
+
* Searches across all workspaces if not found in the specified workspace
|
|
886
|
+
* @param identifier Object containing either userId or userName
|
|
887
|
+
* @param workspaceId Workspace to start searching in (optional)
|
|
888
|
+
* @returns Resolved MotionUser with workspace info, or undefined if not found
|
|
889
|
+
*/
|
|
890
|
+
async resolveUserIdentifier(identifier, workspaceId) {
|
|
891
|
+
try {
|
|
892
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving user identifier', {
|
|
893
|
+
method: 'resolveUserIdentifier',
|
|
894
|
+
userId: identifier.userId,
|
|
895
|
+
userName: identifier.userName,
|
|
896
|
+
workspaceId
|
|
897
|
+
});
|
|
898
|
+
// If userId is provided, search by ID across all workspaces
|
|
899
|
+
if (identifier.userId) {
|
|
900
|
+
const allWorkspaces = await this.getWorkspaces();
|
|
901
|
+
for (const workspace of allWorkspaces) {
|
|
902
|
+
try {
|
|
903
|
+
const users = await this.getUsers(workspace.id);
|
|
904
|
+
const user = users.find(u => u.id === identifier.userId);
|
|
905
|
+
if (user) {
|
|
906
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by ID', {
|
|
907
|
+
method: 'resolveUserIdentifier',
|
|
908
|
+
userId: identifier.userId,
|
|
909
|
+
userName: user.name,
|
|
910
|
+
foundInWorkspaceId: workspace.id
|
|
911
|
+
});
|
|
912
|
+
return user;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
catch (workspaceError) {
|
|
916
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by ID', {
|
|
917
|
+
method: 'resolveUserIdentifier',
|
|
918
|
+
userId: identifier.userId,
|
|
919
|
+
workspaceId: workspace.id,
|
|
920
|
+
error: getErrorMessage(workspaceError)
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// If userName is provided, search by name/email across all workspaces
|
|
926
|
+
if (identifier.userName) {
|
|
927
|
+
const allWorkspaces = await this.getWorkspaces();
|
|
928
|
+
const searchTerm = identifier.userName.toLowerCase();
|
|
929
|
+
for (const workspace of allWorkspaces) {
|
|
930
|
+
try {
|
|
931
|
+
const users = await this.getUsers(workspace.id);
|
|
932
|
+
const user = users.find(u => u.name?.toLowerCase().includes(searchTerm) ||
|
|
933
|
+
u.email?.toLowerCase().includes(searchTerm));
|
|
934
|
+
if (user) {
|
|
935
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by name/email', {
|
|
936
|
+
method: 'resolveUserIdentifier',
|
|
937
|
+
userName: identifier.userName,
|
|
938
|
+
userId: user.id,
|
|
939
|
+
foundInWorkspaceId: workspace.id
|
|
940
|
+
});
|
|
941
|
+
return user;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
catch (workspaceError) {
|
|
945
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by name', {
|
|
946
|
+
method: 'resolveUserIdentifier',
|
|
947
|
+
userName: identifier.userName,
|
|
948
|
+
workspaceId: workspace.id,
|
|
949
|
+
error: getErrorMessage(workspaceError)
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve user identifier', {
|
|
955
|
+
method: 'resolveUserIdentifier',
|
|
956
|
+
userId: identifier.userId,
|
|
957
|
+
userName: identifier.userName,
|
|
958
|
+
workspaceId
|
|
959
|
+
});
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving user identifier', {
|
|
964
|
+
method: 'resolveUserIdentifier',
|
|
965
|
+
userId: identifier.userId,
|
|
966
|
+
userName: identifier.userName,
|
|
967
|
+
error: getErrorMessage(error)
|
|
968
|
+
});
|
|
969
|
+
throw error;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async getProjectByName(projectName, workspaceId) {
|
|
973
|
+
try {
|
|
974
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Finding project by name', {
|
|
975
|
+
method: 'getProjectByName',
|
|
976
|
+
projectName,
|
|
977
|
+
workspaceId
|
|
978
|
+
});
|
|
979
|
+
// First, search in the specified workspace
|
|
980
|
+
const projects = await this.getProjects(workspaceId);
|
|
981
|
+
const project = projects.find(p => p.name === projectName);
|
|
982
|
+
if (project) {
|
|
983
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in specified workspace', {
|
|
984
|
+
method: 'getProjectByName',
|
|
985
|
+
projectName,
|
|
986
|
+
projectId: project.id,
|
|
987
|
+
workspaceId
|
|
988
|
+
});
|
|
989
|
+
return project;
|
|
990
|
+
}
|
|
991
|
+
// If not found in specified workspace, search across all other workspaces
|
|
992
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Project not found in specified workspace, searching all workspaces', {
|
|
993
|
+
method: 'getProjectByName',
|
|
994
|
+
projectName,
|
|
995
|
+
specifiedWorkspaceId: workspaceId
|
|
996
|
+
});
|
|
997
|
+
const allWorkspaces = await this.getWorkspaces();
|
|
998
|
+
const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
|
|
999
|
+
for (const workspace of otherWorkspaces) {
|
|
1000
|
+
try {
|
|
1001
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching workspace for project', {
|
|
1002
|
+
method: 'getProjectByName',
|
|
1003
|
+
projectName,
|
|
1004
|
+
searchingWorkspaceId: workspace.id,
|
|
1005
|
+
searchingWorkspaceName: workspace.name
|
|
1006
|
+
});
|
|
1007
|
+
const workspaceProjects = await this.getProjects(workspace.id);
|
|
1008
|
+
const foundProject = workspaceProjects.find(p => p.name === projectName);
|
|
1009
|
+
if (foundProject) {
|
|
1010
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in different workspace', {
|
|
1011
|
+
method: 'getProjectByName',
|
|
1012
|
+
projectName,
|
|
1013
|
+
projectId: foundProject.id,
|
|
1014
|
+
foundInWorkspaceId: workspace.id,
|
|
1015
|
+
foundInWorkspaceName: workspace.name,
|
|
1016
|
+
originalWorkspaceId: workspaceId
|
|
1017
|
+
});
|
|
1018
|
+
return foundProject;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
catch (workspaceError) {
|
|
1022
|
+
// Log error but continue searching other workspaces
|
|
1023
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for project', {
|
|
1024
|
+
method: 'getProjectByName',
|
|
1025
|
+
projectName,
|
|
1026
|
+
workspaceId: workspace.id,
|
|
1027
|
+
workspaceName: workspace.name,
|
|
1028
|
+
error: getErrorMessage(workspaceError)
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
// Project not found in any workspace
|
|
1033
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project not found by name in any workspace', {
|
|
1034
|
+
method: 'getProjectByName',
|
|
1035
|
+
projectName,
|
|
1036
|
+
searchedWorkspaces: [workspaceId, ...otherWorkspaces.map(w => w.id)],
|
|
1037
|
+
totalWorkspacesSearched: allWorkspaces.length
|
|
1038
|
+
});
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to find project by name', {
|
|
1043
|
+
method: 'getProjectByName',
|
|
1044
|
+
projectName,
|
|
1045
|
+
error: getErrorMessage(error)
|
|
1046
|
+
});
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async searchTasks(query, workspaceId, limit) {
|
|
1051
|
+
try {
|
|
1052
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching tasks', {
|
|
1053
|
+
method: 'searchTasks',
|
|
1054
|
+
query,
|
|
1055
|
+
workspaceId,
|
|
1056
|
+
limit
|
|
1057
|
+
});
|
|
1058
|
+
// Apply search limit to prevent resource exhaustion
|
|
1059
|
+
const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
|
|
1060
|
+
const lowerQuery = query.toLowerCase();
|
|
1061
|
+
let allMatchingTasks = [];
|
|
1062
|
+
// First, search in the specified workspace
|
|
1063
|
+
const primaryTasks = await this.getTasks({
|
|
1064
|
+
workspaceId,
|
|
1065
|
+
limit: effectiveLimit,
|
|
1066
|
+
maxPages: constants_1.LIMITS.MAX_PAGES
|
|
1067
|
+
});
|
|
1068
|
+
const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
|
|
1069
|
+
task.description?.toLowerCase().includes(lowerQuery));
|
|
1070
|
+
allMatchingTasks.push(...primaryMatches);
|
|
1071
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
|
|
1072
|
+
method: 'searchTasks',
|
|
1073
|
+
query,
|
|
1074
|
+
primaryWorkspaceId: workspaceId,
|
|
1075
|
+
primaryMatches: primaryMatches.length
|
|
1076
|
+
});
|
|
1077
|
+
// If we haven't reached the limit, search other workspaces
|
|
1078
|
+
if (allMatchingTasks.length < effectiveLimit) {
|
|
1079
|
+
try {
|
|
1080
|
+
const allWorkspaces = await this.getWorkspaces();
|
|
1081
|
+
const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
|
|
1082
|
+
for (const workspace of otherWorkspaces) {
|
|
1083
|
+
if (allMatchingTasks.length >= effectiveLimit)
|
|
1084
|
+
break;
|
|
1085
|
+
try {
|
|
1086
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for tasks', {
|
|
1087
|
+
method: 'searchTasks',
|
|
1088
|
+
query,
|
|
1089
|
+
searchingWorkspaceId: workspace.id,
|
|
1090
|
+
searchingWorkspaceName: workspace.name
|
|
1091
|
+
});
|
|
1092
|
+
const workspaceTasks = await this.getTasks({
|
|
1093
|
+
workspaceId: workspace.id,
|
|
1094
|
+
limit: effectiveLimit,
|
|
1095
|
+
maxPages: constants_1.LIMITS.MAX_PAGES
|
|
1096
|
+
});
|
|
1097
|
+
const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
|
|
1098
|
+
task.description?.toLowerCase().includes(lowerQuery));
|
|
1099
|
+
allMatchingTasks.push(...workspaceMatches);
|
|
1100
|
+
if (workspaceMatches.length > 0) {
|
|
1101
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
|
|
1102
|
+
method: 'searchTasks',
|
|
1103
|
+
query,
|
|
1104
|
+
workspaceId: workspace.id,
|
|
1105
|
+
workspaceName: workspace.name,
|
|
1106
|
+
matches: workspaceMatches.length
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch (workspaceError) {
|
|
1111
|
+
// Log error but continue searching other workspaces
|
|
1112
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for tasks', {
|
|
1113
|
+
method: 'searchTasks',
|
|
1114
|
+
query,
|
|
1115
|
+
workspaceId: workspace.id,
|
|
1116
|
+
workspaceName: workspace.name,
|
|
1117
|
+
error: getErrorMessage(workspaceError)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
catch (workspaceListError) {
|
|
1123
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
|
|
1124
|
+
method: 'searchTasks',
|
|
1125
|
+
query,
|
|
1126
|
+
error: getErrorMessage(workspaceListError)
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// Apply final limit and return results
|
|
1131
|
+
const finalResults = allMatchingTasks.slice(0, effectiveLimit);
|
|
1132
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task search completed across all workspaces', {
|
|
1133
|
+
method: 'searchTasks',
|
|
1134
|
+
query,
|
|
1135
|
+
totalMatches: allMatchingTasks.length,
|
|
1136
|
+
returnedResults: finalResults.length,
|
|
1137
|
+
limit: effectiveLimit,
|
|
1138
|
+
crossWorkspaceSearch: allMatchingTasks.length > primaryMatches.length
|
|
1139
|
+
});
|
|
1140
|
+
return finalResults;
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
|
|
1144
|
+
method: 'searchTasks',
|
|
1145
|
+
query,
|
|
1146
|
+
error: getErrorMessage(error)
|
|
1147
|
+
});
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async searchProjects(query, workspaceId, limit) {
|
|
1152
|
+
try {
|
|
1153
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching projects', {
|
|
1154
|
+
method: 'searchProjects',
|
|
1155
|
+
query,
|
|
1156
|
+
workspaceId,
|
|
1157
|
+
limit
|
|
1158
|
+
});
|
|
1159
|
+
// Apply search limit to prevent resource exhaustion
|
|
1160
|
+
const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
|
|
1161
|
+
const lowerQuery = query.toLowerCase();
|
|
1162
|
+
let allMatchingProjects = [];
|
|
1163
|
+
// First, search in the specified workspace
|
|
1164
|
+
const primaryProjects = await this.getProjects(workspaceId, constants_1.LIMITS.MAX_PAGES);
|
|
1165
|
+
const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
|
|
1166
|
+
project.description?.toLowerCase().includes(lowerQuery));
|
|
1167
|
+
allMatchingProjects.push(...primaryMatches);
|
|
1168
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
|
|
1169
|
+
method: 'searchProjects',
|
|
1170
|
+
query,
|
|
1171
|
+
primaryWorkspaceId: workspaceId,
|
|
1172
|
+
primaryMatches: primaryMatches.length
|
|
1173
|
+
});
|
|
1174
|
+
// If we haven't reached the limit, search other workspaces
|
|
1175
|
+
if (allMatchingProjects.length < effectiveLimit) {
|
|
1176
|
+
try {
|
|
1177
|
+
const allWorkspaces = await this.getWorkspaces();
|
|
1178
|
+
const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
|
|
1179
|
+
for (const workspace of otherWorkspaces) {
|
|
1180
|
+
if (allMatchingProjects.length >= effectiveLimit)
|
|
1181
|
+
break;
|
|
1182
|
+
try {
|
|
1183
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for projects', {
|
|
1184
|
+
method: 'searchProjects',
|
|
1185
|
+
query,
|
|
1186
|
+
searchingWorkspaceId: workspace.id,
|
|
1187
|
+
searchingWorkspaceName: workspace.name
|
|
1188
|
+
});
|
|
1189
|
+
const workspaceProjects = await this.getProjects(workspace.id, constants_1.LIMITS.MAX_PAGES);
|
|
1190
|
+
const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
|
|
1191
|
+
project.description?.toLowerCase().includes(lowerQuery));
|
|
1192
|
+
allMatchingProjects.push(...workspaceMatches);
|
|
1193
|
+
if (workspaceMatches.length > 0) {
|
|
1194
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
|
|
1195
|
+
method: 'searchProjects',
|
|
1196
|
+
query,
|
|
1197
|
+
workspaceId: workspace.id,
|
|
1198
|
+
workspaceName: workspace.name,
|
|
1199
|
+
matches: workspaceMatches.length
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
catch (workspaceError) {
|
|
1204
|
+
// Log error but continue searching other workspaces
|
|
1205
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for projects', {
|
|
1206
|
+
method: 'searchProjects',
|
|
1207
|
+
query,
|
|
1208
|
+
workspaceId: workspace.id,
|
|
1209
|
+
workspaceName: workspace.name,
|
|
1210
|
+
error: getErrorMessage(workspaceError)
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
catch (workspaceListError) {
|
|
1216
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
|
|
1217
|
+
method: 'searchProjects',
|
|
1218
|
+
query,
|
|
1219
|
+
error: getErrorMessage(workspaceListError)
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// Apply final limit and return results
|
|
1224
|
+
const finalResults = allMatchingProjects.slice(0, effectiveLimit);
|
|
1225
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project search completed across all workspaces', {
|
|
1226
|
+
method: 'searchProjects',
|
|
1227
|
+
query,
|
|
1228
|
+
totalMatches: allMatchingProjects.length,
|
|
1229
|
+
returnedResults: finalResults.length,
|
|
1230
|
+
limit: effectiveLimit,
|
|
1231
|
+
crossWorkspaceSearch: allMatchingProjects.length > primaryMatches.length
|
|
1232
|
+
});
|
|
1233
|
+
return finalResults;
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
|
|
1237
|
+
method: 'searchProjects',
|
|
1238
|
+
query,
|
|
1239
|
+
error: getErrorMessage(error)
|
|
1240
|
+
});
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Get comments for a task with proper pagination support
|
|
1246
|
+
* @param taskId Task ID to get comments for
|
|
1247
|
+
* @param cursor Optional cursor for pagination
|
|
1248
|
+
* @returns Paginated response with comments and metadata
|
|
1249
|
+
*/
|
|
1250
|
+
async getComments(taskId, cursor) {
|
|
1251
|
+
const cacheParams = { taskId, cursor: cursor || null };
|
|
1252
|
+
const cacheKey = `comments:${JSON.stringify(cacheParams)}`;
|
|
1253
|
+
return this.commentCache.withCache(cacheKey, async () => {
|
|
1254
|
+
try {
|
|
1255
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching comments from Motion API', {
|
|
1256
|
+
method: 'getComments',
|
|
1257
|
+
taskId,
|
|
1258
|
+
cursor
|
|
1259
|
+
});
|
|
1260
|
+
const params = new URLSearchParams({ taskId });
|
|
1261
|
+
if (cursor)
|
|
1262
|
+
params.append('cursor', cursor);
|
|
1263
|
+
const response = await this.requestWithRetry(() => this.client.get(`/comments?${params.toString()}`));
|
|
1264
|
+
// Use new response wrapper for consistent handling
|
|
1265
|
+
const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'comments');
|
|
1266
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comments fetched successfully', {
|
|
1267
|
+
method: 'getComments',
|
|
1268
|
+
count: unwrapped.data.length,
|
|
1269
|
+
hasMore: !!unwrapped.meta?.nextCursor,
|
|
1270
|
+
taskId
|
|
1271
|
+
});
|
|
1272
|
+
// Return in our standard paginated format
|
|
1273
|
+
return {
|
|
1274
|
+
data: unwrapped.data,
|
|
1275
|
+
meta: {
|
|
1276
|
+
nextCursor: unwrapped.meta?.nextCursor,
|
|
1277
|
+
pageSize: unwrapped.meta?.pageSize || unwrapped.data.length
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
catch (error) {
|
|
1282
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch comments', {
|
|
1283
|
+
method: 'getComments',
|
|
1284
|
+
error: getErrorMessage(error),
|
|
1285
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1286
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1287
|
+
taskId,
|
|
1288
|
+
cursor
|
|
1289
|
+
});
|
|
1290
|
+
throw this.formatApiError(error, 'fetch comments');
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
async createComment(commentData) {
|
|
1295
|
+
try {
|
|
1296
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating comment in Motion API', {
|
|
1297
|
+
method: 'createComment',
|
|
1298
|
+
taskId: commentData.taskId,
|
|
1299
|
+
contentLength: commentData.content?.length || 0
|
|
1300
|
+
});
|
|
1301
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
1302
|
+
const minimalPayload = (0, constants_1.createMinimalPayload)(commentData);
|
|
1303
|
+
const response = await this.requestWithRetry(() => this.client.post('/comments', minimalPayload));
|
|
1304
|
+
// Invalidate cache after successful creation
|
|
1305
|
+
const cacheKey = `comments:${JSON.stringify({ taskId: commentData.taskId, cursor: null })}`;
|
|
1306
|
+
this.commentCache.invalidate(cacheKey);
|
|
1307
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comment created successfully', {
|
|
1308
|
+
method: 'createComment',
|
|
1309
|
+
commentId: response.data?.id,
|
|
1310
|
+
taskId: commentData.taskId
|
|
1311
|
+
});
|
|
1312
|
+
return response.data;
|
|
1313
|
+
}
|
|
1314
|
+
catch (error) {
|
|
1315
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create comment', {
|
|
1316
|
+
method: 'createComment',
|
|
1317
|
+
error: getErrorMessage(error),
|
|
1318
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1319
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1320
|
+
taskId: commentData?.taskId
|
|
1321
|
+
});
|
|
1322
|
+
throw this.formatApiError(error, 'create comment');
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Fetch custom fields from Motion API
|
|
1327
|
+
* @param workspaceId - Required workspace ID to get custom fields for
|
|
1328
|
+
* @returns Array of custom fields
|
|
1329
|
+
*/
|
|
1330
|
+
async getCustomFields(workspaceId) {
|
|
1331
|
+
const cacheKey = `custom-fields:${workspaceId}`;
|
|
1332
|
+
return this.customFieldCache.withCache(cacheKey, async () => {
|
|
1333
|
+
try {
|
|
1334
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching custom fields from Motion API', {
|
|
1335
|
+
method: 'getCustomFields',
|
|
1336
|
+
workspaceId
|
|
1337
|
+
});
|
|
1338
|
+
const url = `/beta/workspaces/${workspaceId}/custom-fields`;
|
|
1339
|
+
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
1340
|
+
// Beta API returns direct array, not wrapped
|
|
1341
|
+
const fieldsArray = response.data || [];
|
|
1342
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom fields fetched successfully', {
|
|
1343
|
+
method: 'getCustomFields',
|
|
1344
|
+
count: fieldsArray.length,
|
|
1345
|
+
workspaceId
|
|
1346
|
+
});
|
|
1347
|
+
return fieldsArray;
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch custom fields', {
|
|
1351
|
+
method: 'getCustomFields',
|
|
1352
|
+
error: getErrorMessage(error),
|
|
1353
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1354
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1355
|
+
workspaceId
|
|
1356
|
+
});
|
|
1357
|
+
throw this.formatApiError(error, 'fetch custom fields');
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Create a new custom field
|
|
1363
|
+
* @param workspaceId - Required workspace ID to create custom field in
|
|
1364
|
+
* @param fieldData - Data for creating the custom field
|
|
1365
|
+
* @returns The created custom field
|
|
1366
|
+
*/
|
|
1367
|
+
async createCustomField(workspaceId, fieldData) {
|
|
1368
|
+
try {
|
|
1369
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating custom field in Motion API', {
|
|
1370
|
+
method: 'createCustomField',
|
|
1371
|
+
name: fieldData.name,
|
|
1372
|
+
field: fieldData.field,
|
|
1373
|
+
workspaceId
|
|
1374
|
+
});
|
|
1375
|
+
// Transform payload to match Motion API expectations
|
|
1376
|
+
// POST API expects 'type' in request, but returns 'field' in response
|
|
1377
|
+
const apiPayload = {
|
|
1378
|
+
name: fieldData.name,
|
|
1379
|
+
type: fieldData.field, // Motion API POST expects 'type' property in request body
|
|
1380
|
+
...(fieldData.metadata && { metadata: fieldData.metadata })
|
|
1381
|
+
};
|
|
1382
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
1383
|
+
const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
|
|
1384
|
+
const response = await this.requestWithRetry(() => this.client.post(`/beta/workspaces/${workspaceId}/custom-fields`, minimalPayload));
|
|
1385
|
+
// Invalidate cache after successful creation
|
|
1386
|
+
this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
|
|
1387
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field created successfully', {
|
|
1388
|
+
method: 'createCustomField',
|
|
1389
|
+
fieldId: response.data?.id,
|
|
1390
|
+
name: fieldData.name,
|
|
1391
|
+
workspaceId
|
|
1392
|
+
});
|
|
1393
|
+
return response.data;
|
|
1394
|
+
}
|
|
1395
|
+
catch (error) {
|
|
1396
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create custom field', {
|
|
1397
|
+
method: 'createCustomField',
|
|
1398
|
+
error: getErrorMessage(error),
|
|
1399
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1400
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1401
|
+
fieldName: fieldData?.name,
|
|
1402
|
+
workspaceId
|
|
1403
|
+
});
|
|
1404
|
+
throw this.formatApiError(error, 'create custom field');
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Delete a custom field
|
|
1409
|
+
* @param workspaceId - Required workspace ID containing the custom field
|
|
1410
|
+
* @param fieldId - ID of the custom field to delete
|
|
1411
|
+
* @returns Success indicator
|
|
1412
|
+
*/
|
|
1413
|
+
async deleteCustomField(workspaceId, fieldId) {
|
|
1414
|
+
try {
|
|
1415
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting custom field from Motion API', {
|
|
1416
|
+
method: 'deleteCustomField',
|
|
1417
|
+
fieldId,
|
|
1418
|
+
workspaceId
|
|
1419
|
+
});
|
|
1420
|
+
await this.requestWithRetry(() => this.client.delete(`/beta/workspaces/${workspaceId}/custom-fields/${fieldId}`));
|
|
1421
|
+
// Invalidate cache after successful deletion
|
|
1422
|
+
this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
|
|
1423
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field deleted successfully', {
|
|
1424
|
+
method: 'deleteCustomField',
|
|
1425
|
+
fieldId,
|
|
1426
|
+
workspaceId
|
|
1427
|
+
});
|
|
1428
|
+
return { success: true };
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete custom field', {
|
|
1432
|
+
method: 'deleteCustomField',
|
|
1433
|
+
error: getErrorMessage(error),
|
|
1434
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1435
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1436
|
+
fieldId,
|
|
1437
|
+
workspaceId
|
|
1438
|
+
});
|
|
1439
|
+
throw this.formatApiError(error, 'delete custom field');
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Add a custom field to a project
|
|
1444
|
+
* @param projectId - ID of the project
|
|
1445
|
+
* @param fieldId - ID of the custom field
|
|
1446
|
+
* @param value - Optional value for the field
|
|
1447
|
+
* @returns Updated project data
|
|
1448
|
+
*/
|
|
1449
|
+
async addCustomFieldToProject(projectId, fieldId, value) {
|
|
1450
|
+
try {
|
|
1451
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to project', {
|
|
1452
|
+
method: 'addCustomFieldToProject',
|
|
1453
|
+
projectId,
|
|
1454
|
+
fieldId,
|
|
1455
|
+
hasValue: value !== undefined
|
|
1456
|
+
});
|
|
1457
|
+
const requestData = {
|
|
1458
|
+
fieldId,
|
|
1459
|
+
...(value !== undefined && { value })
|
|
1460
|
+
};
|
|
1461
|
+
const response = await this.requestWithRetry(() => this.client.post(`/projects/${projectId}/custom-fields`, requestData));
|
|
1462
|
+
// Invalidate project cache for specific workspace if available
|
|
1463
|
+
if (response.data?.workspaceId) {
|
|
1464
|
+
this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
// Fallback to broader invalidation if workspace unknown
|
|
1468
|
+
this.projectCache.invalidate(`projects:`);
|
|
1469
|
+
}
|
|
1470
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to project successfully', {
|
|
1471
|
+
method: 'addCustomFieldToProject',
|
|
1472
|
+
projectId,
|
|
1473
|
+
fieldId
|
|
1474
|
+
});
|
|
1475
|
+
return response.data;
|
|
1476
|
+
}
|
|
1477
|
+
catch (error) {
|
|
1478
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to project', {
|
|
1479
|
+
method: 'addCustomFieldToProject',
|
|
1480
|
+
error: getErrorMessage(error),
|
|
1481
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1482
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1483
|
+
projectId,
|
|
1484
|
+
fieldId
|
|
1485
|
+
});
|
|
1486
|
+
throw this.formatApiError(error, 'add custom field to project');
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Remove a custom field from a project
|
|
1491
|
+
* @param projectId - ID of the project
|
|
1492
|
+
* @param fieldId - ID of the custom field
|
|
1493
|
+
* @returns Success indicator
|
|
1494
|
+
*/
|
|
1495
|
+
async removeCustomFieldFromProject(projectId, fieldId) {
|
|
1496
|
+
try {
|
|
1497
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from project', {
|
|
1498
|
+
method: 'removeCustomFieldFromProject',
|
|
1499
|
+
projectId,
|
|
1500
|
+
fieldId
|
|
1501
|
+
});
|
|
1502
|
+
await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}/custom-fields/${fieldId}`));
|
|
1503
|
+
// TODO: Invalidate project cache for specific workspace when project data is available
|
|
1504
|
+
// For now, invalidate all project caches
|
|
1505
|
+
this.projectCache.invalidate(`projects:`);
|
|
1506
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from project successfully', {
|
|
1507
|
+
method: 'removeCustomFieldFromProject',
|
|
1508
|
+
projectId,
|
|
1509
|
+
fieldId
|
|
1510
|
+
});
|
|
1511
|
+
return { success: true };
|
|
1512
|
+
}
|
|
1513
|
+
catch (error) {
|
|
1514
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from project', {
|
|
1515
|
+
method: 'removeCustomFieldFromProject',
|
|
1516
|
+
error: getErrorMessage(error),
|
|
1517
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1518
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1519
|
+
projectId,
|
|
1520
|
+
fieldId
|
|
1521
|
+
});
|
|
1522
|
+
throw this.formatApiError(error, 'remove custom field from project');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Add a custom field to a task
|
|
1527
|
+
* @param taskId - ID of the task
|
|
1528
|
+
* @param fieldId - ID of the custom field
|
|
1529
|
+
* @param value - Optional value for the field
|
|
1530
|
+
* @returns Updated task data
|
|
1531
|
+
*/
|
|
1532
|
+
async addCustomFieldToTask(taskId, fieldId, value) {
|
|
1533
|
+
try {
|
|
1534
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to task', {
|
|
1535
|
+
method: 'addCustomFieldToTask',
|
|
1536
|
+
taskId,
|
|
1537
|
+
fieldId,
|
|
1538
|
+
hasValue: value !== undefined
|
|
1539
|
+
});
|
|
1540
|
+
const requestData = {
|
|
1541
|
+
fieldId,
|
|
1542
|
+
...(value !== undefined && { value })
|
|
1543
|
+
};
|
|
1544
|
+
const response = await this.requestWithRetry(() => this.client.post(`/tasks/${taskId}/custom-fields`, requestData));
|
|
1545
|
+
// TODO: Invalidate task cache when implemented
|
|
1546
|
+
// if (response.data?.workspaceId) {
|
|
1547
|
+
// this.taskCache.invalidate(`tasks:workspace:${response.data.workspaceId}`);
|
|
1548
|
+
// }
|
|
1549
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to task successfully', {
|
|
1550
|
+
method: 'addCustomFieldToTask',
|
|
1551
|
+
taskId,
|
|
1552
|
+
fieldId
|
|
1553
|
+
});
|
|
1554
|
+
return response.data;
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to task', {
|
|
1558
|
+
method: 'addCustomFieldToTask',
|
|
1559
|
+
error: getErrorMessage(error),
|
|
1560
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1561
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1562
|
+
taskId,
|
|
1563
|
+
fieldId
|
|
1564
|
+
});
|
|
1565
|
+
throw this.formatApiError(error, 'add custom field to task');
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Remove a custom field from a task
|
|
1570
|
+
* @param taskId - ID of the task
|
|
1571
|
+
* @param fieldId - ID of the custom field
|
|
1572
|
+
* @returns Success indicator
|
|
1573
|
+
*/
|
|
1574
|
+
async removeCustomFieldFromTask(taskId, fieldId) {
|
|
1575
|
+
try {
|
|
1576
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from task', {
|
|
1577
|
+
method: 'removeCustomFieldFromTask',
|
|
1578
|
+
taskId,
|
|
1579
|
+
fieldId
|
|
1580
|
+
});
|
|
1581
|
+
await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}/custom-fields/${fieldId}`));
|
|
1582
|
+
// TODO: Invalidate task cache when implemented
|
|
1583
|
+
// this.taskCache.invalidate(`tasks:`);
|
|
1584
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from task successfully', {
|
|
1585
|
+
method: 'removeCustomFieldFromTask',
|
|
1586
|
+
taskId,
|
|
1587
|
+
fieldId
|
|
1588
|
+
});
|
|
1589
|
+
return { success: true };
|
|
1590
|
+
}
|
|
1591
|
+
catch (error) {
|
|
1592
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from task', {
|
|
1593
|
+
method: 'removeCustomFieldFromTask',
|
|
1594
|
+
error: getErrorMessage(error),
|
|
1595
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1596
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1597
|
+
taskId,
|
|
1598
|
+
fieldId
|
|
1599
|
+
});
|
|
1600
|
+
throw this.formatApiError(error, 'remove custom field from task');
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Fetch recurring tasks from Motion API with automatic pagination
|
|
1605
|
+
* @param workspaceId - Optional workspace ID to filter recurring tasks
|
|
1606
|
+
* @param maxPages - Maximum number of pages to fetch (default: 10)
|
|
1607
|
+
* @returns Array of recurring tasks from all pages
|
|
1608
|
+
*/
|
|
1609
|
+
async getRecurringTasks(workspaceId, maxPages = 10) {
|
|
1610
|
+
const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
|
|
1611
|
+
return this.recurringTaskCache.withCache(cacheKey, async () => {
|
|
1612
|
+
try {
|
|
1613
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
|
|
1614
|
+
method: 'getRecurringTasks',
|
|
1615
|
+
workspaceId,
|
|
1616
|
+
maxPages
|
|
1617
|
+
});
|
|
1618
|
+
// Create a fetch function for pagination utility
|
|
1619
|
+
const fetchPage = async (cursor) => {
|
|
1620
|
+
const params = new URLSearchParams();
|
|
1621
|
+
if (workspaceId)
|
|
1622
|
+
params.append('workspaceId', workspaceId);
|
|
1623
|
+
if (cursor)
|
|
1624
|
+
params.append('cursor', cursor);
|
|
1625
|
+
const queryString = params.toString();
|
|
1626
|
+
const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
|
|
1627
|
+
return this.requestWithRetry(() => this.client.get(url));
|
|
1628
|
+
};
|
|
1629
|
+
// Use pagination utility to fetch all pages
|
|
1630
|
+
const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
|
|
1631
|
+
maxPages,
|
|
1632
|
+
logProgress: true
|
|
1633
|
+
});
|
|
1634
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
|
|
1635
|
+
method: 'getRecurringTasks',
|
|
1636
|
+
totalCount: paginatedResult.totalFetched,
|
|
1637
|
+
pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
|
|
1638
|
+
hasMore: paginatedResult.hasMore,
|
|
1639
|
+
workspaceId
|
|
1640
|
+
});
|
|
1641
|
+
return paginatedResult.items;
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
|
|
1645
|
+
method: 'getRecurringTasks',
|
|
1646
|
+
error: getErrorMessage(error),
|
|
1647
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1648
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1649
|
+
workspaceId
|
|
1650
|
+
});
|
|
1651
|
+
throw this.formatApiError(error, 'fetch recurring tasks');
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Create a new recurring task
|
|
1657
|
+
* @param taskData - Data for creating the recurring task
|
|
1658
|
+
* @returns The created recurring task
|
|
1659
|
+
*/
|
|
1660
|
+
async createRecurringTask(taskData) {
|
|
1661
|
+
try {
|
|
1662
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating recurring task in Motion API', {
|
|
1663
|
+
method: 'createRecurringTask',
|
|
1664
|
+
name: taskData.name,
|
|
1665
|
+
assigneeId: taskData.assigneeId,
|
|
1666
|
+
frequency: taskData.frequency.type,
|
|
1667
|
+
workspaceId: taskData.workspaceId
|
|
1668
|
+
});
|
|
1669
|
+
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
1670
|
+
const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
|
|
1671
|
+
const response = await this.requestWithRetry(() => this.client.post('/recurring-tasks', minimalPayload));
|
|
1672
|
+
// Invalidate cache after successful creation
|
|
1673
|
+
this.recurringTaskCache.invalidate('recurring-tasks:');
|
|
1674
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task created successfully', {
|
|
1675
|
+
method: 'createRecurringTask',
|
|
1676
|
+
taskId: response.data?.id,
|
|
1677
|
+
name: taskData.name
|
|
1678
|
+
});
|
|
1679
|
+
return response.data;
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create recurring task', {
|
|
1683
|
+
method: 'createRecurringTask',
|
|
1684
|
+
error: getErrorMessage(error),
|
|
1685
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1686
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1687
|
+
taskName: taskData?.name
|
|
1688
|
+
});
|
|
1689
|
+
throw this.formatApiError(error, 'create recurring task');
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Delete a recurring task
|
|
1694
|
+
* @param recurringTaskId - ID of the recurring task to delete
|
|
1695
|
+
* @returns Success indicator
|
|
1696
|
+
*/
|
|
1697
|
+
async deleteRecurringTask(recurringTaskId) {
|
|
1698
|
+
try {
|
|
1699
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting recurring task from Motion API', {
|
|
1700
|
+
method: 'deleteRecurringTask',
|
|
1701
|
+
recurringTaskId
|
|
1702
|
+
});
|
|
1703
|
+
await this.requestWithRetry(() => this.client.delete(`/recurring-tasks/${recurringTaskId}`));
|
|
1704
|
+
// Invalidate cache after successful deletion
|
|
1705
|
+
this.recurringTaskCache.invalidate('recurring-tasks:');
|
|
1706
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task deleted successfully', {
|
|
1707
|
+
method: 'deleteRecurringTask',
|
|
1708
|
+
recurringTaskId
|
|
1709
|
+
});
|
|
1710
|
+
return { success: true };
|
|
1711
|
+
}
|
|
1712
|
+
catch (error) {
|
|
1713
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete recurring task', {
|
|
1714
|
+
method: 'deleteRecurringTask',
|
|
1715
|
+
error: getErrorMessage(error),
|
|
1716
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1717
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1718
|
+
recurringTaskId
|
|
1719
|
+
});
|
|
1720
|
+
throw this.formatApiError(error, 'delete recurring task');
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Get available schedule names for auto-scheduling
|
|
1725
|
+
* @param workspaceId - Optional workspace ID to filter schedules (currently unused by Motion API)
|
|
1726
|
+
* @returns Array of schedule names
|
|
1727
|
+
*/
|
|
1728
|
+
async getAvailableScheduleNames(workspaceId) {
|
|
1729
|
+
try {
|
|
1730
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching available schedule names', {
|
|
1731
|
+
method: 'getAvailableScheduleNames',
|
|
1732
|
+
workspaceId
|
|
1733
|
+
});
|
|
1734
|
+
// Fetch all schedules without filters to get available schedule templates
|
|
1735
|
+
const schedules = await this.getSchedules();
|
|
1736
|
+
const scheduleNames = schedules.map(schedule => schedule.name).filter(Boolean);
|
|
1737
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Available schedule names fetched successfully', {
|
|
1738
|
+
method: 'getAvailableScheduleNames',
|
|
1739
|
+
count: scheduleNames.length,
|
|
1740
|
+
scheduleNames
|
|
1741
|
+
});
|
|
1742
|
+
return scheduleNames;
|
|
1743
|
+
}
|
|
1744
|
+
catch (error) {
|
|
1745
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch available schedule names', {
|
|
1746
|
+
method: 'getAvailableScheduleNames',
|
|
1747
|
+
error: getErrorMessage(error),
|
|
1748
|
+
workspaceId
|
|
1749
|
+
});
|
|
1750
|
+
throw this.formatApiError(error, 'fetch available schedule names');
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Fetch schedules from Motion API
|
|
1755
|
+
* @param userId - Optional user ID to filter schedules
|
|
1756
|
+
* @param startDate - Optional start date (ISO 8601) to filter schedules
|
|
1757
|
+
* @param endDate - Optional end date (ISO 8601) to filter schedules
|
|
1758
|
+
* @returns Array of schedules
|
|
1759
|
+
*/
|
|
1760
|
+
async getSchedules(userId, startDate, endDate) {
|
|
1761
|
+
// Use JSON.stringify for deterministic cache key generation to avoid collisions
|
|
1762
|
+
const cacheParams = { userId: userId || null, startDate: startDate || null, endDate: endDate || null };
|
|
1763
|
+
const cacheKey = `schedules:${JSON.stringify(cacheParams)}`;
|
|
1764
|
+
return this.scheduleCache.withCache(cacheKey, async () => {
|
|
1765
|
+
try {
|
|
1766
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching schedules from Motion API', {
|
|
1767
|
+
method: 'getSchedules',
|
|
1768
|
+
userId,
|
|
1769
|
+
startDate,
|
|
1770
|
+
endDate
|
|
1771
|
+
});
|
|
1772
|
+
const params = new URLSearchParams();
|
|
1773
|
+
if (userId)
|
|
1774
|
+
params.append('userId', userId);
|
|
1775
|
+
if (startDate)
|
|
1776
|
+
params.append('startDate', startDate);
|
|
1777
|
+
if (endDate)
|
|
1778
|
+
params.append('endDate', endDate);
|
|
1779
|
+
const queryString = params.toString();
|
|
1780
|
+
const url = queryString ? `/schedules?${queryString}` : '/schedules';
|
|
1781
|
+
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
1782
|
+
// Validate response against schema
|
|
1783
|
+
const validatedResponse = this.validateResponse(response.data, motion_1.SchedulesListResponseSchema, 'getSchedules');
|
|
1784
|
+
// Handle both wrapped and unwrapped responses
|
|
1785
|
+
const schedules = Array.isArray(validatedResponse)
|
|
1786
|
+
? validatedResponse
|
|
1787
|
+
: validatedResponse.schedules || [];
|
|
1788
|
+
const schedulesArray = Array.isArray(schedules) ? schedules : [];
|
|
1789
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Schedules fetched successfully', {
|
|
1790
|
+
method: 'getSchedules',
|
|
1791
|
+
count: schedulesArray.length,
|
|
1792
|
+
userId,
|
|
1793
|
+
startDate,
|
|
1794
|
+
endDate
|
|
1795
|
+
});
|
|
1796
|
+
return schedulesArray;
|
|
1797
|
+
}
|
|
1798
|
+
catch (error) {
|
|
1799
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch schedules', {
|
|
1800
|
+
method: 'getSchedules',
|
|
1801
|
+
error: getErrorMessage(error),
|
|
1802
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1803
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1804
|
+
userId,
|
|
1805
|
+
startDate,
|
|
1806
|
+
endDate
|
|
1807
|
+
});
|
|
1808
|
+
throw this.formatApiError(error, 'fetch schedules');
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Retrieves available workflow statuses from Motion
|
|
1814
|
+
* @param workspaceId - Optional workspace ID to filter statuses
|
|
1815
|
+
* @returns Promise resolving to array of Motion statuses
|
|
1816
|
+
* @throws {Error} If the API request fails
|
|
1817
|
+
*/
|
|
1818
|
+
async getStatuses(workspaceId) {
|
|
1819
|
+
// Use workspace ID for cache key, or 'all' if not specified
|
|
1820
|
+
const cacheKey = workspaceId ? `statuses:workspace:${workspaceId}` : 'statuses:all';
|
|
1821
|
+
return this.statusCache.withCache(cacheKey, async () => {
|
|
1822
|
+
try {
|
|
1823
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching statuses from Motion API', {
|
|
1824
|
+
method: 'getStatuses',
|
|
1825
|
+
workspaceId
|
|
1826
|
+
});
|
|
1827
|
+
const params = new URLSearchParams();
|
|
1828
|
+
if (workspaceId)
|
|
1829
|
+
params.append('workspaceId', workspaceId);
|
|
1830
|
+
const queryString = params.toString();
|
|
1831
|
+
const url = queryString ? `/statuses?${queryString}` : '/statuses';
|
|
1832
|
+
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
1833
|
+
// Validate response against schema
|
|
1834
|
+
const validatedResponse = this.validateResponse(response.data, motion_1.StatusesListResponseSchema, 'statuses');
|
|
1835
|
+
// Extract statuses from validated response
|
|
1836
|
+
const statuses = Array.isArray(validatedResponse)
|
|
1837
|
+
? validatedResponse
|
|
1838
|
+
: ('statuses' in validatedResponse && Array.isArray(validatedResponse.statuses))
|
|
1839
|
+
? validatedResponse.statuses
|
|
1840
|
+
: [];
|
|
1841
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Statuses fetched successfully', {
|
|
1842
|
+
method: 'getStatuses',
|
|
1843
|
+
count: statuses.length,
|
|
1844
|
+
workspaceId
|
|
1845
|
+
});
|
|
1846
|
+
return statuses;
|
|
1847
|
+
}
|
|
1848
|
+
catch (error) {
|
|
1849
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch statuses', {
|
|
1850
|
+
method: 'getStatuses',
|
|
1851
|
+
error: getErrorMessage(error),
|
|
1852
|
+
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1853
|
+
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1854
|
+
workspaceId
|
|
1855
|
+
});
|
|
1856
|
+
throw this.formatApiError(error, 'fetch statuses');
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Get all uncompleted tasks across all workspaces and projects
|
|
1862
|
+
* Filters tasks where status.isResolvedStatus is false or undefined
|
|
1863
|
+
*/
|
|
1864
|
+
async getAllUncompletedTasks(limit) {
|
|
1865
|
+
try {
|
|
1866
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching all uncompleted tasks across workspaces', {
|
|
1867
|
+
method: 'getAllUncompletedTasks',
|
|
1868
|
+
limit
|
|
1869
|
+
});
|
|
1870
|
+
// Apply limit to prevent resource exhaustion
|
|
1871
|
+
const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
|
|
1872
|
+
const allUncompletedTasks = [];
|
|
1873
|
+
try {
|
|
1874
|
+
// Get all workspaces
|
|
1875
|
+
const workspaces = await this.getWorkspaces();
|
|
1876
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching for uncompleted tasks across workspaces', {
|
|
1877
|
+
method: 'getAllUncompletedTasks',
|
|
1878
|
+
totalWorkspaces: workspaces.length
|
|
1879
|
+
});
|
|
1880
|
+
// Fetch tasks from each workspace
|
|
1881
|
+
for (const workspace of workspaces) {
|
|
1882
|
+
if (allUncompletedTasks.length >= effectiveLimit) {
|
|
1883
|
+
break; // Stop if we've reached the limit
|
|
1884
|
+
}
|
|
1885
|
+
try {
|
|
1886
|
+
// Get all tasks from this workspace (all projects)
|
|
1887
|
+
const workspaceTasks = await this.getTasks({
|
|
1888
|
+
workspaceId: workspace.id,
|
|
1889
|
+
limit: effectiveLimit,
|
|
1890
|
+
maxPages: constants_1.LIMITS.MAX_PAGES
|
|
1891
|
+
});
|
|
1892
|
+
// Filter for uncompleted tasks
|
|
1893
|
+
const uncompletedTasks = workspaceTasks.filter(task => {
|
|
1894
|
+
// Task is uncompleted if status is missing or isResolvedStatus is false
|
|
1895
|
+
if (!task.status)
|
|
1896
|
+
return true; // No status = not resolved
|
|
1897
|
+
if (typeof task.status === 'string')
|
|
1898
|
+
return true; // Simple string status = assume not resolved
|
|
1899
|
+
return !task.status.isResolvedStatus; // Object status with isResolvedStatus false
|
|
1900
|
+
});
|
|
1901
|
+
allUncompletedTasks.push(...uncompletedTasks);
|
|
1902
|
+
if (uncompletedTasks.length > 0) {
|
|
1903
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found uncompleted tasks in workspace', {
|
|
1904
|
+
method: 'getAllUncompletedTasks',
|
|
1905
|
+
workspaceId: workspace.id,
|
|
1906
|
+
workspaceName: workspace.name,
|
|
1907
|
+
uncompletedTasks: uncompletedTasks.length,
|
|
1908
|
+
totalTasks: workspaceTasks.length
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
catch (workspaceError) {
|
|
1913
|
+
// Log error but continue with other workspaces
|
|
1914
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch tasks from workspace', {
|
|
1915
|
+
method: 'getAllUncompletedTasks',
|
|
1916
|
+
workspaceId: workspace.id,
|
|
1917
|
+
workspaceName: workspace.name,
|
|
1918
|
+
error: getErrorMessage(workspaceError)
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
catch (workspaceListError) {
|
|
1924
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to get workspace list', {
|
|
1925
|
+
method: 'getAllUncompletedTasks',
|
|
1926
|
+
error: getErrorMessage(workspaceListError)
|
|
1927
|
+
});
|
|
1928
|
+
throw workspaceListError;
|
|
1929
|
+
}
|
|
1930
|
+
// Apply final limit and return results
|
|
1931
|
+
const finalResults = allUncompletedTasks.slice(0, effectiveLimit);
|
|
1932
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All uncompleted tasks fetched successfully', {
|
|
1933
|
+
method: 'getAllUncompletedTasks',
|
|
1934
|
+
totalFound: allUncompletedTasks.length,
|
|
1935
|
+
returned: finalResults.length,
|
|
1936
|
+
limit: effectiveLimit
|
|
1937
|
+
});
|
|
1938
|
+
return finalResults;
|
|
1939
|
+
}
|
|
1940
|
+
catch (error) {
|
|
1941
|
+
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {
|
|
1942
|
+
method: 'getAllUncompletedTasks',
|
|
1943
|
+
error: getErrorMessage(error)
|
|
1944
|
+
});
|
|
1945
|
+
throw error;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
exports.MotionApiService = MotionApiService;
|
|
1950
|
+
//# sourceMappingURL=motionApi.js.map
|