clokio-mcp 0.18.0 → 0.18.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 +17 -9
- package/dist/tools/docs.js +269 -276
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,25 +168,33 @@ that employee:
|
|
|
168
168
|
| `clokio_list_doc_versions` / `clokio_get_doc_version` / `clokio_restore_doc_version` | `GET .../versions`, `GET .../versions/{versionId}`, `POST .../versions/{versionId}/restore` |
|
|
169
169
|
| `clokio_list_doc_attachments` / `clokio_upload_doc_attachment` / `clokio_download_doc_attachment` | `GET` / `POST /docs/{id}/attachments`, `GET .../attachments/download?path=` |
|
|
170
170
|
|
|
171
|
-
Working with
|
|
171
|
+
Working with documents:
|
|
172
172
|
|
|
173
|
+
- **Every docs tool answers `{data}` or `{data, meta}`**, like the task tools:
|
|
174
|
+
the record or rows in `data`; paging, counts, your role and permissions in
|
|
175
|
+
`meta`. `clokio_get_doc` puts the record and its body together in `data`.
|
|
176
|
+
- **Listing covers every scope by default.** `project_id` narrows it: a
|
|
177
|
+
project id, `"none"` for standalone documents only, or `"all"` (default).
|
|
173
178
|
- **Read as Markdown, write as Markdown.** `clokio_get_doc` returns Markdown by
|
|
174
|
-
default (`format: "html"` or `"json"` for the others)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
default (`format: "html"` or `"json"` for the others) without the title;
|
|
180
|
+
pass `include_title: true` to start the body with it as a heading. The
|
|
181
|
+
create/update tools take Markdown by default (`content_format: "html"` or
|
|
182
|
+
`"json"`); HTML you write must stay within Clokio's restricted grammar (a
|
|
183
|
+
`422` names any unsupported tag).
|
|
178
184
|
- **Content writes are optimistic.** `clokio_update_doc_content` REPLACES the
|
|
179
185
|
whole body and requires `base_version` - the `content_version` you last read.
|
|
180
186
|
A `409` means someone else edited in between: re-read, reconcile, write
|
|
181
187
|
again with the new version. Never retry with the same `base_version`.
|
|
182
|
-
- **
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
- **Public share links expire after 30 days** unless you pass `expires_at` or
|
|
189
|
+
`never_expires: true`.
|
|
190
|
+
- **Attachments** list `name`, `size`, `mime` and `uploaded_at`; download by
|
|
191
|
+
`path`. Uploading does not change the text: place the returned `url` in the
|
|
192
|
+
body yourself (an `image` or `fileAttachment` node in Tiptap JSON).
|
|
185
193
|
- **Members are named by `employee_code`** (e.g. `"00080"`), like everywhere
|
|
186
194
|
else in this server; the internal `employee_id` is accepted as an
|
|
187
195
|
alternative. Teams are named by `team_id`, from
|
|
188
196
|
`clokio_list_doc_shareable_teams`. A member's `email` is shown only when the
|
|
189
|
-
key has `employees:pii`.
|
|
197
|
+
key has `employees:pii`; profile pictures only with `include_avatars: true`.
|
|
190
198
|
|
|
191
199
|
## Security
|
|
192
200
|
|
package/dist/tools/docs.js
CHANGED
|
@@ -7,44 +7,42 @@ import { registerTool } from './helpers.js';
|
|
|
7
7
|
* The Docs module, on the Public API (/api/v1/docs...) with CLOKIO_API_KEY.
|
|
8
8
|
*
|
|
9
9
|
* Documents are permissioned per PERSON, so every docs request acts as one
|
|
10
|
-
* employee - the key's EFFECTIVE actor (backend ActsAsAuthority
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* scopes to a key with no acts-as employee, so an ordinary full-access
|
|
16
|
-
* integration key never opens its creator's private documents.
|
|
17
|
-
* Every refusal of that kind is a uniform 403 decided before any document is
|
|
18
|
-
* looked up. The key then sees exactly what that employee sees: a document
|
|
19
|
-
* they cannot see is a 404 (never a 403 - the API will not confirm that an id
|
|
20
|
-
* exists), and a visible document they may not change is a 403. Nothing here
|
|
21
|
-
* re-checks permissions; the API's answer is surfaced verbatim.
|
|
10
|
+
* employee - the key's EFFECTIVE actor (backend ActsAsAuthority): its
|
|
11
|
+
* "acts as" employee, else the key's creator, the latter only when the key
|
|
12
|
+
* lists docs:read / docs:write EXPLICITLY ("*" is not enough for a key with
|
|
13
|
+
* no actor). The API decides everything else: an invisible document is 404,
|
|
14
|
+
* a visible one the actor may not change is 403.
|
|
22
15
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
16
|
+
* TOKEN BUDGET. tools/list is paid by every session, so the access rules are
|
|
17
|
+
* spelled out ONCE, on clokio_list_docs, and every other description points
|
|
18
|
+
* there ("Docs access: see clokio_list_docs"). Each description says only
|
|
19
|
+
* what the model needs to call the tool correctly: what it does, the doc
|
|
20
|
+
* role it needs, and any trap (base_version, the 30-day share link default).
|
|
21
|
+
*
|
|
22
|
+
* RESPONSE SHAPE. The API answers with named keys (`docs`, `doc`, `comment`,
|
|
23
|
+
* `comments`, ...). These tools reshape that into the envelope the task tools
|
|
24
|
+
* return - `{data}` or `{data, meta}` - so a model reads every Clokio tool the
|
|
25
|
+
* same way: the record or rows in `data`, paging / permissions / counts in
|
|
26
|
+
* `meta`. See shape*() below.
|
|
28
27
|
*/
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const docId = z.number().int().positive().describe('The document id');
|
|
28
|
+
const ACCESS = 'Docs access: see clokio_list_docs.';
|
|
29
|
+
const docId = z.number().int().positive().describe('Document id');
|
|
32
30
|
const CONTENT_FORMATS = ['markdown', 'html', 'json'];
|
|
33
31
|
const contentArg = z
|
|
34
32
|
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
35
|
-
.describe('
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.enum(CONTENT_FORMATS)
|
|
33
|
+
.describe('Body. Markdown by default; html = Clokio\'s restricted HTML (a 422 names any unsupported tag); ' +
|
|
34
|
+
'json = Tiptap {"type":"doc","content":[...]}');
|
|
35
|
+
const contentFormatArg = z.enum(CONTENT_FORMATS).optional().describe('markdown (default), html or json');
|
|
36
|
+
const readFormatArg = z.enum(CONTENT_FORMATS).optional().describe('markdown (default), html or json');
|
|
37
|
+
const includeTitleArg = z
|
|
38
|
+
.boolean()
|
|
42
39
|
.optional()
|
|
43
|
-
.describe('
|
|
44
|
-
const
|
|
45
|
-
.
|
|
40
|
+
.describe('Markdown only: start the body with the title as a heading (default false)');
|
|
41
|
+
const scopeArg = z
|
|
42
|
+
.union([z.number().int().positive(), z.literal('all'), z.literal('none')])
|
|
46
43
|
.optional()
|
|
47
|
-
.describe('
|
|
44
|
+
.describe('Project id, "none" (standalone docs only) or "all" (default)');
|
|
45
|
+
const archivedArg = z.enum(['exclude', 'include', 'only']).optional().describe('Default exclude');
|
|
48
46
|
/**
|
|
49
47
|
* Map (content, content_format) onto the ONE content field the API accepts.
|
|
50
48
|
* Sending two content fields is a 422, so exactly one key is produced.
|
|
@@ -80,116 +78,164 @@ function contentBody(content, format = 'markdown') {
|
|
|
80
78
|
}
|
|
81
79
|
return format === 'html' ? { content_html: content } : { content_markdown: content };
|
|
82
80
|
}
|
|
83
|
-
|
|
81
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
82
|
+
/** `{data, meta}`, with meta left out when it would be empty. */
|
|
83
|
+
export function envelope(data, meta) {
|
|
84
|
+
const m = meta ? Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined)) : {};
|
|
85
|
+
return Object.keys(m).length ? { data, meta: m } : { data };
|
|
86
|
+
}
|
|
87
|
+
/** Lift `key` out of the API's data object into `data`; every other key becomes meta. */
|
|
88
|
+
export function liftKey(raw, key) {
|
|
89
|
+
if (!isObj(raw) || !(key in raw))
|
|
90
|
+
return envelope(raw);
|
|
91
|
+
const { [key]: data, ...rest } = raw;
|
|
92
|
+
// A nested meta (the list endpoints put paging in data.meta) is flattened
|
|
93
|
+
// into the same meta object rather than nested a second time.
|
|
94
|
+
const { meta: nested, ...others } = rest;
|
|
95
|
+
return envelope(data, { ...(isObj(nested) ? nested : {}), ...others });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A document read (live or a version): the record and its body together in
|
|
99
|
+
* `data` (the body sits BESIDE the record in the API, which is easy to miss),
|
|
100
|
+
* the reader's permissions and breadcrumbs in `meta`.
|
|
101
|
+
*/
|
|
102
|
+
export function shapeDocRead(raw, recordKey) {
|
|
103
|
+
if (!isObj(raw) || !isObj(raw[recordKey]))
|
|
104
|
+
return envelope(raw);
|
|
105
|
+
const { [recordKey]: record, ...rest } = raw;
|
|
106
|
+
const data = { ...record };
|
|
107
|
+
const meta = {};
|
|
108
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
109
|
+
if (k === 'content' || k === 'content_html' || k === 'content_markdown')
|
|
110
|
+
data[k] = v;
|
|
111
|
+
else
|
|
112
|
+
meta[k] = v;
|
|
113
|
+
}
|
|
114
|
+
return envelope(data, meta);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Drop every profile_pic key, at any depth, unless avatars were asked for. A
|
|
118
|
+
* signed avatar URL is a few hundred characters per person and tells a model
|
|
119
|
+
* nothing.
|
|
120
|
+
*/
|
|
121
|
+
export function stripAvatars(rows, keep = false) {
|
|
122
|
+
if (keep)
|
|
123
|
+
return rows;
|
|
124
|
+
const strip = (v) => {
|
|
125
|
+
if (Array.isArray(v))
|
|
126
|
+
return v.map(strip);
|
|
127
|
+
if (!isObj(v))
|
|
128
|
+
return v;
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const [k, val] of Object.entries(v)) {
|
|
131
|
+
if (k === 'profile_pic')
|
|
132
|
+
continue;
|
|
133
|
+
out[k] = strip(val);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
137
|
+
return strip(rows);
|
|
138
|
+
}
|
|
139
|
+
/** Attachment rows with name / size / mime / uploaded_at first, null when the API omits one. */
|
|
140
|
+
export function shapeAttachments(raw) {
|
|
141
|
+
const rows = isObj(raw) && Array.isArray(raw.attachments) ? raw.attachments : Array.isArray(raw) ? raw : null;
|
|
142
|
+
if (!rows)
|
|
143
|
+
return envelope(raw);
|
|
144
|
+
return envelope(rows.map((r) => {
|
|
145
|
+
if (!isObj(r))
|
|
146
|
+
return r;
|
|
147
|
+
const { name, size, mime, uploaded_at, ...rest } = r;
|
|
148
|
+
return { name: name ?? null, size: size ?? null, mime: mime ?? null, uploaded_at: uploaded_at ?? null, ...rest };
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------- tools
|
|
84
152
|
export function registerDocTools(server, config) {
|
|
85
153
|
// ---------------------------------------------------------------- reading
|
|
86
154
|
registerTool(server, config, {
|
|
87
155
|
name: 'clokio_list_docs',
|
|
88
|
-
description: 'List documents
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
'
|
|
95
|
-
'person who created the key, and only when docs:read / docs:write are listed on the key explicitly (the ' +
|
|
96
|
-
'wildcard "*" is not enough). Otherwise every docs tool answers 403; set an acts-as employee or add the ' +
|
|
97
|
-
'docs scopes explicitly in Settings > API keys.',
|
|
156
|
+
description: 'List documents you can view (all scopes by default; project_id narrows). Rows include effective_role ' +
|
|
157
|
+
'(view/comment/edit/admin): edit or admin means you may write content. ' +
|
|
158
|
+
'DOCS ACCESS (applies to every clokio_*doc* tool): each call acts as the API key\'s "acts as" employee, ' +
|
|
159
|
+
'or, if the key has none, as the key\'s creator - only when the key lists docs:read / docs:write ' +
|
|
160
|
+
'explicitly ("*" is not enough). Otherwise every docs tool returns 403: set an acts-as employee or add ' +
|
|
161
|
+
'the docs scopes in Settings > API keys. Reads need scope docs:read, everything else docs:write. You see ' +
|
|
162
|
+
'exactly what that employee sees; a doc they cannot see is 404.',
|
|
98
163
|
schema: {
|
|
99
|
-
project_id:
|
|
100
|
-
.union([z.number().int().positive(), z.literal('all')])
|
|
101
|
-
.optional()
|
|
102
|
-
.describe('A project id, or "all". Omitted = standalone docs only'),
|
|
164
|
+
project_id: scopeArg,
|
|
103
165
|
parent_id: z
|
|
104
166
|
.union([z.number().int().positive(), z.literal('root')])
|
|
105
167
|
.optional()
|
|
106
|
-
.describe('A doc id to list its children
|
|
107
|
-
q: z.string().max(120).optional().describe('
|
|
108
|
-
archived:
|
|
109
|
-
.enum(['exclude', 'include', 'only'])
|
|
110
|
-
.optional()
|
|
111
|
-
.describe('Archived docs: exclude (default), include, or only'),
|
|
168
|
+
.describe('A doc id to list its children, or "root" for top-level docs only'),
|
|
169
|
+
q: z.string().max(120).optional().describe('Title substring'),
|
|
170
|
+
archived: archivedArg,
|
|
112
171
|
page: z.number().int().min(1).optional(),
|
|
113
|
-
per_page: z.number().int().min(1).max(100).optional().describe('
|
|
172
|
+
per_page: z.number().int().min(1).max(100).optional().describe('Default 50'),
|
|
114
173
|
},
|
|
115
|
-
handler: (args, cfg) => request(cfg, '/docs', {
|
|
174
|
+
handler: async (args, cfg) => liftKey(await request(cfg, '/docs', {
|
|
116
175
|
query: {
|
|
117
|
-
project_id: args.project_id,
|
|
176
|
+
project_id: args.project_id ?? 'all',
|
|
118
177
|
parent_id: args.parent_id,
|
|
119
178
|
q: args.q,
|
|
120
179
|
archived: args.archived,
|
|
121
180
|
page: args.page,
|
|
122
181
|
per_page: args.per_page,
|
|
123
182
|
},
|
|
124
|
-
}),
|
|
183
|
+
}), 'docs'),
|
|
125
184
|
});
|
|
126
185
|
registerTool(server, config, {
|
|
127
186
|
name: 'clokio_get_doc_tree',
|
|
128
|
-
description:
|
|
129
|
-
'plus meta. A doc whose parent you cannot see appears at the top level. Same scope rule as ' +
|
|
130
|
-
'clokio_list_docs: omitting project_id means standalone docs only; "all" means every scope. Needs ' +
|
|
131
|
-
'section docs.view. Scope docs:read. ' + ACTS_AS,
|
|
187
|
+
description: `Documents you can view as a nested tree (children), paged over the top level. ${ACCESS}`,
|
|
132
188
|
schema: {
|
|
133
|
-
project_id:
|
|
134
|
-
|
|
135
|
-
.optional()
|
|
136
|
-
.describe('A project id, or "all". Omitted = standalone docs only'),
|
|
137
|
-
archived: z.enum(['exclude', 'include', 'only']).optional(),
|
|
189
|
+
project_id: scopeArg,
|
|
190
|
+
archived: archivedArg,
|
|
138
191
|
page: z.number().int().min(1).optional(),
|
|
139
|
-
per_page: z.number().int().min(1).max(100).optional()
|
|
192
|
+
per_page: z.number().int().min(1).max(100).optional(),
|
|
140
193
|
},
|
|
141
|
-
handler: (args, cfg) => request(cfg, '/docs', {
|
|
194
|
+
handler: async (args, cfg) => liftKey(await request(cfg, '/docs', {
|
|
142
195
|
query: {
|
|
143
196
|
tree: 1,
|
|
144
|
-
project_id: args.project_id,
|
|
197
|
+
project_id: args.project_id ?? 'all',
|
|
145
198
|
archived: args.archived,
|
|
146
199
|
page: args.page,
|
|
147
200
|
per_page: args.per_page,
|
|
148
201
|
},
|
|
149
|
-
}),
|
|
202
|
+
}), 'tree'),
|
|
150
203
|
});
|
|
151
204
|
registerTool(server, config, {
|
|
152
205
|
name: 'clokio_search_docs',
|
|
153
|
-
description:
|
|
154
|
-
'shape plus `snippet` (~160 chars around the first match). Archived docs are left out. Needs section ' +
|
|
155
|
-
'docs.view. Scope docs:read. ' + ACTS_AS,
|
|
206
|
+
description: `Search titles and body text (not archived), newest first; each row has a snippet. ${ACCESS}`,
|
|
156
207
|
schema: {
|
|
157
|
-
q: z.string().min(2).max(120)
|
|
158
|
-
project_id: z.number().int().positive().optional()
|
|
159
|
-
limit: z.number().int().min(1).max(50).optional().describe('
|
|
208
|
+
q: z.string().min(2).max(120),
|
|
209
|
+
project_id: z.number().int().positive().optional(),
|
|
210
|
+
limit: z.number().int().min(1).max(50).optional().describe('Default 20'),
|
|
160
211
|
},
|
|
161
|
-
handler: (args, cfg) => request(cfg, '/docs/search', {
|
|
162
|
-
query: { q: args.q, project_id: args.project_id, limit: args.limit },
|
|
163
|
-
}),
|
|
212
|
+
handler: async (args, cfg) => liftKey(await request(cfg, '/docs/search', { query: { q: args.q, project_id: args.project_id, limit: args.limit } }), 'docs'),
|
|
164
213
|
});
|
|
165
214
|
registerTool(server, config, {
|
|
166
215
|
name: 'clokio_get_doc',
|
|
167
|
-
description: 'Read
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}`, { query: { format: args.format ?? 'markdown' } }),
|
|
216
|
+
description: 'Read a document: data = the record plus its body; meta = your role and breadcrumbs. Keep ' +
|
|
217
|
+
`data.content_version for clokio_update_doc_content. ${ACCESS}`,
|
|
218
|
+
schema: { id: docId, format: readFormatArg, include_title: includeTitleArg },
|
|
219
|
+
handler: async (args, cfg) => shapeDocRead(await request(cfg, `/docs/${seg(args.id)}`, {
|
|
220
|
+
query: { format: args.format ?? 'markdown', include_title: args.include_title ? 1 : undefined },
|
|
221
|
+
}), 'doc'),
|
|
174
222
|
});
|
|
175
223
|
// ---------------------------------------------------------------- writing
|
|
176
224
|
registerTool(server, config, {
|
|
177
225
|
name: 'clokio_create_doc',
|
|
178
|
-
description: 'Create a document.
|
|
179
|
-
'
|
|
180
|
-
|
|
181
|
-
'Returns data.doc with content_version 0 - the base_version for the first content write. You become the ' +
|
|
182
|
-
'doc\'s admin. Needs section docs.create. Scope docs:write. ' + ACTS_AS,
|
|
226
|
+
description: 'Create a document. parent_id = sub-page (needs edit on the parent); project_id = in that project; ' +
|
|
227
|
+
'neither = standalone (org admins only). Starts at content_version 0. ' +
|
|
228
|
+
ACCESS,
|
|
183
229
|
mutates: true,
|
|
184
230
|
schema: {
|
|
185
231
|
title: z.string().min(1).max(255),
|
|
186
|
-
icon: z.string().max(10).optional().describe('
|
|
232
|
+
icon: z.string().max(10).optional().describe('One emoji'),
|
|
187
233
|
project_id: z.number().int().positive().optional(),
|
|
188
|
-
parent_id: z.number().int().positive().optional()
|
|
234
|
+
parent_id: z.number().int().positive().optional(),
|
|
189
235
|
content: contentArg.optional(),
|
|
190
236
|
content_format: contentFormatArg,
|
|
191
237
|
},
|
|
192
|
-
handler: (args, cfg) => {
|
|
238
|
+
handler: async (args, cfg) => {
|
|
193
239
|
const body = {
|
|
194
240
|
title: args.title,
|
|
195
241
|
icon: args.icon,
|
|
@@ -200,101 +246,84 @@ export function registerDocTools(server, config) {
|
|
|
200
246
|
const blank = typeof args.content === 'string' && args.content.trim() === '';
|
|
201
247
|
if (!blank)
|
|
202
248
|
Object.assign(body, contentBody(args.content, args.content_format));
|
|
203
|
-
return request(cfg, '/docs', { method: 'POST', body });
|
|
249
|
+
return liftKey(await request(cfg, '/docs', { method: 'POST', body }), 'doc');
|
|
204
250
|
},
|
|
205
251
|
});
|
|
206
252
|
registerTool(server, config, {
|
|
207
253
|
name: 'clokio_update_doc_content',
|
|
208
|
-
description: 'REPLACE
|
|
209
|
-
'
|
|
210
|
-
|
|
211
|
-
'reconcile, and write again with the new version (the 409 names current_version and edited_by). ' +
|
|
212
|
-
'Markdown by default; an empty string clears the doc. Returns {content_version, last_edited_at}. ' +
|
|
213
|
-
'Needs section docs.create and edit on the doc (a read-only share is 403). Scope docs:write. ' + ACTS_AS,
|
|
254
|
+
description: 'REPLACE the whole body (needs edit). base_version = the content_version you last read. On 409 do not ' +
|
|
255
|
+
'retry the same base_version: re-read, reconcile, write with the new version. Empty content clears. ' +
|
|
256
|
+
ACCESS,
|
|
214
257
|
mutates: true,
|
|
215
258
|
schema: {
|
|
216
259
|
id: docId,
|
|
217
260
|
content: contentArg,
|
|
218
261
|
content_format: contentFormatArg,
|
|
219
|
-
base_version: z.number().int().min(0)
|
|
262
|
+
base_version: z.number().int().min(0),
|
|
220
263
|
},
|
|
221
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/content`, {
|
|
264
|
+
handler: async (args, cfg) => envelope(await request(cfg, `/docs/${seg(args.id)}/content`, {
|
|
222
265
|
method: 'PUT',
|
|
223
266
|
body: { ...contentBody(args.content, args.content_format), base_version: args.base_version },
|
|
224
|
-
}),
|
|
267
|
+
})),
|
|
225
268
|
});
|
|
226
269
|
registerTool(server, config, {
|
|
227
270
|
name: 'clokio_update_doc_meta',
|
|
228
|
-
description: 'Change
|
|
229
|
-
'
|
|
230
|
-
|
|
231
|
-
'parent or project use clokio_move_doc. Returns data.doc and `changed` (fields that actually changed). ' +
|
|
232
|
-
'Needs section docs.create. Scope docs:write. ' + ACTS_AS,
|
|
271
|
+
description: 'Change metadata; send only what changes. title/icon/full_width/sort_order need edit; visibility, ' +
|
|
272
|
+
'inherit_permissions, is_archived need admin. Use clokio_move_doc for parent/project. ' +
|
|
273
|
+
ACCESS,
|
|
233
274
|
mutates: true,
|
|
234
275
|
schema: {
|
|
235
276
|
id: docId,
|
|
236
277
|
title: z.string().min(1).max(255).optional(),
|
|
237
|
-
icon: z.string().max(10).nullable().optional().describe('
|
|
278
|
+
icon: z.string().max(10).nullable().optional().describe('null clears'),
|
|
238
279
|
full_width: z.boolean().optional(),
|
|
239
280
|
sort_order: z.number().int().min(0).optional(),
|
|
240
281
|
visibility: z.enum(['private', 'project', 'organization']).optional(),
|
|
241
282
|
inherit_permissions: z.boolean().optional(),
|
|
242
283
|
is_archived: z.boolean().optional(),
|
|
243
284
|
},
|
|
244
|
-
handler: (args, cfg) => {
|
|
285
|
+
handler: async (args, cfg) => {
|
|
245
286
|
const { id, ...fields } = args;
|
|
246
287
|
const body = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
247
288
|
if (Object.keys(body).length === 0) {
|
|
248
289
|
throw new ClokioApiError(0, 'Nothing to change: pass at least one field.');
|
|
249
290
|
}
|
|
250
|
-
return request(cfg, `/docs/${seg(id)}`, { method: 'PATCH', body });
|
|
291
|
+
return liftKey(await request(cfg, `/docs/${seg(id)}`, { method: 'PATCH', body }), 'doc');
|
|
251
292
|
},
|
|
252
293
|
});
|
|
253
294
|
registerTool(server, config, {
|
|
254
295
|
name: 'clokio_move_doc',
|
|
255
|
-
description: 'Move a document
|
|
256
|
-
|
|
257
|
-
'standalone". With a parent_id the parent\'s project wins. Moving into its own sub-tree is 422; into a ' +
|
|
258
|
-
'project needs the right to create tasks there; into standalone needs docs.manage. Needs section ' +
|
|
259
|
-
'docs.create and admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
296
|
+
description: 'Move a document and its sub-pages (needs admin). Send both parent_id and project_id; null = top ' +
|
|
297
|
+
`level / standalone. With a parent, the parent's project wins. ${ACCESS}`,
|
|
260
298
|
mutates: true,
|
|
261
299
|
schema: {
|
|
262
300
|
id: docId,
|
|
263
|
-
parent_id: z.number().int().positive().nullable()
|
|
264
|
-
project_id: z
|
|
265
|
-
|
|
266
|
-
.int()
|
|
267
|
-
.positive()
|
|
268
|
-
.nullable()
|
|
269
|
-
.describe('Target project id, or null for standalone (ignored when parent_id is set)'),
|
|
270
|
-
sort_order: z.number().int().min(0).optional().describe('Position among the new siblings'),
|
|
301
|
+
parent_id: z.number().int().positive().nullable(),
|
|
302
|
+
project_id: z.number().int().positive().nullable(),
|
|
303
|
+
sort_order: z.number().int().min(0).optional(),
|
|
271
304
|
},
|
|
272
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/move`, {
|
|
305
|
+
handler: async (args, cfg) => liftKey(await request(cfg, `/docs/${seg(args.id)}/move`, {
|
|
273
306
|
method: 'POST',
|
|
274
307
|
body: { parent_id: args.parent_id, project_id: args.project_id, sort_order: args.sort_order },
|
|
275
|
-
}),
|
|
308
|
+
}), 'doc'),
|
|
276
309
|
});
|
|
277
310
|
registerTool(server, config, {
|
|
278
311
|
name: 'clokio_archive_doc',
|
|
279
|
-
description:
|
|
280
|
-
'clokio_restore_doc. Needs section docs.create and admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
312
|
+
description: `Archive a document (needs admin; reversible with clokio_restore_doc). ${ACCESS}`,
|
|
281
313
|
mutates: true,
|
|
282
314
|
schema: { id: docId },
|
|
283
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/archive`, { method: 'POST' }),
|
|
315
|
+
handler: async (args, cfg) => liftKey(await request(cfg, `/docs/${seg(args.id)}/archive`, { method: 'POST' }), 'doc'),
|
|
284
316
|
});
|
|
285
317
|
registerTool(server, config, {
|
|
286
318
|
name: 'clokio_restore_doc',
|
|
287
|
-
description:
|
|
288
|
-
'and admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
319
|
+
description: `Un-archive a document (needs admin). ${ACCESS}`,
|
|
289
320
|
mutates: true,
|
|
290
321
|
schema: { id: docId },
|
|
291
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/restore`, { method: 'POST' }),
|
|
322
|
+
handler: async (args, cfg) => liftKey(await request(cfg, `/docs/${seg(args.id)}/restore`, { method: 'POST' }), 'doc'),
|
|
292
323
|
});
|
|
293
324
|
registerTool(server, config, {
|
|
294
325
|
name: 'clokio_delete_doc',
|
|
295
|
-
description:
|
|
296
|
-
'restored through the API - prefer clokio_archive_doc unless deletion is explicitly wanted. Needs ' +
|
|
297
|
-
'section docs.manage and admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
326
|
+
description: `DELETE a document (needs admin and docs.manage; not restorable via the API - prefer archive). ${ACCESS}`,
|
|
298
327
|
mutates: 'destructive',
|
|
299
328
|
schema: { id: docId },
|
|
300
329
|
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}`, { method: 'DELETE' }),
|
|
@@ -302,36 +331,49 @@ export function registerDocTools(server, config) {
|
|
|
302
331
|
// -------------------------------------------------------------- templates
|
|
303
332
|
registerTool(server, config, {
|
|
304
333
|
name: 'clokio_list_doc_templates',
|
|
305
|
-
description:
|
|
306
|
-
'create a doc outside any project). Needs section docs.view. Scope docs:read. ' + ACTS_AS,
|
|
334
|
+
description: `List document templates (key, name, description). ${ACCESS}`,
|
|
307
335
|
schema: {},
|
|
308
|
-
handler: (_args, cfg) => request(cfg, '/docs/templates'),
|
|
336
|
+
handler: async (_args, cfg) => liftKey(await request(cfg, '/docs/templates'), 'templates'),
|
|
309
337
|
});
|
|
310
338
|
registerTool(server, config, {
|
|
311
339
|
name: 'clokio_create_doc_from_template',
|
|
312
|
-
description:
|
|
313
|
-
'project_id it lands in that project (needs the right to create tasks there); without, it is standalone ' +
|
|
314
|
-
'(needs docs.manage). Returns data.doc. Needs section docs.create. Scope docs:write. ' + ACTS_AS,
|
|
340
|
+
description: `Create a top-level document from a template key. No project_id = standalone (admins only). ${ACCESS}`,
|
|
315
341
|
mutates: true,
|
|
316
342
|
schema: {
|
|
317
|
-
template: z.string().min(1).max(60)
|
|
318
|
-
title: z.string().max(255).optional()
|
|
343
|
+
template: z.string().min(1).max(60),
|
|
344
|
+
title: z.string().max(255).optional(),
|
|
319
345
|
project_id: z.number().int().positive().optional(),
|
|
320
346
|
},
|
|
321
|
-
handler: (args, cfg) => request(cfg, '/docs/from-template', {
|
|
347
|
+
handler: async (args, cfg) => liftKey(await request(cfg, '/docs/from-template', {
|
|
322
348
|
method: 'POST',
|
|
323
349
|
body: { template: args.template, title: args.title, project_id: args.project_id },
|
|
324
|
-
}),
|
|
350
|
+
}), 'doc'),
|
|
325
351
|
});
|
|
326
352
|
// ---------------------------------------------------------------- sharing
|
|
327
353
|
registerTool(server, config, {
|
|
328
354
|
name: 'clokio_list_doc_members',
|
|
329
|
-
description: 'Who a document is shared with
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
description: 'Who a document is shared with (needs admin): data = members, teams, inherited (change those on the ' +
|
|
356
|
+
`parent); meta = visibility, inherit_permissions, share_link. ${ACCESS}`,
|
|
357
|
+
schema: {
|
|
358
|
+
id: docId,
|
|
359
|
+
include_avatars: z.boolean().optional().describe('Include profile pictures (default false)'),
|
|
360
|
+
},
|
|
361
|
+
handler: async (args, cfg) => {
|
|
362
|
+
const raw = await request(cfg, `/docs/${seg(args.id)}/members`, {
|
|
363
|
+
query: { include_avatars: args.include_avatars ? 1 : undefined },
|
|
364
|
+
});
|
|
365
|
+
if (!isObj(raw))
|
|
366
|
+
return envelope(raw);
|
|
367
|
+
const { members, teams, inherited, ...meta } = raw;
|
|
368
|
+
const keep = args.include_avatars === true;
|
|
369
|
+
return envelope({ members: stripAvatars(members, keep), teams, inherited: stripAvatars(inherited, keep) }, meta);
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
registerTool(server, config, {
|
|
373
|
+
name: 'clokio_list_doc_shareable_teams',
|
|
374
|
+
description: `Teams this document can be shared with (needs admin); use id as team_id. ${ACCESS}`,
|
|
333
375
|
schema: { id: docId },
|
|
334
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/
|
|
376
|
+
handler: async (args, cfg) => liftKey(await request(cfg, `/docs/${seg(args.id)}/shareable-teams`), 'teams'),
|
|
335
377
|
});
|
|
336
378
|
const memberRole = z.enum(['view', 'comment', 'edit', 'admin']);
|
|
337
379
|
// A person is named by employee_code (preferred - the way every other tool
|
|
@@ -341,13 +383,8 @@ export function registerDocTools(server, config) {
|
|
|
341
383
|
.string()
|
|
342
384
|
.regex(/^[A-Za-z0-9_-]{1,50}$/, 'employee_code must be 1-50 chars of letters, digits, _ or -')
|
|
343
385
|
.optional()
|
|
344
|
-
.describe('
|
|
345
|
-
employee_id: z
|
|
346
|
-
.number()
|
|
347
|
-
.int()
|
|
348
|
-
.positive()
|
|
349
|
-
.optional()
|
|
350
|
-
.describe('Alternative to employee_code: Clokio\'s INTERNAL employee id (as clokio_list_doc_members reports it)'),
|
|
386
|
+
.describe('Zero-padded, e.g. "00080" (preferred)'),
|
|
387
|
+
employee_id: z.number().int().positive().optional().describe('Internal id, instead of employee_code'),
|
|
351
388
|
};
|
|
352
389
|
/** Exactly one of employee_code / employee_id, as the API requires. */
|
|
353
390
|
function person(args) {
|
|
@@ -360,45 +397,37 @@ export function registerDocTools(server, config) {
|
|
|
360
397
|
}
|
|
361
398
|
registerTool(server, config, {
|
|
362
399
|
name: 'clokio_add_doc_member',
|
|
363
|
-
description:
|
|
364
|
-
'employee_code (preferred) or employee_id. role: view, comment, edit or admin. The person must be an ' +
|
|
365
|
-
'active member of your organisation (422 otherwise) and is notified on a first grant. Needs section ' +
|
|
366
|
-
'docs.manage and admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
400
|
+
description: `Share a document with a person or change their role (needs admin; they are notified once). ${ACCESS}`,
|
|
367
401
|
mutates: true,
|
|
368
402
|
schema: { id: docId, ...personArgs, role: memberRole },
|
|
369
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/members`, {
|
|
403
|
+
handler: async (args, cfg) => envelope(await request(cfg, `/docs/${seg(args.id)}/members`, {
|
|
370
404
|
method: 'POST',
|
|
371
405
|
body: { ...person(args), role: args.role },
|
|
372
|
-
}),
|
|
406
|
+
})),
|
|
373
407
|
});
|
|
374
408
|
registerTool(server, config, {
|
|
375
409
|
name: 'clokio_update_doc_member',
|
|
376
|
-
description:
|
|
377
|
-
'404 if the person has no direct grant; demoting the last admin is 422. Needs section docs.manage and ' +
|
|
378
|
-
'admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
410
|
+
description: `Change a person's direct role (needs admin; demoting the last admin is 422). ${ACCESS}`,
|
|
379
411
|
mutates: true,
|
|
380
412
|
schema: { id: docId, ...personArgs, role: memberRole },
|
|
381
|
-
handler: (args, cfg) => {
|
|
413
|
+
handler: async (args, cfg) => {
|
|
382
414
|
const who = person(args);
|
|
383
415
|
// An id has its own path route; a code rides in the body of the
|
|
384
416
|
// collection route (PATCH /docs/{id}/members).
|
|
385
|
-
return 'employee_id' in who
|
|
386
|
-
? request(cfg, `/docs/${seg(args.id)}/members/${seg(who.employee_id)}`, {
|
|
417
|
+
return envelope('employee_id' in who
|
|
418
|
+
? await request(cfg, `/docs/${seg(args.id)}/members/${seg(who.employee_id)}`, {
|
|
387
419
|
method: 'PATCH',
|
|
388
420
|
body: { role: args.role },
|
|
389
421
|
})
|
|
390
|
-
: request(cfg, `/docs/${seg(args.id)}/members`, {
|
|
422
|
+
: await request(cfg, `/docs/${seg(args.id)}/members`, {
|
|
391
423
|
method: 'PATCH',
|
|
392
424
|
body: { ...who, role: args.role },
|
|
393
|
-
});
|
|
425
|
+
}));
|
|
394
426
|
},
|
|
395
427
|
});
|
|
396
428
|
registerTool(server, config, {
|
|
397
429
|
name: 'clokio_remove_doc_member',
|
|
398
|
-
description:
|
|
399
|
-
'left, to revoke their grant) or employee_id. Removing the ' +
|
|
400
|
-
'last admin is 422; inherited access must be removed on the parent. Needs section docs.manage and admin ' +
|
|
401
|
-
'on the doc. Scope docs:write. ' + ACTS_AS,
|
|
430
|
+
description: `Remove a person's direct grant (needs admin; a code may name someone who has left). ${ACCESS}`,
|
|
402
431
|
mutates: 'destructive',
|
|
403
432
|
schema: { id: docId, ...personArgs },
|
|
404
433
|
handler: (args, cfg) => {
|
|
@@ -414,71 +443,58 @@ export function registerDocTools(server, config) {
|
|
|
414
443
|
});
|
|
415
444
|
},
|
|
416
445
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
description: 'The organisation\'s teams this document can be shared with ({id, name, color, members_count}) - use the ' +
|
|
420
|
-
'id as team_id in ' +
|
|
421
|
-
'clokio_add_doc_team. Needs section docs.view and admin on the doc. Scope docs:read. ' + ACTS_AS,
|
|
422
|
-
schema: { id: docId },
|
|
423
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/shareable-teams`),
|
|
424
|
-
});
|
|
425
|
-
const teamRole = z.enum(['view', 'comment', 'edit']).describe('Teams cannot be admins');
|
|
426
|
-
const teamId = z
|
|
427
|
-
.number()
|
|
428
|
-
.int()
|
|
429
|
-
.positive()
|
|
430
|
-
.describe('The team id (from clokio_list_doc_shareable_teams, or teams[].team_id in clokio_list_doc_members)');
|
|
446
|
+
const teamRole = z.enum(['view', 'comment', 'edit']);
|
|
447
|
+
const teamId = z.number().int().positive().describe('From clokio_list_doc_shareable_teams');
|
|
431
448
|
registerTool(server, config, {
|
|
432
449
|
name: 'clokio_add_doc_team',
|
|
433
|
-
description:
|
|
434
|
-
'Find team ids with clokio_list_doc_shareable_teams; current team grants are in clokio_list_doc_members. Needs section docs.manage and admin on the ' +
|
|
435
|
-
'doc. Scope docs:write. ' + ACTS_AS,
|
|
450
|
+
description: `Share a document with a team (needs admin; teams cannot be admins). ${ACCESS}`,
|
|
436
451
|
mutates: true,
|
|
437
452
|
schema: { id: docId, team_id: teamId, role: teamRole },
|
|
438
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/teams`, {
|
|
453
|
+
handler: async (args, cfg) => envelope(await request(cfg, `/docs/${seg(args.id)}/teams`, {
|
|
439
454
|
method: 'POST',
|
|
440
455
|
body: { team_id: args.team_id, role: args.role },
|
|
441
|
-
}),
|
|
456
|
+
})),
|
|
442
457
|
});
|
|
443
458
|
registerTool(server, config, {
|
|
444
459
|
name: 'clokio_update_doc_team',
|
|
445
|
-
description:
|
|
446
|
-
'doc. Scope docs:write. ' + ACTS_AS,
|
|
460
|
+
description: `Change a team's role on a document (needs admin). ${ACCESS}`,
|
|
447
461
|
mutates: true,
|
|
448
462
|
schema: { id: docId, team_id: teamId, role: teamRole },
|
|
449
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/teams/${seg(args.team_id)}`, {
|
|
463
|
+
handler: async (args, cfg) => envelope(await request(cfg, `/docs/${seg(args.id)}/teams/${seg(args.team_id)}`, {
|
|
450
464
|
method: 'PATCH',
|
|
451
465
|
body: { role: args.role },
|
|
452
|
-
}),
|
|
466
|
+
})),
|
|
453
467
|
});
|
|
454
468
|
registerTool(server, config, {
|
|
455
469
|
name: 'clokio_remove_doc_team',
|
|
456
|
-
description:
|
|
470
|
+
description: `Remove a team's grant (needs admin). ${ACCESS}`,
|
|
457
471
|
mutates: 'destructive',
|
|
458
472
|
schema: { id: docId, team_id: teamId },
|
|
459
473
|
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/teams/${seg(args.team_id)}`, { method: 'DELETE' }),
|
|
460
474
|
});
|
|
461
475
|
registerTool(server, config, {
|
|
462
476
|
name: 'clokio_create_doc_share_link',
|
|
463
|
-
description: 'Create
|
|
464
|
-
|
|
465
|
-
'admin on the doc. Scope docs:write. ' + ACTS_AS,
|
|
477
|
+
description: 'Create or REPLACE the public read link (needs admin; the old URL stops working). Expires in 30 days ' +
|
|
478
|
+
`unless expires_at or never_expires is given. ${ACCESS}`,
|
|
466
479
|
mutates: true,
|
|
467
480
|
schema: {
|
|
468
481
|
id: docId,
|
|
469
|
-
expires_at: z
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
482
|
+
expires_at: z.string().optional().describe('Future date or ISO 8601 timestamp'),
|
|
483
|
+
never_expires: z.boolean().optional().describe('No expiry (overrides the 30-day default)'),
|
|
484
|
+
},
|
|
485
|
+
handler: async (args, cfg) => {
|
|
486
|
+
if (args.never_expires && args.expires_at) {
|
|
487
|
+
throw new ClokioApiError(0, 'Pass expires_at or never_expires, not both.');
|
|
488
|
+
}
|
|
489
|
+
return envelope(await request(cfg, `/docs/${seg(args.id)}/share-link`, {
|
|
490
|
+
method: 'POST',
|
|
491
|
+
body: { expires_at: args.expires_at, never_expires: args.never_expires },
|
|
492
|
+
}));
|
|
473
493
|
},
|
|
474
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/share-link`, {
|
|
475
|
-
method: 'POST',
|
|
476
|
-
body: { expires_at: args.expires_at },
|
|
477
|
-
}),
|
|
478
494
|
});
|
|
479
495
|
registerTool(server, config, {
|
|
480
496
|
name: 'clokio_revoke_doc_share_link',
|
|
481
|
-
description:
|
|
497
|
+
description: `Revoke the public link (needs admin). ${ACCESS}`,
|
|
482
498
|
mutates: 'destructive',
|
|
483
499
|
schema: { id: docId },
|
|
484
500
|
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/share-link`, { method: 'DELETE' }),
|
|
@@ -486,124 +502,101 @@ export function registerDocTools(server, config) {
|
|
|
486
502
|
// --------------------------------------------------------------- comments
|
|
487
503
|
registerTool(server, config, {
|
|
488
504
|
name: 'clokio_list_doc_comments',
|
|
489
|
-
description:
|
|
490
|
-
'{unresolved, resolved, total}. Needs section docs.view and view on the doc. Scope docs:read. ' + ACTS_AS,
|
|
505
|
+
description: `Comment threads, newest first, replies nested; meta has counts. ${ACCESS}`,
|
|
491
506
|
schema: {
|
|
492
507
|
id: docId,
|
|
493
|
-
filter: z
|
|
494
|
-
|
|
495
|
-
.optional()
|
|
496
|
-
.describe('Which threads: unresolved (default), resolved or all'),
|
|
497
|
-
block_id: z.string().max(40).optional().describe('Only threads anchored to this block'),
|
|
508
|
+
filter: z.enum(['unresolved', 'resolved', 'all']).optional().describe('Default unresolved'),
|
|
509
|
+
block_id: z.string().max(40).optional(),
|
|
498
510
|
},
|
|
499
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/comments`, {
|
|
511
|
+
handler: async (args, cfg) => liftKey(stripAvatars(await request(cfg, `/docs/${seg(args.id)}/comments`, {
|
|
500
512
|
query: { filter: args.filter, block_id: args.block_id },
|
|
501
|
-
}),
|
|
513
|
+
})), 'comments'),
|
|
502
514
|
});
|
|
503
515
|
registerTool(server, config, {
|
|
504
516
|
name: 'clokio_add_doc_comment',
|
|
505
|
-
description:
|
|
506
|
-
'422). The body accepts Markdown (converted to HTML) or HTML. @Full Name mentions of active colleagues ' +
|
|
507
|
-
'notify them. Needs section docs.create and comment on the doc. Scope docs:write. ' + ACTS_AS,
|
|
517
|
+
description: `Comment, or reply to a thread with parent_id (needs comment). Markdown or HTML; @Full Name notifies. ${ACCESS}`,
|
|
508
518
|
mutates: true,
|
|
509
519
|
schema: {
|
|
510
520
|
id: docId,
|
|
511
|
-
body: z.string().min(1).max(20000)
|
|
512
|
-
parent_id: z.number().int().positive().optional()
|
|
513
|
-
block_id: z.string().max(40).optional()
|
|
521
|
+
body: z.string().min(1).max(20000),
|
|
522
|
+
parent_id: z.number().int().positive().optional(),
|
|
523
|
+
block_id: z.string().max(40).optional(),
|
|
514
524
|
},
|
|
515
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/comments`, {
|
|
525
|
+
handler: async (args, cfg) => liftKey(stripAvatars(await request(cfg, `/docs/${seg(args.id)}/comments`, {
|
|
516
526
|
method: 'POST',
|
|
517
527
|
body: { body: markdownToHtml(args.body), parent_id: args.parent_id, block_id: args.block_id },
|
|
518
|
-
}),
|
|
528
|
+
})), 'comment'),
|
|
519
529
|
});
|
|
520
530
|
registerTool(server, config, {
|
|
521
531
|
name: 'clokio_resolve_doc_comment',
|
|
522
|
-
description:
|
|
523
|
-
'comment on the doc. Scope docs:write. ' + ACTS_AS,
|
|
532
|
+
description: `Resolve a comment thread (needs comment). ${ACCESS}`,
|
|
524
533
|
mutates: true,
|
|
525
534
|
schema: { id: docId, comment_id: z.number().int().positive() },
|
|
526
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/comments/${seg(args.comment_id)}/resolve`, { method: 'POST' }),
|
|
535
|
+
handler: async (args, cfg) => liftKey(stripAvatars(await request(cfg, `/docs/${seg(args.id)}/comments/${seg(args.comment_id)}/resolve`, { method: 'POST' })), 'comment'),
|
|
527
536
|
});
|
|
528
537
|
registerTool(server, config, {
|
|
529
538
|
name: 'clokio_reopen_doc_comment',
|
|
530
|
-
description:
|
|
539
|
+
description: `Reopen a resolved comment thread (needs comment). ${ACCESS}`,
|
|
531
540
|
mutates: true,
|
|
532
541
|
schema: { id: docId, comment_id: z.number().int().positive() },
|
|
533
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/comments/${seg(args.comment_id)}/reopen`, { method: 'POST' }),
|
|
542
|
+
handler: async (args, cfg) => liftKey(stripAvatars(await request(cfg, `/docs/${seg(args.id)}/comments/${seg(args.comment_id)}/reopen`, { method: 'POST' })), 'comment'),
|
|
534
543
|
});
|
|
535
544
|
// --------------------------------------------------------------- versions
|
|
536
545
|
registerTool(server, config, {
|
|
537
546
|
name: 'clokio_list_doc_versions',
|
|
538
|
-
description:
|
|
539
|
-
'is manual_save, auto_save, restore, pre_delete or conflict_backup. Pass the `id` (not the `version` ' +
|
|
540
|
-
'number) to the other version tools. Needs section docs.view and view on the doc. Scope docs:read. ' + ACTS_AS,
|
|
547
|
+
description: `Version history, newest first. Pass a row's id (not its version number) to the other version tools. ${ACCESS}`,
|
|
541
548
|
schema: { id: docId },
|
|
542
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/versions`),
|
|
549
|
+
handler: async (args, cfg) => liftKey(await request(cfg, `/docs/${seg(args.id)}/versions`), 'versions'),
|
|
543
550
|
});
|
|
544
551
|
registerTool(server, config, {
|
|
545
552
|
name: 'clokio_get_doc_version',
|
|
546
|
-
description:
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
553
|
+
description: `Read one version snapshot. ${ACCESS}`,
|
|
554
|
+
schema: {
|
|
555
|
+
id: docId,
|
|
556
|
+
version_id: z.number().int().positive(),
|
|
557
|
+
format: readFormatArg,
|
|
558
|
+
include_title: includeTitleArg,
|
|
559
|
+
},
|
|
560
|
+
handler: async (args, cfg) => shapeDocRead(await request(cfg, `/docs/${seg(args.id)}/versions/${seg(args.version_id)}`, {
|
|
561
|
+
query: { format: args.format ?? 'markdown', include_title: args.include_title ? 1 : undefined },
|
|
562
|
+
}), 'version'),
|
|
552
563
|
});
|
|
553
564
|
registerTool(server, config, {
|
|
554
565
|
name: 'clokio_restore_doc_version',
|
|
555
|
-
description:
|
|
556
|
-
'{content_version, version}: content_version MOVES, so use the returned value as base_version for the ' +
|
|
557
|
-
'next content write. Needs section docs.create and edit on the doc. Scope docs:write. ' + ACTS_AS,
|
|
566
|
+
description: `Restore a snapshot over the content (needs edit). content_version moves: use the returned one. ${ACCESS}`,
|
|
558
567
|
mutates: true,
|
|
559
568
|
schema: { id: docId, version_id: z.number().int().positive() },
|
|
560
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/versions/${seg(args.version_id)}/restore`, { method: 'POST' }),
|
|
569
|
+
handler: async (args, cfg) => envelope(await request(cfg, `/docs/${seg(args.id)}/versions/${seg(args.version_id)}/restore`, { method: 'POST' })),
|
|
561
570
|
});
|
|
562
571
|
// ------------------------------------------------------------ attachments
|
|
563
572
|
registerTool(server, config, {
|
|
564
573
|
name: 'clokio_list_doc_attachments',
|
|
565
|
-
description:
|
|
566
|
-
'file removed from the text is still listed. Download with clokio_download_doc_attachment using `path` ' +
|
|
567
|
-
'(the `url` values need a browser session). Needs section docs.view and view on the doc. Scope docs:read. ' + ACTS_AS,
|
|
574
|
+
description: `Files uploaded to a document: name, size, mime, uploaded_at, path (for download). ${ACCESS}`,
|
|
568
575
|
schema: { id: docId },
|
|
569
|
-
handler: (args, cfg) => request(cfg, `/docs/${seg(args.id)}/attachments`),
|
|
576
|
+
handler: async (args, cfg) => shapeAttachments(await request(cfg, `/docs/${seg(args.id)}/attachments`)),
|
|
570
577
|
});
|
|
571
578
|
registerTool(server, config, {
|
|
572
579
|
name: 'clokio_upload_doc_attachment',
|
|
573
|
-
description: 'Upload a LOCAL
|
|
574
|
-
|
|
575
|
-
'(HEIC becomes JPEG)/mp4/webm/mov, up to 50 MB. Returns {kind, url, path, name, size, mime}. Uploading ' +
|
|
576
|
-
'does NOT change the text: to show it, write content (content_format json) with an `image` node ' +
|
|
577
|
-
'(src = url) for media or a `fileAttachment` node (url, name, size, mime) for a file. Needs section ' +
|
|
578
|
-
'docs.create and edit on the doc. Scope docs:write. ' + ACTS_AS,
|
|
580
|
+
description: 'Upload a LOCAL file (needs edit). kind file (default, 100 MB) or media (images/video, 50 MB). Does not ' +
|
|
581
|
+
`change the text: reference the returned url in the body yourself. ${ACCESS}`,
|
|
579
582
|
mutates: true,
|
|
580
583
|
schema: {
|
|
581
584
|
id: docId,
|
|
582
|
-
file_path: z.string().describe('Path
|
|
583
|
-
kind: z.enum(['file', 'media']).optional()
|
|
585
|
+
file_path: z.string().describe('Path on THIS machine'),
|
|
586
|
+
kind: z.enum(['file', 'media']).optional(),
|
|
584
587
|
},
|
|
585
|
-
handler: (args, cfg) => requestUpload(cfg, `/docs/${seg(args.id)}/attachments`, args.file_path, { kind: args.kind ?? 'file' }),
|
|
588
|
+
handler: async (args, cfg) => envelope(await requestUpload(cfg, `/docs/${seg(args.id)}/attachments`, args.file_path, { kind: args.kind ?? 'file' })),
|
|
586
589
|
});
|
|
587
590
|
registerTool(server, config, {
|
|
588
591
|
name: 'clokio_download_doc_attachment',
|
|
589
|
-
description: 'Download
|
|
590
|
-
|
|
591
|
-
'returns {saved_to, sha256, mime_type, file_size}. Without it, returns the content in the most usable ' +
|
|
592
|
-
'form: text as text, a PDF as extracted text plus page count, a ZIP as its entry list, an image as an ' +
|
|
593
|
-
'image you can see, anything else as a type-and-size description. Large text/PDF is truncated (use ' +
|
|
594
|
-
'save_path for the whole file). Needs section docs.view and view on the doc. Scope docs:read. ' + ACTS_AS,
|
|
592
|
+
description: 'Download an attachment by its path. save_path writes it to disk; otherwise text, PDF text, ZIP entries ' +
|
|
593
|
+
`or an image come back inline. ${ACCESS}`,
|
|
595
594
|
schema: {
|
|
596
595
|
id: docId,
|
|
597
|
-
path: z.string().min(1).max(1024).describe('
|
|
598
|
-
file_name: z
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
.describe('The attachment `name` from the listing. Drives type detection (text/PDF/zip/image)'),
|
|
602
|
-
save_path: z
|
|
603
|
-
.string()
|
|
604
|
-
.optional()
|
|
605
|
-
.describe('Write the file here instead of returning its content; parent folders are created'),
|
|
606
|
-
overwrite: z.boolean().optional().describe('Allow save_path to replace an existing file (default false)'),
|
|
596
|
+
path: z.string().min(1).max(1024).describe('`path` from clokio_list_doc_attachments'),
|
|
597
|
+
file_name: z.string().optional().describe('`name` from the listing (type detection)'),
|
|
598
|
+
save_path: z.string().optional(),
|
|
599
|
+
overwrite: z.boolean().optional().describe('Replace an existing save_path file'),
|
|
607
600
|
},
|
|
608
601
|
handler: (args, cfg) => downloadDocAttachment(cfg, args.id, args.path, {
|
|
609
602
|
fileName: args.file_name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clokio-mcp",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.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": {
|