@hauptsache.net/clickup-mcp 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -31
- package/dist/cli.js +6 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -408
- package/dist/shared/config.d.ts +10 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +31 -0
- package/dist/shared/types.d.ts +13 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +30 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +185 -0
- package/dist/tools/list-tools.d.ts +4 -0
- package/dist/tools/list-tools.d.ts.map +1 -0
- package/dist/tools/list-tools.js +278 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +182 -0
- package/dist/tools/space-tools.d.ts +3 -0
- package/dist/tools/space-tools.d.ts.map +1 -0
- package/dist/tools/space-tools.js +61 -0
- package/dist/tools/task-tools.d.ts +9 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +610 -0
- package/dist/tools/time-tools.d.ts +4 -0
- package/dist/tools/time-tools.d.ts.map +1 -0
- package/dist/tools/time-tools.js +251 -0
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isTaskId = isTaskId;
|
|
7
|
+
exports.getCurrentUser = getCurrentUser;
|
|
8
|
+
exports.limitImages = limitImages;
|
|
9
|
+
exports.getSpaceDetails = getSpaceDetails;
|
|
10
|
+
exports.getTaskSearchIndex = getTaskSearchIndex;
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const fuse_js_1 = __importDefault(require("fuse.js"));
|
|
13
|
+
const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a string looks like a valid ClickUp task ID
|
|
16
|
+
* Valid task IDs are 6-9 characters long and contain only alphanumeric characters
|
|
17
|
+
*/
|
|
18
|
+
function isTaskId(str) {
|
|
19
|
+
// Task IDs are 6-9 characters long and contain only alphanumeric characters
|
|
20
|
+
return /^[a-z0-9]{6,9}$/i.test(str);
|
|
21
|
+
}
|
|
22
|
+
// Cache for current user info to avoid repeated API calls
|
|
23
|
+
let cachedUserData = null;
|
|
24
|
+
/**
|
|
25
|
+
* Get current authenticated user information from ClickUp API
|
|
26
|
+
* Caches the result after first successful fetch to avoid repeated API calls during the session
|
|
27
|
+
*/
|
|
28
|
+
async function getCurrentUser() {
|
|
29
|
+
// Return cached data if available
|
|
30
|
+
if (cachedUserData) {
|
|
31
|
+
return cachedUserData;
|
|
32
|
+
}
|
|
33
|
+
const userResponse = await fetch("https://api.clickup.com/api/v2/user", {
|
|
34
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
35
|
+
});
|
|
36
|
+
if (!userResponse.ok) {
|
|
37
|
+
throw new Error(`Error fetching user info: ${userResponse.status} ${userResponse.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
// Cache the result for future calls
|
|
40
|
+
cachedUserData = await userResponse.json();
|
|
41
|
+
return cachedUserData;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Limits the number of images in the content array, replacing excess images with text placeholders
|
|
45
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
46
|
+
*
|
|
47
|
+
* @param content Array of content blocks that may contain images
|
|
48
|
+
* @param maxImages Maximum number of images to keep
|
|
49
|
+
* @returns Modified content array with limited images
|
|
50
|
+
*/
|
|
51
|
+
function limitImages(content, maxImages) {
|
|
52
|
+
// Count how many images we have
|
|
53
|
+
const imageIndices = [];
|
|
54
|
+
// Find all image blocks
|
|
55
|
+
content.forEach((block, index) => {
|
|
56
|
+
if (block.type === "image") {
|
|
57
|
+
imageIndices.push(index);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// If we have fewer images than the limit, return the original content
|
|
61
|
+
if (imageIndices.length <= maxImages) {
|
|
62
|
+
return content;
|
|
63
|
+
}
|
|
64
|
+
// Determine which images to keep (the most recent ones)
|
|
65
|
+
// We want to keep the last 'maxImages' images
|
|
66
|
+
const imagesToRemove = imageIndices.slice(0, imageIndices.length - maxImages);
|
|
67
|
+
// Create a new content array with excess images replaced by text
|
|
68
|
+
return content.map((block, index) => {
|
|
69
|
+
if (block.type === "image" && imagesToRemove.includes(index)) {
|
|
70
|
+
return {
|
|
71
|
+
type: "text",
|
|
72
|
+
text: "[Image removed due to size limitations. Only the most recent images are shown.]",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return block;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const spaceCache = new Map(); // Global cache for space details promises
|
|
79
|
+
/**
|
|
80
|
+
* Function to get space details, using a cache to avoid redundant fetches
|
|
81
|
+
*/
|
|
82
|
+
function getSpaceDetails(spaceId) {
|
|
83
|
+
if (!spaceId) {
|
|
84
|
+
return Promise.reject(new Error('Invalid space ID'));
|
|
85
|
+
}
|
|
86
|
+
const cachedSpace = spaceCache.get(spaceId);
|
|
87
|
+
if (cachedSpace) {
|
|
88
|
+
return cachedSpace;
|
|
89
|
+
}
|
|
90
|
+
const fetchPromise = fetch(`https://api.clickup.com/api/v2/space/${spaceId}`, { headers: { Authorization: config_1.CONFIG.apiKey } })
|
|
91
|
+
.then(res => {
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(`Error fetching space ${spaceId}: ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
return res.json();
|
|
96
|
+
})
|
|
97
|
+
.catch(error => {
|
|
98
|
+
console.error(`Network error fetching space ${spaceId}:`, error);
|
|
99
|
+
throw new Error(`Error fetching space ${spaceId}: ${error}`);
|
|
100
|
+
});
|
|
101
|
+
spaceCache.set(spaceId, fetchPromise);
|
|
102
|
+
return fetchPromise;
|
|
103
|
+
}
|
|
104
|
+
// Task search index management
|
|
105
|
+
const taskIndices = new Map();
|
|
106
|
+
/**
|
|
107
|
+
* Get or create a task search index with specified filters
|
|
108
|
+
*/
|
|
109
|
+
async function getTaskSearchIndex(space_ids, list_ids, assignees) {
|
|
110
|
+
// Create cache key from sorted filter arrays
|
|
111
|
+
const key = JSON.stringify({
|
|
112
|
+
space_ids: space_ids?.sort(),
|
|
113
|
+
list_ids: list_ids?.sort(),
|
|
114
|
+
assignees: assignees?.sort()
|
|
115
|
+
});
|
|
116
|
+
// Check for existing valid index
|
|
117
|
+
const cachedIndex = taskIndices.get(key);
|
|
118
|
+
if (cachedIndex) {
|
|
119
|
+
return cachedIndex;
|
|
120
|
+
}
|
|
121
|
+
// Fetch tasks with specified filters
|
|
122
|
+
console.error(`Refreshing task index for filters: ${key}`);
|
|
123
|
+
const tasks = await fetchTasks(space_ids, list_ids, assignees);
|
|
124
|
+
const index = createFuseIndex(tasks);
|
|
125
|
+
// Store with auto-cleanup
|
|
126
|
+
taskIndices.set(key, index);
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
taskIndices.delete(key);
|
|
129
|
+
console.error(`Auto-cleaned index for filters: ${key}`);
|
|
130
|
+
}, GLOBAL_REFRESH_INTERVAL);
|
|
131
|
+
console.error(`Task index created with ${tasks.length} tasks`);
|
|
132
|
+
return index;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Fetch tasks using team endpoint with dynamic filters
|
|
136
|
+
*/
|
|
137
|
+
async function fetchTasks(space_ids, list_ids, assignees) {
|
|
138
|
+
const queryParams = ['order_by=updated', 'subtasks=true'];
|
|
139
|
+
// Add filter parameters
|
|
140
|
+
if (space_ids?.length) {
|
|
141
|
+
space_ids.forEach(id => queryParams.push(`space_ids[]=${id}`));
|
|
142
|
+
}
|
|
143
|
+
if (list_ids?.length) {
|
|
144
|
+
list_ids.forEach(id => queryParams.push(`list_ids[]=${id}`));
|
|
145
|
+
}
|
|
146
|
+
if (assignees?.length) {
|
|
147
|
+
assignees.forEach(id => queryParams.push(`assignees[]=${id}`));
|
|
148
|
+
}
|
|
149
|
+
const queryString = queryParams.join('&');
|
|
150
|
+
// Fetch multiple pages in parallel
|
|
151
|
+
const maxPages = space_ids?.length || list_ids?.length || assignees?.length ? 10 : 30; // Fewer pages for filtered searches
|
|
152
|
+
const taskListsPromises = [...Array(maxPages)].map(async (_, i) => {
|
|
153
|
+
const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/task?${queryString}&page=${i}`;
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
156
|
+
return await res.json();
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`Error fetching page ${i}:`, e);
|
|
160
|
+
return { tasks: [] };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const taskLists = await Promise.all(taskListsPromises);
|
|
164
|
+
return taskLists.flatMap(taskList => taskList.tasks || []);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Create a Fuse index from tasks array
|
|
168
|
+
*/
|
|
169
|
+
function createFuseIndex(tasks) {
|
|
170
|
+
return new fuse_js_1.default(tasks, {
|
|
171
|
+
keys: [
|
|
172
|
+
{ name: 'name', weight: 0.7 },
|
|
173
|
+
{ name: 'id', weight: 0.6 },
|
|
174
|
+
{ name: 'text_content', weight: 0.5 },
|
|
175
|
+
{ name: 'tags.name', weight: 0.4 },
|
|
176
|
+
{ name: 'assignees.username', weight: 0.4 },
|
|
177
|
+
{ name: 'list.name', weight: 0.3 },
|
|
178
|
+
{ name: 'folder.name', weight: 0.2 },
|
|
179
|
+
{ name: 'space.name', weight: 0.1 }
|
|
180
|
+
],
|
|
181
|
+
includeScore: true,
|
|
182
|
+
threshold: 0.4,
|
|
183
|
+
minMatchCharLength: 2,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-tools.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QA4OtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAuEvD"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerListToolsRead = registerListToolsRead;
|
|
4
|
+
exports.registerListToolsWrite = registerListToolsWrite;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const config_1 = require("../shared/config");
|
|
7
|
+
function registerListToolsRead(server) {
|
|
8
|
+
server.tool("listLists", "Lists all lists and folders in a space. They might also be referred to as boards or tables. Shows both direct lists (folderless) and folders containing lists. If folder_id is provided, lists only the lists within that specific folder.", {
|
|
9
|
+
space_id: zod_1.z.string().min(1).describe("The ID of the space to list content from"),
|
|
10
|
+
folder_id: zod_1.z.string().optional().describe("Optional folder ID. If provided, lists only lists within this folder. If not provided, shows both folders and folderless lists in the space."),
|
|
11
|
+
archived: zod_1.z.boolean().optional().describe("Include archived items (default: false)")
|
|
12
|
+
}, async ({ space_id, folder_id, archived = false }) => {
|
|
13
|
+
try {
|
|
14
|
+
if (folder_id) {
|
|
15
|
+
// List lists within a specific folder
|
|
16
|
+
const url = `https://api.clickup.com/api/v2/folder/${folder_id}/list${archived ? '?archived=true' : ''}`;
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const errorText = await response.text();
|
|
22
|
+
console.error(`API Error: ${response.status} ${response.statusText}`, errorText);
|
|
23
|
+
throw new Error(`Error fetching lists in folder ${folder_id}: ${response.status} ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
if (!data.lists || !Array.isArray(data.lists)) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: `No lists found in folder ${folder_id}.` }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const listBlocks = data.lists.map((list) => ({
|
|
32
|
+
type: "text",
|
|
33
|
+
text: [
|
|
34
|
+
`type: list`,
|
|
35
|
+
`list_id: ${list.id}`,
|
|
36
|
+
`name: ${list.name}`,
|
|
37
|
+
`space_id: ${list.space?.id || space_id}`,
|
|
38
|
+
`folder_id: ${folder_id}`,
|
|
39
|
+
`task_count: ${list.task_count || 0}`,
|
|
40
|
+
`private: ${list.private || false}`,
|
|
41
|
+
...(list.archived ? ['archived: true'] : []),
|
|
42
|
+
...(list.color ? [`color: ${list.color}`] : []),
|
|
43
|
+
...(list.access ? [`access: ${list.access}`] : []),
|
|
44
|
+
].join('\n')
|
|
45
|
+
}));
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `Found ${data.lists.length} list(s) in folder ${folder_id}:`
|
|
51
|
+
},
|
|
52
|
+
...listBlocks
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// List both folders and folderless lists in the space (parallel requests)
|
|
58
|
+
const archiveQuery = archived ? '?archived=true' : '';
|
|
59
|
+
const [foldersResponse, listsResponse] = await Promise.all([
|
|
60
|
+
fetch(`https://api.clickup.com/api/v2/space/${space_id}/folder${archiveQuery}`, {
|
|
61
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
62
|
+
}),
|
|
63
|
+
fetch(`https://api.clickup.com/api/v2/space/${space_id}/list${archiveQuery}`, {
|
|
64
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
65
|
+
})
|
|
66
|
+
]);
|
|
67
|
+
const results = [];
|
|
68
|
+
let totalItems = 0;
|
|
69
|
+
// Process folders
|
|
70
|
+
if (foldersResponse.ok) {
|
|
71
|
+
const foldersData = await foldersResponse.json();
|
|
72
|
+
if (foldersData.folders && Array.isArray(foldersData.folders)) {
|
|
73
|
+
const folderBlocks = foldersData.folders.map((folder) => ({
|
|
74
|
+
type: "text",
|
|
75
|
+
text: [
|
|
76
|
+
`type: folder`,
|
|
77
|
+
`folder_id: ${folder.id}`,
|
|
78
|
+
`name: ${folder.name}`,
|
|
79
|
+
`space_id: ${space_id}`,
|
|
80
|
+
`task_count: ${folder.task_count || 0}`,
|
|
81
|
+
`private: ${folder.private || false}`,
|
|
82
|
+
...(folder.archived ? ['archived: true'] : []),
|
|
83
|
+
...(folder.color ? [`color: ${folder.color}`] : []),
|
|
84
|
+
`contains_lists: true`
|
|
85
|
+
].join('\n')
|
|
86
|
+
}));
|
|
87
|
+
results.push(...folderBlocks);
|
|
88
|
+
totalItems += foldersData.folders.length;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.warn(`Failed to fetch folders: ${foldersResponse.status} ${foldersResponse.statusText}`);
|
|
93
|
+
}
|
|
94
|
+
// Process folderless lists
|
|
95
|
+
if (listsResponse.ok) {
|
|
96
|
+
const listsData = await listsResponse.json();
|
|
97
|
+
if (listsData.lists && Array.isArray(listsData.lists)) {
|
|
98
|
+
const listBlocks = listsData.lists.map((list) => ({
|
|
99
|
+
type: "text",
|
|
100
|
+
text: [
|
|
101
|
+
`type: list`,
|
|
102
|
+
`list_id: ${list.id}`,
|
|
103
|
+
`name: ${list.name}`,
|
|
104
|
+
`space_id: ${list.space?.id || space_id}`,
|
|
105
|
+
`task_count: ${list.task_count || 0}`,
|
|
106
|
+
`private: ${list.private || false}`,
|
|
107
|
+
...(list.archived ? ['archived: true'] : []),
|
|
108
|
+
...(list.color ? [`color: ${list.color}`] : []),
|
|
109
|
+
...(list.access ? [`access: ${list.access}`] : []),
|
|
110
|
+
].join('\n')
|
|
111
|
+
}));
|
|
112
|
+
results.push(...listBlocks);
|
|
113
|
+
totalItems += listsData.lists.length;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.warn(`Failed to fetch lists: ${listsResponse.status} ${listsResponse.statusText}`);
|
|
118
|
+
}
|
|
119
|
+
if (totalItems === 0) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: `No folders or lists found in space ${space_id}.` }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: `Found ${totalItems} item(s) in space ${space_id} (folders and lists):`
|
|
129
|
+
},
|
|
130
|
+
...results
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('Error fetching space content:', error);
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `Error fetching space content: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
server.tool("getListInfo", [
|
|
148
|
+
"Gets comprehensive information about a list including description and available statuses.",
|
|
149
|
+
"Use this before creating tasks to understand the list context and available statuses for new tasks.",
|
|
150
|
+
"IMPORTANT: The list description often contains valuable project context, requirements, or guidelines - read and consider this information when creating or updating tasks in this list."
|
|
151
|
+
].join("\n"), {
|
|
152
|
+
list_id: zod_1.z.string().min(1).describe("The list ID to get information for")
|
|
153
|
+
}, async ({ list_id }) => {
|
|
154
|
+
try {
|
|
155
|
+
// Get list details including statuses (try to get markdown content)
|
|
156
|
+
const listResponse = await fetch(`https://api.clickup.com/api/v2/list/${list_id}?include_markdown_description=true`, {
|
|
157
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
158
|
+
});
|
|
159
|
+
if (!listResponse.ok) {
|
|
160
|
+
throw new Error(`Error fetching list details: ${listResponse.status} ${listResponse.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
const listData = await listResponse.json();
|
|
163
|
+
const responseLines = [
|
|
164
|
+
`List Information:`,
|
|
165
|
+
`list_id: ${list_id}`,
|
|
166
|
+
`name: ${listData.name}`,
|
|
167
|
+
`folder: ${listData.folder?.name || 'No folder'}`,
|
|
168
|
+
`space: ${listData.space?.name || 'Unknown'} (${listData.space?.id || 'N/A'})`,
|
|
169
|
+
`archived: ${listData.archived || false}`,
|
|
170
|
+
`task_count: ${listData.task_count || 0}`,
|
|
171
|
+
];
|
|
172
|
+
// Add description if available (check both content and markdown fields)
|
|
173
|
+
const description = listData.markdown_description || listData.markdown_content || listData.content;
|
|
174
|
+
if (description) {
|
|
175
|
+
responseLines.push(`description: ${description}`);
|
|
176
|
+
}
|
|
177
|
+
// Add available statuses
|
|
178
|
+
if (listData.statuses && Array.isArray(listData.statuses)) {
|
|
179
|
+
const statuses = listData.statuses.map((status) => ({
|
|
180
|
+
name: status.status,
|
|
181
|
+
color: status.color || 'none',
|
|
182
|
+
type: status.type || 'custom'
|
|
183
|
+
}));
|
|
184
|
+
responseLines.push('');
|
|
185
|
+
responseLines.push(`Available statuses (${statuses.length} total):`);
|
|
186
|
+
statuses.forEach((status) => {
|
|
187
|
+
responseLines.push(` - ${status.name} (${status.type})`);
|
|
188
|
+
});
|
|
189
|
+
responseLines.push('');
|
|
190
|
+
responseLines.push(`Valid status names for createTask/updateTask: ${statuses.map((s) => s.name).join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
responseLines.push('');
|
|
194
|
+
responseLines.push('No statuses found for this list.');
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: responseLines.join('\n')
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error('Error getting list info:', error);
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `Error getting list info: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function registerListToolsWrite(server) {
|
|
219
|
+
server.tool("updateListInfo", [
|
|
220
|
+
"Appends documentation or context to a list's description.",
|
|
221
|
+
"SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
|
|
222
|
+
"Use this to add project context, requirements, or guidelines that LLMs should consider when working with tasks in this list.",
|
|
223
|
+
"Content is appended in markdown format with timestamp for tracking changes."
|
|
224
|
+
].join("\n"), {
|
|
225
|
+
list_id: zod_1.z.string().min(1).describe("The list ID to update"),
|
|
226
|
+
append_description: zod_1.z.string().min(1).describe("Markdown content to APPEND to existing list description (preserves existing content for safety)")
|
|
227
|
+
}, async ({ list_id, append_description }) => {
|
|
228
|
+
try {
|
|
229
|
+
// Get current list info including description (try to get markdown content)
|
|
230
|
+
const listResponse = await fetch(`https://api.clickup.com/api/v2/list/${list_id}?include_markdown_description=true`, {
|
|
231
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
232
|
+
});
|
|
233
|
+
if (!listResponse.ok) {
|
|
234
|
+
throw new Error(`Error fetching list: ${listResponse.status} ${listResponse.statusText}`);
|
|
235
|
+
}
|
|
236
|
+
const listData = await listResponse.json();
|
|
237
|
+
// Handle append-only description update with markdown support
|
|
238
|
+
const currentDescription = listData.markdown_description || listData.markdown_content || listData.content || "";
|
|
239
|
+
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
240
|
+
const separator = currentDescription.trim() ? "\n\n---\n" : "";
|
|
241
|
+
const finalDescription = currentDescription + separator + `**Edit (${timestamp}):** ${append_description}`;
|
|
242
|
+
// Update the list description using markdown_content
|
|
243
|
+
const updateResponse = await fetch(`https://api.clickup.com/api/v2/list/${list_id}`, {
|
|
244
|
+
method: 'PUT',
|
|
245
|
+
headers: {
|
|
246
|
+
Authorization: config_1.CONFIG.apiKey,
|
|
247
|
+
'Content-Type': 'application/json'
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
markdown_content: finalDescription
|
|
251
|
+
})
|
|
252
|
+
});
|
|
253
|
+
if (!updateResponse.ok) {
|
|
254
|
+
const errorData = await updateResponse.json().catch(() => ({}));
|
|
255
|
+
throw new Error(`Error updating list: ${updateResponse.status} ${updateResponse.statusText} - ${JSON.stringify(errorData)}`);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: `Successfully appended content to list "${listData.name}". The new content has been added with timestamp (${timestamp}) while preserving existing description.`,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
console.error('Error updating list info:', error);
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `Error updating list info: ${error instanceof Error ? error.message : String(error)}`,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-tools.d.ts","sourceRoot":"","sources":["../../src/tools/search-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAQlE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,QAsMpD"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSearchTools = registerSearchTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const config_1 = require("../shared/config");
|
|
6
|
+
const utils_1 = require("../shared/utils");
|
|
7
|
+
const task_tools_1 = require("./task-tools");
|
|
8
|
+
const MAX_SEARCH_RESULTS = 50;
|
|
9
|
+
function registerSearchTools(server) {
|
|
10
|
+
// Dynamically construct the searchTasks description
|
|
11
|
+
const searchTasksDescriptionBase = [
|
|
12
|
+
"Searches tasks by name, content, assignees, and ID (case insensitive) with fuzzy matching and support for multiple search terms (OR logic).",
|
|
13
|
+
"Can filter by multiple list_ids, space_ids, todo status, or tasks assigned to the current user. If no search terms provided, returns most recently updated tasks.",
|
|
14
|
+
// Placeholder for language-specific guidance
|
|
15
|
+
"You'll get a rough overview of the tasks that match the search terms, sorted by relevance.",
|
|
16
|
+
"Always use getTaskById to get more specific information if a task is relevant.",
|
|
17
|
+
];
|
|
18
|
+
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
19
|
+
searchTasksDescriptionBase.splice(2, 0, `For optimal results, as your ClickUp tasks may be primarily in '${config_1.CONFIG.primaryLanguageHint}', consider providing search terms in English and '${config_1.CONFIG.primaryLanguageHint}'.`);
|
|
20
|
+
}
|
|
21
|
+
server.tool("searchTasks", searchTasksDescriptionBase.join("\n"), {
|
|
22
|
+
terms: zod_1.z
|
|
23
|
+
.array(zod_1.z.string())
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Array of search terms (OR logic). Can include task IDs. Optional - if not provided, returns most recent tasks."),
|
|
26
|
+
list_ids: zod_1.z
|
|
27
|
+
.array(zod_1.z.string())
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Filter tasks to specific list IDs"),
|
|
30
|
+
space_ids: zod_1.z
|
|
31
|
+
.array(zod_1.z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Filter tasks to specific space IDs"),
|
|
34
|
+
todo: zod_1.z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Filter for open/todo tasks only (exclude done tasks)"),
|
|
38
|
+
assigned_to_me: zod_1.z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Filter for tasks assigned to the current user"),
|
|
42
|
+
}, async ({ terms, list_ids, space_ids, todo, assigned_to_me }) => {
|
|
43
|
+
// Get current user ID if filtering by assigned_to_me
|
|
44
|
+
let assignees;
|
|
45
|
+
if (assigned_to_me) {
|
|
46
|
+
try {
|
|
47
|
+
const userData = await (0, utils_1.getCurrentUser)();
|
|
48
|
+
assignees = [userData.user.id];
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: "Error: Failed to get current user information.",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const searchIndex = await (0, utils_1.getTaskSearchIndex)(space_ids, list_ids, assignees);
|
|
62
|
+
if (!searchIndex) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: "No tasks available or index could not be built.",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Early return for no search terms
|
|
73
|
+
if (!terms || terms.length === 0) {
|
|
74
|
+
let allTasks = searchIndex._docs || [];
|
|
75
|
+
if (todo) {
|
|
76
|
+
allTasks = allTasks.filter((task) => task.status.type !== "done");
|
|
77
|
+
}
|
|
78
|
+
// Sort by updated date (most recent first) and limit
|
|
79
|
+
const resultTasks = allTasks
|
|
80
|
+
.sort((a, b) => {
|
|
81
|
+
const dateA = parseInt(a.date_updated || "0");
|
|
82
|
+
const dateB = parseInt(b.date_updated || "0");
|
|
83
|
+
return dateB - dateA;
|
|
84
|
+
})
|
|
85
|
+
.slice(0, MAX_SEARCH_RESULTS);
|
|
86
|
+
if (resultTasks.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: "No tasks found.",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Filter valid search terms
|
|
101
|
+
const validTerms = terms.filter(term => term && term.trim().length > 0);
|
|
102
|
+
if (validTerms.length === 0) {
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: "No valid search terms provided.",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const uniqueResults = new Map();
|
|
113
|
+
// Search with each term
|
|
114
|
+
validTerms.forEach(term => {
|
|
115
|
+
const results = searchIndex.search(term.toLowerCase());
|
|
116
|
+
results.forEach(result => {
|
|
117
|
+
if (result.item && typeof result.item.id === 'string') {
|
|
118
|
+
const currentScore = result.score ?? 1;
|
|
119
|
+
const existing = uniqueResults.get(result.item.id);
|
|
120
|
+
if (!existing || currentScore < existing.score) {
|
|
121
|
+
uniqueResults.set(result.item.id, {
|
|
122
|
+
item: result.item,
|
|
123
|
+
score: currentScore
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// Task ID Fallback Logic
|
|
130
|
+
const potentialTaskIds = validTerms.filter(utils_1.isTaskId);
|
|
131
|
+
const foundTaskIdsByFuse = new Set(Array.from(uniqueResults.keys()).map(id => id.toLowerCase()));
|
|
132
|
+
const taskIdsToFetchDirectly = potentialTaskIds.filter(id => {
|
|
133
|
+
const lowerId = id.toLowerCase();
|
|
134
|
+
return !foundTaskIdsByFuse.has(lowerId);
|
|
135
|
+
});
|
|
136
|
+
if (taskIdsToFetchDirectly.length > 0) {
|
|
137
|
+
console.error(`Attempting direct fetch for task IDs: ${taskIdsToFetchDirectly.join(', ')}`);
|
|
138
|
+
const directFetchPromises = taskIdsToFetchDirectly.map(async (id) => {
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}`, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
const task = await response.json();
|
|
143
|
+
if (task && typeof task.id === 'string') {
|
|
144
|
+
const existing = uniqueResults.get(task.id);
|
|
145
|
+
if (!existing || 0 < existing.score) {
|
|
146
|
+
uniqueResults.set(task.id, { item: task, score: 0 });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return task;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error(`Error directly fetching task ${id}:`, error);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
await Promise.all(directFetchPromises);
|
|
159
|
+
}
|
|
160
|
+
let resultTasks = Array.from(uniqueResults.values())
|
|
161
|
+
.sort((a, b) => a.score - b.score)
|
|
162
|
+
.map(entry => entry.item);
|
|
163
|
+
if (todo) {
|
|
164
|
+
resultTasks = resultTasks.filter((task) => task.status.type !== "done");
|
|
165
|
+
}
|
|
166
|
+
// Apply result limit
|
|
167
|
+
resultTasks = resultTasks.slice(0, MAX_SEARCH_RESULTS);
|
|
168
|
+
if (resultTasks.length === 0) {
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: "No tasks found matching the search criteria.",
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"space-tools.d.ts","sourceRoot":"","sources":["../../src/tools/space-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAiEnD"}
|