rootr-cli 0.20.0 → 0.23.3
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.ko.md +51 -2
- package/README.md +53 -7
- package/bin/rootr.js +186 -11
- package/lib/client.js +73 -2
- package/lib/mcp-tools/attachments.js +58 -1
- package/lib/mcp-tools/booking.js +21 -1
- package/lib/mcp-tools/databases.js +289 -4
- package/lib/mcp-tools/forms.js +6 -1
- package/lib/mcp-tools/pages.js +35 -8
- package/lib/mcp-tools/presentations.js +19 -0
- package/lib/mcp-tools/workspaces.js +5 -0
- package/package.json +1 -1
package/lib/mcp-tools/pages.js
CHANGED
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
const BLOCK_GUIDE =
|
|
27
27
|
'A page is `blocks[]` (ordered, max 200) + `config`. Each block is {id?, type, props}. ' +
|
|
28
28
|
'You assemble a page from these block types:\n' +
|
|
29
|
-
'· hero {title, subtitle?, align?:"left"|"center", background?:{kind:"color",color}|{kind:"image",attachmentId}, button?:{label,target}} — the opening banner
|
|
29
|
+
'· hero {title, subtitle?, align?:"left"|"center", size?:"normal"|"screen", background?:{kind:"color",color}|{kind:"image",attachmentId}, button?:{label,target}} — the opening banner. ' +
|
|
30
|
+
'size:"screen" makes it a FULL-SCREEN landing hero: edge to edge and one viewport tall (minus the top name bar, capped at 1200px), big headline, content centred vertically ' +
|
|
31
|
+
'(default "normal" = a compact banner inside the text column)\n' +
|
|
30
32
|
'· text {body (markdown), title?} — prose section; markdown is sanitized when rendered\n' +
|
|
31
33
|
'· features {columns?:1|2|3, items:[{title, body, icon?(emoji)}]} — "what it does" grid\n' +
|
|
32
34
|
'· gallery {images:[{attachmentId, caption?}]} — images must be workspace attachments (no external image URLs)\n' +
|
|
@@ -40,12 +42,35 @@ const BLOCK_GUIDE =
|
|
|
40
42
|
'· liveTable {source:{nodeId}, columns?:[name], rowLimit?(1-200), orderBy?:{column, dir:"asc"|"desc"}}\n' +
|
|
41
43
|
'· liveChart {source:{nodeId, sheet?, range?}, chart, labelColumn, valueColumn?, agg?, …} — a LIVE chart (46 kinds, see CHARTS below)\n' +
|
|
42
44
|
'· form {formNodeId(FORM node), headline?, submitLabel?} — visitors submit right on the page\n' +
|
|
43
|
-
'·
|
|
45
|
+
'· booking {bookingNodeId(DATABASE node id — a table that is published with visitor booking turned on, see rootr_set_booking), headline?, type?} — ' +
|
|
46
|
+
'a visitor scheduling calendar right on the page; `type` shows only one of the table\'s booking types. ' +
|
|
47
|
+
'bookingNodeId must be the node ID (names/paths are not resolved here); a table that is not published or has booking off renders as "not taking bookings"\n' +
|
|
48
|
+
'· custom {html, height?(40-4000, default 260), minHeight?:"none"|"screen", bleed?:boolean} — free HTML/CSS, rendered inside a locked-down sandboxed iframe ' +
|
|
44
49
|
'(no scripts, no same-origin, no network). Use it for a look the other blocks cannot make. Page theme colors are ' +
|
|
45
50
|
'available inside as var(--pg-bg|text|muted|faint|card|line|accent|on-accent) and var(--pg-font). Scripts and ' +
|
|
46
|
-
'on*= handlers are stripped on save — do not put behaviour here, only markup and CSS
|
|
51
|
+
'on*= handlers are stripped on save — do not put behaviour here, only markup and CSS. ' +
|
|
52
|
+
'bleed:true makes the frame span the FULL page width with no gap or padding around it, so consecutive bleed blocks ' +
|
|
53
|
+
'stack into seamless edge-to-edge colour bands (the sandbox is unchanged). Inside a bleed block paint your own ' +
|
|
54
|
+
'full-width background and keep the content in a centred max-width wrapper; still set `height` (the frame does not auto-size). ' +
|
|
55
|
+
'minHeight:"screen" makes the frame ONE VIEWPORT TALL on every device (same rule as hero size:"screen": minus the top name bar, capped at 1200px); ' +
|
|
56
|
+
'a numeric `height` given with it becomes the FLOOR — frame height = max(one screen, height) — so give the height your content ' +
|
|
57
|
+
'needs on a short phone and nothing is clipped. Inside, take the frame height with `height:100%` (html and body already are 100%) ' +
|
|
58
|
+
'and centre the content with flexbox. bleed:true + minHeight:"screen" = a full-screen section; stack several for a ' +
|
|
59
|
+
'section-per-screen landing page.\n' +
|
|
47
60
|
'LAYOUT: any block takes `width:"full"|"twoThirds"|"half"|"third"` — neighbouring blocks sit side by side (phones stack them). ' +
|
|
48
|
-
'That is how a page becomes a DASHBOARD: a row of `chart:"kpi"` blocks at width "third", then charts at width "half"
|
|
61
|
+
'That is how a page becomes a DASHBOARD: a row of `chart:"kpi"` blocks at width "third", then charts at width "half". ' +
|
|
62
|
+
'A hero with size:"screen" and a custom block with bleed:true always take the whole row — `width` is ignored (not stored) on them.\n' +
|
|
63
|
+
'REVEAL (scroll-in animation): any block takes `reveal:"fade"|"fade-up"` (default "none" = always visible). On the published page ' +
|
|
64
|
+
'the block appears ONCE when it scrolls into view (its top passes ~85% of the viewport height; blocks already on the first screen ' +
|
|
65
|
+
'play right after load): "fade" fades in, "fade-up" also rises a little. A hero staggers title → subtitle → button while its ' +
|
|
66
|
+
'background stays put. On a custom block, reveal ALSO starts the CSS animations inside the frame at that moment — the sandboxed ' +
|
|
67
|
+
'frame is created only when the block scrolls in, so `@keyframes`/`animation` in your html run from 0s exactly when the visitor ' +
|
|
68
|
+
'sees the section (without reveal they start at page load and are over before anyone scrolls down). Write those animations so ' +
|
|
69
|
+
'the content is still fine without them and wrap them in `@media (prefers-reduced-motion: no-preference)`. Visitors who ask ' +
|
|
70
|
+
'for reduced motion, and browsers without IntersectionObserver, simply see everything at once; nothing shifts layout. The editor ' +
|
|
71
|
+
'always shows blocks (the effect plays in its preview mode and on the published page).\n' +
|
|
72
|
+
'LANDING PAGE: start with hero size:"screen", follow with custom bleed:true bands (add minHeight:"screen" where a section should fill the screen), and set config.navEnabled:false so the hero owns the first screen. ' +
|
|
73
|
+
'All other blocks stay in the centred text column.\n' +
|
|
49
74
|
'CHARTS (liveChart): chart ∈ bar|barH|stackedBar|stackedBarH|percentBar|lollipop|dumbbell|bullet|waterfall|pareto|radar|polarBar|line|step|area|stackedArea|percentArea|combo|slope|stream|calendar|pie|donut|halfDonut|rose|funnel|treemap|sunburst|waffle|histogram|boxplot|heatmap|scatter|bubble|sankey|chord|network|tree|kpi|gauge|progress|ring|candlestick|gantt|wordCloud|pivot. ' +
|
|
50
75
|
'source = a DATABASE, SPREADSHEET or LOG node (sheet: {nodeId, sheet?(name), range?:"A1:F200"} — first row = column names, formula results are read). ' +
|
|
51
76
|
'Category charts: labelColumn (dates are bucketed: dateBucket day|week|month|quarter|year|weekday|hour, empty periods filled with 0) + ' +
|
|
@@ -69,8 +94,10 @@ const BLOCK_GUIDE =
|
|
|
69
94
|
'for a document in the same workspace. Internal path strings are rejected. A node link renders as the in-app ' +
|
|
70
95
|
'document for members, as the public address on the published page, and is hidden entirely if that document is ' +
|
|
71
96
|
'not published — so link to nodes rather than pasting addresses.\n' +
|
|
72
|
-
'CONFIG: {theme, accentColor?, navEnabled?}. theme is one of classic | minimal | warm | pastel | editorial | mono | ' +
|
|
73
|
-
'dark | midnight. accentColor is a hex like "#1A4A7A"; leave it out and the theme picks a matching accent.
|
|
97
|
+
'CONFIG: {theme, accentColor?, navEnabled?, metaPixelId?}. theme is one of classic | minimal | warm | pastel | editorial | mono | ' +
|
|
98
|
+
'dark | midnight. accentColor is a hex like "#1A4A7A"; leave it out and the theme picks a matching accent.' +
|
|
99
|
+
' metaPixelId is the numeric Meta (Facebook/Instagram) Pixel ID: when set, the PUBLISHED page reports PageView and, when a visitor books through a booking block, Schedule to that pixel (never in the editor); send "" to turn it off. ' +
|
|
100
|
+
'config is replaced as a whole on update, so resend the keys you want to keep. ' +
|
|
74
101
|
'Every published page carries the Rootr signature at the bottom.\n' +
|
|
75
102
|
'Block ids are optional — omit them and they are generated. Keep the ids you get back if you want to edit one block later.';
|
|
76
103
|
|
|
@@ -104,7 +131,7 @@ export function registerPageTools(server) {
|
|
|
104
131
|
config: z
|
|
105
132
|
.record(z.unknown())
|
|
106
133
|
.optional()
|
|
107
|
-
.describe('{theme, accentColor?, navEnabled?} — theme: classic|minimal|warm|pastel|editorial|mono|dark|midnight'),
|
|
134
|
+
.describe('{theme, accentColor?, navEnabled?, metaPixelId?} — theme: classic|minimal|warm|pastel|editorial|mono|dark|midnight; metaPixelId: numeric Meta Pixel ID'),
|
|
108
135
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
109
136
|
},
|
|
110
137
|
},
|
|
@@ -186,7 +213,7 @@ export function registerPageTools(server) {
|
|
|
186
213
|
.array(z.record(z.unknown()))
|
|
187
214
|
.optional()
|
|
188
215
|
.describe('COMPLETE blocks array to set — any block you omit is deleted'),
|
|
189
|
-
config: z.record(z.unknown()).optional().describe('{theme, accentColor?, navEnabled?}'),
|
|
216
|
+
config: z.record(z.unknown()).optional().describe('{theme, accentColor?, navEnabled?, metaPixelId?} — replaces the whole config'),
|
|
190
217
|
},
|
|
191
218
|
},
|
|
192
219
|
async ({ pageId, blocks, config }) => {
|
|
@@ -24,6 +24,8 @@ const AUTHORING_GUIDE =
|
|
|
24
24
|
'placement does not draw the image. For `images[]`, x/y/w/h give freeform PPT-style placement in 1280x720 canvas ' +
|
|
25
25
|
'coords (drawn only when both x and y are set). ALWAYS add an `alt` caption to every image — alt text is part of the graph text spine, so a captioned image ' +
|
|
26
26
|
'connects into the knowledge graph while an uncaptioned one does not.\n' +
|
|
27
|
+
'Videos (Team plan and above — a Free workspace gets 403 PLAN_UPGRADE_REQUIRED when it ADDS a video; existing ones stay editable/removable): `videos[]` (max 20 per slide) holds {id?, src, title?, poster?, x?, y?, w?, h?} in 1280x720 canvas coords (default 640x360 centred). `src` is an uploaded video file URL (upload mp4/webm ≤75MB via rootr_upload_attachment with nodeId = this deck, then use /api/v1/attachments/{id}/raw), an https video-file URL, or a YouTube/Vimeo link; `poster` is an image URL (thumbnail). `title` is a caption and joins the graph text spine. Slide `html` may instead contain <video src="…/v1/attachments/{id}/raw" controls>, <div data-video-embed="https://www.youtube.com/watch?v=…">, or a YouTube/Vimeo <iframe>. Uploaded videos play through short-lived signed URLs (`vt=` query) the server adds when it returns the slide — never write `vt=` yourself; it is stripped on save.\n' +
|
|
28
|
+
'Animations (all plans): in slide `html`, add data-anim="<effect>" to any element to animate it when the slide opens in view/present mode. Entrance effects: fade, fade-up, fade-down, fly-left, fly-right, zoom, pop, blur. Looping effects: pulse, float, spin, blink, shake. Duration/delay go in inline CSS variables, e.g. <div data-anim="fade-up" style="--anim-delay:.4s;--anim-dur:.8s">; stagger delays to set the order. Thumbnails and PDF/PPTX exports show the finished state. Your own @keyframes/animation CSS in the html also plays.\n' +
|
|
27
29
|
'Use kind to mark each slide\'s role: "cover" (deck title slide), "section" (chapter divider), "content" (body slide, ' +
|
|
28
30
|
'the default), "closing" (last slide / call to action).\n' +
|
|
29
31
|
'Page numbers: "content" and "section" slides automatically show a page number that follows the slide order (it ' +
|
|
@@ -70,6 +72,19 @@ const imageShape = z
|
|
|
70
72
|
})
|
|
71
73
|
.describe('A slide image. Set src (uploaded/https/data: URI) or prompt (AI-generate); always add an alt caption.');
|
|
72
74
|
|
|
75
|
+
const videoShape = z
|
|
76
|
+
.object({
|
|
77
|
+
id: z.string().optional().describe('Stable video id. Auto-generated by the editor if omitted.'),
|
|
78
|
+
src: z.string().describe('Uploaded video URL (/api/v1/attachments/{id}/raw), https video file URL, or a YouTube/Vimeo link'),
|
|
79
|
+
title: z.string().optional().describe('Caption — part of the graph text spine'),
|
|
80
|
+
poster: z.string().optional().describe('Thumbnail image URL for file videos'),
|
|
81
|
+
x: z.number().optional().describe('Left in 1280x720 canvas px'),
|
|
82
|
+
y: z.number().optional().describe('Top in 1280x720 canvas px'),
|
|
83
|
+
w: z.number().optional().describe('Width px (default 640)'),
|
|
84
|
+
h: z.number().optional().describe('Height px (default 360)'),
|
|
85
|
+
})
|
|
86
|
+
.describe('A video placed on a slide. Team plan and above.');
|
|
87
|
+
|
|
73
88
|
const slideShape = z
|
|
74
89
|
.object({
|
|
75
90
|
id: z.string().optional().describe('Unique slide id, e.g. "SLD-001". Auto-generated if omitted.'),
|
|
@@ -108,6 +123,10 @@ const slideShape = z
|
|
|
108
123
|
.array(imageShape)
|
|
109
124
|
.optional()
|
|
110
125
|
.describe('Additional inserted/uploaded images placed on the slide (up to 50). Each should carry an alt caption.'),
|
|
126
|
+
videos: z
|
|
127
|
+
.array(videoShape)
|
|
128
|
+
.optional()
|
|
129
|
+
.describe('Videos placed on the slide (Team plan+, up to 20): {src (uploaded /attachments/{id}/raw URL, https video file, or YouTube/Vimeo link), title?, poster?, x?, y?, w?, h? in 1280x720 coords}.'),
|
|
111
130
|
code: z.string().optional().describe('Code-first React slide source (max design freedom, advanced use)'),
|
|
112
131
|
html: z.string().optional().describe('Rendered self-contained slide HTML (usually machine-produced, not hand-authored)'),
|
|
113
132
|
notes: z.string().optional().describe('Speaker notes — text spine'),
|
|
@@ -41,6 +41,11 @@ const ACCESS_HINT =
|
|
|
41
41
|
'lock) and `database.notifications` (who to notify on row changes). A database property may carry ' +
|
|
42
42
|
'`searchExcluded: true` (values stay out of the search index behind ask / AI explore / similar rows — not a read permission) and a `type: "vector"` ' +
|
|
43
43
|
'property needs `dimensions` (an embedding column agents fill with rootr_set_row_vectors). ' +
|
|
44
|
+
'A phone/email/name property may carry `dedupe: true` (warn when the same person is entered again) with ' +
|
|
45
|
+
'`dedupeShow: ["진행상태","특이사항","콜 이력"]` (fields of the existing row the warning shows — a relation shows its latest 5 rows); ' +
|
|
46
|
+
'a relation property may carry `relationFilter: [{ propId, op: "is"|"is_not_empty", values? }]` (which linked rows may be picked). ' +
|
|
47
|
+
'A board view config may carry `cardProps: ["면접예정일","연락처"]` (fields on each card, rollups too) and any view `rowColors` ' +
|
|
48
|
+
'(`[{ propId, op: "is"|"is_not_empty", values?, color }]`). ' +
|
|
44
49
|
'A DOCUMENT\'s `content` may embed a live ' +
|
|
45
50
|
'table/sheet/deck with a line `[[embed:<name of a node in this same tree>]]` or `[[embed:/absolute/path]]` — it is ' +
|
|
46
51
|
'linked to the real node after the whole tree is built (an unknown name rolls the whole tree back).';
|