@hauptsache.net/clickup-mcp 1.7.0 → 1.7.3
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 +9 -4
- package/dist/clickup-text.d.ts +15 -0
- package/dist/clickup-text.d.ts.map +1 -1
- package/dist/clickup-text.js +202 -21
- package/dist/shared/attachments.d.ts +37 -16
- package/dist/shared/attachments.d.ts.map +1 -1
- package/dist/shared/attachments.js +45 -31
- package/dist/shared/config.d.ts +1 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +41 -1
- package/dist/shared/utils.d.ts +1 -1
- package/dist/shared/utils.js +3 -3
- package/dist/tools/task-tools.js +3 -3
- package/dist/tools/task-write-tools.d.ts.map +1 -1
- package/dist/tools/task-write-tools.js +297 -51
- package/dist/tools/time-tools.js +2 -2
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@ exports.isClickUpAttachmentUrl = isClickUpAttachmentUrl;
|
|
|
4
4
|
exports.resolveImageSource = resolveImageSource;
|
|
5
5
|
exports.captionToFilename = captionToFilename;
|
|
6
6
|
exports.uploadTaskAttachment = uploadTaskAttachment;
|
|
7
|
-
exports.
|
|
7
|
+
exports.resolveMarkdownImages = resolveMarkdownImages;
|
|
8
|
+
exports.uploadResolvedImages = uploadResolvedImages;
|
|
8
9
|
exports.toAttachmentMap = toAttachmentMap;
|
|
9
10
|
const buffer_1 = require("buffer");
|
|
10
11
|
const crypto_1 = require("crypto");
|
|
@@ -185,31 +186,51 @@ async function uploadTaskAttachment(taskId, filename, bytes, mimeType) {
|
|
|
185
186
|
return attachment;
|
|
186
187
|
}
|
|
187
188
|
/**
|
|
188
|
-
*
|
|
189
|
+
* Phase 1 of attaching images: resolve every source without writing anything.
|
|
189
190
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* Reads local files, downloads http(s) URLs, decodes data URIs and validates
|
|
192
|
+
* magic bytes and size. Failures are collected instead of thrown so the caller
|
|
193
|
+
* can report every broken reference at once - and abort before anything is
|
|
194
|
+
* posted to ClickUp. Identical sources are resolved once.
|
|
194
195
|
*/
|
|
195
|
-
async function
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
const seen = new
|
|
196
|
+
async function resolveMarkdownImages(images, baseDir = process.cwd()) {
|
|
197
|
+
const resolved = [];
|
|
198
|
+
const failures = [];
|
|
199
|
+
const seen = new Set();
|
|
199
200
|
for (const { src, alt } of images) {
|
|
200
|
-
|
|
201
|
-
if (cached) {
|
|
202
|
-
results.push(cached);
|
|
201
|
+
if (seen.has(src)) {
|
|
203
202
|
continue;
|
|
204
203
|
}
|
|
205
|
-
|
|
204
|
+
seen.add(src);
|
|
205
|
+
try {
|
|
206
|
+
resolved.push({ src, alt, resolved: await resolveImageSource(src, baseDir) });
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
210
|
+
console.error(`Cannot use image "${src}": ${message}`);
|
|
211
|
+
failures.push({ src, error: message });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { resolved, failures };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Phase 2: upload the resolved images to the task.
|
|
218
|
+
*
|
|
219
|
+
* Uploads run sequentially: a typical comment has a handful of screenshots, and
|
|
220
|
+
* N uploads plus one write call stays well inside ClickUp's 100 calls/minute.
|
|
221
|
+
* Stops at the first upload error - an API failure is unlikely to heal mid-batch,
|
|
222
|
+
* and everything uploaded so far is returned so the caller can tell a retry to
|
|
223
|
+
* reference those CDN URLs directly instead of uploading again.
|
|
224
|
+
*/
|
|
225
|
+
async function uploadResolvedImages(taskId, images) {
|
|
226
|
+
const uploaded = [];
|
|
227
|
+
for (const { src, alt, resolved } of images) {
|
|
206
228
|
try {
|
|
207
|
-
const resolved = await resolveImageSource(src, baseDir);
|
|
208
229
|
if (resolved.kind === "existing") {
|
|
209
230
|
// Already on ClickUp's CDN - synthesise the minimal attachment shape so
|
|
210
231
|
// the fragment builder has something to work with.
|
|
211
232
|
const name = decodeURIComponent((0, path_1.basename)(new URL(resolved.url).pathname));
|
|
212
|
-
|
|
233
|
+
uploaded.push({
|
|
213
234
|
src,
|
|
214
235
|
attachment: {
|
|
215
236
|
id: name,
|
|
@@ -221,34 +242,27 @@ async function uploadMarkdownImages(taskId, images, baseDir = process.cwd()) {
|
|
|
221
242
|
thumbnail_medium: resolved.url,
|
|
222
243
|
thumbnail_large: resolved.url,
|
|
223
244
|
},
|
|
224
|
-
};
|
|
245
|
+
});
|
|
225
246
|
}
|
|
226
247
|
else {
|
|
227
248
|
const filename = captionToFilename(alt, resolved.suggestedName);
|
|
228
249
|
const attachment = await uploadTaskAttachment(taskId, filename, resolved.bytes, resolved.mimeType);
|
|
229
|
-
|
|
250
|
+
uploaded.push({ src, attachment });
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
253
|
catch (error) {
|
|
233
254
|
const message = error instanceof Error ? error.message : "unknown error";
|
|
234
255
|
console.error(`Failed to attach image "${src}": ${message}`);
|
|
235
|
-
|
|
256
|
+
return { uploaded, failure: { src, error: message } };
|
|
236
257
|
}
|
|
237
|
-
seen.set(src, result);
|
|
238
|
-
results.push(result);
|
|
239
258
|
}
|
|
240
|
-
return
|
|
259
|
+
return { uploaded, failure: null };
|
|
241
260
|
}
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
* Sources whose upload failed are absent, so the converters fall back to text.
|
|
245
|
-
*/
|
|
246
|
-
function toAttachmentMap(results) {
|
|
261
|
+
/** Map from markdown `src` to the attachment that should be embedded for it. */
|
|
262
|
+
function toAttachmentMap(uploaded) {
|
|
247
263
|
const map = new Map();
|
|
248
|
-
for (const
|
|
249
|
-
|
|
250
|
-
map.set(result.src, result.attachment);
|
|
251
|
-
}
|
|
264
|
+
for (const { src, attachment } of uploaded) {
|
|
265
|
+
map.set(src, attachment);
|
|
252
266
|
}
|
|
253
267
|
return map;
|
|
254
268
|
}
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAkDvF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAkDvF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AA8CxD,eAAO,MAAM,MAAM;;;;;;;;;CAqBlB,CAAC"}
|
package/dist/shared/config.js
CHANGED
|
@@ -48,6 +48,36 @@ if (rawMode === 'read-minimal' || rawMode === 'read') {
|
|
|
48
48
|
else if (rawMode && rawMode !== 'write') {
|
|
49
49
|
console.error(`Invalid CLICKUP_MCP_MODE "${rawMode}". Using default "write". Valid options: read-minimal, read, write`);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Numeric settings are exposed as optional MCPB user_config fields, so their env
|
|
53
|
+
* values can arrive blank or - if the host does not substitute an unset optional
|
|
54
|
+
* field - as the literal `${user_config.x}` placeholder. Both mean "not configured"
|
|
55
|
+
* and must fall back to the default rather than fail or turn into NaN.
|
|
56
|
+
*/
|
|
57
|
+
function readOptionalEnv(name) {
|
|
58
|
+
const raw = process.env[name]?.trim();
|
|
59
|
+
if (!raw || /^\$\{.*}$/.test(raw)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A typo must not silently change a limit: `parseFloat("Infinity")` would disable
|
|
66
|
+
* the comment age check altogether and `parseFloat("abc")` would yield NaN, which
|
|
67
|
+
* compares false against every size and so lifts the upload limit instead of
|
|
68
|
+
* enforcing it. Fail at startup instead.
|
|
69
|
+
*/
|
|
70
|
+
function parseNumericEnv(name, fallback, { min, expectation }) {
|
|
71
|
+
const raw = readOptionalEnv(name);
|
|
72
|
+
if (raw === undefined) {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
const value = Number(raw);
|
|
76
|
+
if (!Number.isFinite(value) || value < min) {
|
|
77
|
+
throw new Error(`Invalid ${name} "${raw}". ${expectation}`);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
51
81
|
exports.CONFIG = {
|
|
52
82
|
apiKey: process.env.CLICKUP_API_KEY,
|
|
53
83
|
teamId: process.env.CLICKUP_TEAM_ID,
|
|
@@ -56,7 +86,17 @@ exports.CONFIG = {
|
|
|
56
86
|
// Upper bound for a single image uploaded to ClickUp. Unlike maxResponseSizeMB this is
|
|
57
87
|
// not about context window budget - it only guards against accidentally pushing huge
|
|
58
88
|
// files into a ticket.
|
|
59
|
-
maxUploadSizeMB:
|
|
89
|
+
maxUploadSizeMB: parseNumericEnv("MAX_UPLOAD_SIZE_MB", 10, {
|
|
90
|
+
min: 1,
|
|
91
|
+
expectation: "Expected a positive number of megabytes.",
|
|
92
|
+
}),
|
|
93
|
+
// How long after creation a comment may still be edited. ClickUp has no way to tell
|
|
94
|
+
// "written by this MCP" apart from "written by the token owner in the UI", so this
|
|
95
|
+
// window is the actual guard against rewriting history. 0 disables editing entirely.
|
|
96
|
+
commentEditWindowHours: parseNumericEnv("CLICKUP_COMMENT_EDIT_WINDOW_HOURS", 24, {
|
|
97
|
+
min: 0,
|
|
98
|
+
expectation: "Expected a non-negative number of hours (0 disables editComment).",
|
|
99
|
+
}),
|
|
60
100
|
primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
|
|
61
101
|
mode: mcpMode,
|
|
62
102
|
};
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Fuse from 'fuse.js';
|
|
2
2
|
/**
|
|
3
3
|
* Checks if a string looks like a valid ClickUp task ID
|
|
4
|
-
* Valid task IDs are 6-
|
|
4
|
+
* Valid task IDs are 6-16 characters long and contain only alphanumeric characters
|
|
5
5
|
*/
|
|
6
6
|
export declare function isTaskId(str: string): boolean;
|
|
7
7
|
/**
|
package/dist/shared/utils.js
CHANGED
|
@@ -23,11 +23,11 @@ const fuse_js_1 = __importDefault(require("fuse.js"));
|
|
|
23
23
|
const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
|
|
24
24
|
/**
|
|
25
25
|
* Checks if a string looks like a valid ClickUp task ID
|
|
26
|
-
* Valid task IDs are 6-
|
|
26
|
+
* Valid task IDs are 6-16 characters long and contain only alphanumeric characters
|
|
27
27
|
*/
|
|
28
28
|
function isTaskId(str) {
|
|
29
|
-
// Task IDs are 6-
|
|
30
|
-
return /^[a-z0-9]{6,
|
|
29
|
+
// Task IDs are 6-16 characters long and contain only alphanumeric characters
|
|
30
|
+
return /^[a-z0-9]{6,16}$/i.test(str);
|
|
31
31
|
}
|
|
32
32
|
// Cache for current user info to avoid repeated API calls and race conditions
|
|
33
33
|
let cachedUserPromise = null;
|
package/dist/tools/task-tools.js
CHANGED
|
@@ -17,11 +17,11 @@ function registerTaskToolsRead(server, userData) {
|
|
|
17
17
|
id: zod_1.z
|
|
18
18
|
.string()
|
|
19
19
|
.min(6)
|
|
20
|
-
.max(
|
|
20
|
+
.max(16)
|
|
21
21
|
.refine(val => (0, utils_1.isTaskId)(val), {
|
|
22
|
-
message: "Task ID must be 6-
|
|
22
|
+
message: "Task ID must be 6-16 alphanumeric characters only"
|
|
23
23
|
})
|
|
24
|
-
.describe(`The 6-
|
|
24
|
+
.describe(`The 6-16 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
|
|
25
25
|
}, {
|
|
26
26
|
readOnlyHint: true
|
|
27
27
|
}, async ({ id }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"task-write-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-write-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA+IpE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAumBtE"}
|