clokio-mcp 0.26.0 → 0.27.1
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/tools/projects.js +220 -0
- package/dist/tools/tasks.js +271 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ acting. An explicit code on a tool call always takes precedence.
|
|
|
65
65
|
|
|
66
66
|
Optional: set `CLOKIO_TOOLSETS` to register only the tool groups you need, e.g.
|
|
67
67
|
`CLOKIO_TOOLSETS=tasks`. This cuts the per-session tool-list cost - a
|
|
68
|
-
task-focused session drops from
|
|
68
|
+
task-focused session drops from 129 tools to 42. Optional groups are `tasks`,
|
|
69
69
|
`employees`, `attendance` (which includes leave, time entries and logging time), `docs` and
|
|
70
70
|
`tickets`;
|
|
71
71
|
combine them with commas. If your key cannot open documents (no acts-as
|
|
@@ -76,13 +76,13 @@ through it.
|
|
|
76
76
|
|
|
77
77
|
## Tools
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
129 tools across seven areas. Read-only tools carry the MCP `readOnlyHint`; writes
|
|
80
80
|
are marked mutating (deletes are `destructive`).
|
|
81
81
|
|
|
82
82
|
| Area | Tools |
|
|
83
83
|
|------|-------|
|
|
84
|
-
| **Tasks** | list (with `compact` / `sort` / `updated_since` / `recurring`), get (with `comments_limit`; names the ticket a task was opened from), create, update (incl. recurring series: set a rule, pause/resume, stop; labels: replace, add, remove), delete, bulk update (status, priority, due date, add / remove labels), set assignees (a shared board's client people as `{employee_code, organization_id}`), list a board's people, list/add/edit comments, list attachments (with `is_latest` + `latest_only`), upload, download one (save to disk, or read PDF text / ZIP entries / images / text inline), download all to a folder, read images pasted into descriptions and comments, activity (with organisations, paged), list/add/remove dependencies, set custom field (an option by value or label), set custom fields on many tasks (`clokio_bulk_set_task_custom_field`, partial success); `clokio_update_task` takes `custom_fields` too |
|
|
85
|
-
| **Projects & clients** | list projects, project statuses, project custom fields, project contacts, get client, add client contact |
|
|
84
|
+
| **Tasks** | list (with `compact` / `fields` / `sort` / `updated_since` / `updated_before` / `recurring`, no due date, no assignee, a custom field value, title-only search), a project's task summary (counts by status / priority / label / assignee, overdue and stale lists, per option of a release dropdown: `clokio_get_project_task_summary`), a project's activity log (`clokio_list_project_activity`), get (with `comments_limit`; names the ticket a task was opened from), create, update (incl. recurring series: set a rule, pause/resume, stop; labels: replace, add, remove; parent task; assignees in the same call), delete, bulk update (status, priority, due date, estimate, add / remove labels), set assignees (a shared board's client people as `{employee_code, organization_id}`), list a board's people, list/add/edit comments, list attachments (with `is_latest` + `latest_only`), upload, download one (save to disk, or read PDF text / ZIP entries / images / text inline), download all to a folder, read images pasted into descriptions and comments, activity (with organisations, paged), list/add/remove dependencies, set custom field (an option by value or label), set custom fields on many tasks (`clokio_bulk_set_task_custom_field`, partial success); `clokio_update_task` takes `custom_fields` too |
|
|
85
|
+
| **Projects & clients** | list projects, project statuses (and create / edit / delete them; a status in use moves its tasks to `move_to`), project custom fields (and create / edit / delete them, e.g. a release dropdown; a delete removes the stored values too), project contacts, get client, add client contact |
|
|
86
86
|
| **Reference** | task statuses, task labels, locations, list webhooks (read-only), whoami |
|
|
87
87
|
| **Employees** | list (name search), lookup by PIN, task-stats, inbox (mentions, new assignments, others' status changes; with the tickets toolset, also the acting employee's open tickets), create, set status |
|
|
88
88
|
| **Attendance** | daily, range, per-employee, monthly summary, clock in/out, break start/end, time entries (local times in the person's zone; edit / delete your own), leaves (with `days`, half days counted 0.5), days of leave taken per type and year (`clokio_get_leave_days_taken`; the old name `clokio_get_leave_balances` still works; Clokio tracks no quota or remaining balance; leave and time-entry lists take `employee_code` and at most 92 days per call), **log time on a task**, start / stop / read the timer (see [Logging time](#logging-time)) |
|
package/dist/tools/projects.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { request } from '../client.js';
|
|
3
3
|
import { registerTool } from './helpers.js';
|
|
4
4
|
import { VERSION } from '../version.js';
|
|
5
|
+
import { FIELD_KEY_PATTERN } from './tasks.js';
|
|
5
6
|
/** Projects, clients, and the board-configuration + reference lookups. */
|
|
6
7
|
export function registerProjectTools(server, config) {
|
|
7
8
|
registerTool(server, config, {
|
|
@@ -29,6 +30,225 @@ export function registerProjectTools(server, config) {
|
|
|
29
30
|
schema: { project_id: z.number().int() },
|
|
30
31
|
handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/custom-fields`),
|
|
31
32
|
});
|
|
33
|
+
// ── Board configuration writes (0.27.0, backend #5874 part 4) ──
|
|
34
|
+
// projects:write. Every rule is the web editor's own copy on the server;
|
|
35
|
+
// nothing is judged here beyond the shapes. Delete since 0.27.1 (backend
|
|
36
|
+
// #5874 QA round 1), with the web's delete rules on the server.
|
|
37
|
+
const fieldOptions = z
|
|
38
|
+
.array(z.object({
|
|
39
|
+
value: z.string().min(1).max(100).describe('The stored value tasks hold; keep it stable'),
|
|
40
|
+
label: z.string().max(100).optional().describe('What people see; defaults to the value'),
|
|
41
|
+
color: z
|
|
42
|
+
.string()
|
|
43
|
+
.regex(/^#[0-9a-fA-F]{6}$/, 'color must be #rrggbb')
|
|
44
|
+
.optional(),
|
|
45
|
+
}))
|
|
46
|
+
.min(1)
|
|
47
|
+
.max(200);
|
|
48
|
+
const fieldType = z.enum(['text', 'number', 'date', 'dropdown', 'checkbox', 'multi_select', 'url', 'email']);
|
|
49
|
+
const actorCode = z
|
|
50
|
+
.string()
|
|
51
|
+
.regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code: letters, digits, _ or -')
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Checked by the server (an unknown code is refused); a configuration edit writes no activity row');
|
|
54
|
+
const statusColor = z
|
|
55
|
+
.string()
|
|
56
|
+
.regex(/^#[0-9a-fA-F]{6}$/, 'color must be #rrggbb')
|
|
57
|
+
.nullable()
|
|
58
|
+
.optional();
|
|
59
|
+
registerTool(server, config, {
|
|
60
|
+
name: 'clokio_create_project_custom_field',
|
|
61
|
+
mutates: true,
|
|
62
|
+
description: 'Create a custom field on a project (the board\'s columns). type is one of text, number, date, dropdown, ' +
|
|
63
|
+
'checkbox, multi_select, url, email. A dropdown or multi_select NEEDS options ({value, label?, color?}; ' +
|
|
64
|
+
'the label defaults to the value); every other type takes none. The field key is generated from the ' +
|
|
65
|
+
'name and never changes (it is what clokio_set_task_custom_field and the list filters take). An API ' +
|
|
66
|
+
'key creates PUBLIC fields only. A release or sprint is a dropdown field: create it once, add an ' +
|
|
67
|
+
'option per release with clokio_update_project_custom_field, set it on tasks with ' +
|
|
68
|
+
'clokio_bulk_set_task_custom_field, read it with clokio_get_project_task_summary group_by_custom_field. ' +
|
|
69
|
+
'Needs the projects:write scope.',
|
|
70
|
+
schema: {
|
|
71
|
+
project_id: z.number().int(),
|
|
72
|
+
name: z.string().min(1).max(50),
|
|
73
|
+
type: fieldType,
|
|
74
|
+
options: fieldOptions.optional(),
|
|
75
|
+
is_required: z.boolean().optional(),
|
|
76
|
+
actor_employee_code: actorCode,
|
|
77
|
+
},
|
|
78
|
+
handler: (args, cfg) => {
|
|
79
|
+
const optioned = args.type === 'dropdown' || args.type === 'multi_select';
|
|
80
|
+
if (optioned && args.options === undefined) {
|
|
81
|
+
throw new Error(`A ${args.type} field needs options: give at least one {value, label}.`);
|
|
82
|
+
}
|
|
83
|
+
if (!optioned && args.options !== undefined) {
|
|
84
|
+
throw new Error(`A ${args.type} field takes no options; only dropdown and multi_select do.`);
|
|
85
|
+
}
|
|
86
|
+
const body = { name: args.name, type: args.type };
|
|
87
|
+
if (args.options !== undefined)
|
|
88
|
+
body.options = args.options;
|
|
89
|
+
if (args.is_required !== undefined)
|
|
90
|
+
body.is_required = args.is_required;
|
|
91
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
92
|
+
if (actor)
|
|
93
|
+
body.actor_employee_code = actor;
|
|
94
|
+
return request(cfg, `/projects/${args.project_id}/custom-fields`, { method: 'POST', body });
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
registerTool(server, config, {
|
|
98
|
+
name: 'clokio_update_project_custom_field',
|
|
99
|
+
mutates: true,
|
|
100
|
+
description: 'Edit a project custom field by its field_key: rename it (the key stays), change its type, flip ' +
|
|
101
|
+
'is_required, or replace its options with the WHOLE new list. To add a release, read the field with ' +
|
|
102
|
+
'clokio_get_project_custom_fields and send every option plus the new one; renaming a label or ' +
|
|
103
|
+
'reordering is free (tasks store the value). Removing an option tasks still hold is refused naming ' +
|
|
104
|
+
'it; so is a type change that would leave stored answers unreadable. A private field answers not ' +
|
|
105
|
+
'found. Needs the projects:write scope.',
|
|
106
|
+
schema: {
|
|
107
|
+
project_id: z.number().int(),
|
|
108
|
+
field_key: z.string().regex(FIELD_KEY_PATTERN, 'field_key: letters, digits, _ or -'),
|
|
109
|
+
name: z.string().min(1).max(50).optional(),
|
|
110
|
+
type: fieldType.optional(),
|
|
111
|
+
options: fieldOptions.optional(),
|
|
112
|
+
is_required: z.boolean().optional(),
|
|
113
|
+
actor_employee_code: actorCode,
|
|
114
|
+
},
|
|
115
|
+
handler: (args, cfg) => {
|
|
116
|
+
const body = {};
|
|
117
|
+
for (const key of ['name', 'type', 'options', 'is_required']) {
|
|
118
|
+
if (args[key] !== undefined)
|
|
119
|
+
body[key] = args[key];
|
|
120
|
+
}
|
|
121
|
+
if (Object.keys(body).length === 0) {
|
|
122
|
+
throw new Error('Nothing to change: give at least one of name, type, options, is_required.');
|
|
123
|
+
}
|
|
124
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
125
|
+
if (actor)
|
|
126
|
+
body.actor_employee_code = actor;
|
|
127
|
+
return request(cfg, `/projects/${args.project_id}/custom-fields/${args.field_key}`, { method: 'PATCH', body });
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
registerTool(server, config, {
|
|
131
|
+
name: 'clokio_create_project_status',
|
|
132
|
+
mutates: true,
|
|
133
|
+
description: 'Add a status (a board column) to a project. The name must be new in the project (compared ignoring ' +
|
|
134
|
+
'case); the slug is generated from it and never changes. A new status is always ACTIVE: a project has ' +
|
|
135
|
+
'exactly one completed status, which always stays the last column. position (0-based) places it. A ' +
|
|
136
|
+
'project still using the organisation\'s statuses gets its own copy first, so no other project ' +
|
|
137
|
+
'changes. Needs the projects:write scope.',
|
|
138
|
+
schema: {
|
|
139
|
+
project_id: z.number().int(),
|
|
140
|
+
name: z.string().min(1).max(50),
|
|
141
|
+
color: statusColor.describe('#rrggbb'),
|
|
142
|
+
position: z.number().int().min(0).max(200).optional().describe('0-based place on the board'),
|
|
143
|
+
actor_employee_code: actorCode,
|
|
144
|
+
},
|
|
145
|
+
handler: (args, cfg) => {
|
|
146
|
+
const body = { name: args.name };
|
|
147
|
+
if (args.color !== undefined)
|
|
148
|
+
body.color = args.color;
|
|
149
|
+
if (args.position !== undefined)
|
|
150
|
+
body.position = args.position;
|
|
151
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
152
|
+
if (actor)
|
|
153
|
+
body.actor_employee_code = actor;
|
|
154
|
+
return request(cfg, `/projects/${args.project_id}/statuses`, { method: 'POST', body });
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
registerTool(server, config, {
|
|
158
|
+
name: 'clokio_update_project_status',
|
|
159
|
+
mutates: true,
|
|
160
|
+
description: 'Edit a project status by its slug (clokio_get_project_statuses): rename (the slug stays), recolour, ' +
|
|
161
|
+
'move (position, 0-based; the completed status stays last) or change category. A status cannot become ' +
|
|
162
|
+
'a second completed one, and the completed one cannot become active. To remove a status use ' +
|
|
163
|
+
'clokio_delete_project_status. Needs the projects:write scope.',
|
|
164
|
+
schema: {
|
|
165
|
+
project_id: z.number().int(),
|
|
166
|
+
slug: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/, 'slug: letters, digits, _ or -'),
|
|
167
|
+
name: z.string().min(1).max(50).optional(),
|
|
168
|
+
color: statusColor.describe('#rrggbb, or null for the default colour'),
|
|
169
|
+
category: z.enum(['active', 'done']).optional(),
|
|
170
|
+
position: z.number().int().min(0).max(200).optional().describe('0-based place on the board'),
|
|
171
|
+
actor_employee_code: actorCode,
|
|
172
|
+
},
|
|
173
|
+
handler: (args, cfg) => {
|
|
174
|
+
const body = {};
|
|
175
|
+
for (const key of ['name', 'color', 'category', 'position']) {
|
|
176
|
+
if (args[key] !== undefined)
|
|
177
|
+
body[key] = args[key];
|
|
178
|
+
}
|
|
179
|
+
if (Object.keys(body).length === 0) {
|
|
180
|
+
throw new Error('Nothing to change: give at least one of name, color, category, position.');
|
|
181
|
+
}
|
|
182
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
183
|
+
if (actor)
|
|
184
|
+
body.actor_employee_code = actor;
|
|
185
|
+
return request(cfg, `/projects/${args.project_id}/statuses/${args.slug}`, { method: 'PATCH', body });
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
registerTool(server, config, {
|
|
189
|
+
name: 'clokio_delete_project_status',
|
|
190
|
+
mutates: 'destructive',
|
|
191
|
+
description: 'Delete a project status by its slug (clokio_get_project_statuses), with the web editor\'s rules. The ' +
|
|
192
|
+
'DEFAULT status (where new tasks start) and the COMPLETED status cannot be deleted. When tasks still use ' +
|
|
193
|
+
'the status, move_to is REQUIRED: another slug of the same project, of the SAME kind (an active status ' +
|
|
194
|
+
'for an active one); every task on the deleted status ' +
|
|
195
|
+
'(deleted tasks included) moves there in the same step, and each move is written to that task\'s ' +
|
|
196
|
+
'activity log, credited to the actor. Without move_to the server answers how many tasks use it and ' +
|
|
197
|
+
'deletes nothing. A project still using the organisation\'s statuses gets its own copy only when the ' +
|
|
198
|
+
'delete happens, so no other project changes. Needs the projects:write scope.',
|
|
199
|
+
schema: {
|
|
200
|
+
project_id: z.number().int(),
|
|
201
|
+
slug: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/, 'slug: letters, digits, _ or -'),
|
|
202
|
+
move_to: z
|
|
203
|
+
.string()
|
|
204
|
+
.regex(/^[A-Za-z0-9_-]{1,100}$/, 'move_to: a status slug')
|
|
205
|
+
.optional()
|
|
206
|
+
.describe('Where the tasks on this status go: another slug of the same project. Required when any task uses it'),
|
|
207
|
+
actor_employee_code: actorCode,
|
|
208
|
+
},
|
|
209
|
+
handler: (args, cfg) => {
|
|
210
|
+
if (args.move_to !== undefined && args.move_to === args.slug) {
|
|
211
|
+
throw new Error('move_to must be another status, not the one being deleted.');
|
|
212
|
+
}
|
|
213
|
+
const body = {};
|
|
214
|
+
if (args.move_to !== undefined)
|
|
215
|
+
body.move_to = args.move_to;
|
|
216
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
217
|
+
if (actor)
|
|
218
|
+
body.actor_employee_code = actor;
|
|
219
|
+
return request(cfg, `/projects/${args.project_id}/statuses/${args.slug}`, { method: 'DELETE', body });
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
registerTool(server, config, {
|
|
223
|
+
name: 'clokio_delete_project_custom_field',
|
|
224
|
+
mutates: 'destructive',
|
|
225
|
+
description: 'Delete a project custom field by its field_key, with the web editor\'s rule: the field is removed ' +
|
|
226
|
+
'TOGETHER WITH every value stored for it on every task, and this cannot be undone; there is no archive. ' +
|
|
227
|
+
'A field that holds any value needs confirm: true: without it the server answers how many tasks hold a ' +
|
|
228
|
+
'value and deletes nothing. A field with no values deletes without confirm. The answer says how many ' +
|
|
229
|
+
'tasks held a value (affected_task_count). Check usage first with clokio_list_tasks custom_field or ' +
|
|
230
|
+
'clokio_get_project_task_summary group_by_custom_field when that matters. A private field answers not ' +
|
|
231
|
+
'found. Needs the projects:write scope.',
|
|
232
|
+
schema: {
|
|
233
|
+
project_id: z.number().int(),
|
|
234
|
+
field_key: z.string().regex(FIELD_KEY_PATTERN, 'field_key: letters, digits, _ or -'),
|
|
235
|
+
confirm: z
|
|
236
|
+
.boolean()
|
|
237
|
+
.optional()
|
|
238
|
+
.describe('true = delete the field AND every value stored for it. Required when the field holds any value; ' +
|
|
239
|
+
'omit it first to learn how many tasks hold one'),
|
|
240
|
+
actor_employee_code: actorCode,
|
|
241
|
+
},
|
|
242
|
+
handler: (args, cfg) => {
|
|
243
|
+
const body = {};
|
|
244
|
+
if (args.confirm !== undefined)
|
|
245
|
+
body.confirm = args.confirm;
|
|
246
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
247
|
+
if (actor)
|
|
248
|
+
body.actor_employee_code = actor;
|
|
249
|
+
return request(cfg, `/projects/${args.project_id}/custom-fields/${args.field_key}`, { method: 'DELETE', body });
|
|
250
|
+
},
|
|
251
|
+
});
|
|
32
252
|
registerTool(server, config, {
|
|
33
253
|
name: 'clokio_get_project_contacts',
|
|
34
254
|
description: "The contacts for a project's client.",
|
package/dist/tools/tasks.js
CHANGED
|
@@ -7,6 +7,16 @@ import { asAttachmentArray, downloadAttachment, downloadTaskMedia, inlineMediaRo
|
|
|
7
7
|
const employeeCode = z
|
|
8
8
|
.string()
|
|
9
9
|
.regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code must be 1-50 chars of letters, digits, _ or -');
|
|
10
|
+
/**
|
|
11
|
+
* A custom field's field_key, by the backend's own rule: the route
|
|
12
|
+
* `->where('fieldKey', '[A-Za-z0-9_-]{1,50}')` and the 50-char `key` column
|
|
13
|
+
* (keys are generated with Str::slug). Every field_key input uses it, so a
|
|
14
|
+
* key such as `a]b` or `x&y` never reaches a query string or a path.
|
|
15
|
+
*/
|
|
16
|
+
export const FIELD_KEY_PATTERN = /^[A-Za-z0-9_-]{1,50}$/;
|
|
17
|
+
const fieldKey = z.string().regex(FIELD_KEY_PATTERN, 'field_key must be 1-50 chars of letters, digits, _ or -');
|
|
18
|
+
/** THE estimated_hours ceiling of every Public API write (create, update, bulk): the backend's ESTIMATED_HOURS_MAX. */
|
|
19
|
+
export const ESTIMATED_HOURS_MAX = 9999.99;
|
|
10
20
|
/**
|
|
11
21
|
* One custom field value as the API takes it: text / number / date string,
|
|
12
22
|
* true / false for a checkbox, an option's value OR label for a dropdown, a
|
|
@@ -18,7 +28,7 @@ const customFieldValue = z
|
|
|
18
28
|
.describe('The value; a dropdown option by its value or its label; null clears');
|
|
19
29
|
/** field_key -> value, at least one and at most 50 fields (the server's cap). */
|
|
20
30
|
const customFieldsMap = z
|
|
21
|
-
.record(
|
|
31
|
+
.record(fieldKey, customFieldValue)
|
|
22
32
|
.refine((m) => Object.keys(m).length >= 1 && Object.keys(m).length <= 50, {
|
|
23
33
|
message: 'custom_fields must name 1-50 fields',
|
|
24
34
|
})
|
|
@@ -279,6 +289,32 @@ export function renderActivity(res) {
|
|
|
279
289
|
`Page ${meta.current_page ?? '-'} of ${meta.last_page ?? '-'} (total ${meta.total ?? '-'}); pass page to continue.`,
|
|
280
290
|
].join('\n');
|
|
281
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* 0.27.0 (backend #5874 part 4): a project's activity feed, one change per
|
|
294
|
+
* line like renderActivity, each naming its task, then the keyset cursor to
|
|
295
|
+
* continue with. Values are flattened the same way, so a value cannot print
|
|
296
|
+
* something that looks like further lines.
|
|
297
|
+
*/
|
|
298
|
+
export function renderProjectActivity(res) {
|
|
299
|
+
const rows = Array.isArray(res) ? res : (res?.data ?? []);
|
|
300
|
+
const meta = res?.meta ?? {};
|
|
301
|
+
const flat = (v) => String(v).replace(/\s+/g, ' ').trim();
|
|
302
|
+
const value = (v) => {
|
|
303
|
+
if (v === null || v === undefined)
|
|
304
|
+
return '-';
|
|
305
|
+
const f = flat(typeof v === 'object' ? JSON.stringify(v) : v);
|
|
306
|
+
return f === '' ? '-' : f;
|
|
307
|
+
};
|
|
308
|
+
const task = (t) => t ? `task #${flat(t.id)}${t.title ? ` "${flat(t.title)}"` : ''}${t.deleted ? ' (deleted)' : ''}` : 'task -';
|
|
309
|
+
const line = (a) => `[${flat(a.created_at)}] ${task(a.task)} - ${flat(activityActor(a.employee))}: ${flat(a.action)}${a.field ? ` ${flat(a.field)}` : ''}` +
|
|
310
|
+
(a.old_value != null || a.new_value != null ? ` (${value(a.old_value)} -> ${value(a.new_value)})` : '');
|
|
311
|
+
return [
|
|
312
|
+
...(rows.length ? rows.map(line) : ['No activity.']),
|
|
313
|
+
meta.next_cursor
|
|
314
|
+
? `More: pass cursor="${flat(meta.next_cursor)}" (with the same filters) for the next page.`
|
|
315
|
+
: 'End of the log for these filters.',
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
282
318
|
/**
|
|
283
319
|
* #4 (0.23.0): the ticket a task was opened from, as `linked_ticket:
|
|
284
320
|
* "ticket #<id> <url>"` first in the answer, replacing the raw `ticket`
|
|
@@ -294,6 +330,58 @@ export function withLinkedTicket(task) {
|
|
|
294
330
|
}
|
|
295
331
|
return rest;
|
|
296
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* The list filters of backend task #5874 part 3, turned into GET /tasks query
|
|
335
|
+
* keys: `custom_field` (an object) becomes custom_field[<field_key>]=<value>,
|
|
336
|
+
* a boolean checkbox value 1 / 0. The contradictions the server refuses with a
|
|
337
|
+
* 422 are refused here first, in the same words, so no request is spent on
|
|
338
|
+
* them.
|
|
339
|
+
*/
|
|
340
|
+
/**
|
|
341
|
+
* The keys `fields` may name on GET /tasks (0.27.1, backend #5874 QA round 1):
|
|
342
|
+
* the compact row's keys plus custom_fields. The server's own list is
|
|
343
|
+
* PublicTaskResource::SELECTABLE_FIELDS; an unknown name is its 422.
|
|
344
|
+
*/
|
|
345
|
+
export const TASK_LIST_FIELDS = [
|
|
346
|
+
'id',
|
|
347
|
+
'title',
|
|
348
|
+
'status',
|
|
349
|
+
'priority',
|
|
350
|
+
'due_date',
|
|
351
|
+
'parent_id',
|
|
352
|
+
'project_id',
|
|
353
|
+
'assignee_codes',
|
|
354
|
+
'external_assignees',
|
|
355
|
+
'labels',
|
|
356
|
+
'updated_at',
|
|
357
|
+
'custom_fields',
|
|
358
|
+
];
|
|
359
|
+
export function listFilterQuery(args) {
|
|
360
|
+
if (args.no_due_date === true && (args.due_before !== undefined || args.due_after !== undefined)) {
|
|
361
|
+
throw new Error('no_due_date=true cannot be combined with due_before or due_after: a task with no due date is never due before or after anything. Drop one of them.');
|
|
362
|
+
}
|
|
363
|
+
if (args.no_assignee === true && args.assignee_employee_code !== undefined) {
|
|
364
|
+
throw new Error('no_assignee=true cannot be combined with assignee_employee_code: a task with no assignee is held by nobody. Drop one of them.');
|
|
365
|
+
}
|
|
366
|
+
const out = {
|
|
367
|
+
no_due_date: args.no_due_date === undefined ? undefined : args.no_due_date ? 1 : 0,
|
|
368
|
+
no_assignee: args.no_assignee === undefined ? undefined : args.no_assignee ? 1 : 0,
|
|
369
|
+
search_in: args.search_in,
|
|
370
|
+
updated_before: args.updated_before,
|
|
371
|
+
};
|
|
372
|
+
if (args.custom_field !== undefined) {
|
|
373
|
+
if (args.project_id === undefined) {
|
|
374
|
+
throw new Error('custom_field filters need project_id: custom fields belong to one project. Add project_id; clokio_get_project_custom_fields lists its field keys.');
|
|
375
|
+
}
|
|
376
|
+
for (const [key, value] of Object.entries(args.custom_field)) {
|
|
377
|
+
if (!FIELD_KEY_PATTERN.test(key)) {
|
|
378
|
+
throw new Error(`custom_field key "${key}" is not a field_key: 1-50 chars of letters, digits, _ or -.`);
|
|
379
|
+
}
|
|
380
|
+
out[`custom_field[${key}]`] = typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
297
385
|
export function registerTaskTools(server, config) {
|
|
298
386
|
registerTool(server, config, {
|
|
299
387
|
name: 'clokio_list_tasks',
|
|
@@ -301,14 +389,44 @@ export function registerTaskTools(server, config) {
|
|
|
301
389
|
'your own code. An UNKNOWN query parameter is a 422 naming it, so use exactly these names. Paginate ' +
|
|
302
390
|
'with page, or crawl with cursor (preferred past one page). Every task carries a task_url.\n\n' +
|
|
303
391
|
'The response is {data, meta}. meta carries total, per_page, current_page and next_cursor - read ' +
|
|
304
|
-
'next_cursor and pass it back as `cursor` for the following page; it is null on the last one.'
|
|
392
|
+
'next_cursor and pass it back as `cursor` for the following page; it is null on the last one. ' +
|
|
393
|
+
'For counts rather than rows (how many open tasks per status, label or person, what is overdue or ' +
|
|
394
|
+
'stale on one board), use clokio_get_project_task_summary instead of paging.',
|
|
305
395
|
schema: {
|
|
306
396
|
project_id: z.number().int().optional(),
|
|
307
397
|
status: z.string().optional().describe('Status slug, e.g. "in_progress"'),
|
|
308
398
|
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
|
309
399
|
assignee_employee_code: employeeCode.optional().describe('employee_code, e.g. "00080"'),
|
|
310
400
|
label: z.string().optional(),
|
|
311
|
-
search: z.string().optional().describe('Free text over title and description'),
|
|
401
|
+
search: z.string().optional().describe('Free text over title and description (see search_in)'),
|
|
402
|
+
search_in: z
|
|
403
|
+
.enum(['title', 'all'])
|
|
404
|
+
.optional()
|
|
405
|
+
.describe('Where `search` looks: "title" only, or "all" = title and description (the default)'),
|
|
406
|
+
no_due_date: z
|
|
407
|
+
.boolean()
|
|
408
|
+
.optional()
|
|
409
|
+
.describe('true = only tasks with NO due date; false = only tasks that have one; omit for no filter. ' +
|
|
410
|
+
'Cannot be combined with due_before / due_after when true.'),
|
|
411
|
+
no_assignee: z
|
|
412
|
+
.boolean()
|
|
413
|
+
.optional()
|
|
414
|
+
.describe('true = only tasks nobody is assigned to; false = only tasks somebody holds; omit for no filter. ' +
|
|
415
|
+
'A client person on a shared board, or a departed employee, still counts as holding a task. ' +
|
|
416
|
+
'Cannot be combined with assignee_employee_code when true.'),
|
|
417
|
+
custom_field: z
|
|
418
|
+
.record(fieldKey, z.union([z.string().min(1).max(500), z.number(), z.boolean()]))
|
|
419
|
+
.refine((m) => Object.keys(m).length >= 1 && Object.keys(m).length <= 20, {
|
|
420
|
+
message: 'custom_field must name 1-20 fields',
|
|
421
|
+
})
|
|
422
|
+
.optional()
|
|
423
|
+
.describe('Match custom field values: an object of field_key -> value, e.g. {"stage": "In build", "escalated": true}. ' +
|
|
424
|
+
'Several fields are ANDed. REQUIRES project_id (fields belong to one project); the keys are the ' +
|
|
425
|
+
'field_key values clokio_get_project_custom_fields lists, and a key that is not a public field of ' +
|
|
426
|
+
'that project is refused naming it. Per type: dropdown = the option value or its label (a label ' +
|
|
427
|
+
'two options share is refused asking for the value); multi-select = the task\'s list CONTAINS that ' +
|
|
428
|
+
'option; checkbox = true (ticked) or false (not ticked, an unanswered checkbox included); text, ' +
|
|
429
|
+
'url and email = exact match, ignoring case; number and date (YYYY-MM-DD) = exact.'),
|
|
312
430
|
open: z.boolean().optional().describe('true = only tasks whose status is not a "done" one'),
|
|
313
431
|
recurring: z
|
|
314
432
|
.boolean()
|
|
@@ -338,6 +456,11 @@ export function registerTaskTools(server, config) {
|
|
|
338
456
|
.string()
|
|
339
457
|
.optional()
|
|
340
458
|
.describe('ISO-8601. Incremental sync: only tasks changed at or after this instant'),
|
|
459
|
+
updated_before: z
|
|
460
|
+
.string()
|
|
461
|
+
.optional()
|
|
462
|
+
.describe('YYYY-MM-DD or ISO-8601, organization timezone. Only tasks last updated BEFORE this (exclusive); ' +
|
|
463
|
+
'the other half of updated_since'),
|
|
341
464
|
include_comments: z.boolean().optional(),
|
|
342
465
|
per_page: z.number().int().min(1).max(100).optional(),
|
|
343
466
|
page: z.number().int().optional(),
|
|
@@ -352,6 +475,14 @@ export function registerTaskTools(server, config) {
|
|
|
352
475
|
'and updated_at - dropping the HTML description and flattening the nested project/assignee ' +
|
|
353
476
|
'objects to ids. Resolve a project_id with clokio_list_projects, and fetch the one task you ' +
|
|
354
477
|
'actually need in full with clokio_get_task (its URL is /tasks/{id}/view).'),
|
|
478
|
+
fields: z
|
|
479
|
+
.array(z.enum(TASK_LIST_FIELDS))
|
|
480
|
+
.min(1)
|
|
481
|
+
.max(TASK_LIST_FIELDS.length)
|
|
482
|
+
.optional()
|
|
483
|
+
.describe('Return ONLY these keys per row, in this order (implies compact), e.g. ["id", "title", "status"]. ' +
|
|
484
|
+
'Allowed: ' + TASK_LIST_FIELDS.join(', ') + '. custom_fields is the full row\'s object of public ' +
|
|
485
|
+
'field values. The smallest scan there is.'),
|
|
355
486
|
sort: z
|
|
356
487
|
.enum([
|
|
357
488
|
'due_date',
|
|
@@ -371,6 +502,7 @@ export function registerTaskTools(server, config) {
|
|
|
371
502
|
},
|
|
372
503
|
handler: (args, cfg) => request(cfg, '/tasks', {
|
|
373
504
|
query: {
|
|
505
|
+
...listFilterQuery(args),
|
|
374
506
|
project_id: args.project_id,
|
|
375
507
|
status: args.status,
|
|
376
508
|
priority: args.priority,
|
|
@@ -392,9 +524,87 @@ export function registerTaskTools(server, config) {
|
|
|
392
524
|
cursor: args.cursor,
|
|
393
525
|
sort: args.sort,
|
|
394
526
|
compact: args.compact === undefined ? undefined : args.compact ? 1 : 0,
|
|
527
|
+
fields: args.fields === undefined ? undefined : [...new Set(args.fields)].join(','),
|
|
395
528
|
},
|
|
396
529
|
}),
|
|
397
530
|
});
|
|
531
|
+
registerTool(server, config, {
|
|
532
|
+
name: 'clokio_get_project_task_summary',
|
|
533
|
+
description: "One project's task numbers in one call, counted by the server: tasks by status (the project's own " +
|
|
534
|
+
'statuses, each with is_open), by priority (low / medium / high / urgent / none), by label name and by ' +
|
|
535
|
+
'assignee, plus unassigned and no_due_date counts; and two short lists of compact task rows: overdue ' +
|
|
536
|
+
"(open tasks due before today in the organisation's time zone, oldest due first) and stale (open tasks " +
|
|
537
|
+
'not updated for stale_days, least recently updated first), each at most 50 rows with a total. ' +
|
|
538
|
+
'OPEN tasks only unless include_closed is true. Subtasks count as tasks of their own unless top_level ' +
|
|
539
|
+
'is true. A task held by several people is counted for EACH of them in by_assignee, so those numbers ' +
|
|
540
|
+
'can sum above total. The answer carries a definitions block naming the rule behind every number, and ' +
|
|
541
|
+
'echoes stale_days. Use this for "how is project X doing" questions; use clokio_list_tasks for the rows ' +
|
|
542
|
+
'themselves. Needs tasks:read.',
|
|
543
|
+
schema: {
|
|
544
|
+
project_id: z.number().int(),
|
|
545
|
+
include_closed: z
|
|
546
|
+
.boolean()
|
|
547
|
+
.optional()
|
|
548
|
+
.describe('true also counts closed tasks (status category done). overdue and stale stay open tasks'),
|
|
549
|
+
top_level: z
|
|
550
|
+
.boolean()
|
|
551
|
+
.optional()
|
|
552
|
+
.describe('true counts top-level tasks only (subtasks excluded), overdue and stale included'),
|
|
553
|
+
stale_days: z
|
|
554
|
+
.number()
|
|
555
|
+
.int()
|
|
556
|
+
.min(1)
|
|
557
|
+
.max(365)
|
|
558
|
+
.optional()
|
|
559
|
+
.describe('Days without an update (updated_at) that make an open task stale. Default 14, range 1-365'),
|
|
560
|
+
group_by_custom_field: fieldKey
|
|
561
|
+
.optional()
|
|
562
|
+
.describe('The field_key of a PUBLIC dropdown or multi-select field of this project (clokio_get_project_custom_fields). ' +
|
|
563
|
+
'Adds by_custom_field: tasks per option (value and label) with open / closed / total, plus none for tasks ' +
|
|
564
|
+
'with no answer; a multi-select task counts under each option it holds. This answers "what is in each ' +
|
|
565
|
+
'release / sprint". Any other field type, a private field or an unknown key is refused (422).'),
|
|
566
|
+
},
|
|
567
|
+
handler: (args, cfg) => request(cfg, `/projects/${args.project_id}/task-summary`, {
|
|
568
|
+
query: {
|
|
569
|
+
include_closed: args.include_closed === undefined ? undefined : args.include_closed ? 1 : 0,
|
|
570
|
+
top_level: args.top_level === undefined ? undefined : args.top_level ? 1 : 0,
|
|
571
|
+
stale_days: args.stale_days,
|
|
572
|
+
group_by_custom_field: args.group_by_custom_field,
|
|
573
|
+
},
|
|
574
|
+
}),
|
|
575
|
+
});
|
|
576
|
+
registerTool(server, config, {
|
|
577
|
+
name: 'clokio_list_project_activity',
|
|
578
|
+
description: "One project's activity log, newest first: every change on every task of the board (deleted tasks " +
|
|
579
|
+
'included), one line per change naming the task, who acted (name, code, organisation) and what changed. ' +
|
|
580
|
+
"since / until narrow it (a plain date or time is read in the organisation's time zone; until is " +
|
|
581
|
+
'exclusive), action limits it to some actions (e.g. ["status_changed", "assigned"]). Pages of per_page ' +
|
|
582
|
+
'(1-100, default 25); the answer ends with the cursor to pass back for the next page, with the same ' +
|
|
583
|
+
"filters. Only PUBLIC custom fields' history appears, and internal comments never do. Use " +
|
|
584
|
+
'clokio_get_task_activity for one task. Needs tasks:read.',
|
|
585
|
+
schema: {
|
|
586
|
+
project_id: z.number().int(),
|
|
587
|
+
since: z.string().optional().describe('YYYY-MM-DD or ISO-8601: rows at or after this instant'),
|
|
588
|
+
until: z.string().optional().describe('YYYY-MM-DD or ISO-8601: rows before this instant (exclusive)'),
|
|
589
|
+
action: z
|
|
590
|
+
.array(z.string().min(1).max(60))
|
|
591
|
+
.min(1)
|
|
592
|
+
.max(30)
|
|
593
|
+
.optional()
|
|
594
|
+
.describe('Only these actions, e.g. ["status_changed", "assigned", "custom_field_changed"]'),
|
|
595
|
+
cursor: z.string().max(500).optional().describe('The cursor the previous page ended with'),
|
|
596
|
+
per_page: z.number().int().min(1).max(100).optional(),
|
|
597
|
+
},
|
|
598
|
+
handler: async (args, cfg) => renderProjectActivity(await request(cfg, `/projects/${args.project_id}/activities`, {
|
|
599
|
+
query: {
|
|
600
|
+
since: args.since,
|
|
601
|
+
until: args.until,
|
|
602
|
+
action: args.action,
|
|
603
|
+
cursor: args.cursor,
|
|
604
|
+
per_page: args.per_page,
|
|
605
|
+
},
|
|
606
|
+
})),
|
|
607
|
+
});
|
|
398
608
|
registerTool(server, config, {
|
|
399
609
|
name: 'clokio_get_task',
|
|
400
610
|
description: 'Get one task by id, with its full detail (assignees, labels, status, custom fields, task_url) and ' +
|
|
@@ -438,7 +648,7 @@ export function registerTaskTools(server, config) {
|
|
|
438
648
|
status: z.string().optional().describe("Status slug valid for THIS project's board"),
|
|
439
649
|
due_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
440
650
|
start_date: z.string().optional().describe('YYYY-MM-DD'),
|
|
441
|
-
estimated_hours: z.number().min(0).max(
|
|
651
|
+
estimated_hours: z.number().min(0).max(ESTIMATED_HOURS_MAX).optional(),
|
|
442
652
|
parent_task_id: z.number().int().optional().describe('Makes this a subtask of that task'),
|
|
443
653
|
assignee_employee_codes: z
|
|
444
654
|
.array(assigneeReference)
|
|
@@ -510,7 +720,14 @@ export function registerTaskTools(server, config) {
|
|
|
510
720
|
'\n\n' +
|
|
511
721
|
'`custom_fields` (object field_key -> value) sets several custom fields in the same call, with the ' +
|
|
512
722
|
'rules of clokio_set_task_custom_field (option labels accepted, null clears). Every field is checked ' +
|
|
513
|
-
'before anything is written: one bad field refuses the whole update and nothing changes
|
|
723
|
+
'before anything is written: one bad field refuses the whole update and nothing changes.\n\n' +
|
|
724
|
+
'`parent_task_id` makes the task a subtask of another task on the SAME project (null makes it ' +
|
|
725
|
+
'top-level again). Subtasks go one level deep: a subtask cannot be a parent, a task that has subtasks ' +
|
|
726
|
+
'cannot become one, and a task cannot be its own parent.\n\n' +
|
|
727
|
+
'`assignee_employee_codes` (with `assignee_mode`: replace by default, add, remove) changes the ' +
|
|
728
|
+
'assignees in the same call, with exactly the rules of clokio_set_task_assignees (a client\'s person ' +
|
|
729
|
+
'as {employee_code, organization_id}; assigning makes the person a watcher). A refused person refuses ' +
|
|
730
|
+
'the WHOLE update: nothing in the call is written.',
|
|
514
731
|
schema: {
|
|
515
732
|
id: z.number().int(),
|
|
516
733
|
title: z.string().max(255).optional(),
|
|
@@ -519,8 +736,15 @@ export function registerTaskTools(server, config) {
|
|
|
519
736
|
status: z.string().optional().describe("Status slug valid for this task's board"),
|
|
520
737
|
due_date: z.string().nullable().optional().describe('YYYY-MM-DD, or null to clear'),
|
|
521
738
|
start_date: z.string().nullable().optional().describe('YYYY-MM-DD, or null to clear'),
|
|
522
|
-
estimated_hours: z.number().min(0).max(
|
|
523
|
-
project_id: z
|
|
739
|
+
estimated_hours: z.number().min(0).max(ESTIMATED_HOURS_MAX).nullable().optional(),
|
|
740
|
+
project_id: z
|
|
741
|
+
.number()
|
|
742
|
+
.int()
|
|
743
|
+
.optional()
|
|
744
|
+
.describe('Move the task to another project. Its subtasks move with it, and a status the new board lacks becomes ' +
|
|
745
|
+
'that board\'s default. A SUBTASK moves only when the same call sends parent_task_id: null (it ' +
|
|
746
|
+
'becomes a top-level task there) or the id of a task on the new project (it moves under it); ' +
|
|
747
|
+
'otherwise the server refuses it.'),
|
|
524
748
|
actor_employee_code: employeeCode.optional().describe('Who to credit in the activity log; omit to use the key issuer / CLOKIO_DEFAULT_ACTOR'),
|
|
525
749
|
recurrence: recurrenceInput
|
|
526
750
|
.nullable()
|
|
@@ -534,6 +758,23 @@ export function registerTaskTools(server, config) {
|
|
|
534
758
|
add_label_names: labelNames.describe('Labels to ADD, keeping the others. A missing name is CREATED; one the task already carries changes nothing'),
|
|
535
759
|
remove_label_names: labelNames.describe('Labels to REMOVE, keeping the others. A name the task does not carry, or that does not exist, changes nothing (never created)'),
|
|
536
760
|
custom_fields: customFieldsMap.optional(),
|
|
761
|
+
parent_task_id: z
|
|
762
|
+
.number()
|
|
763
|
+
.int()
|
|
764
|
+
.min(1)
|
|
765
|
+
.nullable()
|
|
766
|
+
.optional()
|
|
767
|
+
.describe('Hang the task under this task (same project, one level deep), or null to make it top-level'),
|
|
768
|
+
assignee_employee_codes: z
|
|
769
|
+
.array(assigneeReference)
|
|
770
|
+
.min(1)
|
|
771
|
+
.max(50)
|
|
772
|
+
.optional()
|
|
773
|
+
.describe('Assignees, as on clokio_set_task_assignees: a code of your organisation, or {employee_code, organization_id} for a client\'s person'),
|
|
774
|
+
assignee_mode: z
|
|
775
|
+
.enum(['replace', 'add', 'remove'])
|
|
776
|
+
.optional()
|
|
777
|
+
.describe('How assignee_employee_codes applies: replace (default) overwrites, add appends, remove takes off'),
|
|
537
778
|
},
|
|
538
779
|
handler: (args, cfg) => {
|
|
539
780
|
// Only keys the caller actually supplied may go in the body: PATCH
|
|
@@ -552,10 +793,16 @@ export function registerTaskTools(server, config) {
|
|
|
552
793
|
'actor_employee_code',
|
|
553
794
|
'label_names',
|
|
554
795
|
'custom_fields',
|
|
796
|
+
'parent_task_id',
|
|
797
|
+
'assignee_employee_codes',
|
|
798
|
+
'assignee_mode',
|
|
555
799
|
]) {
|
|
556
800
|
if (args[key] !== undefined)
|
|
557
801
|
body[key] = args[key];
|
|
558
802
|
}
|
|
803
|
+
if (args.assignee_mode !== undefined && args.assignee_employee_codes === undefined) {
|
|
804
|
+
throw new Error('assignee_mode needs assignee_employee_codes.');
|
|
805
|
+
}
|
|
559
806
|
refuseLabelConflicts(args, true);
|
|
560
807
|
// An empty add/remove list is not a change: it is not sent.
|
|
561
808
|
if (hasNames(args.add_label_names))
|
|
@@ -622,11 +869,15 @@ export function registerTaskTools(server, config) {
|
|
|
622
869
|
.max(50)
|
|
623
870
|
.describe('Each a code of your organisation, or {employee_code, organization_id} for a client\'s person on a shared board'),
|
|
624
871
|
mode: z.enum(['add', 'replace', 'remove']).describe('Required'),
|
|
872
|
+
actor_employee_code: employeeCode.optional().describe('Who to credit in the activity log; omit to use the key issuer / CLOKIO_DEFAULT_ACTOR'),
|
|
873
|
+
},
|
|
874
|
+
handler: async (args, cfg) => {
|
|
875
|
+
const body = { employee_codes: args.employee_codes, mode: args.mode };
|
|
876
|
+
const actor = args.actor_employee_code ?? cfg.defaultActor;
|
|
877
|
+
if (actor)
|
|
878
|
+
body.actor_employee_code = actor;
|
|
879
|
+
return compactAssignees(args.id, await request(cfg, `/tasks/${args.id}/assignees`, { method: 'PATCH', body }), cfg.baseUrl);
|
|
625
880
|
},
|
|
626
|
-
handler: async (args, cfg) => compactAssignees(args.id, await request(cfg, `/tasks/${args.id}/assignees`, {
|
|
627
|
-
method: 'PATCH',
|
|
628
|
-
body: { employee_codes: args.employee_codes, mode: args.mode },
|
|
629
|
-
}), cfg.baseUrl),
|
|
630
881
|
});
|
|
631
882
|
registerTool(server, config, {
|
|
632
883
|
name: 'clokio_list_project_people',
|
|
@@ -945,7 +1196,7 @@ export function registerTaskTools(server, config) {
|
|
|
945
1196
|
'clokio_bulk_set_task_custom_field.',
|
|
946
1197
|
schema: {
|
|
947
1198
|
id: z.number().int(),
|
|
948
|
-
field_key:
|
|
1199
|
+
field_key: fieldKey,
|
|
949
1200
|
value: customFieldValue,
|
|
950
1201
|
actor_employee_code: employeeCode.optional().describe('Who to credit in the activity log; omit to use the key issuer / CLOKIO_DEFAULT_ACTOR'),
|
|
951
1202
|
},
|
|
@@ -985,11 +1236,12 @@ export function registerTaskTools(server, config) {
|
|
|
985
1236
|
registerTool(server, config, {
|
|
986
1237
|
name: 'clokio_bulk_update_tasks',
|
|
987
1238
|
mutates: true,
|
|
988
|
-
description: 'Apply ONE change (any of status, priority, due_date, add_label_names, remove_label_names) to up to 200 ' +
|
|
1239
|
+
description: 'Apply ONE change (any of status, priority, due_date, estimated_hours, add_label_names, remove_label_names) to up to 200 ' +
|
|
989
1240
|
'tasks at once - for cleaning up a board without a call per task. Partial success is normal: the result ' +
|
|
990
1241
|
'is {updated, not_found, invalid_status}. not_found lists ids that do not exist in your org (skipped, not ' +
|
|
991
1242
|
'fatal); invalid_status lists tasks whose board does not have the given status slug (such a task is ' +
|
|
992
|
-
'skipped whole, its labels included).
|
|
1243
|
+
'skipped whole, its labels included). estimated_hours (0-9999.99, null clears) sets one estimate on every ' +
|
|
1244
|
+
'task; a task already holding it changes nothing. Give at least one of status/priority/due_date/estimated_hours/add_label_names/' +
|
|
993
1245
|
'remove_label_names (an empty list does not count). Labels are a change only: add_label_names adds (a missing name is CREATED once in ' +
|
|
994
1246
|
'your organisation), remove_label_names removes (never creates); a label a task already carries, or one ' +
|
|
995
1247
|
'it does not carry, changes nothing for that task. To REPLACE a task\'s whole label set use ' +
|
|
@@ -1001,6 +1253,7 @@ export function registerTaskTools(server, config) {
|
|
|
1001
1253
|
status: z.string().optional().describe('Status slug - validated per task against its own board'),
|
|
1002
1254
|
priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
|
|
1003
1255
|
due_date: z.string().nullable().optional().describe('YYYY-MM-DD, or null to clear'),
|
|
1256
|
+
estimated_hours: z.number().min(0).max(ESTIMATED_HOURS_MAX).nullable().optional().describe('Hours, 0-9999.99, set on every task; null clears'),
|
|
1004
1257
|
actor_employee_code: employeeCode.optional().describe('Who to credit; omit to use the key issuer'),
|
|
1005
1258
|
add_label_names: labelNames.describe('Labels to ADD to every task. A missing name is CREATED once in your organisation'),
|
|
1006
1259
|
remove_label_names: labelNames.describe('Labels to REMOVE from every task. An unknown name changes nothing (never created)'),
|
|
@@ -1009,9 +1262,10 @@ export function registerTaskTools(server, config) {
|
|
|
1009
1262
|
if (args.status === undefined &&
|
|
1010
1263
|
args.priority === undefined &&
|
|
1011
1264
|
args.due_date === undefined &&
|
|
1265
|
+
args.estimated_hours === undefined &&
|
|
1012
1266
|
!hasNames(args.add_label_names) &&
|
|
1013
1267
|
!hasNames(args.remove_label_names)) {
|
|
1014
|
-
throw new Error('Give at least one of: status, priority, due_date, add_label_names, remove_label_names.');
|
|
1268
|
+
throw new Error('Give at least one of: status, priority, due_date, estimated_hours, add_label_names, remove_label_names.');
|
|
1015
1269
|
}
|
|
1016
1270
|
refuseLabelConflicts(args, false);
|
|
1017
1271
|
const body = { ids: args.ids };
|
|
@@ -1021,6 +1275,8 @@ export function registerTaskTools(server, config) {
|
|
|
1021
1275
|
body.priority = args.priority;
|
|
1022
1276
|
if (args.due_date !== undefined)
|
|
1023
1277
|
body.due_date = args.due_date;
|
|
1278
|
+
if (args.estimated_hours !== undefined)
|
|
1279
|
+
body.estimated_hours = args.estimated_hours;
|
|
1024
1280
|
if (hasNames(args.add_label_names))
|
|
1025
1281
|
body.add_label_names = args.add_label_names;
|
|
1026
1282
|
if (hasNames(args.remove_label_names))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clokio-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Clokio - manage tasks, projects, clients, employees, attendance and documents from Claude and any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|