rootr-cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -4
- package/bin/rootr.js +109 -3
- package/lib/client.js +531 -1
- package/lib/config.js +30 -1
- package/lib/context.js +38 -0
- package/lib/mcp-tools/ask.js +1 -0
- package/lib/mcp-tools/attachments.js +110 -0
- package/lib/mcp-tools/crm.js +711 -0
- package/lib/mcp-tools/databases.js +5 -0
- package/lib/mcp-tools/documents.js +153 -1
- package/lib/mcp-tools/forms.js +206 -0
- package/lib/mcp-tools/issues.js +6 -0
- package/lib/mcp-tools/logs.js +5 -0
- package/lib/mcp-tools/misc.js +2 -0
- package/lib/mcp-tools/presentations.js +356 -0
- package/lib/mcp-tools/recordings.js +426 -0
- package/lib/mcp-tools/shared.js +18 -3
- package/lib/mcp-tools/spreadsheets.js +224 -0
- package/lib/mcp-tools/webhooks.js +3 -0
- package/lib/mcp-tools/whiteboards.js +166 -0
- package/lib/mcp-tools/workspaces.js +172 -9
- package/lib/mcp.js +98 -5
- package/package.json +22 -3
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authoring guide, repeated (shortened) across every tool description so an
|
|
7
|
+
* agent that only sees ONE of these tools still gets the rules: assertion
|
|
8
|
+
* titles, text-spine-carries-meaning, 16:9 canvas, kind for slide role.
|
|
9
|
+
*/
|
|
10
|
+
const AUTHORING_GUIDE =
|
|
11
|
+
'Authoring guide: give each slide ONE assertion-style title (a claim, e.g. "Latency dropped 40% after the cache fix" — ' +
|
|
12
|
+
'not a topic label like "Latency"), plus blocks[] (bullet-like {id, heading?, body?, icon?} items) and, where useful, ' +
|
|
13
|
+
'a diagram ({type:"mermaid", code}) or images. Canvas is 16:9 (1280x720). Meaning must live in TEXT — ' +
|
|
14
|
+
'title/blocks[].heading/blocks[].body/notes are what auto-connects into the knowledge graph; diagrams/code/html and the ' +
|
|
15
|
+
'image pixels themselves are visual-only and are NOT indexed, so never put facts ONLY in a diagram or picture.\n' +
|
|
16
|
+
'Images: `image` is the single primary/background image (cover, section, full-bleed); `images[]` holds additional ' +
|
|
17
|
+
'inserted images placed on the slide. Each image is {id?, src?, alt?, placement?, prompt?, x?, y?, w?, h?}. ' +
|
|
18
|
+
'Set `src` to an uploaded file URL (upload via POST /v1/attachments/upload, then use /api/v1/attachments/{id}/raw), ' +
|
|
19
|
+
'an https URL, or a data: URI — you cannot upload the file bytes through these MCP tools, only reference the resulting URL. ' +
|
|
20
|
+
'To get a REAL image, call rootr_generate_image with an English prompt (say "no text"), then put the returned `url` ' +
|
|
21
|
+
'into the slide image\'s `src`; rootr_remove_image_background cuts out an image\'s background. ' +
|
|
22
|
+
'`placement` is a layout hint ("full"|"right"|"top"|...); x/y/w/h give freeform PPT-style placement in 1280x720 canvas ' +
|
|
23
|
+
'coords. ALWAYS add an `alt` caption to every image — alt text is part of the graph text spine, so a captioned image ' +
|
|
24
|
+
'connects into the knowledge graph while an uncaptioned one does not.\n' +
|
|
25
|
+
'Use kind to mark each slide\'s role: "cover" (deck title slide), "section" (chapter divider), "content" (body slide, ' +
|
|
26
|
+
'the default), "closing" (last slide / call to action).\n' +
|
|
27
|
+
'Page numbers: "content" and "section" slides automatically show a page number that follows the slide order (it ' +
|
|
28
|
+
're-numbers itself when slides are reordered), so you do NOT set it yourself. "cover"/"closing" have none by design.';
|
|
29
|
+
|
|
30
|
+
/** Fill in slide/block ids that were omitted, so agents don't have to invent unique ids by hand. */
|
|
31
|
+
function ensureSlideIds(slides) {
|
|
32
|
+
if (!Array.isArray(slides)) return slides;
|
|
33
|
+
return slides.map((s, i) => {
|
|
34
|
+
const slide = { ...s, id: s.id || `SLD-${String(i + 1).padStart(3, '0')}` };
|
|
35
|
+
if (Array.isArray(slide.blocks)) {
|
|
36
|
+
slide.blocks = slide.blocks.map((b, bi) => ({ ...b, id: b.id || `${slide.id}-B${bi + 1}` }));
|
|
37
|
+
}
|
|
38
|
+
return slide;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const imageShape = z
|
|
43
|
+
.object({
|
|
44
|
+
id: z.string().optional().describe('Stable image id (for multi-image slides). Auto-generated if omitted.'),
|
|
45
|
+
src: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
'Image URL: an uploaded file (/api/v1/attachments/{id}/raw — upload first via POST /v1/attachments/upload), ' +
|
|
50
|
+
'an https URL, or a data: URI. Omit and set `prompt` instead to have it AI-generated later.',
|
|
51
|
+
),
|
|
52
|
+
alt: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Caption / alt text. Part of the knowledge-graph text spine — ALWAYS set this so the image is indexed.'),
|
|
56
|
+
prompt: z.string().optional().describe('English AI-generation prompt, no text baked into the image (used when src is absent)'),
|
|
57
|
+
placement: z.string().optional().describe('Layout hint: "full" | "right" | "top" | ...'),
|
|
58
|
+
x: z.number().optional().describe('Freeform placement X in 1280x720 canvas coords'),
|
|
59
|
+
y: z.number().optional().describe('Freeform placement Y in 1280x720 canvas coords'),
|
|
60
|
+
w: z.number().optional().describe('Freeform placement width in canvas coords'),
|
|
61
|
+
h: z.number().optional().describe('Freeform placement height in canvas coords'),
|
|
62
|
+
})
|
|
63
|
+
.describe('A slide image. Set src (uploaded/https/data: URI) or prompt (AI-generate); always add an alt caption.');
|
|
64
|
+
|
|
65
|
+
const slideShape = z
|
|
66
|
+
.object({
|
|
67
|
+
id: z.string().optional().describe('Unique slide id, e.g. "SLD-001". Auto-generated if omitted.'),
|
|
68
|
+
kind: z.string().optional().describe('cover | section | content | closing'),
|
|
69
|
+
layout: z.string().optional().describe('Layout hint, e.g. "2-col" | "kpi-grid" | "timeline"'),
|
|
70
|
+
title: z.string().optional().describe('Assertion-style title (the claim this slide makes) — text spine'),
|
|
71
|
+
blocks: z
|
|
72
|
+
.array(
|
|
73
|
+
z.object({
|
|
74
|
+
id: z.string().optional().describe('Unique block id. Auto-generated if omitted.'),
|
|
75
|
+
heading: z.string().optional(),
|
|
76
|
+
body: z.string().optional(),
|
|
77
|
+
icon: z.string().optional().describe('Semantic hint, e.g. "shield" | "calendar" | "chip"'),
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Bullet-like items — text spine'),
|
|
82
|
+
diagram: z
|
|
83
|
+
.object({ type: z.literal('mermaid').optional(), code: z.string() })
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Mermaid diagram; visual only, not indexed — keep the facts in title/blocks too'),
|
|
86
|
+
image: imageShape
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Single primary/background image (cover, section, full-bleed). See imageShape fields.'),
|
|
89
|
+
images: z
|
|
90
|
+
.array(imageShape)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Additional inserted/uploaded images placed on the slide (up to 50). Each should carry an alt caption.'),
|
|
93
|
+
code: z.string().optional().describe('Code-first React slide source (max design freedom, advanced use)'),
|
|
94
|
+
html: z.string().optional().describe('Rendered self-contained slide HTML (usually machine-produced, not hand-authored)'),
|
|
95
|
+
notes: z.string().optional().describe('Speaker notes — text spine'),
|
|
96
|
+
})
|
|
97
|
+
.describe('A single slide. See tool description for the authoring guide.');
|
|
98
|
+
|
|
99
|
+
const themeShape = z
|
|
100
|
+
.object({
|
|
101
|
+
colors: z
|
|
102
|
+
.object({
|
|
103
|
+
primary: z.string().optional(),
|
|
104
|
+
secondary: z.string().optional(),
|
|
105
|
+
accent: z.string().optional(),
|
|
106
|
+
bg: z.string().optional(),
|
|
107
|
+
text: z.string().optional(),
|
|
108
|
+
muted: z.string().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
111
|
+
fonts: z.object({ heading: z.string().optional(), body: z.string().optional() }).optional(),
|
|
112
|
+
keyVisualStyle: z.string().optional(),
|
|
113
|
+
})
|
|
114
|
+
.optional()
|
|
115
|
+
.describe('Deck theme; missing keys fall back to sane defaults');
|
|
116
|
+
|
|
117
|
+
/** PRESENTATION (slide deck) node tools: create/read/update deck, append + reorder + patch slides. */
|
|
118
|
+
export function registerPresentationTools(server) {
|
|
119
|
+
server.registerTool(
|
|
120
|
+
'rootr_create_presentation',
|
|
121
|
+
{
|
|
122
|
+
title: 'Create a Rootr presentation (slide deck)',
|
|
123
|
+
description:
|
|
124
|
+
'Create a new PRESENTATION node (a slide deck) in a Rootr (루터) workspace, optionally seeded with an ' +
|
|
125
|
+
'initial theme and slides. ' +
|
|
126
|
+
AUTHORING_GUIDE,
|
|
127
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
128
|
+
inputSchema: {
|
|
129
|
+
name: z.string().optional().describe('Deck name/title'),
|
|
130
|
+
parentId: z.string().optional().describe('Parent folder/node id to create the deck under'),
|
|
131
|
+
theme: themeShape,
|
|
132
|
+
slides: z.array(slideShape).optional().describe('Initial slides, in order'),
|
|
133
|
+
config: z.record(z.unknown()).optional().describe('Free-form deck config/settings object'),
|
|
134
|
+
icon: z.string().optional().describe('Emoji icon for the deck node'),
|
|
135
|
+
workspace: z
|
|
136
|
+
.string()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
async ({ name, parentId, theme, slides, config, icon, workspace }) => {
|
|
142
|
+
try {
|
|
143
|
+
const ws = requireWorkspace(workspace);
|
|
144
|
+
const client = makeClient(ws);
|
|
145
|
+
const result = await client.createPresentation(ws, {
|
|
146
|
+
name,
|
|
147
|
+
parentId,
|
|
148
|
+
theme,
|
|
149
|
+
slides: ensureSlideIds(slides),
|
|
150
|
+
config,
|
|
151
|
+
icon,
|
|
152
|
+
});
|
|
153
|
+
return jsonResult(result);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return errorResult(err);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
server.registerTool(
|
|
161
|
+
'rootr_read_presentation',
|
|
162
|
+
{
|
|
163
|
+
title: 'Read a Rootr presentation',
|
|
164
|
+
description:
|
|
165
|
+
'Read a Rootr (루터) PRESENTATION node — its theme and full slides array. Read this before ' +
|
|
166
|
+
'rootr_update_presentation / rootr_append_presentation_slides / rootr_reorder_presentation_slides so you ' +
|
|
167
|
+
'know the current slide ids and content.',
|
|
168
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
169
|
+
inputSchema: {
|
|
170
|
+
presentationId: z.string().describe('PRESENTATION node id'),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
async ({ presentationId }) => {
|
|
174
|
+
try {
|
|
175
|
+
const client = makeClient();
|
|
176
|
+
const result = await client.getPresentation(presentationId);
|
|
177
|
+
return jsonResult(result);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return errorResult(err);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
server.registerTool(
|
|
185
|
+
'rootr_update_presentation',
|
|
186
|
+
{
|
|
187
|
+
title: 'Update a Rootr presentation (theme/slides/config)',
|
|
188
|
+
description:
|
|
189
|
+
'Merge-patch a Rootr (루터) PRESENTATION: update theme and/or config, or REPLACE THE WHOLE slides array ' +
|
|
190
|
+
'(if you pass `slides`, it replaces every existing slide — call rootr_read_presentation first and resend ' +
|
|
191
|
+
'slides you want to keep, or use rootr_append_presentation_slides / rootr_update_presentation_slide instead ' +
|
|
192
|
+
'for incremental changes). ' +
|
|
193
|
+
AUTHORING_GUIDE,
|
|
194
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
195
|
+
inputSchema: {
|
|
196
|
+
presentationId: z.string().describe('PRESENTATION node id'),
|
|
197
|
+
theme: themeShape,
|
|
198
|
+
slides: z
|
|
199
|
+
.array(slideShape)
|
|
200
|
+
.optional()
|
|
201
|
+
.describe('COMPLETE slides array to set — omitted existing slides will be dropped'),
|
|
202
|
+
config: z.record(z.unknown()).optional().describe('Free-form deck config/settings object'),
|
|
203
|
+
icon: z.string().optional().describe('Emoji icon for the deck node'),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
async ({ presentationId, theme, slides, config, icon }) => {
|
|
207
|
+
try {
|
|
208
|
+
const client = makeClient();
|
|
209
|
+
const result = await client.updatePresentation(presentationId, {
|
|
210
|
+
theme,
|
|
211
|
+
slides: ensureSlideIds(slides),
|
|
212
|
+
config,
|
|
213
|
+
icon,
|
|
214
|
+
});
|
|
215
|
+
return jsonResult(result);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return errorResult(err);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
server.registerTool(
|
|
223
|
+
'rootr_append_presentation_slides',
|
|
224
|
+
{
|
|
225
|
+
title: 'Append slides to a Rootr presentation (preferred way to add slides)',
|
|
226
|
+
description:
|
|
227
|
+
'Append one or more slides to the END of a Rootr (루터) PRESENTATION, without touching existing slides. ' +
|
|
228
|
+
'PREFER this over rootr_update_presentation when you are only adding new slides. ' +
|
|
229
|
+
AUTHORING_GUIDE,
|
|
230
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
231
|
+
inputSchema: {
|
|
232
|
+
presentationId: z.string().describe('PRESENTATION node id'),
|
|
233
|
+
slides: z.array(slideShape).describe('Slides to append, in order'),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
async ({ presentationId, slides }) => {
|
|
237
|
+
try {
|
|
238
|
+
const client = makeClient();
|
|
239
|
+
const result = await client.appendPresentationSlides(presentationId, ensureSlideIds(slides));
|
|
240
|
+
return jsonResult(result);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return errorResult(err);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
server.registerTool(
|
|
248
|
+
'rootr_update_presentation_slide',
|
|
249
|
+
{
|
|
250
|
+
title: 'Update ONE slide of a Rootr presentation',
|
|
251
|
+
description:
|
|
252
|
+
'Merge-patch a single slide by slideId, without touching the rest of the deck. Good for a precise, ' +
|
|
253
|
+
'targeted edit (e.g. rewrite one slide\'s title/blocks); use rootr_append_presentation_slides instead when ' +
|
|
254
|
+
'you are only adding new slides. Call rootr_read_presentation first to get the exact slideId. ' +
|
|
255
|
+
AUTHORING_GUIDE,
|
|
256
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
257
|
+
inputSchema: {
|
|
258
|
+
presentationId: z.string().describe('PRESENTATION node id'),
|
|
259
|
+
slideId: z.string().describe('Slide id to update (must belong to the deck)'),
|
|
260
|
+
slide: slideShape.describe('Partial slide fields to merge into the existing slide'),
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
async ({ presentationId, slideId, slide }) => {
|
|
264
|
+
try {
|
|
265
|
+
const client = makeClient();
|
|
266
|
+
const result = await client.updatePresentationSlide(presentationId, slideId, slide);
|
|
267
|
+
return jsonResult(result);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return errorResult(err);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
server.registerTool(
|
|
275
|
+
'rootr_reorder_presentation_slides',
|
|
276
|
+
{
|
|
277
|
+
title: 'Reorder the slides of a Rootr presentation',
|
|
278
|
+
description:
|
|
279
|
+
'Reorder a Rootr (루터) PRESENTATION\'s slides. `order` must be the FULL list of the deck\'s slide ids, ' +
|
|
280
|
+
'in the new desired order — call rootr_read_presentation first to get the current ids.',
|
|
281
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
282
|
+
inputSchema: {
|
|
283
|
+
presentationId: z.string().describe('PRESENTATION node id'),
|
|
284
|
+
order: z.array(z.string()).describe('Complete ordered array of slide ids'),
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
async ({ presentationId, order }) => {
|
|
288
|
+
try {
|
|
289
|
+
const client = makeClient();
|
|
290
|
+
const result = await client.reorderPresentationSlides(presentationId, order);
|
|
291
|
+
return jsonResult(result);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
return errorResult(err);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
server.registerTool(
|
|
299
|
+
'rootr_generate_image',
|
|
300
|
+
{
|
|
301
|
+
title: 'Generate an image from a text prompt (for a slide)',
|
|
302
|
+
description:
|
|
303
|
+
'Generate an image from a text prompt with AI (Gemini) and store it as a workspace attachment. Use this ' +
|
|
304
|
+
'when building a Rootr (루터) presentation so slides get real, on-brand visuals instead of empty image ' +
|
|
305
|
+
'placeholders — generate the image, then put the returned `url` into a slide image\'s `src` via ' +
|
|
306
|
+
'rootr_update_presentation_slide (image or images[]). Returns { id, url, filename, mimeType, size }.\n' +
|
|
307
|
+
'Prompt tips: describe a clean, flat, presentation-ready illustration; specify colors to match the deck ' +
|
|
308
|
+
'theme; and say "no text" — baked-in text is not indexed and often renders wrong (put the meaning in the ' +
|
|
309
|
+
'slide title/blocks instead, and always give the image an `alt` caption). Consumes AI credits.',
|
|
310
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
311
|
+
inputSchema: {
|
|
312
|
+
prompt: z.string().describe('English image description; ask for "no text" in the image'),
|
|
313
|
+
nodeId: z.string().optional().describe('Presentation/node id to attach the image to (permission-checked)'),
|
|
314
|
+
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
async ({ prompt, nodeId, workspace }) => {
|
|
318
|
+
try {
|
|
319
|
+
const client = makeClient();
|
|
320
|
+
const ws = requireWorkspace(workspace);
|
|
321
|
+
const result = await client.generateImage(ws, { prompt, nodeId });
|
|
322
|
+
return jsonResult(result);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return errorResult(err);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
server.registerTool(
|
|
330
|
+
'rootr_remove_image_background',
|
|
331
|
+
{
|
|
332
|
+
title: 'Remove the background of an image',
|
|
333
|
+
description:
|
|
334
|
+
'Remove the background of an existing Rootr (루터) workspace attachment (e.g. a generated or uploaded image) ' +
|
|
335
|
+
'and store the result as a new transparent PNG. Pass the source attachment id; returns { id, url, ... } for ' +
|
|
336
|
+
'the cut-out image, which you can then place on a slide. Useful for product shots / logos / subjects that ' +
|
|
337
|
+
'should sit on the slide without a boxed background.',
|
|
338
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
339
|
+
inputSchema: {
|
|
340
|
+
attachmentId: z.string().describe('Source attachment id (from rootr_generate_image or an upload)'),
|
|
341
|
+
nodeId: z.string().optional().describe('Presentation/node id to attach the result to (permission-checked)'),
|
|
342
|
+
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
async ({ attachmentId, nodeId, workspace }) => {
|
|
346
|
+
try {
|
|
347
|
+
const client = makeClient();
|
|
348
|
+
const ws = requireWorkspace(workspace);
|
|
349
|
+
const result = await client.removeImageBackground(ws, { attachmentId, nodeId });
|
|
350
|
+
return jsonResult(result);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
return errorResult(err);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
}
|