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