motionmcp 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/handlers/CustomFieldHandler.d.ts.map +1 -1
- package/dist/handlers/CustomFieldHandler.js +18 -2
- package/dist/handlers/CustomFieldHandler.js.map +1 -1
- package/dist/handlers/HandlerFactory.d.ts +0 -5
- package/dist/handlers/HandlerFactory.d.ts.map +1 -1
- package/dist/handlers/HandlerFactory.js +0 -15
- package/dist/handlers/HandlerFactory.js.map +1 -1
- package/dist/handlers/RecurringTaskHandler.d.ts.map +1 -1
- package/dist/handlers/RecurringTaskHandler.js +7 -2
- package/dist/handlers/RecurringTaskHandler.js.map +1 -1
- package/dist/handlers/TaskHandler.d.ts.map +1 -1
- package/dist/handlers/TaskHandler.js +7 -0
- package/dist/handlers/TaskHandler.js.map +1 -1
- package/dist/mcp-server.js +3 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/services/api/ApiClient.d.ts +22 -0
- package/dist/services/api/ApiClient.d.ts.map +1 -0
- package/dist/services/api/ApiClient.js +198 -0
- package/dist/services/api/ApiClient.js.map +1 -0
- package/dist/services/api/CacheManager.d.ts +21 -0
- package/dist/services/api/CacheManager.d.ts.map +1 -0
- package/dist/services/api/CacheManager.js +25 -0
- package/dist/services/api/CacheManager.js.map +1 -0
- package/dist/services/api/comments.d.ts +8 -0
- package/dist/services/api/comments.d.ts.map +1 -0
- package/dist/services/api/comments.js +86 -0
- package/dist/services/api/comments.js.map +1 -0
- package/dist/services/api/customFields.d.ts +21 -0
- package/dist/services/api/customFields.d.ts.map +1 -0
- package/dist/services/api/customFields.js +277 -0
- package/dist/services/api/customFields.js.map +1 -0
- package/dist/services/api/index.d.ts +22 -0
- package/dist/services/api/index.d.ts.map +1 -0
- package/dist/services/api/index.js +64 -0
- package/dist/services/api/index.js.map +1 -0
- package/dist/services/api/projects.d.ts +18 -0
- package/dist/services/api/projects.d.ts.map +1 -0
- package/dist/services/api/projects.js +346 -0
- package/dist/services/api/projects.js.map +1 -0
- package/dist/services/api/recurringTasks.d.ts +15 -0
- package/dist/services/api/recurringTasks.d.ts.map +1 -0
- package/dist/services/api/recurringTasks.js +143 -0
- package/dist/services/api/recurringTasks.js.map +1 -0
- package/dist/services/api/schedules.d.ts +8 -0
- package/dist/services/api/schedules.d.ts.map +1 -0
- package/dist/services/api/schedules.js +70 -0
- package/dist/services/api/schedules.js.map +1 -0
- package/dist/services/api/search.d.ts +29 -0
- package/dist/services/api/search.d.ts.map +1 -0
- package/dist/services/api/search.js +415 -0
- package/dist/services/api/search.js.map +1 -0
- package/dist/services/api/statuses.d.ts +7 -0
- package/dist/services/api/statuses.d.ts.map +1 -0
- package/dist/services/api/statuses.js +67 -0
- package/dist/services/api/statuses.js.map +1 -0
- package/dist/services/api/tasks.d.ts +34 -0
- package/dist/services/api/tasks.d.ts.map +1 -0
- package/dist/services/api/tasks.js +474 -0
- package/dist/services/api/tasks.js.map +1 -0
- package/dist/services/api/types.d.ts +54 -0
- package/dist/services/api/types.d.ts.map +1 -0
- package/dist/services/api/types.js +9 -0
- package/dist/services/api/types.js.map +1 -0
- package/dist/services/api/users.d.ts +8 -0
- package/dist/services/api/users.d.ts.map +1 -0
- package/dist/services/api/users.js +93 -0
- package/dist/services/api/users.js.map +1 -0
- package/dist/services/api/workspaces.d.ts +7 -0
- package/dist/services/api/workspaces.d.ts.map +1 -0
- package/dist/services/api/workspaces.js +59 -0
- package/dist/services/api/workspaces.js.map +1 -0
- package/dist/services/motionApi.d.ts +34 -180
- package/dist/services/motionApi.d.ts.map +1 -1
- package/dist/services/motionApi.js +84 -2163
- package/dist/services/motionApi.js.map +1 -1
- package/dist/tools/ToolConfigurator.d.ts +1 -7
- package/dist/tools/ToolConfigurator.d.ts.map +1 -1
- package/dist/tools/ToolConfigurator.js +0 -31
- package/dist/tools/ToolConfigurator.js.map +1 -1
- package/dist/tools/ToolDefinitions.js +1 -1
- package/dist/tools/ToolDefinitions.js.map +1 -1
- package/dist/tools/ToolRegistry.d.ts +1 -22
- package/dist/tools/ToolRegistry.d.ts.map +1 -1
- package/dist/tools/ToolRegistry.js +0 -31
- package/dist/tools/ToolRegistry.js.map +1 -1
- package/dist/types/motion.d.ts +0 -7
- package/dist/types/motion.d.ts.map +1 -1
- package/dist/utils/cache.d.ts +0 -5
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +0 -8
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/constants.d.ts +4 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +7 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/errorHandling.d.ts +0 -12
- package/dist/utils/errorHandling.d.ts.map +1 -1
- package/dist/utils/errorHandling.js +2 -23
- package/dist/utils/errorHandling.js.map +1 -1
- package/dist/utils/errors.d.ts +89 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +276 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +6 -5
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/parameterUtils.d.ts +10 -0
- package/dist/utils/parameterUtils.d.ts.map +1 -1
- package/dist/utils/parameterUtils.js +61 -5
- package/dist/utils/parameterUtils.js.map +1 -1
- package/dist/utils/responseFormatters.js +16 -16
- package/dist/utils/responseFormatters.js.map +1 -1
- package/dist/utils/serverInstructions.d.ts +2 -0
- package/dist/utils/serverInstructions.d.ts.map +1 -0
- package/dist/utils/serverInstructions.js +21 -0
- package/dist/utils/serverInstructions.js.map +1 -0
- package/dist/utils/userFacingErrors.d.ts +0 -8
- package/dist/utils/userFacingErrors.d.ts.map +1 -1
- package/dist/utils/userFacingErrors.js +0 -19
- package/dist/utils/userFacingErrors.js.map +1 -1
- package/dist/utils/workspaceResolver.js +12 -12
- package/dist/utils/workspaceResolver.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,2254 +1,175 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
})();
|
|
2
|
+
/**
|
|
3
|
+
* MotionApiService — thin facade that delegates to resource modules.
|
|
4
|
+
*
|
|
5
|
+
* All business logic lives in `src/services/api/*.ts` modules. This class
|
|
6
|
+
* preserves the original public API surface so existing consumers (handlers,
|
|
7
|
+
* tests) don't need to change.
|
|
8
|
+
*/
|
|
35
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
10
|
exports.MotionApiService = void 0;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (error instanceof Error) {
|
|
51
|
-
return error.message;
|
|
52
|
-
}
|
|
53
|
-
return String(error);
|
|
54
|
-
}
|
|
11
|
+
const ApiClient_1 = require("./api/ApiClient");
|
|
12
|
+
const CacheManager_1 = require("./api/CacheManager");
|
|
13
|
+
// Resource module imports
|
|
14
|
+
const workspaces_1 = require("./api/workspaces");
|
|
15
|
+
const users_1 = require("./api/users");
|
|
16
|
+
const tasks_1 = require("./api/tasks");
|
|
17
|
+
const projects_1 = require("./api/projects");
|
|
18
|
+
const comments_1 = require("./api/comments");
|
|
19
|
+
const customFields_1 = require("./api/customFields");
|
|
20
|
+
const recurringTasks_1 = require("./api/recurringTasks");
|
|
21
|
+
const schedules_1 = require("./api/schedules");
|
|
22
|
+
const statuses_1 = require("./api/statuses");
|
|
23
|
+
const search_1 = require("./api/search");
|
|
55
24
|
class MotionApiService {
|
|
56
|
-
/**
|
|
57
|
-
* Validate API response against schema
|
|
58
|
-
* Handles strict/lenient/off modes based on configuration
|
|
59
|
-
*/
|
|
60
|
-
validateResponse(data, schema, context) {
|
|
61
|
-
if (motion_1.VALIDATION_CONFIG.mode === 'off') {
|
|
62
|
-
return data;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
return schema.parse(data);
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
if (error instanceof zod_1.z.ZodError) {
|
|
69
|
-
const errorDetails = {
|
|
70
|
-
context,
|
|
71
|
-
validationErrors: error.issues,
|
|
72
|
-
...(motion_1.VALIDATION_CONFIG.includeDataInLogs ? { receivedData: data } : {})
|
|
73
|
-
};
|
|
74
|
-
if (motion_1.VALIDATION_CONFIG.logErrors) {
|
|
75
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `API response validation failed for ${context}`, errorDetails);
|
|
76
|
-
}
|
|
77
|
-
if (motion_1.VALIDATION_CONFIG.mode === 'strict') {
|
|
78
|
-
throw new Error(`Invalid API response structure for ${context}: ${error.message}`);
|
|
79
|
-
}
|
|
80
|
-
// Lenient mode: return original data and hope for the best
|
|
81
|
-
return data;
|
|
82
|
-
}
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
25
|
constructor(apiKey) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API key not found in environment variables', {
|
|
90
|
-
component: 'MotionApiService',
|
|
91
|
-
method: 'constructor'
|
|
92
|
-
});
|
|
93
|
-
throw new Error('MOTION_API_KEY environment variable is required');
|
|
94
|
-
}
|
|
95
|
-
this.apiKey = resolvedApiKey;
|
|
96
|
-
this.baseUrl = 'https://api.usemotion.com/v1';
|
|
97
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Initializing Motion API service', {
|
|
98
|
-
component: 'MotionApiService',
|
|
99
|
-
baseUrl: this.baseUrl
|
|
100
|
-
});
|
|
101
|
-
this.client = axios_1.default.create({
|
|
102
|
-
baseURL: this.baseUrl,
|
|
103
|
-
headers: {
|
|
104
|
-
'X-API-Key': this.apiKey,
|
|
105
|
-
'Content-Type': 'application/json'
|
|
106
|
-
},
|
|
107
|
-
timeout: constants_1.API_CONFIG.TIMEOUT_MS
|
|
108
|
-
});
|
|
109
|
-
// Initialize cache instances with TTL from constants (converted to ms)
|
|
110
|
-
this.workspaceCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES);
|
|
111
|
-
this.userCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.USERS);
|
|
112
|
-
this.projectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
|
|
113
|
-
this.singleProjectCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.PROJECTS);
|
|
114
|
-
this.commentCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.COMMENTS);
|
|
115
|
-
this.customFieldCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.CUSTOM_FIELDS);
|
|
116
|
-
this.recurringTaskCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.RECURRING_TASKS);
|
|
117
|
-
this.scheduleCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.SCHEDULES);
|
|
118
|
-
this.statusCache = new cache_1.SimpleCache(constants_1.CACHE_TTL.WORKSPACES); // 10 minutes, like workspaces
|
|
119
|
-
this.client.interceptors.response.use((response) => {
|
|
120
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Motion API response successful', {
|
|
121
|
-
url: response.config?.url,
|
|
122
|
-
method: response.config?.method?.toUpperCase(),
|
|
123
|
-
status: response.status,
|
|
124
|
-
component: 'MotionApiService'
|
|
125
|
-
});
|
|
126
|
-
return response;
|
|
127
|
-
}, (error) => {
|
|
128
|
-
const errorData = error.response?.data;
|
|
129
|
-
const errorDetails = {
|
|
130
|
-
url: error.config?.url,
|
|
131
|
-
method: error.config?.method?.toUpperCase(),
|
|
132
|
-
status: error.response?.status,
|
|
133
|
-
statusText: error.response?.statusText,
|
|
134
|
-
apiMessage: errorData?.message,
|
|
135
|
-
apiCode: errorData?.code,
|
|
136
|
-
errorMessage: error.message,
|
|
137
|
-
component: 'MotionApiService'
|
|
138
|
-
};
|
|
139
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Motion API request failed', errorDetails);
|
|
140
|
-
// Re-throw original AxiosError so requestWithRetry can detect it
|
|
141
|
-
// via axios.isAxiosError() for retry/rate-limit handling
|
|
142
|
-
return Promise.reject(error);
|
|
143
|
-
});
|
|
26
|
+
this._api = new ApiClient_1.ApiClient(apiKey);
|
|
27
|
+
this._cache = new CacheManager_1.CacheManager();
|
|
144
28
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* @param action - Description of the action that failed (e.g., 'fetch', 'create', 'update')
|
|
149
|
-
* @param resourceType - Type of resource being operated on (e.g., 'project', 'task')
|
|
150
|
-
* @param resourceId - Optional ID of the resource
|
|
151
|
-
* @param resourceName - Optional name of the resource
|
|
152
|
-
* @returns UserFacingError with both user-friendly and technical messages
|
|
153
|
-
*/
|
|
154
|
-
formatApiError(error, action, resourceType, resourceId, resourceName) {
|
|
155
|
-
const context = (0, userFacingErrors_1.createErrorContext)(action, resourceType, resourceId, resourceName);
|
|
156
|
-
return (0, userFacingErrors_1.createUserFacingError)(error, context);
|
|
29
|
+
/** ResourceContext for delegating to extracted resource modules. */
|
|
30
|
+
get _ctx() {
|
|
31
|
+
return { api: this._api, cache: this._cache };
|
|
157
32
|
}
|
|
158
33
|
// ========================================
|
|
159
|
-
//
|
|
34
|
+
// WORKSPACE API METHODS
|
|
160
35
|
// ========================================
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
* Retries only safe/read-only requests (GET) on transient failures:
|
|
164
|
-
* - HTTP 5xx
|
|
165
|
-
* - HTTP 429
|
|
166
|
-
* - Network errors with no HTTP response
|
|
167
|
-
*/
|
|
168
|
-
async requestWithRetry(request) {
|
|
169
|
-
for (let attempt = 1; attempt <= constants_1.RETRY_CONFIG.MAX_RETRIES; attempt++) {
|
|
170
|
-
try {
|
|
171
|
-
return await request();
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (!axios_1.default.isAxiosError(error)) {
|
|
175
|
-
throw error; // Not a network error, re-throw immediately
|
|
176
|
-
}
|
|
177
|
-
const status = error.response?.status;
|
|
178
|
-
const requestMethod = error.config?.method?.toUpperCase() || 'GET';
|
|
179
|
-
const isSafeMethod = requestMethod === 'GET';
|
|
180
|
-
const isNetworkError = !error.response;
|
|
181
|
-
const isRetryableStatus = status === 429 || (status !== undefined && status >= 500);
|
|
182
|
-
const isRetryable = isSafeMethod && (isNetworkError || isRetryableStatus);
|
|
183
|
-
if (!isRetryable || attempt === constants_1.RETRY_CONFIG.MAX_RETRIES) {
|
|
184
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, `Request failed and will not be retried`, {
|
|
185
|
-
status,
|
|
186
|
-
httpMethod: requestMethod,
|
|
187
|
-
attempt,
|
|
188
|
-
maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
|
|
189
|
-
isRetryable,
|
|
190
|
-
component: 'MotionApiService',
|
|
191
|
-
method: 'requestWithRetry'
|
|
192
|
-
});
|
|
193
|
-
throw error; // Final attempt failed or error is not retryable
|
|
194
|
-
}
|
|
195
|
-
// Handle Retry-After header for 429
|
|
196
|
-
const retryAfterHeaderRaw = error.response?.headers['retry-after'];
|
|
197
|
-
const retryAfterHeader = Array.isArray(retryAfterHeaderRaw) ? retryAfterHeaderRaw[0] : retryAfterHeaderRaw;
|
|
198
|
-
let delay = 0;
|
|
199
|
-
if (status === 429 && retryAfterHeader) {
|
|
200
|
-
const retryAfterSeconds = Number(retryAfterHeader);
|
|
201
|
-
if (!isNaN(retryAfterSeconds) && retryAfterSeconds >= 0) {
|
|
202
|
-
delay = retryAfterSeconds * 1000;
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
const retryAfterDate = Date.parse(retryAfterHeader);
|
|
206
|
-
if (!isNaN(retryAfterDate)) {
|
|
207
|
-
delay = Math.max(0, retryAfterDate - Date.now());
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// If no Retry-After header, use exponential backoff with jitter
|
|
212
|
-
if (delay === 0) {
|
|
213
|
-
const backoff = constants_1.RETRY_CONFIG.INITIAL_BACKOFF_MS * Math.pow(constants_1.RETRY_CONFIG.BACKOFF_MULTIPLIER, attempt - 1);
|
|
214
|
-
const jitter = backoff * constants_1.RETRY_CONFIG.JITTER_FACTOR * Math.random();
|
|
215
|
-
delay = Math.min(backoff + jitter, constants_1.RETRY_CONFIG.MAX_BACKOFF_MS);
|
|
216
|
-
}
|
|
217
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, `Request failed, retrying`, {
|
|
218
|
-
attempt,
|
|
219
|
-
maxRetries: constants_1.RETRY_CONFIG.MAX_RETRIES,
|
|
220
|
-
delayMs: Math.round(delay),
|
|
221
|
-
error: error.message,
|
|
222
|
-
status,
|
|
223
|
-
httpMethod: requestMethod,
|
|
224
|
-
isNetworkError,
|
|
225
|
-
component: 'MotionApiService',
|
|
226
|
-
method: 'requestWithRetry'
|
|
227
|
-
});
|
|
228
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// Should never reach here, but TypeScript requires a return or throw
|
|
232
|
-
throw new Error('Max retries exceeded');
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Merge truncation metadata from multiple paginated sources without mutating source objects.
|
|
236
|
-
* If sources disagree on reason/limit, drop those fields to avoid misleading aggregate metadata.
|
|
237
|
-
*/
|
|
238
|
-
mergeTruncationMetadata(aggregate, source) {
|
|
239
|
-
if (!source?.wasTruncated) {
|
|
240
|
-
return aggregate;
|
|
241
|
-
}
|
|
242
|
-
if (!aggregate?.wasTruncated) {
|
|
243
|
-
return { ...source };
|
|
244
|
-
}
|
|
245
|
-
const merged = { ...aggregate, wasTruncated: true };
|
|
246
|
-
if (aggregate.reason !== source.reason) {
|
|
247
|
-
delete merged.reason;
|
|
248
|
-
delete merged.limit;
|
|
249
|
-
return merged;
|
|
250
|
-
}
|
|
251
|
-
if (source.reason !== undefined) {
|
|
252
|
-
merged.reason = source.reason;
|
|
253
|
-
}
|
|
254
|
-
if (aggregate.limit !== source.limit) {
|
|
255
|
-
delete merged.limit;
|
|
256
|
-
}
|
|
257
|
-
else if (source.limit !== undefined) {
|
|
258
|
-
merged.limit = source.limit;
|
|
259
|
-
}
|
|
260
|
-
return merged;
|
|
36
|
+
async getWorkspaces(ids) {
|
|
37
|
+
return (0, workspaces_1.getWorkspaces)(this._ctx, ids);
|
|
261
38
|
}
|
|
262
39
|
// ========================================
|
|
263
|
-
//
|
|
40
|
+
// USER API METHODS
|
|
264
41
|
// ========================================
|
|
265
|
-
async
|
|
266
|
-
|
|
267
|
-
// Validate limit parameter if provided
|
|
268
|
-
if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
|
|
269
|
-
throw new Error('limit must be a non-negative integer');
|
|
270
|
-
}
|
|
271
|
-
const cacheKey = `projects:workspace:${workspaceId}:maxPages:${maxPages ?? 'default'}:limit:${limit ?? 'none'}`;
|
|
272
|
-
// Check cache - return items only (no stale truncation info)
|
|
273
|
-
const cachedItems = this.projectCache.get(cacheKey);
|
|
274
|
-
if (cachedItems !== null) {
|
|
275
|
-
return { items: cachedItems };
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from Motion API', {
|
|
279
|
-
method: 'getProjects',
|
|
280
|
-
workspaceId,
|
|
281
|
-
maxPages,
|
|
282
|
-
limit
|
|
283
|
-
});
|
|
284
|
-
// Create a fetch function for potential pagination
|
|
285
|
-
const fetchPage = async (cursor) => {
|
|
286
|
-
const params = new URLSearchParams();
|
|
287
|
-
params.append('workspaceId', workspaceId);
|
|
288
|
-
if (cursor) {
|
|
289
|
-
params.append('cursor', cursor);
|
|
290
|
-
}
|
|
291
|
-
const queryString = params.toString();
|
|
292
|
-
const url = `/projects?${queryString}`;
|
|
293
|
-
return this.requestWithRetry(() => this.client.get(url));
|
|
294
|
-
};
|
|
295
|
-
try {
|
|
296
|
-
// Attempt pagination-aware fetch with new response wrapper
|
|
297
|
-
const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'projects', {
|
|
298
|
-
maxPages,
|
|
299
|
-
logProgress: false,
|
|
300
|
-
...(limit ? { maxItems: limit } : {})
|
|
301
|
-
});
|
|
302
|
-
const projects = paginatedResult.items;
|
|
303
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully with pagination', {
|
|
304
|
-
method: 'getProjects',
|
|
305
|
-
totalCount: projects.length,
|
|
306
|
-
hasMore: paginatedResult.hasMore,
|
|
307
|
-
workspaceId
|
|
308
|
-
});
|
|
309
|
-
// Cache only items, not truncation metadata
|
|
310
|
-
this.projectCache.set(cacheKey, projects);
|
|
311
|
-
return { items: projects, truncation: paginatedResult.truncation };
|
|
312
|
-
}
|
|
313
|
-
catch (paginationError) {
|
|
314
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
|
|
315
|
-
method: 'getProjects',
|
|
316
|
-
error: paginationError instanceof Error ? paginationError.message : String(paginationError)
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
// Use new response wrapper for single page fallback
|
|
320
|
-
const response = await fetchPage();
|
|
321
|
-
const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'projects');
|
|
322
|
-
let projects = unwrapped.data;
|
|
323
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Projects fetched successfully (single page)', {
|
|
324
|
-
method: 'getProjects',
|
|
325
|
-
count: projects.length,
|
|
326
|
-
workspaceId
|
|
327
|
-
});
|
|
328
|
-
// Do not cache fallback results. If pagination failed, this may be only the first page.
|
|
329
|
-
return { items: projects };
|
|
330
|
-
}
|
|
331
|
-
catch (error) {
|
|
332
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects', {
|
|
333
|
-
method: 'getProjects',
|
|
334
|
-
error: getErrorMessage(error),
|
|
335
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
336
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
337
|
-
});
|
|
338
|
-
throw this.formatApiError(error, 'fetch', 'project');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
async getAllProjects() {
|
|
342
|
-
try {
|
|
343
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching projects from all workspaces', {
|
|
344
|
-
method: 'getAllProjects'
|
|
345
|
-
});
|
|
346
|
-
const allWorkspaces = await this.getWorkspaces();
|
|
347
|
-
const allProjects = [];
|
|
348
|
-
let aggregateTruncation;
|
|
349
|
-
for (const workspace of allWorkspaces) {
|
|
350
|
-
try {
|
|
351
|
-
const { items, truncation } = await this.getProjects(workspace.id);
|
|
352
|
-
allProjects.push(...items);
|
|
353
|
-
aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, truncation);
|
|
354
|
-
}
|
|
355
|
-
catch (workspaceError) {
|
|
356
|
-
// Log error but continue with other workspaces
|
|
357
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch projects from workspace', {
|
|
358
|
-
method: 'getAllProjects',
|
|
359
|
-
workspaceId: workspace.id,
|
|
360
|
-
workspaceName: workspace.name,
|
|
361
|
-
error: getErrorMessage(workspaceError)
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All projects fetched successfully', {
|
|
366
|
-
method: 'getAllProjects',
|
|
367
|
-
totalProjects: allProjects.length,
|
|
368
|
-
workspaceCount: allWorkspaces.length
|
|
369
|
-
});
|
|
370
|
-
if (aggregateTruncation) {
|
|
371
|
-
aggregateTruncation.returnedCount = allProjects.length;
|
|
372
|
-
}
|
|
373
|
-
return { items: allProjects, truncation: aggregateTruncation };
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch projects from all workspaces', {
|
|
377
|
-
method: 'getAllProjects',
|
|
378
|
-
error: getErrorMessage(error)
|
|
379
|
-
});
|
|
380
|
-
throw this.formatApiError(error, 'fetch', 'project');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
async getProject(projectId) {
|
|
384
|
-
const cacheKey = `project:${projectId}`;
|
|
385
|
-
return this.singleProjectCache.withCache(cacheKey, async () => {
|
|
386
|
-
try {
|
|
387
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single project from Motion API', {
|
|
388
|
-
method: 'getProject',
|
|
389
|
-
projectId
|
|
390
|
-
});
|
|
391
|
-
const response = await this.requestWithRetry(() => this.client.get(`/projects/${projectId}`));
|
|
392
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched project', {
|
|
393
|
-
method: 'getProject',
|
|
394
|
-
projectId,
|
|
395
|
-
projectName: response.data.name
|
|
396
|
-
});
|
|
397
|
-
return response.data;
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch project', {
|
|
401
|
-
method: 'getProject',
|
|
402
|
-
projectId,
|
|
403
|
-
error: getErrorMessage(error),
|
|
404
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
405
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
406
|
-
});
|
|
407
|
-
throw this.formatApiError(error, 'fetch', 'project', projectId);
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
async createProject(projectData) {
|
|
412
|
-
try {
|
|
413
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating project in Motion API', {
|
|
414
|
-
method: 'createProject',
|
|
415
|
-
projectName: projectData.name,
|
|
416
|
-
workspaceId: projectData.workspaceId
|
|
417
|
-
});
|
|
418
|
-
if (!projectData.workspaceId) {
|
|
419
|
-
throw new Error('Workspace ID is required to create a project');
|
|
420
|
-
}
|
|
421
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
422
|
-
const minimalPayload = (0, constants_1.createMinimalPayload)(projectData);
|
|
423
|
-
// Debug logging: log the exact payload being sent to API
|
|
424
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for project creation', {
|
|
425
|
-
method: 'createProject',
|
|
426
|
-
payload: JSON.stringify(minimalPayload, null, 2)
|
|
427
|
-
});
|
|
428
|
-
const response = await this.requestWithRetry(() => this.client.post('/projects', minimalPayload));
|
|
429
|
-
// Invalidate cache after successful creation
|
|
430
|
-
this.projectCache.invalidate(`projects:workspace:${projectData.workspaceId}`);
|
|
431
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project created successfully', {
|
|
432
|
-
method: 'createProject',
|
|
433
|
-
projectId: response.data.id,
|
|
434
|
-
projectName: response.data.name
|
|
435
|
-
});
|
|
436
|
-
return response.data;
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create project', {
|
|
440
|
-
method: 'createProject',
|
|
441
|
-
error: getErrorMessage(error),
|
|
442
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
443
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
444
|
-
fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
|
|
445
|
-
});
|
|
446
|
-
throw this.formatApiError(error, 'create', 'project', undefined, projectData.name);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
// Note: Project update/delete are not in the public API docs but appear to be functional
|
|
450
|
-
async updateProject(projectId, updates) {
|
|
451
|
-
try {
|
|
452
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating project in Motion API', {
|
|
453
|
-
method: 'updateProject',
|
|
454
|
-
projectId,
|
|
455
|
-
updates: Object.keys(updates)
|
|
456
|
-
});
|
|
457
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
458
|
-
const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
|
|
459
|
-
const response = await this.requestWithRetry(() => this.client.patch(`/projects/${projectId}`, minimalUpdates));
|
|
460
|
-
// Invalidate cache after successful update
|
|
461
|
-
this.projectCache.invalidate(`projects:workspace:${response.data.workspaceId}`);
|
|
462
|
-
this.singleProjectCache.invalidate(`project:${projectId}`);
|
|
463
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project updated successfully', {
|
|
464
|
-
method: 'updateProject',
|
|
465
|
-
projectId,
|
|
466
|
-
projectName: response.data.name
|
|
467
|
-
});
|
|
468
|
-
return response.data;
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update project', {
|
|
472
|
-
method: 'updateProject',
|
|
473
|
-
projectId,
|
|
474
|
-
error: getErrorMessage(error),
|
|
475
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
476
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
477
|
-
});
|
|
478
|
-
throw this.formatApiError(error, 'update', 'project', projectId);
|
|
479
|
-
}
|
|
42
|
+
async getUsers(workspaceId, teamId) {
|
|
43
|
+
return (0, users_1.getUsers)(this._ctx, workspaceId, teamId);
|
|
480
44
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
try {
|
|
484
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting project from Motion API', {
|
|
485
|
-
method: 'deleteProject',
|
|
486
|
-
projectId
|
|
487
|
-
});
|
|
488
|
-
await this.requestWithRetry(() => this.client.delete(`/projects/${projectId}`));
|
|
489
|
-
// Invalidate all project caches since we don't know the workspace ID
|
|
490
|
-
this.projectCache.invalidate();
|
|
491
|
-
this.singleProjectCache.invalidate(`project:${projectId}`);
|
|
492
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project deleted successfully', {
|
|
493
|
-
method: 'deleteProject',
|
|
494
|
-
projectId
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
catch (error) {
|
|
498
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete project', {
|
|
499
|
-
method: 'deleteProject',
|
|
500
|
-
projectId,
|
|
501
|
-
error: getErrorMessage(error),
|
|
502
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
503
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
504
|
-
});
|
|
505
|
-
throw this.formatApiError(error, 'delete', 'project', projectId);
|
|
506
|
-
}
|
|
45
|
+
async getCurrentUser() {
|
|
46
|
+
return (0, users_1.getCurrentUser)(this._ctx);
|
|
507
47
|
}
|
|
508
48
|
// ========================================
|
|
509
49
|
// TASK API METHODS
|
|
510
50
|
// ========================================
|
|
511
51
|
async getTasks(options) {
|
|
512
|
-
|
|
513
|
-
// Validate limit parameter if provided
|
|
514
|
-
if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
|
|
515
|
-
throw new Error('limit must be a non-negative integer');
|
|
516
|
-
}
|
|
517
|
-
try {
|
|
518
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching tasks from Motion API', {
|
|
519
|
-
method: 'getTasks',
|
|
520
|
-
workspaceId,
|
|
521
|
-
projectId,
|
|
522
|
-
status,
|
|
523
|
-
includeAllStatuses,
|
|
524
|
-
assigneeId,
|
|
525
|
-
priority,
|
|
526
|
-
dueDate,
|
|
527
|
-
labelsCount: labels?.length,
|
|
528
|
-
maxPages
|
|
529
|
-
});
|
|
530
|
-
// Client-side filters for params not supported by the API
|
|
531
|
-
const applyClientFilters = (tasks) => {
|
|
532
|
-
let filtered = tasks;
|
|
533
|
-
if (priority) {
|
|
534
|
-
filtered = filtered.filter(t => t.priority === priority);
|
|
535
|
-
}
|
|
536
|
-
if (dueDate) {
|
|
537
|
-
// Compare date portion only (YYYY-MM-DD)
|
|
538
|
-
filtered = filtered.filter(t => {
|
|
539
|
-
if (!t.dueDate)
|
|
540
|
-
return false;
|
|
541
|
-
const taskDate = t.dueDate.substring(0, 10);
|
|
542
|
-
return taskDate <= dueDate;
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
return filtered;
|
|
546
|
-
};
|
|
547
|
-
// Create a fetch function for potential pagination
|
|
548
|
-
const fetchPage = async (cursor) => {
|
|
549
|
-
const params = new URLSearchParams();
|
|
550
|
-
if (workspaceId) {
|
|
551
|
-
params.append('workspaceId', workspaceId);
|
|
552
|
-
}
|
|
553
|
-
if (projectId) {
|
|
554
|
-
params.append('projectId', projectId);
|
|
555
|
-
}
|
|
556
|
-
if (status) {
|
|
557
|
-
if (Array.isArray(status)) {
|
|
558
|
-
// Deduplicate and skip empty strings before appending
|
|
559
|
-
// Note: Motion API supports repeated status= params for multi-value filtering
|
|
560
|
-
const uniqueStatuses = Array.from(new Set(status));
|
|
561
|
-
for (const s of uniqueStatuses) {
|
|
562
|
-
if (s) {
|
|
563
|
-
params.append('status', s);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
params.append('status', status);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
if (includeAllStatuses) {
|
|
572
|
-
params.append('includeAllStatuses', 'true');
|
|
573
|
-
}
|
|
574
|
-
if (assigneeId) {
|
|
575
|
-
params.append('assigneeId', assigneeId);
|
|
576
|
-
}
|
|
577
|
-
// Note: priority and dueDate are NOT valid API query params — filtered client-side after fetch
|
|
578
|
-
if (name) {
|
|
579
|
-
params.append('name', name);
|
|
580
|
-
}
|
|
581
|
-
if (labels && labels.length > 0) {
|
|
582
|
-
// API accepts 'label' (singular) as a string parameter
|
|
583
|
-
for (const label of labels) {
|
|
584
|
-
if (label) {
|
|
585
|
-
params.append('label', label);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (cursor) {
|
|
590
|
-
params.append('cursor', cursor);
|
|
591
|
-
}
|
|
592
|
-
const queryString = params.toString();
|
|
593
|
-
const url = queryString ? `/tasks?${queryString}` : '/tasks';
|
|
594
|
-
return this.requestWithRetry(() => this.client.get(url));
|
|
595
|
-
};
|
|
596
|
-
try {
|
|
597
|
-
// When client-side filters are active, don't cap pagination with maxItems
|
|
598
|
-
// because valid matches may exist beyond the first batch. Fetch all pages
|
|
599
|
-
// and apply the limit after filtering instead.
|
|
600
|
-
const hasClientFilters = Boolean(priority || dueDate);
|
|
601
|
-
const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'tasks', {
|
|
602
|
-
maxPages,
|
|
603
|
-
logProgress: false, // Less verbose for tasks
|
|
604
|
-
...(!hasClientFilters && limit ? { maxItems: limit } : {})
|
|
605
|
-
});
|
|
606
|
-
let filteredItems = applyClientFilters(paginatedResult.items);
|
|
607
|
-
let truncation = paginatedResult.truncation;
|
|
608
|
-
// When client-side filters reduced the result set and pagination was also truncated,
|
|
609
|
-
// update the truncation info so the notice accurately reflects what the user sees
|
|
610
|
-
if (hasClientFilters && filteredItems.length < paginatedResult.items.length && truncation?.wasTruncated) {
|
|
611
|
-
truncation = {
|
|
612
|
-
...truncation,
|
|
613
|
-
clientFiltered: true,
|
|
614
|
-
fetchedCount: paginatedResult.items.length,
|
|
615
|
-
returnedCount: filteredItems.length,
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
// Apply limit after client-side filtering
|
|
619
|
-
if (hasClientFilters && limit && limit > 0 && filteredItems.length > limit) {
|
|
620
|
-
truncation = {
|
|
621
|
-
wasTruncated: true,
|
|
622
|
-
returnedCount: limit,
|
|
623
|
-
reason: 'max_items',
|
|
624
|
-
limit,
|
|
625
|
-
};
|
|
626
|
-
filteredItems = filteredItems.slice(0, limit);
|
|
627
|
-
}
|
|
628
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully with pagination', {
|
|
629
|
-
method: 'getTasks',
|
|
630
|
-
totalCount: paginatedResult.totalFetched,
|
|
631
|
-
returnedCount: filteredItems.length,
|
|
632
|
-
clientFiltered: filteredItems.length !== paginatedResult.items.length,
|
|
633
|
-
hasMore: paginatedResult.hasMore,
|
|
634
|
-
workspaceId,
|
|
635
|
-
projectId,
|
|
636
|
-
limitApplied: limit
|
|
637
|
-
});
|
|
638
|
-
return { items: filteredItems, truncation };
|
|
639
|
-
}
|
|
640
|
-
catch (paginationError) {
|
|
641
|
-
// Fallback to simple fetch if pagination fails
|
|
642
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Pagination failed, falling back to simple fetch', {
|
|
643
|
-
method: 'getTasks',
|
|
644
|
-
error: paginationError instanceof Error ? paginationError.message : String(paginationError)
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
// Use new response wrapper for single page fallback
|
|
648
|
-
const response = await fetchPage();
|
|
649
|
-
const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'tasks');
|
|
650
|
-
let tasks = applyClientFilters(unwrapped.data);
|
|
651
|
-
// Apply limit if specified
|
|
652
|
-
if (limit && limit > 0) {
|
|
653
|
-
tasks = tasks.slice(0, limit);
|
|
654
|
-
}
|
|
655
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Tasks fetched successfully (single page)', {
|
|
656
|
-
method: 'getTasks',
|
|
657
|
-
count: tasks.length,
|
|
658
|
-
workspaceId,
|
|
659
|
-
projectId,
|
|
660
|
-
limitApplied: limit
|
|
661
|
-
});
|
|
662
|
-
return { items: tasks };
|
|
663
|
-
}
|
|
664
|
-
catch (error) {
|
|
665
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch tasks', {
|
|
666
|
-
method: 'getTasks',
|
|
667
|
-
error: getErrorMessage(error),
|
|
668
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
669
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
670
|
-
});
|
|
671
|
-
throw this.formatApiError(error, 'fetch', 'task');
|
|
672
|
-
}
|
|
52
|
+
return (0, tasks_1.getTasks)(this._ctx, options);
|
|
673
53
|
}
|
|
674
54
|
async getTask(taskId) {
|
|
675
|
-
|
|
676
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching single task from Motion API', {
|
|
677
|
-
method: 'getTask',
|
|
678
|
-
taskId
|
|
679
|
-
});
|
|
680
|
-
const response = await this.requestWithRetry(() => this.client.get(`/tasks/${taskId}`));
|
|
681
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Successfully fetched task', {
|
|
682
|
-
method: 'getTask',
|
|
683
|
-
taskId,
|
|
684
|
-
taskName: response.data.name
|
|
685
|
-
});
|
|
686
|
-
return response.data;
|
|
687
|
-
}
|
|
688
|
-
catch (error) {
|
|
689
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch task', {
|
|
690
|
-
method: 'getTask',
|
|
691
|
-
taskId,
|
|
692
|
-
error: getErrorMessage(error),
|
|
693
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
694
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
695
|
-
});
|
|
696
|
-
throw this.formatApiError(error, 'fetch', 'task', taskId);
|
|
697
|
-
}
|
|
55
|
+
return (0, tasks_1.getTask)(this._ctx, taskId);
|
|
698
56
|
}
|
|
699
57
|
async createTask(taskData) {
|
|
700
|
-
|
|
701
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating task in Motion API', {
|
|
702
|
-
method: 'createTask',
|
|
703
|
-
taskName: taskData.name,
|
|
704
|
-
workspaceId: taskData.workspaceId,
|
|
705
|
-
projectId: taskData.projectId
|
|
706
|
-
});
|
|
707
|
-
if (!taskData.workspaceId) {
|
|
708
|
-
throw new Error('Workspace ID is required to create a task');
|
|
709
|
-
}
|
|
710
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
711
|
-
const minimalPayload = (0, constants_1.createMinimalPayload)(taskData);
|
|
712
|
-
// Debug logging: log the exact payload being sent to API
|
|
713
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'API payload for task creation', {
|
|
714
|
-
method: 'createTask',
|
|
715
|
-
payload: JSON.stringify(minimalPayload, null, 2)
|
|
716
|
-
});
|
|
717
|
-
const response = await this.requestWithRetry(() => this.client.post('/tasks', minimalPayload));
|
|
718
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task created successfully', {
|
|
719
|
-
method: 'createTask',
|
|
720
|
-
taskId: response.data.id,
|
|
721
|
-
taskName: response.data.name
|
|
722
|
-
});
|
|
723
|
-
return response.data;
|
|
724
|
-
}
|
|
725
|
-
catch (error) {
|
|
726
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create task', {
|
|
727
|
-
method: 'createTask',
|
|
728
|
-
error: getErrorMessage(error),
|
|
729
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
730
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
731
|
-
fullErrorResponse: (0, axios_1.isAxiosError)(error) ? JSON.stringify(error.response?.data, null, 2) : undefined
|
|
732
|
-
});
|
|
733
|
-
throw this.formatApiError(error, 'create', 'task', undefined, taskData.name);
|
|
734
|
-
}
|
|
58
|
+
return (0, tasks_1.createTask)(this._ctx, taskData);
|
|
735
59
|
}
|
|
736
|
-
// Note: API docs list name and workspaceId as required for PATCH /tasks/{id},
|
|
737
|
-
// but the API appears to accept partial updates without them. Not enforced here.
|
|
738
60
|
async updateTask(taskId, updates) {
|
|
739
|
-
|
|
740
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Updating task in Motion API', {
|
|
741
|
-
method: 'updateTask',
|
|
742
|
-
taskId,
|
|
743
|
-
updates: Object.keys(updates)
|
|
744
|
-
});
|
|
745
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
746
|
-
const minimalUpdates = (0, constants_1.createMinimalPayload)(updates);
|
|
747
|
-
const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}`, minimalUpdates));
|
|
748
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task updated successfully', {
|
|
749
|
-
method: 'updateTask',
|
|
750
|
-
taskId,
|
|
751
|
-
taskName: response.data.name
|
|
752
|
-
});
|
|
753
|
-
return response.data;
|
|
754
|
-
}
|
|
755
|
-
catch (error) {
|
|
756
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to update task', {
|
|
757
|
-
method: 'updateTask',
|
|
758
|
-
taskId,
|
|
759
|
-
error: getErrorMessage(error),
|
|
760
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
761
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
762
|
-
});
|
|
763
|
-
throw this.formatApiError(error, 'update', 'task', taskId);
|
|
764
|
-
}
|
|
61
|
+
return (0, tasks_1.updateTask)(this._ctx, taskId, updates);
|
|
765
62
|
}
|
|
766
63
|
async deleteTask(taskId) {
|
|
767
|
-
|
|
768
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting task from Motion API', {
|
|
769
|
-
method: 'deleteTask',
|
|
770
|
-
taskId
|
|
771
|
-
});
|
|
772
|
-
await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}`));
|
|
773
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task deleted successfully', {
|
|
774
|
-
method: 'deleteTask',
|
|
775
|
-
taskId
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
catch (error) {
|
|
779
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete task', {
|
|
780
|
-
method: 'deleteTask',
|
|
781
|
-
taskId,
|
|
782
|
-
error: getErrorMessage(error),
|
|
783
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
784
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
785
|
-
});
|
|
786
|
-
throw this.formatApiError(error, 'delete', 'task', taskId);
|
|
787
|
-
}
|
|
64
|
+
return (0, tasks_1.deleteTask)(this._ctx, taskId);
|
|
788
65
|
}
|
|
789
66
|
async moveTask(taskId, targetWorkspaceId, assigneeId) {
|
|
790
|
-
|
|
791
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Moving task in Motion API', {
|
|
792
|
-
method: 'moveTask',
|
|
793
|
-
taskId,
|
|
794
|
-
targetWorkspaceId,
|
|
795
|
-
assigneeId
|
|
796
|
-
});
|
|
797
|
-
// API requires workspaceId, optionally accepts assigneeId
|
|
798
|
-
const moveData = {
|
|
799
|
-
workspaceId: targetWorkspaceId,
|
|
800
|
-
};
|
|
801
|
-
if (assigneeId !== undefined)
|
|
802
|
-
moveData.assigneeId = assigneeId;
|
|
803
|
-
const response = await this.requestWithRetry(() => this.client.patch(`/tasks/${taskId}/move`, moveData));
|
|
804
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task moved successfully', {
|
|
805
|
-
method: 'moveTask',
|
|
806
|
-
taskId,
|
|
807
|
-
targetWorkspaceId,
|
|
808
|
-
assigneeId
|
|
809
|
-
});
|
|
810
|
-
// Docs say 200 with task object, but handle 204 No Content defensively
|
|
811
|
-
return response.status === 204 ? undefined : response.data;
|
|
812
|
-
}
|
|
813
|
-
catch (error) {
|
|
814
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to move task', {
|
|
815
|
-
method: 'moveTask',
|
|
816
|
-
taskId,
|
|
817
|
-
targetWorkspaceId,
|
|
818
|
-
assigneeId,
|
|
819
|
-
error: getErrorMessage(error),
|
|
820
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
821
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
822
|
-
});
|
|
823
|
-
throw this.formatApiError(error, 'move', 'task', taskId);
|
|
824
|
-
}
|
|
67
|
+
return (0, tasks_1.moveTask)(this._ctx, taskId, targetWorkspaceId, assigneeId);
|
|
825
68
|
}
|
|
826
69
|
async unassignTask(taskId) {
|
|
827
|
-
|
|
828
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Unassigning task in Motion API', {
|
|
829
|
-
method: 'unassignTask',
|
|
830
|
-
taskId
|
|
831
|
-
});
|
|
832
|
-
const response = await this.requestWithRetry(() => this.client.delete(`/tasks/${taskId}/assignee`));
|
|
833
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task unassigned successfully', {
|
|
834
|
-
method: 'unassignTask',
|
|
835
|
-
taskId
|
|
836
|
-
});
|
|
837
|
-
// Response undocumented — handle 204 No Content defensively
|
|
838
|
-
return response.status === 204 ? undefined : response.data;
|
|
839
|
-
}
|
|
840
|
-
catch (error) {
|
|
841
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to unassign task', {
|
|
842
|
-
method: 'unassignTask',
|
|
843
|
-
taskId,
|
|
844
|
-
error: getErrorMessage(error),
|
|
845
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
846
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
847
|
-
});
|
|
848
|
-
throw this.formatApiError(error, 'unassign', 'task', taskId);
|
|
849
|
-
}
|
|
70
|
+
return (0, tasks_1.unassignTask)(this._ctx, taskId);
|
|
850
71
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
// ========================================
|
|
854
|
-
async getWorkspaces(ids) {
|
|
855
|
-
// Skip caching when filtering by IDs — filtered results are unlikely to be reused
|
|
856
|
-
if (ids && ids.length > 0) {
|
|
857
|
-
return this.fetchWorkspaces(ids);
|
|
858
|
-
}
|
|
859
|
-
return this.workspaceCache.withCache('workspaces', async () => {
|
|
860
|
-
return this.fetchWorkspaces();
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
async fetchWorkspaces(ids) {
|
|
864
|
-
try {
|
|
865
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching workspaces from Motion API', {
|
|
866
|
-
method: 'getWorkspaces',
|
|
867
|
-
filterIds: ids
|
|
868
|
-
});
|
|
869
|
-
const params = new URLSearchParams();
|
|
870
|
-
if (ids && ids.length > 0) {
|
|
871
|
-
for (const id of ids) {
|
|
872
|
-
params.append('ids', id);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
const queryString = params.toString();
|
|
876
|
-
const url = queryString ? `/workspaces?${queryString}` : '/workspaces';
|
|
877
|
-
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
878
|
-
// Validate the response structure
|
|
879
|
-
const validatedResponse = this.validateResponse(response.data, motion_1.WorkspacesListResponseSchema, 'getWorkspaces');
|
|
880
|
-
// Extract workspaces array (handle both wrapped and unwrapped responses)
|
|
881
|
-
const workspaces = Array.isArray(validatedResponse)
|
|
882
|
-
? validatedResponse
|
|
883
|
-
: validatedResponse.workspaces;
|
|
884
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Workspaces fetched successfully', {
|
|
885
|
-
method: 'getWorkspaces',
|
|
886
|
-
count: workspaces.length,
|
|
887
|
-
workspaceNames: workspaces.map((w) => w.name)
|
|
888
|
-
});
|
|
889
|
-
return workspaces;
|
|
890
|
-
}
|
|
891
|
-
catch (error) {
|
|
892
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch workspaces', {
|
|
893
|
-
method: 'getWorkspaces',
|
|
894
|
-
error: getErrorMessage(error),
|
|
895
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
896
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
897
|
-
});
|
|
898
|
-
throw this.formatApiError(error, 'fetch', 'workspace');
|
|
899
|
-
}
|
|
72
|
+
async getAllUncompletedTasks(limit, assigneeId) {
|
|
73
|
+
return (0, tasks_1.getAllUncompletedTasks)(this._ctx, limit, assigneeId);
|
|
900
74
|
}
|
|
901
75
|
// ========================================
|
|
902
|
-
//
|
|
76
|
+
// PROJECT API METHODS
|
|
903
77
|
// ========================================
|
|
904
|
-
async
|
|
905
|
-
|
|
906
|
-
const cacheKey = parts.length > 0 ? `users:${parts.join(':')}` : 'users:all';
|
|
907
|
-
return this.userCache.withCache(cacheKey, async () => {
|
|
908
|
-
try {
|
|
909
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching users from Motion API', {
|
|
910
|
-
method: 'getUsers',
|
|
911
|
-
workspaceId,
|
|
912
|
-
teamId
|
|
913
|
-
});
|
|
914
|
-
const params = new URLSearchParams();
|
|
915
|
-
if (workspaceId) {
|
|
916
|
-
params.append('workspaceId', workspaceId);
|
|
917
|
-
}
|
|
918
|
-
if (teamId) {
|
|
919
|
-
params.append('teamId', teamId);
|
|
920
|
-
}
|
|
921
|
-
const queryString = params.toString();
|
|
922
|
-
const url = queryString ? `/users?${queryString}` : '/users';
|
|
923
|
-
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
924
|
-
// The Motion API might wrap the users in a 'users' array
|
|
925
|
-
const usersData = response.data?.users || response.data || [];
|
|
926
|
-
const users = Array.isArray(usersData) ? usersData : [];
|
|
927
|
-
if (!Array.isArray(response.data?.users) && !Array.isArray(response.data)) {
|
|
928
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Unexpected users response shape', {
|
|
929
|
-
method: 'getUsers',
|
|
930
|
-
workspaceId,
|
|
931
|
-
responseType: response.data === null ? 'null' : typeof response.data
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Users fetched successfully', {
|
|
935
|
-
method: 'getUsers',
|
|
936
|
-
count: users.length,
|
|
937
|
-
workspaceId
|
|
938
|
-
});
|
|
939
|
-
return users;
|
|
940
|
-
}
|
|
941
|
-
catch (error) {
|
|
942
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch users', {
|
|
943
|
-
method: 'getUsers',
|
|
944
|
-
error: getErrorMessage(error),
|
|
945
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
946
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
947
|
-
});
|
|
948
|
-
throw this.formatApiError(error, 'fetch', 'user');
|
|
949
|
-
}
|
|
950
|
-
});
|
|
78
|
+
async getProjects(workspaceId, options) {
|
|
79
|
+
return (0, projects_1.getProjects)(this._ctx, workspaceId, options);
|
|
951
80
|
}
|
|
952
|
-
async
|
|
953
|
-
|
|
954
|
-
// Use userCache but with a special single-user wrapper
|
|
955
|
-
const cachedUsers = await this.userCache.withCache(cacheKey, async () => {
|
|
956
|
-
try {
|
|
957
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching current user from Motion API', {
|
|
958
|
-
method: 'getCurrentUser'
|
|
959
|
-
});
|
|
960
|
-
const response = await this.requestWithRetry(() => this.client.get('/users/me'));
|
|
961
|
-
const user = response.data;
|
|
962
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Current user fetched successfully', {
|
|
963
|
-
method: 'getCurrentUser',
|
|
964
|
-
userId: user.id,
|
|
965
|
-
email: user.email
|
|
966
|
-
});
|
|
967
|
-
return [user]; // Wrap in array for cache compatibility
|
|
968
|
-
}
|
|
969
|
-
catch (error) {
|
|
970
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch current user', {
|
|
971
|
-
method: 'getCurrentUser',
|
|
972
|
-
error: getErrorMessage(error),
|
|
973
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
974
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
975
|
-
});
|
|
976
|
-
throw this.formatApiError(error, 'fetch', 'user');
|
|
977
|
-
}
|
|
978
|
-
});
|
|
979
|
-
const user = cachedUsers[0];
|
|
980
|
-
if (!user) {
|
|
981
|
-
throw this.formatApiError(new Error('No user returned from API'), 'fetch', 'user');
|
|
982
|
-
}
|
|
983
|
-
return user;
|
|
81
|
+
async getAllProjects() {
|
|
82
|
+
return (0, projects_1.getAllProjects)(this._ctx);
|
|
984
83
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
// ========================================
|
|
988
|
-
/**
|
|
989
|
-
* Resolves a project identifier (either projectId or projectName) to a MotionProject
|
|
990
|
-
* Searches across all workspaces if not found in the specified workspace
|
|
991
|
-
* @param identifier Object containing either projectId or projectName
|
|
992
|
-
* @param workspaceId Workspace to start searching in
|
|
993
|
-
* @returns Resolved MotionProject with workspace info, or undefined if not found
|
|
994
|
-
*/
|
|
995
|
-
async resolveProjectIdentifier(identifier, workspaceId) {
|
|
996
|
-
try {
|
|
997
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving project identifier', {
|
|
998
|
-
method: 'resolveProjectIdentifier',
|
|
999
|
-
projectId: identifier.projectId,
|
|
1000
|
-
projectName: identifier.projectName,
|
|
1001
|
-
workspaceId
|
|
1002
|
-
});
|
|
1003
|
-
// If projectId is provided, try to get it directly
|
|
1004
|
-
if (identifier.projectId) {
|
|
1005
|
-
try {
|
|
1006
|
-
const project = await this.getProject(identifier.projectId);
|
|
1007
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by ID', {
|
|
1008
|
-
method: 'resolveProjectIdentifier',
|
|
1009
|
-
projectId: identifier.projectId,
|
|
1010
|
-
projectName: project.name,
|
|
1011
|
-
workspaceId: project.workspaceId
|
|
1012
|
-
});
|
|
1013
|
-
return project;
|
|
1014
|
-
}
|
|
1015
|
-
catch (error) {
|
|
1016
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project by ID', {
|
|
1017
|
-
method: 'resolveProjectIdentifier',
|
|
1018
|
-
projectId: identifier.projectId,
|
|
1019
|
-
error: getErrorMessage(error)
|
|
1020
|
-
});
|
|
1021
|
-
// Fall through to projectName resolution if projectId fails
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
// If projectName is provided (or projectId failed), resolve by name across workspaces
|
|
1025
|
-
if (identifier.projectName) {
|
|
1026
|
-
const project = await this.getProjectByName(identifier.projectName, workspaceId);
|
|
1027
|
-
if (project) {
|
|
1028
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project resolved by name across workspaces', {
|
|
1029
|
-
method: 'resolveProjectIdentifier',
|
|
1030
|
-
projectName: identifier.projectName,
|
|
1031
|
-
projectId: project.id,
|
|
1032
|
-
foundInWorkspaceId: project.workspaceId
|
|
1033
|
-
});
|
|
1034
|
-
return project;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve project identifier', {
|
|
1038
|
-
method: 'resolveProjectIdentifier',
|
|
1039
|
-
projectId: identifier.projectId,
|
|
1040
|
-
projectName: identifier.projectName,
|
|
1041
|
-
workspaceId
|
|
1042
|
-
});
|
|
1043
|
-
return undefined;
|
|
1044
|
-
}
|
|
1045
|
-
catch (error) {
|
|
1046
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving project identifier', {
|
|
1047
|
-
method: 'resolveProjectIdentifier',
|
|
1048
|
-
projectId: identifier.projectId,
|
|
1049
|
-
projectName: identifier.projectName,
|
|
1050
|
-
error: getErrorMessage(error)
|
|
1051
|
-
});
|
|
1052
|
-
throw error;
|
|
1053
|
-
}
|
|
84
|
+
async getProject(projectId) {
|
|
85
|
+
return (0, projects_1.getProject)(this._ctx, projectId);
|
|
1054
86
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
* Searches across all workspaces if not found in the specified workspace unless strictWorkspace is true
|
|
1058
|
-
* @param identifier Object containing either userId or userName
|
|
1059
|
-
* @param workspaceId Workspace to start searching in (optional)
|
|
1060
|
-
* @param options Resolution behavior options
|
|
1061
|
-
* @returns Resolved MotionUser with workspace info, or undefined if not found
|
|
1062
|
-
*/
|
|
1063
|
-
async resolveUserIdentifier(identifier, workspaceId, options) {
|
|
1064
|
-
try {
|
|
1065
|
-
const strictWorkspace = options?.strictWorkspace === true;
|
|
1066
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Resolving user identifier', {
|
|
1067
|
-
method: 'resolveUserIdentifier',
|
|
1068
|
-
userId: identifier.userId,
|
|
1069
|
-
userName: identifier.userName,
|
|
1070
|
-
workspaceId,
|
|
1071
|
-
strictWorkspace
|
|
1072
|
-
});
|
|
1073
|
-
// Build ordered workspace IDs: specified workspace first (or only when strict).
|
|
1074
|
-
const allWorkspaces = await this.getWorkspaces();
|
|
1075
|
-
let orderedWorkspaceIds;
|
|
1076
|
-
if (workspaceId) {
|
|
1077
|
-
if (strictWorkspace) {
|
|
1078
|
-
orderedWorkspaceIds = [workspaceId];
|
|
1079
|
-
}
|
|
1080
|
-
else {
|
|
1081
|
-
const allWorkspaceIds = allWorkspaces.map(workspace => workspace.id);
|
|
1082
|
-
if (allWorkspaceIds.includes(workspaceId)) {
|
|
1083
|
-
const otherWorkspaceIds = allWorkspaceIds.filter(id => id !== workspaceId);
|
|
1084
|
-
orderedWorkspaceIds = [workspaceId, ...otherWorkspaceIds];
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
// Keep prior behavior when workspaceId is unknown: search known workspaces.
|
|
1088
|
-
orderedWorkspaceIds = allWorkspaceIds;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
else {
|
|
1093
|
-
orderedWorkspaceIds = allWorkspaces.map(workspace => workspace.id);
|
|
1094
|
-
}
|
|
1095
|
-
// If userId is provided, search by ID
|
|
1096
|
-
if (identifier.userId) {
|
|
1097
|
-
for (const searchWorkspaceId of orderedWorkspaceIds) {
|
|
1098
|
-
try {
|
|
1099
|
-
const users = await this.getUsers(searchWorkspaceId);
|
|
1100
|
-
const user = users.find(u => u.id === identifier.userId);
|
|
1101
|
-
if (user) {
|
|
1102
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by ID', {
|
|
1103
|
-
method: 'resolveUserIdentifier',
|
|
1104
|
-
userId: identifier.userId,
|
|
1105
|
-
userName: user.name,
|
|
1106
|
-
foundInWorkspaceId: searchWorkspaceId
|
|
1107
|
-
});
|
|
1108
|
-
return user;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
catch (workspaceError) {
|
|
1112
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by ID', {
|
|
1113
|
-
method: 'resolveUserIdentifier',
|
|
1114
|
-
userId: identifier.userId,
|
|
1115
|
-
workspaceId: searchWorkspaceId,
|
|
1116
|
-
error: getErrorMessage(workspaceError)
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
// If userName is provided, search by name/email
|
|
1122
|
-
if (identifier.userName) {
|
|
1123
|
-
const searchTerm = identifier.userName.toLowerCase();
|
|
1124
|
-
for (const searchWorkspaceId of orderedWorkspaceIds) {
|
|
1125
|
-
try {
|
|
1126
|
-
const users = await this.getUsers(searchWorkspaceId);
|
|
1127
|
-
const user = users.find(u => u.name?.toLowerCase().includes(searchTerm) ||
|
|
1128
|
-
u.email?.toLowerCase().includes(searchTerm));
|
|
1129
|
-
if (user) {
|
|
1130
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'User resolved by name/email', {
|
|
1131
|
-
method: 'resolveUserIdentifier',
|
|
1132
|
-
userName: identifier.userName,
|
|
1133
|
-
userId: user.id,
|
|
1134
|
-
foundInWorkspaceId: searchWorkspaceId
|
|
1135
|
-
});
|
|
1136
|
-
return user;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
catch (workspaceError) {
|
|
1140
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for user by name', {
|
|
1141
|
-
method: 'resolveUserIdentifier',
|
|
1142
|
-
userName: identifier.userName,
|
|
1143
|
-
workspaceId: searchWorkspaceId,
|
|
1144
|
-
error: getErrorMessage(workspaceError)
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to resolve user identifier', {
|
|
1150
|
-
method: 'resolveUserIdentifier',
|
|
1151
|
-
userId: identifier.userId,
|
|
1152
|
-
userName: identifier.userName,
|
|
1153
|
-
workspaceId
|
|
1154
|
-
});
|
|
1155
|
-
return undefined;
|
|
1156
|
-
}
|
|
1157
|
-
catch (error) {
|
|
1158
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Error resolving user identifier', {
|
|
1159
|
-
method: 'resolveUserIdentifier',
|
|
1160
|
-
userId: identifier.userId,
|
|
1161
|
-
userName: identifier.userName,
|
|
1162
|
-
error: getErrorMessage(error)
|
|
1163
|
-
});
|
|
1164
|
-
throw error;
|
|
1165
|
-
}
|
|
87
|
+
async createProject(projectData) {
|
|
88
|
+
return (0, projects_1.createProject)(this._ctx, projectData);
|
|
1166
89
|
}
|
|
1167
|
-
async
|
|
1168
|
-
|
|
1169
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Finding project by name', {
|
|
1170
|
-
method: 'getProjectByName',
|
|
1171
|
-
projectName,
|
|
1172
|
-
workspaceId
|
|
1173
|
-
});
|
|
1174
|
-
// First, search in the specified workspace (if provided)
|
|
1175
|
-
if (workspaceId) {
|
|
1176
|
-
const { items: projects } = await this.getProjects(workspaceId);
|
|
1177
|
-
const project = projects.find(p => p.name === projectName);
|
|
1178
|
-
if (project) {
|
|
1179
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project found by name in specified workspace', {
|
|
1180
|
-
method: 'getProjectByName',
|
|
1181
|
-
projectName,
|
|
1182
|
-
projectId: project.id,
|
|
1183
|
-
workspaceId
|
|
1184
|
-
});
|
|
1185
|
-
return project;
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
// If not found in specified workspace (or none specified), search across all other workspaces
|
|
1189
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Project not found in specified workspace, searching all workspaces', {
|
|
1190
|
-
method: 'getProjectByName',
|
|
1191
|
-
projectName,
|
|
1192
|
-
specifiedWorkspaceId: workspaceId
|
|
1193
|
-
});
|
|
1194
|
-
const allWorkspaces = await this.getWorkspaces();
|
|
1195
|
-
const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
|
|
1196
|
-
for (const workspace of otherWorkspaces) {
|
|
1197
|
-
try {
|
|
1198
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching workspace for project', {
|
|
1199
|
-
method: 'getProjectByName',
|
|
1200
|
-
projectName,
|
|
1201
|
-
searchingWorkspaceId: workspace.id,
|
|
1202
|
-
searchingWorkspaceName: workspace.name
|
|
1203
|
-
});
|
|
1204
|
-
const { items: workspaceProjects } = await this.getProjects(workspace.id);
|
|
1205
|
-
const foundProject = workspaceProjects.find(p => p.name === projectName);
|
|
1206
|
-
if (foundProject) {
|
|
1207
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project found by name in different workspace', {
|
|
1208
|
-
method: 'getProjectByName',
|
|
1209
|
-
projectName,
|
|
1210
|
-
projectId: foundProject.id,
|
|
1211
|
-
foundInWorkspaceId: workspace.id,
|
|
1212
|
-
foundInWorkspaceName: workspace.name,
|
|
1213
|
-
originalWorkspaceId: workspaceId
|
|
1214
|
-
});
|
|
1215
|
-
return foundProject;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
catch (workspaceError) {
|
|
1219
|
-
// Log error but continue searching other workspaces
|
|
1220
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for project', {
|
|
1221
|
-
method: 'getProjectByName',
|
|
1222
|
-
projectName,
|
|
1223
|
-
workspaceId: workspace.id,
|
|
1224
|
-
workspaceName: workspace.name,
|
|
1225
|
-
error: getErrorMessage(workspaceError)
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
// Project not found in any workspace
|
|
1230
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Project not found by name in any workspace', {
|
|
1231
|
-
method: 'getProjectByName',
|
|
1232
|
-
projectName,
|
|
1233
|
-
searchedWorkspaces: [workspaceId, ...otherWorkspaces.map(w => w.id)],
|
|
1234
|
-
totalWorkspacesSearched: allWorkspaces.length
|
|
1235
|
-
});
|
|
1236
|
-
return undefined;
|
|
1237
|
-
}
|
|
1238
|
-
catch (error) {
|
|
1239
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to find project by name', {
|
|
1240
|
-
method: 'getProjectByName',
|
|
1241
|
-
projectName,
|
|
1242
|
-
error: getErrorMessage(error)
|
|
1243
|
-
});
|
|
1244
|
-
throw error;
|
|
1245
|
-
}
|
|
90
|
+
async updateProject(projectId, updates) {
|
|
91
|
+
return (0, projects_1.updateProject)(this._ctx, projectId, updates);
|
|
1246
92
|
}
|
|
1247
|
-
async
|
|
1248
|
-
|
|
1249
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching tasks', {
|
|
1250
|
-
method: 'searchTasks',
|
|
1251
|
-
query,
|
|
1252
|
-
workspaceId,
|
|
1253
|
-
limit
|
|
1254
|
-
});
|
|
1255
|
-
// Apply search limit to prevent resource exhaustion
|
|
1256
|
-
const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
|
|
1257
|
-
const lowerQuery = query.toLowerCase();
|
|
1258
|
-
const allMatchingTasks = [];
|
|
1259
|
-
let aggregateTruncation;
|
|
1260
|
-
// First, search in the specified workspace
|
|
1261
|
-
const { items: primaryTasks, truncation: primaryTruncation } = await this.getTasks({
|
|
1262
|
-
workspaceId,
|
|
1263
|
-
limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit),
|
|
1264
|
-
maxPages: constants_1.LIMITS.MAX_PAGES
|
|
1265
|
-
});
|
|
1266
|
-
aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, primaryTruncation);
|
|
1267
|
-
const primaryMatches = primaryTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
|
|
1268
|
-
task.description?.toLowerCase().includes(lowerQuery));
|
|
1269
|
-
allMatchingTasks.push(...primaryMatches.slice(0, effectiveLimit));
|
|
1270
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
|
|
1271
|
-
method: 'searchTasks',
|
|
1272
|
-
query,
|
|
1273
|
-
primaryWorkspaceId: workspaceId,
|
|
1274
|
-
primaryMatches: primaryMatches.length,
|
|
1275
|
-
keptMatches: allMatchingTasks.length
|
|
1276
|
-
});
|
|
1277
|
-
// If we haven't reached the limit, search other workspaces
|
|
1278
|
-
if (allMatchingTasks.length < effectiveLimit) {
|
|
1279
|
-
try {
|
|
1280
|
-
const allWorkspaces = await this.getWorkspaces();
|
|
1281
|
-
const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
|
|
1282
|
-
for (const workspace of otherWorkspaces) {
|
|
1283
|
-
if (allMatchingTasks.length >= effectiveLimit)
|
|
1284
|
-
break;
|
|
1285
|
-
try {
|
|
1286
|
-
// Calculate fetch limit before API call (defense-in-depth)
|
|
1287
|
-
const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingTasks.length, effectiveLimit);
|
|
1288
|
-
if (fetchLimit <= 0)
|
|
1289
|
-
break;
|
|
1290
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for tasks', {
|
|
1291
|
-
method: 'searchTasks',
|
|
1292
|
-
query,
|
|
1293
|
-
searchingWorkspaceId: workspace.id,
|
|
1294
|
-
searchingWorkspaceName: workspace.name,
|
|
1295
|
-
remainingNeeded: effectiveLimit - allMatchingTasks.length
|
|
1296
|
-
});
|
|
1297
|
-
const { items: workspaceTasks, truncation: wsTruncation } = await this.getTasks({
|
|
1298
|
-
workspaceId: workspace.id,
|
|
1299
|
-
limit: fetchLimit,
|
|
1300
|
-
maxPages: constants_1.LIMITS.MAX_PAGES
|
|
1301
|
-
});
|
|
1302
|
-
aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, wsTruncation);
|
|
1303
|
-
const workspaceMatches = workspaceTasks.filter(task => task.name?.toLowerCase().includes(lowerQuery) ||
|
|
1304
|
-
task.description?.toLowerCase().includes(lowerQuery));
|
|
1305
|
-
// Only add as many as we still need
|
|
1306
|
-
const remaining = effectiveLimit - allMatchingTasks.length;
|
|
1307
|
-
allMatchingTasks.push(...workspaceMatches.slice(0, remaining));
|
|
1308
|
-
if (workspaceMatches.length > 0) {
|
|
1309
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
|
|
1310
|
-
method: 'searchTasks',
|
|
1311
|
-
query,
|
|
1312
|
-
workspaceId: workspace.id,
|
|
1313
|
-
workspaceName: workspace.name,
|
|
1314
|
-
matches: workspaceMatches.length,
|
|
1315
|
-
keptMatches: Math.min(workspaceMatches.length, remaining)
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
catch (workspaceError) {
|
|
1320
|
-
// Log error but continue searching other workspaces
|
|
1321
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for tasks', {
|
|
1322
|
-
method: 'searchTasks',
|
|
1323
|
-
query,
|
|
1324
|
-
workspaceId: workspace.id,
|
|
1325
|
-
workspaceName: workspace.name,
|
|
1326
|
-
error: getErrorMessage(workspaceError)
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
catch (workspaceListError) {
|
|
1332
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
|
|
1333
|
-
method: 'searchTasks',
|
|
1334
|
-
query,
|
|
1335
|
-
error: getErrorMessage(workspaceListError)
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
// Results are already limited during collection, no need to slice again
|
|
1340
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Task search completed across all workspaces', {
|
|
1341
|
-
method: 'searchTasks',
|
|
1342
|
-
query,
|
|
1343
|
-
returnedResults: allMatchingTasks.length,
|
|
1344
|
-
limit: effectiveLimit
|
|
1345
|
-
});
|
|
1346
|
-
if (aggregateTruncation) {
|
|
1347
|
-
aggregateTruncation.returnedCount = allMatchingTasks.length;
|
|
1348
|
-
}
|
|
1349
|
-
return { items: allMatchingTasks, truncation: aggregateTruncation };
|
|
1350
|
-
}
|
|
1351
|
-
catch (error) {
|
|
1352
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search tasks', {
|
|
1353
|
-
method: 'searchTasks',
|
|
1354
|
-
query,
|
|
1355
|
-
error: getErrorMessage(error)
|
|
1356
|
-
});
|
|
1357
|
-
throw error;
|
|
1358
|
-
}
|
|
93
|
+
async deleteProject(projectId) {
|
|
94
|
+
return (0, projects_1.deleteProject)(this._ctx, projectId);
|
|
1359
95
|
}
|
|
1360
|
-
async
|
|
1361
|
-
|
|
1362
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching projects', {
|
|
1363
|
-
method: 'searchProjects',
|
|
1364
|
-
query,
|
|
1365
|
-
workspaceId,
|
|
1366
|
-
limit
|
|
1367
|
-
});
|
|
1368
|
-
// Apply search limit to prevent resource exhaustion
|
|
1369
|
-
const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
|
|
1370
|
-
const lowerQuery = query.toLowerCase();
|
|
1371
|
-
const allMatchingProjects = [];
|
|
1372
|
-
let aggregateTruncation;
|
|
1373
|
-
// First, search in the specified workspace
|
|
1374
|
-
const { items: primaryProjects, truncation: primaryTruncation } = await this.getProjects(workspaceId, {
|
|
1375
|
-
maxPages: constants_1.LIMITS.MAX_PAGES,
|
|
1376
|
-
limit: (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit)
|
|
1377
|
-
});
|
|
1378
|
-
aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, primaryTruncation);
|
|
1379
|
-
const primaryMatches = primaryProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
|
|
1380
|
-
project.description?.toLowerCase().includes(lowerQuery));
|
|
1381
|
-
allMatchingProjects.push(...primaryMatches.slice(0, effectiveLimit));
|
|
1382
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Primary workspace search completed', {
|
|
1383
|
-
method: 'searchProjects',
|
|
1384
|
-
query,
|
|
1385
|
-
primaryWorkspaceId: workspaceId,
|
|
1386
|
-
primaryMatches: primaryMatches.length,
|
|
1387
|
-
keptMatches: allMatchingProjects.length
|
|
1388
|
-
});
|
|
1389
|
-
// If we haven't reached the limit, search other workspaces
|
|
1390
|
-
if (allMatchingProjects.length < effectiveLimit) {
|
|
1391
|
-
try {
|
|
1392
|
-
const allWorkspaces = await this.getWorkspaces();
|
|
1393
|
-
const otherWorkspaces = allWorkspaces.filter(w => w.id !== workspaceId);
|
|
1394
|
-
for (const workspace of otherWorkspaces) {
|
|
1395
|
-
if (allMatchingProjects.length >= effectiveLimit)
|
|
1396
|
-
break;
|
|
1397
|
-
try {
|
|
1398
|
-
// Calculate fetch limit before API call (defense-in-depth)
|
|
1399
|
-
const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allMatchingProjects.length, effectiveLimit);
|
|
1400
|
-
if (fetchLimit <= 0)
|
|
1401
|
-
break;
|
|
1402
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching additional workspace for projects', {
|
|
1403
|
-
method: 'searchProjects',
|
|
1404
|
-
query,
|
|
1405
|
-
searchingWorkspaceId: workspace.id,
|
|
1406
|
-
searchingWorkspaceName: workspace.name,
|
|
1407
|
-
remainingNeeded: effectiveLimit - allMatchingProjects.length
|
|
1408
|
-
});
|
|
1409
|
-
const { items: workspaceProjects, truncation: wsTruncation } = await this.getProjects(workspace.id, {
|
|
1410
|
-
maxPages: constants_1.LIMITS.MAX_PAGES,
|
|
1411
|
-
limit: fetchLimit
|
|
1412
|
-
});
|
|
1413
|
-
aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, wsTruncation);
|
|
1414
|
-
const workspaceMatches = workspaceProjects.filter(project => project.name?.toLowerCase().includes(lowerQuery) ||
|
|
1415
|
-
project.description?.toLowerCase().includes(lowerQuery));
|
|
1416
|
-
// Only add as many as we still need
|
|
1417
|
-
const remaining = effectiveLimit - allMatchingProjects.length;
|
|
1418
|
-
allMatchingProjects.push(...workspaceMatches.slice(0, remaining));
|
|
1419
|
-
if (workspaceMatches.length > 0) {
|
|
1420
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found additional matches in workspace', {
|
|
1421
|
-
method: 'searchProjects',
|
|
1422
|
-
query,
|
|
1423
|
-
workspaceId: workspace.id,
|
|
1424
|
-
workspaceName: workspace.name,
|
|
1425
|
-
matches: workspaceMatches.length,
|
|
1426
|
-
keptMatches: Math.min(workspaceMatches.length, remaining)
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
catch (workspaceError) {
|
|
1431
|
-
// Log error but continue searching other workspaces
|
|
1432
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to search workspace for projects', {
|
|
1433
|
-
method: 'searchProjects',
|
|
1434
|
-
query,
|
|
1435
|
-
workspaceId: workspace.id,
|
|
1436
|
-
workspaceName: workspace.name,
|
|
1437
|
-
error: getErrorMessage(workspaceError)
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
catch (workspaceListError) {
|
|
1443
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to get workspace list for cross-workspace search', {
|
|
1444
|
-
method: 'searchProjects',
|
|
1445
|
-
query,
|
|
1446
|
-
error: getErrorMessage(workspaceListError)
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
// Results are already limited during collection, no need to slice again
|
|
1451
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Project search completed across all workspaces', {
|
|
1452
|
-
method: 'searchProjects',
|
|
1453
|
-
query,
|
|
1454
|
-
returnedResults: allMatchingProjects.length,
|
|
1455
|
-
limit: effectiveLimit
|
|
1456
|
-
});
|
|
1457
|
-
if (aggregateTruncation) {
|
|
1458
|
-
aggregateTruncation.returnedCount = allMatchingProjects.length;
|
|
1459
|
-
}
|
|
1460
|
-
return { items: allMatchingProjects, truncation: aggregateTruncation };
|
|
1461
|
-
}
|
|
1462
|
-
catch (error) {
|
|
1463
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to search projects', {
|
|
1464
|
-
method: 'searchProjects',
|
|
1465
|
-
query,
|
|
1466
|
-
error: getErrorMessage(error)
|
|
1467
|
-
});
|
|
1468
|
-
throw error;
|
|
1469
|
-
}
|
|
96
|
+
async getProjectByName(projectName, workspaceId) {
|
|
97
|
+
return (0, projects_1.getProjectByName)(this._ctx, projectName, workspaceId);
|
|
1470
98
|
}
|
|
1471
99
|
// ========================================
|
|
1472
100
|
// COMMENT API METHODS
|
|
1473
101
|
// ========================================
|
|
1474
|
-
/**
|
|
1475
|
-
* Get comments for a task with proper pagination support
|
|
1476
|
-
* @param taskId Task ID to get comments for
|
|
1477
|
-
* @param cursor Optional cursor for pagination
|
|
1478
|
-
* @returns Paginated response with comments and metadata
|
|
1479
|
-
*/
|
|
1480
102
|
async getComments(taskId, cursor) {
|
|
1481
|
-
|
|
1482
|
-
const cacheKey = `comments:${JSON.stringify(cacheParams)}`;
|
|
1483
|
-
return this.commentCache.withCache(cacheKey, async () => {
|
|
1484
|
-
try {
|
|
1485
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching comments from Motion API', {
|
|
1486
|
-
method: 'getComments',
|
|
1487
|
-
taskId,
|
|
1488
|
-
cursor
|
|
1489
|
-
});
|
|
1490
|
-
const params = new URLSearchParams({ taskId });
|
|
1491
|
-
if (cursor)
|
|
1492
|
-
params.append('cursor', cursor);
|
|
1493
|
-
const response = await this.requestWithRetry(() => this.client.get(`/comments?${params.toString()}`));
|
|
1494
|
-
// Use new response wrapper for consistent handling
|
|
1495
|
-
const unwrapped = (0, responseWrapper_1.unwrapApiResponse)(response.data, 'comments');
|
|
1496
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comments fetched successfully', {
|
|
1497
|
-
method: 'getComments',
|
|
1498
|
-
count: unwrapped.data.length,
|
|
1499
|
-
hasMore: !!unwrapped.meta?.nextCursor,
|
|
1500
|
-
taskId
|
|
1501
|
-
});
|
|
1502
|
-
// Return in our standard paginated format
|
|
1503
|
-
return {
|
|
1504
|
-
data: unwrapped.data,
|
|
1505
|
-
meta: {
|
|
1506
|
-
nextCursor: unwrapped.meta?.nextCursor,
|
|
1507
|
-
pageSize: unwrapped.meta?.pageSize || unwrapped.data.length
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1510
|
-
}
|
|
1511
|
-
catch (error) {
|
|
1512
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch comments', {
|
|
1513
|
-
method: 'getComments',
|
|
1514
|
-
error: getErrorMessage(error),
|
|
1515
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1516
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1517
|
-
taskId,
|
|
1518
|
-
cursor
|
|
1519
|
-
});
|
|
1520
|
-
throw this.formatApiError(error, 'fetch', 'comment');
|
|
1521
|
-
}
|
|
1522
|
-
});
|
|
103
|
+
return (0, comments_1.getComments)(this._ctx, taskId, cursor);
|
|
1523
104
|
}
|
|
1524
105
|
async createComment(commentData) {
|
|
1525
|
-
|
|
1526
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating comment in Motion API', {
|
|
1527
|
-
method: 'createComment',
|
|
1528
|
-
taskId: commentData.taskId,
|
|
1529
|
-
contentLength: commentData.content?.length || 0
|
|
1530
|
-
});
|
|
1531
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
1532
|
-
const minimalPayload = (0, constants_1.createMinimalPayload)(commentData);
|
|
1533
|
-
const response = await this.requestWithRetry(() => this.client.post('/comments', minimalPayload));
|
|
1534
|
-
// Invalidate all cached pages for this task's comments (cursor variants included)
|
|
1535
|
-
const cachePrefix = `comments:{"taskId":${JSON.stringify(commentData.taskId)}`;
|
|
1536
|
-
this.commentCache.invalidate(cachePrefix);
|
|
1537
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Comment created successfully', {
|
|
1538
|
-
method: 'createComment',
|
|
1539
|
-
commentId: response.data?.id,
|
|
1540
|
-
taskId: commentData.taskId
|
|
1541
|
-
});
|
|
1542
|
-
return response.data;
|
|
1543
|
-
}
|
|
1544
|
-
catch (error) {
|
|
1545
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create comment', {
|
|
1546
|
-
method: 'createComment',
|
|
1547
|
-
error: getErrorMessage(error),
|
|
1548
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1549
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1550
|
-
taskId: commentData?.taskId
|
|
1551
|
-
});
|
|
1552
|
-
throw this.formatApiError(error, 'create', 'comment');
|
|
1553
|
-
}
|
|
106
|
+
return (0, comments_1.createComment)(this._ctx, commentData);
|
|
1554
107
|
}
|
|
1555
108
|
// ========================================
|
|
1556
109
|
// CUSTOM FIELD API METHODS
|
|
1557
110
|
// ========================================
|
|
1558
|
-
/**
|
|
1559
|
-
* Fetch custom fields from Motion API
|
|
1560
|
-
* @param workspaceId - Required workspace ID to get custom fields for
|
|
1561
|
-
* @returns Array of custom fields
|
|
1562
|
-
*/
|
|
1563
111
|
async getCustomFields(workspaceId) {
|
|
1564
|
-
|
|
1565
|
-
return this.customFieldCache.withCache(cacheKey, async () => {
|
|
1566
|
-
try {
|
|
1567
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching custom fields from Motion API', {
|
|
1568
|
-
method: 'getCustomFields',
|
|
1569
|
-
workspaceId
|
|
1570
|
-
});
|
|
1571
|
-
const url = `/beta/workspaces/${workspaceId}/custom-fields`;
|
|
1572
|
-
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
1573
|
-
// Beta API returns direct array, not wrapped
|
|
1574
|
-
const fieldsArray = response.data || [];
|
|
1575
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom fields fetched successfully', {
|
|
1576
|
-
method: 'getCustomFields',
|
|
1577
|
-
count: fieldsArray.length,
|
|
1578
|
-
workspaceId
|
|
1579
|
-
});
|
|
1580
|
-
return fieldsArray;
|
|
1581
|
-
}
|
|
1582
|
-
catch (error) {
|
|
1583
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch custom fields', {
|
|
1584
|
-
method: 'getCustomFields',
|
|
1585
|
-
error: getErrorMessage(error),
|
|
1586
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1587
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1588
|
-
workspaceId
|
|
1589
|
-
});
|
|
1590
|
-
throw this.formatApiError(error, 'fetch', 'custom field');
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
112
|
+
return (0, customFields_1.getCustomFields)(this._ctx, workspaceId);
|
|
1593
113
|
}
|
|
1594
|
-
/**
|
|
1595
|
-
* Create a new custom field
|
|
1596
|
-
* @param workspaceId - Required workspace ID to create custom field in
|
|
1597
|
-
* @param fieldData - Data for creating the custom field
|
|
1598
|
-
* @returns The created custom field
|
|
1599
|
-
*/
|
|
1600
114
|
async createCustomField(workspaceId, fieldData) {
|
|
1601
|
-
|
|
1602
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating custom field in Motion API', {
|
|
1603
|
-
method: 'createCustomField',
|
|
1604
|
-
name: fieldData.name,
|
|
1605
|
-
field: fieldData.field,
|
|
1606
|
-
workspaceId
|
|
1607
|
-
});
|
|
1608
|
-
// Transform payload to match Motion API expectations
|
|
1609
|
-
// POST API expects 'type' in request, but returns 'field' in response
|
|
1610
|
-
const apiPayload = {
|
|
1611
|
-
name: fieldData.name,
|
|
1612
|
-
type: fieldData.field, // Motion API POST expects 'type' property in request body
|
|
1613
|
-
...(fieldData.required !== undefined && { required: fieldData.required }),
|
|
1614
|
-
...(fieldData.metadata && { metadata: fieldData.metadata })
|
|
1615
|
-
};
|
|
1616
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
1617
|
-
const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
|
|
1618
|
-
const response = await this.requestWithRetry(() => this.client.post(`/beta/workspaces/${workspaceId}/custom-fields`, minimalPayload));
|
|
1619
|
-
// Invalidate cache after successful creation
|
|
1620
|
-
this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
|
|
1621
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field created successfully', {
|
|
1622
|
-
method: 'createCustomField',
|
|
1623
|
-
fieldId: response.data?.id,
|
|
1624
|
-
name: fieldData.name,
|
|
1625
|
-
workspaceId
|
|
1626
|
-
});
|
|
1627
|
-
return response.data;
|
|
1628
|
-
}
|
|
1629
|
-
catch (error) {
|
|
1630
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create custom field', {
|
|
1631
|
-
method: 'createCustomField',
|
|
1632
|
-
error: getErrorMessage(error),
|
|
1633
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1634
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1635
|
-
fieldName: fieldData?.name,
|
|
1636
|
-
workspaceId
|
|
1637
|
-
});
|
|
1638
|
-
throw this.formatApiError(error, 'create', 'custom field', undefined, fieldData.name);
|
|
1639
|
-
}
|
|
115
|
+
return (0, customFields_1.createCustomField)(this._ctx, workspaceId, fieldData);
|
|
1640
116
|
}
|
|
1641
|
-
/**
|
|
1642
|
-
* Delete a custom field
|
|
1643
|
-
* @param workspaceId - Required workspace ID containing the custom field
|
|
1644
|
-
* @param fieldId - ID of the custom field to delete
|
|
1645
|
-
* @returns Success indicator
|
|
1646
|
-
*/
|
|
1647
117
|
async deleteCustomField(workspaceId, fieldId) {
|
|
1648
|
-
|
|
1649
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting custom field from Motion API', {
|
|
1650
|
-
method: 'deleteCustomField',
|
|
1651
|
-
fieldId,
|
|
1652
|
-
workspaceId
|
|
1653
|
-
});
|
|
1654
|
-
await this.requestWithRetry(() => this.client.delete(`/beta/workspaces/${workspaceId}/custom-fields/${fieldId}`));
|
|
1655
|
-
// Invalidate cache after successful deletion
|
|
1656
|
-
this.customFieldCache.invalidate(`custom-fields:${workspaceId}`);
|
|
1657
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field deleted successfully', {
|
|
1658
|
-
method: 'deleteCustomField',
|
|
1659
|
-
fieldId,
|
|
1660
|
-
workspaceId
|
|
1661
|
-
});
|
|
1662
|
-
return { success: true };
|
|
1663
|
-
}
|
|
1664
|
-
catch (error) {
|
|
1665
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete custom field', {
|
|
1666
|
-
method: 'deleteCustomField',
|
|
1667
|
-
error: getErrorMessage(error),
|
|
1668
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1669
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1670
|
-
fieldId,
|
|
1671
|
-
workspaceId
|
|
1672
|
-
});
|
|
1673
|
-
throw this.formatApiError(error, 'delete', 'custom field', fieldId);
|
|
1674
|
-
}
|
|
118
|
+
return (0, customFields_1.deleteCustomField)(this._ctx, workspaceId, fieldId);
|
|
1675
119
|
}
|
|
1676
|
-
/**
|
|
1677
|
-
* Add a custom field to a project
|
|
1678
|
-
* @param projectId - ID of the project
|
|
1679
|
-
* @param fieldId - ID of the custom field
|
|
1680
|
-
* @param value - Optional value for the field
|
|
1681
|
-
* @returns The field value as returned by the API ({ type, value })
|
|
1682
|
-
*/
|
|
1683
120
|
async addCustomFieldToProject(projectId, fieldId, value, fieldType) {
|
|
1684
|
-
|
|
1685
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to project', {
|
|
1686
|
-
method: 'addCustomFieldToProject',
|
|
1687
|
-
projectId,
|
|
1688
|
-
fieldId,
|
|
1689
|
-
hasValue: value !== undefined
|
|
1690
|
-
});
|
|
1691
|
-
// API expects: { customFieldInstanceId, value: { type, value } }
|
|
1692
|
-
// Value type uses camelCase per API docs (e.g., text, multiSelect)
|
|
1693
|
-
const requestData = {
|
|
1694
|
-
customFieldInstanceId: fieldId,
|
|
1695
|
-
};
|
|
1696
|
-
if (value !== undefined) {
|
|
1697
|
-
if (value === null) {
|
|
1698
|
-
requestData.value = fieldType !== undefined
|
|
1699
|
-
? { type: fieldType, value: null }
|
|
1700
|
-
: { value: null };
|
|
1701
|
-
}
|
|
1702
|
-
else {
|
|
1703
|
-
if (!fieldType) {
|
|
1704
|
-
throw new Error('Field type is required when setting a non-null custom field value');
|
|
1705
|
-
}
|
|
1706
|
-
requestData.value = {
|
|
1707
|
-
type: fieldType,
|
|
1708
|
-
value
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
const response = await this.requestWithRetry(() => this.client.post(`/beta/custom-field-values/project/${projectId}`, requestData));
|
|
1713
|
-
// Invalidate project cache broadly — the API response is { type, value },
|
|
1714
|
-
// not a full project object, so we don't have workspace context
|
|
1715
|
-
this.projectCache.invalidate(`projects:`);
|
|
1716
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to project successfully', {
|
|
1717
|
-
method: 'addCustomFieldToProject',
|
|
1718
|
-
projectId,
|
|
1719
|
-
fieldId
|
|
1720
|
-
});
|
|
1721
|
-
return response.data;
|
|
1722
|
-
}
|
|
1723
|
-
catch (error) {
|
|
1724
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to project', {
|
|
1725
|
-
method: 'addCustomFieldToProject',
|
|
1726
|
-
error: getErrorMessage(error),
|
|
1727
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1728
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1729
|
-
projectId,
|
|
1730
|
-
fieldId
|
|
1731
|
-
});
|
|
1732
|
-
throw this.formatApiError(error, 'update', 'project', projectId);
|
|
1733
|
-
}
|
|
121
|
+
return (0, customFields_1.addCustomFieldToProject)(this._ctx, projectId, fieldId, value, fieldType);
|
|
1734
122
|
}
|
|
1735
|
-
/**
|
|
1736
|
-
* Remove a custom field from a project
|
|
1737
|
-
* @param projectId - ID of the project
|
|
1738
|
-
* @param valueId - ID of the custom field value
|
|
1739
|
-
* @returns Success indicator
|
|
1740
|
-
*/
|
|
1741
123
|
async removeCustomFieldFromProject(projectId, valueId) {
|
|
1742
|
-
|
|
1743
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from project', {
|
|
1744
|
-
method: 'removeCustomFieldFromProject',
|
|
1745
|
-
projectId,
|
|
1746
|
-
valueId
|
|
1747
|
-
});
|
|
1748
|
-
await this.requestWithRetry(() => this.client.delete(`/beta/custom-field-values/project/${projectId}/custom-fields/${valueId}`));
|
|
1749
|
-
// Invalidate all project caches since we don't have workspace context here
|
|
1750
|
-
this.projectCache.invalidate(`projects:`);
|
|
1751
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from project successfully', {
|
|
1752
|
-
method: 'removeCustomFieldFromProject',
|
|
1753
|
-
projectId,
|
|
1754
|
-
valueId
|
|
1755
|
-
});
|
|
1756
|
-
return { success: true };
|
|
1757
|
-
}
|
|
1758
|
-
catch (error) {
|
|
1759
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from project', {
|
|
1760
|
-
method: 'removeCustomFieldFromProject',
|
|
1761
|
-
error: getErrorMessage(error),
|
|
1762
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1763
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1764
|
-
projectId,
|
|
1765
|
-
valueId
|
|
1766
|
-
});
|
|
1767
|
-
throw this.formatApiError(error, 'update', 'project', projectId);
|
|
1768
|
-
}
|
|
124
|
+
return (0, customFields_1.removeCustomFieldFromProject)(this._ctx, projectId, valueId);
|
|
1769
125
|
}
|
|
1770
|
-
/**
|
|
1771
|
-
* Add a custom field to a task
|
|
1772
|
-
* @param taskId - ID of the task
|
|
1773
|
-
* @param fieldId - ID of the custom field
|
|
1774
|
-
* @param value - Optional value for the field
|
|
1775
|
-
* @returns The field value as returned by the API ({ type, value })
|
|
1776
|
-
*/
|
|
1777
126
|
async addCustomFieldToTask(taskId, fieldId, value, fieldType) {
|
|
1778
|
-
|
|
1779
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Adding custom field to task', {
|
|
1780
|
-
method: 'addCustomFieldToTask',
|
|
1781
|
-
taskId,
|
|
1782
|
-
fieldId,
|
|
1783
|
-
hasValue: value !== undefined
|
|
1784
|
-
});
|
|
1785
|
-
// API expects: { customFieldInstanceId, value: { type, value } }
|
|
1786
|
-
// Value type uses camelCase per API docs (e.g., text, multiSelect)
|
|
1787
|
-
const requestData = {
|
|
1788
|
-
customFieldInstanceId: fieldId,
|
|
1789
|
-
};
|
|
1790
|
-
if (value !== undefined) {
|
|
1791
|
-
if (value === null) {
|
|
1792
|
-
requestData.value = fieldType !== undefined
|
|
1793
|
-
? { type: fieldType, value: null }
|
|
1794
|
-
: { value: null };
|
|
1795
|
-
}
|
|
1796
|
-
else {
|
|
1797
|
-
if (!fieldType) {
|
|
1798
|
-
throw new Error('Field type is required when setting a non-null custom field value');
|
|
1799
|
-
}
|
|
1800
|
-
requestData.value = {
|
|
1801
|
-
type: fieldType,
|
|
1802
|
-
value
|
|
1803
|
-
};
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
const response = await this.requestWithRetry(() => this.client.post(`/beta/custom-field-values/task/${taskId}`, requestData));
|
|
1807
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field added to task successfully', {
|
|
1808
|
-
method: 'addCustomFieldToTask',
|
|
1809
|
-
taskId,
|
|
1810
|
-
fieldId
|
|
1811
|
-
});
|
|
1812
|
-
return response.data;
|
|
1813
|
-
}
|
|
1814
|
-
catch (error) {
|
|
1815
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to add custom field to task', {
|
|
1816
|
-
method: 'addCustomFieldToTask',
|
|
1817
|
-
error: getErrorMessage(error),
|
|
1818
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1819
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1820
|
-
taskId,
|
|
1821
|
-
fieldId
|
|
1822
|
-
});
|
|
1823
|
-
throw this.formatApiError(error, 'update', 'task', taskId);
|
|
1824
|
-
}
|
|
127
|
+
return (0, customFields_1.addCustomFieldToTask)(this._ctx, taskId, fieldId, value, fieldType);
|
|
1825
128
|
}
|
|
1826
|
-
/**
|
|
1827
|
-
* Remove a custom field from a task
|
|
1828
|
-
* @param taskId - ID of the task
|
|
1829
|
-
* @param valueId - ID of the custom field value
|
|
1830
|
-
* @returns Success indicator
|
|
1831
|
-
*/
|
|
1832
129
|
async removeCustomFieldFromTask(taskId, valueId) {
|
|
1833
|
-
|
|
1834
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Removing custom field from task', {
|
|
1835
|
-
method: 'removeCustomFieldFromTask',
|
|
1836
|
-
taskId,
|
|
1837
|
-
valueId
|
|
1838
|
-
});
|
|
1839
|
-
await this.requestWithRetry(() => this.client.delete(`/beta/custom-field-values/task/${taskId}/custom-fields/${valueId}`));
|
|
1840
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Custom field removed from task successfully', {
|
|
1841
|
-
method: 'removeCustomFieldFromTask',
|
|
1842
|
-
taskId,
|
|
1843
|
-
valueId
|
|
1844
|
-
});
|
|
1845
|
-
return { success: true };
|
|
1846
|
-
}
|
|
1847
|
-
catch (error) {
|
|
1848
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to remove custom field from task', {
|
|
1849
|
-
method: 'removeCustomFieldFromTask',
|
|
1850
|
-
error: getErrorMessage(error),
|
|
1851
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1852
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1853
|
-
taskId,
|
|
1854
|
-
valueId
|
|
1855
|
-
});
|
|
1856
|
-
throw this.formatApiError(error, 'update', 'task', taskId);
|
|
1857
|
-
}
|
|
130
|
+
return (0, customFields_1.removeCustomFieldFromTask)(this._ctx, taskId, valueId);
|
|
1858
131
|
}
|
|
1859
132
|
// ========================================
|
|
1860
133
|
// RECURRING TASK API METHODS
|
|
1861
134
|
// ========================================
|
|
1862
|
-
/**
|
|
1863
|
-
* Fetch recurring tasks from Motion API with automatic pagination
|
|
1864
|
-
* @param workspaceId - Optional workspace ID to filter recurring tasks
|
|
1865
|
-
* @param options - Optional configuration for maxPages and limit
|
|
1866
|
-
* @returns Array of recurring tasks from all pages
|
|
1867
|
-
*/
|
|
1868
135
|
async getRecurringTasks(workspaceId, options) {
|
|
1869
|
-
|
|
1870
|
-
const cacheKey = workspaceId ? `recurring-tasks:workspace:${workspaceId}` : 'recurring-tasks:all';
|
|
1871
|
-
// Check cache - return items only (no stale truncation info)
|
|
1872
|
-
const cachedItems = this.recurringTaskCache.get(cacheKey);
|
|
1873
|
-
if (cachedItems !== null) {
|
|
1874
|
-
return { items: cachedItems };
|
|
1875
|
-
}
|
|
1876
|
-
try {
|
|
1877
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching recurring tasks from Motion API with pagination', {
|
|
1878
|
-
method: 'getRecurringTasks',
|
|
1879
|
-
workspaceId,
|
|
1880
|
-
maxPages,
|
|
1881
|
-
limit
|
|
1882
|
-
});
|
|
1883
|
-
// Create a fetch function for pagination utility
|
|
1884
|
-
const fetchPage = async (cursor) => {
|
|
1885
|
-
const params = new URLSearchParams();
|
|
1886
|
-
if (workspaceId)
|
|
1887
|
-
params.append('workspaceId', workspaceId);
|
|
1888
|
-
if (cursor)
|
|
1889
|
-
params.append('cursor', cursor);
|
|
1890
|
-
const queryString = params.toString();
|
|
1891
|
-
const url = queryString ? `/recurring-tasks?${queryString}` : '/recurring-tasks';
|
|
1892
|
-
return this.requestWithRetry(() => this.client.get(url));
|
|
1893
|
-
};
|
|
1894
|
-
// Use pagination utility to fetch all pages
|
|
1895
|
-
const paginatedResult = await (0, paginationNew_1.fetchAllPages)(fetchPage, 'recurring-tasks', {
|
|
1896
|
-
maxPages,
|
|
1897
|
-
logProgress: true,
|
|
1898
|
-
...(limit ? { maxItems: limit } : {})
|
|
1899
|
-
});
|
|
1900
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring tasks fetched successfully with pagination', {
|
|
1901
|
-
method: 'getRecurringTasks',
|
|
1902
|
-
totalCount: paginatedResult.totalFetched,
|
|
1903
|
-
pagesProcessed: Math.ceil(paginatedResult.totalFetched / 50), // Assuming ~50 items per page
|
|
1904
|
-
hasMore: paginatedResult.hasMore,
|
|
1905
|
-
workspaceId
|
|
1906
|
-
});
|
|
1907
|
-
// Cache only items, not truncation metadata
|
|
1908
|
-
this.recurringTaskCache.set(cacheKey, paginatedResult.items);
|
|
1909
|
-
return { items: paginatedResult.items, truncation: paginatedResult.truncation };
|
|
1910
|
-
}
|
|
1911
|
-
catch (error) {
|
|
1912
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch recurring tasks', {
|
|
1913
|
-
method: 'getRecurringTasks',
|
|
1914
|
-
error: getErrorMessage(error),
|
|
1915
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1916
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1917
|
-
workspaceId
|
|
1918
|
-
});
|
|
1919
|
-
throw this.formatApiError(error, 'fetch', 'recurring task');
|
|
1920
|
-
}
|
|
136
|
+
return (0, recurringTasks_1.getRecurringTasks)(this._ctx, workspaceId, options);
|
|
1921
137
|
}
|
|
1922
|
-
/**
|
|
1923
|
-
* Create a new recurring task
|
|
1924
|
-
* @param taskData - Data for creating the recurring task
|
|
1925
|
-
* @returns The created recurring task
|
|
1926
|
-
*/
|
|
1927
138
|
async createRecurringTask(taskData) {
|
|
1928
|
-
|
|
1929
|
-
// Validate frequency object before transformation
|
|
1930
|
-
const freqValidation = (0, frequencyTransform_1.validateFrequencyObject)(taskData.frequency);
|
|
1931
|
-
if (!freqValidation.valid) {
|
|
1932
|
-
throw new Error(`Invalid frequency object: ${freqValidation.reason || 'Unknown reason'}`);
|
|
1933
|
-
}
|
|
1934
|
-
// Transform frequency object to API string format
|
|
1935
|
-
const frequencyString = (0, frequencyTransform_1.transformFrequencyToApiString)(taskData.frequency);
|
|
1936
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Creating recurring task in Motion API', {
|
|
1937
|
-
method: 'createRecurringTask',
|
|
1938
|
-
name: taskData.name,
|
|
1939
|
-
assigneeId: taskData.assigneeId,
|
|
1940
|
-
frequency: frequencyString,
|
|
1941
|
-
originalFrequency: taskData.frequency,
|
|
1942
|
-
workspaceId: taskData.workspaceId
|
|
1943
|
-
});
|
|
1944
|
-
// Build API payload: extract endDate from frequency object to top-level,
|
|
1945
|
-
// replace frequency object with transformed string
|
|
1946
|
-
const { frequency: _freqObj, ...restTaskData } = taskData;
|
|
1947
|
-
const apiPayload = {
|
|
1948
|
-
...restTaskData,
|
|
1949
|
-
frequency: frequencyString,
|
|
1950
|
-
...(taskData.frequency.endDate && { endDate: taskData.frequency.endDate })
|
|
1951
|
-
};
|
|
1952
|
-
// Create minimal payload by removing empty/null values to avoid validation errors
|
|
1953
|
-
const minimalPayload = (0, constants_1.createMinimalPayload)(apiPayload);
|
|
1954
|
-
const response = await this.requestWithRetry(() => this.client.post('/recurring-tasks', minimalPayload));
|
|
1955
|
-
// Invalidate cache after successful creation
|
|
1956
|
-
this.recurringTaskCache.invalidate('recurring-tasks:');
|
|
1957
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task created successfully', {
|
|
1958
|
-
method: 'createRecurringTask',
|
|
1959
|
-
taskId: response.data?.id,
|
|
1960
|
-
name: taskData.name
|
|
1961
|
-
});
|
|
1962
|
-
return response.data;
|
|
1963
|
-
}
|
|
1964
|
-
catch (error) {
|
|
1965
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to create recurring task', {
|
|
1966
|
-
method: 'createRecurringTask',
|
|
1967
|
-
error: getErrorMessage(error),
|
|
1968
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
1969
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
1970
|
-
taskName: taskData?.name
|
|
1971
|
-
});
|
|
1972
|
-
throw this.formatApiError(error, 'create', 'recurring task', undefined, taskData.name);
|
|
1973
|
-
}
|
|
139
|
+
return (0, recurringTasks_1.createRecurringTask)(this._ctx, taskData);
|
|
1974
140
|
}
|
|
1975
|
-
/**
|
|
1976
|
-
* Delete a recurring task
|
|
1977
|
-
* @param recurringTaskId - ID of the recurring task to delete
|
|
1978
|
-
* @returns Success indicator
|
|
1979
|
-
*/
|
|
1980
141
|
async deleteRecurringTask(recurringTaskId) {
|
|
1981
|
-
|
|
1982
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Deleting recurring task from Motion API', {
|
|
1983
|
-
method: 'deleteRecurringTask',
|
|
1984
|
-
recurringTaskId
|
|
1985
|
-
});
|
|
1986
|
-
await this.requestWithRetry(() => this.client.delete(`/recurring-tasks/${recurringTaskId}`));
|
|
1987
|
-
// Invalidate cache after successful deletion
|
|
1988
|
-
this.recurringTaskCache.invalidate('recurring-tasks:');
|
|
1989
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Recurring task deleted successfully', {
|
|
1990
|
-
method: 'deleteRecurringTask',
|
|
1991
|
-
recurringTaskId
|
|
1992
|
-
});
|
|
1993
|
-
return { success: true };
|
|
1994
|
-
}
|
|
1995
|
-
catch (error) {
|
|
1996
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to delete recurring task', {
|
|
1997
|
-
method: 'deleteRecurringTask',
|
|
1998
|
-
error: getErrorMessage(error),
|
|
1999
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
2000
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
2001
|
-
recurringTaskId
|
|
2002
|
-
});
|
|
2003
|
-
throw this.formatApiError(error, 'delete', 'recurring task', recurringTaskId);
|
|
2004
|
-
}
|
|
142
|
+
return (0, recurringTasks_1.deleteRecurringTask)(this._ctx, recurringTaskId);
|
|
2005
143
|
}
|
|
2006
144
|
// ========================================
|
|
2007
145
|
// SCHEDULE API METHODS
|
|
2008
146
|
// ========================================
|
|
2009
|
-
/**
|
|
2010
|
-
* Get available schedule names for auto-scheduling
|
|
2011
|
-
* @param workspaceId - Optional workspace ID to filter schedules (currently unused by Motion API)
|
|
2012
|
-
* @returns Array of schedule names
|
|
2013
|
-
*/
|
|
2014
147
|
async getAvailableScheduleNames(workspaceId) {
|
|
2015
|
-
|
|
2016
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching available schedule names', {
|
|
2017
|
-
method: 'getAvailableScheduleNames',
|
|
2018
|
-
workspaceId
|
|
2019
|
-
});
|
|
2020
|
-
// Fetch all schedules without filters to get available schedule templates
|
|
2021
|
-
const schedules = await this.getSchedules();
|
|
2022
|
-
const scheduleNames = schedules.map(schedule => schedule.name).filter(Boolean);
|
|
2023
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Available schedule names fetched successfully', {
|
|
2024
|
-
method: 'getAvailableScheduleNames',
|
|
2025
|
-
count: scheduleNames.length,
|
|
2026
|
-
scheduleNames
|
|
2027
|
-
});
|
|
2028
|
-
return scheduleNames;
|
|
2029
|
-
}
|
|
2030
|
-
catch (error) {
|
|
2031
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch available schedule names', {
|
|
2032
|
-
method: 'getAvailableScheduleNames',
|
|
2033
|
-
error: getErrorMessage(error),
|
|
2034
|
-
workspaceId
|
|
2035
|
-
});
|
|
2036
|
-
throw this.formatApiError(error, 'fetch', 'schedule');
|
|
2037
|
-
}
|
|
148
|
+
return (0, schedules_1.getAvailableScheduleNames)(this._ctx, workspaceId);
|
|
2038
149
|
}
|
|
2039
|
-
/**
|
|
2040
|
-
* Fetch schedules from Motion API.
|
|
2041
|
-
* The Motion API GET /schedules accepts no query parameters.
|
|
2042
|
-
* @returns Array of schedules
|
|
2043
|
-
*/
|
|
2044
150
|
async getSchedules() {
|
|
2045
|
-
|
|
2046
|
-
return this.scheduleCache.withCache(cacheKey, async () => {
|
|
2047
|
-
try {
|
|
2048
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching schedules from Motion API', {
|
|
2049
|
-
method: 'getSchedules'
|
|
2050
|
-
});
|
|
2051
|
-
const response = await this.requestWithRetry(() => this.client.get('/schedules'));
|
|
2052
|
-
// Validate response against schema
|
|
2053
|
-
const validatedResponse = this.validateResponse(response.data, motion_1.SchedulesListResponseSchema, 'getSchedules');
|
|
2054
|
-
// Handle both wrapped and unwrapped responses
|
|
2055
|
-
const schedules = Array.isArray(validatedResponse)
|
|
2056
|
-
? validatedResponse
|
|
2057
|
-
: validatedResponse.schedules || [];
|
|
2058
|
-
const schedulesArray = Array.isArray(schedules) ? schedules : [];
|
|
2059
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Schedules fetched successfully', {
|
|
2060
|
-
method: 'getSchedules',
|
|
2061
|
-
count: schedulesArray.length
|
|
2062
|
-
});
|
|
2063
|
-
return schedulesArray;
|
|
2064
|
-
}
|
|
2065
|
-
catch (error) {
|
|
2066
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch schedules', {
|
|
2067
|
-
method: 'getSchedules',
|
|
2068
|
-
error: getErrorMessage(error),
|
|
2069
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
2070
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined
|
|
2071
|
-
});
|
|
2072
|
-
throw this.formatApiError(error, 'fetch', 'schedule');
|
|
2073
|
-
}
|
|
2074
|
-
});
|
|
151
|
+
return (0, schedules_1.getSchedules)(this._ctx);
|
|
2075
152
|
}
|
|
2076
153
|
// ========================================
|
|
2077
154
|
// STATUS API METHODS
|
|
2078
155
|
// ========================================
|
|
2079
|
-
/**
|
|
2080
|
-
* Retrieves available workflow statuses from Motion
|
|
2081
|
-
* @param workspaceId - Optional workspace ID to filter statuses
|
|
2082
|
-
* @returns Promise resolving to array of Motion statuses
|
|
2083
|
-
* @throws {Error} If the API request fails
|
|
2084
|
-
*/
|
|
2085
156
|
async getStatuses(workspaceId) {
|
|
2086
|
-
|
|
2087
|
-
const cacheKey = workspaceId ? `statuses:workspace:${workspaceId}` : 'statuses:all';
|
|
2088
|
-
return this.statusCache.withCache(cacheKey, async () => {
|
|
2089
|
-
try {
|
|
2090
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Fetching statuses from Motion API', {
|
|
2091
|
-
method: 'getStatuses',
|
|
2092
|
-
workspaceId
|
|
2093
|
-
});
|
|
2094
|
-
const params = new URLSearchParams();
|
|
2095
|
-
if (workspaceId)
|
|
2096
|
-
params.append('workspaceId', workspaceId);
|
|
2097
|
-
const queryString = params.toString();
|
|
2098
|
-
const url = queryString ? `/statuses?${queryString}` : '/statuses';
|
|
2099
|
-
const response = await this.requestWithRetry(() => this.client.get(url));
|
|
2100
|
-
// Validate response against schema
|
|
2101
|
-
const validatedResponse = this.validateResponse(response.data, motion_1.StatusesListResponseSchema, 'statuses');
|
|
2102
|
-
// Extract statuses from validated response; fail loudly on unknown shape to avoid caching empty results.
|
|
2103
|
-
let statuses;
|
|
2104
|
-
if (Array.isArray(validatedResponse)) {
|
|
2105
|
-
statuses = validatedResponse;
|
|
2106
|
-
}
|
|
2107
|
-
else if (validatedResponse &&
|
|
2108
|
-
typeof validatedResponse === 'object' &&
|
|
2109
|
-
'statuses' in validatedResponse &&
|
|
2110
|
-
Array.isArray(validatedResponse.statuses)) {
|
|
2111
|
-
statuses = validatedResponse.statuses;
|
|
2112
|
-
}
|
|
2113
|
-
else {
|
|
2114
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Unexpected statuses response shape', {
|
|
2115
|
-
method: 'getStatuses',
|
|
2116
|
-
workspaceId,
|
|
2117
|
-
responseType: validatedResponse === null ? 'null' : typeof validatedResponse
|
|
2118
|
-
});
|
|
2119
|
-
throw new Error('Invalid statuses response shape from Motion API');
|
|
2120
|
-
}
|
|
2121
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'Statuses fetched successfully', {
|
|
2122
|
-
method: 'getStatuses',
|
|
2123
|
-
count: statuses.length,
|
|
2124
|
-
workspaceId
|
|
2125
|
-
});
|
|
2126
|
-
return statuses;
|
|
2127
|
-
}
|
|
2128
|
-
catch (error) {
|
|
2129
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch statuses', {
|
|
2130
|
-
method: 'getStatuses',
|
|
2131
|
-
error: getErrorMessage(error),
|
|
2132
|
-
apiStatus: (0, axios_1.isAxiosError)(error) ? error.response?.status : undefined,
|
|
2133
|
-
apiMessage: (0, axios_1.isAxiosError)(error) ? error.response?.data?.message : undefined,
|
|
2134
|
-
workspaceId
|
|
2135
|
-
});
|
|
2136
|
-
throw this.formatApiError(error, 'fetch', 'status');
|
|
2137
|
-
}
|
|
2138
|
-
});
|
|
157
|
+
return (0, statuses_1.getStatuses)(this._ctx, workspaceId);
|
|
2139
158
|
}
|
|
2140
159
|
// ========================================
|
|
2141
|
-
//
|
|
160
|
+
// SEARCH AND RESOLUTION METHODS
|
|
2142
161
|
// ========================================
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
// Apply limit to prevent resource exhaustion
|
|
2155
|
-
const effectiveLimit = limit || constants_1.LIMITS.MAX_SEARCH_RESULTS;
|
|
2156
|
-
const allUncompletedTasks = [];
|
|
2157
|
-
let aggregateTruncation;
|
|
2158
|
-
try {
|
|
2159
|
-
// Get all workspaces
|
|
2160
|
-
const workspaces = await this.getWorkspaces();
|
|
2161
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Searching for uncompleted tasks across workspaces', {
|
|
2162
|
-
method: 'getAllUncompletedTasks',
|
|
2163
|
-
totalWorkspaces: workspaces.length
|
|
2164
|
-
});
|
|
2165
|
-
// Fetch tasks from each workspace
|
|
2166
|
-
for (const workspace of workspaces) {
|
|
2167
|
-
if (allUncompletedTasks.length >= effectiveLimit) {
|
|
2168
|
-
break; // Stop if we've reached the limit
|
|
2169
|
-
}
|
|
2170
|
-
try {
|
|
2171
|
-
// Calculate fetch limit before API call (defense-in-depth)
|
|
2172
|
-
const fetchLimit = (0, paginationNew_1.calculateAdaptiveFetchLimit)(allUncompletedTasks.length, effectiveLimit);
|
|
2173
|
-
if (fetchLimit <= 0)
|
|
2174
|
-
break;
|
|
2175
|
-
// Get tasks from this workspace with adaptive limit
|
|
2176
|
-
const { items: workspaceTasks, truncation: wsTruncation } = await this.getTasks({
|
|
2177
|
-
workspaceId: workspace.id,
|
|
2178
|
-
assigneeId,
|
|
2179
|
-
limit: fetchLimit,
|
|
2180
|
-
maxPages: constants_1.LIMITS.MAX_PAGES
|
|
2181
|
-
});
|
|
2182
|
-
aggregateTruncation = this.mergeTruncationMetadata(aggregateTruncation, wsTruncation);
|
|
2183
|
-
// Filter for uncompleted tasks
|
|
2184
|
-
const uncompletedTasks = workspaceTasks.filter(task => {
|
|
2185
|
-
// Task is uncompleted if status is missing or isResolvedStatus is false
|
|
2186
|
-
if (!task.status)
|
|
2187
|
-
return true; // No status = not resolved
|
|
2188
|
-
if (typeof task.status === 'string') {
|
|
2189
|
-
const resolvedStatusNames = new Set([
|
|
2190
|
-
'completed',
|
|
2191
|
-
'complete',
|
|
2192
|
-
'done',
|
|
2193
|
-
'closed',
|
|
2194
|
-
'resolved',
|
|
2195
|
-
'canceled',
|
|
2196
|
-
'cancelled'
|
|
2197
|
-
]);
|
|
2198
|
-
return !resolvedStatusNames.has(task.status.trim().toLowerCase());
|
|
2199
|
-
}
|
|
2200
|
-
return !task.status.isResolvedStatus; // Object status with isResolvedStatus false
|
|
2201
|
-
});
|
|
2202
|
-
// Only add as many as we still need
|
|
2203
|
-
const remaining = effectiveLimit - allUncompletedTasks.length;
|
|
2204
|
-
allUncompletedTasks.push(...uncompletedTasks.slice(0, remaining));
|
|
2205
|
-
if (uncompletedTasks.length > 0) {
|
|
2206
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.DEBUG, 'Found uncompleted tasks in workspace', {
|
|
2207
|
-
method: 'getAllUncompletedTasks',
|
|
2208
|
-
workspaceId: workspace.id,
|
|
2209
|
-
workspaceName: workspace.name,
|
|
2210
|
-
uncompletedTasks: uncompletedTasks.length,
|
|
2211
|
-
keptTasks: Math.min(uncompletedTasks.length, remaining),
|
|
2212
|
-
totalTasks: workspaceTasks.length
|
|
2213
|
-
});
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
catch (workspaceError) {
|
|
2217
|
-
// Log error but continue with other workspaces
|
|
2218
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.WARN, 'Failed to fetch tasks from workspace', {
|
|
2219
|
-
method: 'getAllUncompletedTasks',
|
|
2220
|
-
workspaceId: workspace.id,
|
|
2221
|
-
workspaceName: workspace.name,
|
|
2222
|
-
error: getErrorMessage(workspaceError)
|
|
2223
|
-
});
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
catch (workspaceListError) {
|
|
2228
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to get workspace list', {
|
|
2229
|
-
method: 'getAllUncompletedTasks',
|
|
2230
|
-
error: getErrorMessage(workspaceListError)
|
|
2231
|
-
});
|
|
2232
|
-
throw workspaceListError;
|
|
2233
|
-
}
|
|
2234
|
-
// Results are already limited during collection, no need to slice again
|
|
2235
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.INFO, 'All uncompleted tasks fetched successfully', {
|
|
2236
|
-
method: 'getAllUncompletedTasks',
|
|
2237
|
-
returned: allUncompletedTasks.length,
|
|
2238
|
-
limit: effectiveLimit
|
|
2239
|
-
});
|
|
2240
|
-
if (aggregateTruncation) {
|
|
2241
|
-
aggregateTruncation.returnedCount = allUncompletedTasks.length;
|
|
2242
|
-
}
|
|
2243
|
-
return { items: allUncompletedTasks, truncation: aggregateTruncation };
|
|
2244
|
-
}
|
|
2245
|
-
catch (error) {
|
|
2246
|
-
(0, logger_1.mcpLog)(constants_1.LOG_LEVELS.ERROR, 'Failed to fetch all uncompleted tasks', {
|
|
2247
|
-
method: 'getAllUncompletedTasks',
|
|
2248
|
-
error: getErrorMessage(error)
|
|
2249
|
-
});
|
|
2250
|
-
throw error;
|
|
2251
|
-
}
|
|
162
|
+
async searchTasks(query, workspaceId, limit) {
|
|
163
|
+
return (0, search_1.searchTasks)(this._ctx, query, workspaceId, limit);
|
|
164
|
+
}
|
|
165
|
+
async searchProjects(query, workspaceId, limit) {
|
|
166
|
+
return (0, search_1.searchProjects)(this._ctx, query, workspaceId, limit);
|
|
167
|
+
}
|
|
168
|
+
async resolveProjectIdentifier(identifier, workspaceId) {
|
|
169
|
+
return (0, search_1.resolveProjectIdentifier)(this._ctx, identifier, workspaceId);
|
|
170
|
+
}
|
|
171
|
+
async resolveUserIdentifier(identifier, workspaceId, options) {
|
|
172
|
+
return (0, search_1.resolveUserIdentifier)(this._ctx, identifier, workspaceId, options);
|
|
2252
173
|
}
|
|
2253
174
|
}
|
|
2254
175
|
exports.MotionApiService = MotionApiService;
|