rootr-cli 0.26.0 → 0.30.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/README.ko.md +38 -1
- package/README.md +56 -3
- package/bin/rootr.js +481 -9
- package/lib/client.js +92 -1
- package/lib/domain-billing.js +52 -0
- package/lib/mcp-tools/databases.js +110 -5
- package/lib/mcp-tools/documents.js +17 -1
- package/lib/mcp-tools/domains.js +236 -0
- package/lib/mcp-tools/forms.js +20 -2
- package/lib/mcp-tools/pages.js +37 -0
- package/lib/mcp-tools/public.js +13 -0
- package/lib/mcp-tools/workspaces.js +64 -1
- package/lib/mcp.js +2 -0
- package/package.json +1 -1
package/lib/mcp-tools/forms.js
CHANGED
|
@@ -35,6 +35,18 @@ const fieldShape = z
|
|
|
35
35
|
.boolean()
|
|
36
36
|
.optional()
|
|
37
37
|
.describe('date only: false = a single date (no end-date input). Omitted/true = start + optional end.'),
|
|
38
|
+
multiple: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('file only: true = accept SEVERAL files; the answer becomes an array of { id, filename, size }. Default: one file.'),
|
|
42
|
+
maxFiles: z
|
|
43
|
+
.number()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('file + multiple only: most files one person may attach (1–20, default 10).'),
|
|
46
|
+
allowImages: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('text/textarea only: let respondents insert images into the answer (stored as  markdown inside the text).'),
|
|
38
50
|
showIf: z
|
|
39
51
|
.object({ fieldId: z.string(), equals: z.string() })
|
|
40
52
|
.optional()
|
|
@@ -46,7 +58,9 @@ const FIELDS_GUIDE =
|
|
|
46
58
|
'Fields are typed inputs shown to respondents. Basic: text | textarea | number | select | multi_select | ' +
|
|
47
59
|
'checkbox | date. Contact: email | phone | url (each is format-checked server-side). Survey: rating (stars) | ' +
|
|
48
60
|
'scale (numbered buttons) | csat (1-5) | nps (0-10) | matrix (rows x options, Likert) | ranking (drag order). ' +
|
|
49
|
-
'Also: time (HH:MM) and file (
|
|
61
|
+
'Also: time (HH:MM) and file (one file by default; `multiple: true` + optional `maxFiles` (1-20, default 10) ' +
|
|
62
|
+
'takes several). `allowImages: true` on a text/textarea question lets people paste/insert images into the ' +
|
|
63
|
+
'answer (kept as  inside the text). ' +
|
|
50
64
|
'select/multi_select/matrix/ranking need `options` ({name,color}[]); matrix also needs `rows`. ' +
|
|
51
65
|
'Per-field extras: `help` (a hint line), `placeholder`, `page` (split into multiple pages), `hidden` + URL ' +
|
|
52
66
|
'params (record the traffic source), `showIf` (show only when an earlier answer matches), `range: false` on a ' +
|
|
@@ -217,7 +231,10 @@ export function registerFormTools(server) {
|
|
|
217
231
|
description:
|
|
218
232
|
'Submit one response to a Rootr (루터) FORM on behalf of the caller (authenticated submission, not the ' +
|
|
219
233
|
'public/anonymous share-link flow). `values` is keyed by field id — call rootr_read_form first to get the ' +
|
|
220
|
-
'exact field ids and types.'
|
|
234
|
+
'exact field ids and types. File questions: upload each file first (rootr_upload_attachment, or ' +
|
|
235
|
+
'rootr_create_upload_url → PUT → rootr_complete_upload) and pass `{ id, filename }` — an ARRAY of those when ' +
|
|
236
|
+
'the field has `multiple: true` (more than `maxFiles` is refused; several files on a single-file question is ' +
|
|
237
|
+
'refused). Images in a text answer with `allowImages` go in as markdown: `` using the upload\'s url.',
|
|
221
238
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
222
239
|
inputSchema: {
|
|
223
240
|
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
@@ -273,6 +290,7 @@ export function registerFormTools(server) {
|
|
|
273
290
|
title: 'Fix an already-submitted Rootr form response',
|
|
274
291
|
description:
|
|
275
292
|
'Correct one response that was already submitted to a Rootr (루터) FORM. `values` REPLACES the whole ' +
|
|
293
|
+
'(file answers too — to add a file to a `multiple` question send the old array plus the new { id, filename }) ' +
|
|
276
294
|
'answer set and is re-checked against the form\'s current fields (required/type rules), so send every ' +
|
|
277
295
|
'answer you want to keep — call rootr_list_form_responses first. Use this instead of deleting and ' +
|
|
278
296
|
're-submitting when you want the original submitter and submission time preserved.',
|
package/lib/mcp-tools/pages.js
CHANGED
|
@@ -309,4 +309,41 @@ export function registerPageTools(server) {
|
|
|
309
309
|
}
|
|
310
310
|
},
|
|
311
311
|
);
|
|
312
|
+
server.registerTool(
|
|
313
|
+
'rootr_page_analytics',
|
|
314
|
+
{
|
|
315
|
+
title: 'Turn on / read visit tracking for a published page',
|
|
316
|
+
description:
|
|
317
|
+
'GA-style visit tracking for a Rootr (루터) PAGE, stored in the workspace itself. action "enable" creates, next to the page, ' +
|
|
318
|
+
'a folder "<page> · 방문 분석" (Analytics) holding: a LOG node (one entry per visitor action — visit, scroll 25/50/75/90%, ' +
|
|
319
|
+
'block seen, click, form seen/started/submitted, booking steps, time on page — with channel/utm/referrer/device/browser/' +
|
|
320
|
+
'country columns), a DATABASE "일별 방문" (Daily visits: one row per KST day — visitors, sessions, new visitors, page views, ' +
|
|
321
|
+
'avg seconds, read-half/read-to-end sessions, clicks, form submits, bookings, conversions, conversion rate; filled every 15 min), ' +
|
|
322
|
+
'and a PAGE dashboard of live charts over both. Read the numbers with rootr_read_database (daily table), ' +
|
|
323
|
+
'rootr_query_log_entries / rootr_log_stats (the log) or rootr_read_page_live_data (the dashboard). ' +
|
|
324
|
+
'Only the PUBLISHED page records (rootr_publish_node, Team plan); the editor preview never does. "disable" stops recording ' +
|
|
325
|
+
'and keeps every node. "refresh" recounts the daily table now (default 90 days). "status" (default) returns the node ids, ' +
|
|
326
|
+
'last visit time and last-7-day totals. Field names follow `locale` (ko|en, first enable only). No cookies; visitors are ' +
|
|
327
|
+
'random per-page tags, IPs are never stored.',
|
|
328
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
329
|
+
inputSchema: {
|
|
330
|
+
pageId: z.string().describe(`PAGE node id${NODE_TARGET_HINT}`),
|
|
331
|
+
action: z.enum(['status', 'enable', 'disable', 'refresh']).optional().describe('default "status"'),
|
|
332
|
+
locale: z.enum(['ko', 'en']).optional().describe('column/node names on first enable (default ko)'),
|
|
333
|
+
days: z.number().int().min(1).max(400).optional().describe('refresh: how many days back to recount (default 90)'),
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
async ({ pageId, action, locale, days }) => {
|
|
337
|
+
try {
|
|
338
|
+
const client = makeClient();
|
|
339
|
+
pageId = await resolveNodeId(client, pageId);
|
|
340
|
+
if (action === 'enable') return jsonResult(await client.setPageAnalytics(pageId, { enabled: true, locale }));
|
|
341
|
+
if (action === 'disable') return jsonResult(await client.setPageAnalytics(pageId, { enabled: false }));
|
|
342
|
+
if (action === 'refresh') return jsonResult(await client.refreshPageAnalytics(pageId, days));
|
|
343
|
+
return jsonResult(await client.getPageAnalytics(pageId));
|
|
344
|
+
} catch (err) {
|
|
345
|
+
return errorResult(err);
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
);
|
|
312
349
|
}
|
package/lib/mcp-tools/public.js
CHANGED
|
@@ -94,6 +94,8 @@ export function registerPublicTools(server) {
|
|
|
94
94
|
'PLAN_UPGRADE_REQUIRED; a password-locked page is always hidden and cannot be re-exposed until unlocked); ' +
|
|
95
95
|
'`{"design": {"layout": "board", "skin": "paper"}}` changes how a published TABLE looks — it does not ' +
|
|
96
96
|
'have to be a table (cards, gallery, list, board, timeline, calendar, chart, ranking). ' +
|
|
97
|
+
'`{"seoDescription": "...", "seoImage": "<attachment id>"}` sets the search-result description and link-preview ' +
|
|
98
|
+
'image (search engines are pinged right away). ' +
|
|
97
99
|
'`{"isPublic": true}` on a PAGE needs the Team plan (Free → 403 PLAN_UPGRADE_REQUIRED); closing it works on any plan. ' +
|
|
98
100
|
'For DOCUMENT nodes use rootr_set_document_public instead — documents keep their own settings (skins, ' +
|
|
99
101
|
'edit proposals) on the older path. Owner-level action (MANAGE): use an account key (PAT).',
|
|
@@ -106,6 +108,17 @@ export function registerPublicTools(server) {
|
|
|
106
108
|
noIndex: z.boolean().optional(),
|
|
107
109
|
expiresAt: z.string().nullable().optional().describe('ISO date, or null to clear'),
|
|
108
110
|
password: z.string().nullable().optional().describe('null or "" removes the lock'),
|
|
111
|
+
seoDescription: z
|
|
112
|
+
.string()
|
|
113
|
+
.max(300)
|
|
114
|
+
.nullable()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe('Search-result / link-preview description (~160 chars recommended). null or "" falls back to the opening text.'),
|
|
117
|
+
seoImage: z
|
|
118
|
+
.string()
|
|
119
|
+
.nullable()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('Link-preview cover image: an image attachment id from the same workspace (rootr_upload_attachment). null removes it.'),
|
|
109
122
|
view: z.string().optional().describe('DATABASE only: view id the public page opens with'),
|
|
110
123
|
design: DB_DESIGN_SCHEMA.optional().describe(
|
|
111
124
|
'DATABASE only: how the public page presents the table. `layout` — table, cards, gallery, list, ' +
|
|
@@ -43,7 +43,7 @@ const ACCESS_HINT =
|
|
|
43
43
|
'property needs `dimensions` (an embedding column agents fill with rootr_set_row_vectors). ' +
|
|
44
44
|
'A phone/email/name property may carry `dedupe: true` (warn when the same person is entered again) with ' +
|
|
45
45
|
'`dedupeShow: ["상태","메모","통화 기록"]` (fields of the existing row the warning shows — a relation shows its latest 5 rows) and ' +
|
|
46
|
-
'`dedupeCarry: { label: "재등록",
|
|
46
|
+
'`dedupeCarry: { label: "재등록", action: "carry"|"merge_into_old"|"merge_all"|"ask", sources: "all", columns: { "메모": { append: { sep: " / " } } }, exclude: ["접수일"], mark: { "재등록 여부": true, "구분": { map: { "A": "A(재등록)" }, else: "{value} (재등록)" } }, cases: [{ when: { within: { column: "접수일", days: 0 } }, action: "merge_all", mark: {} }, { action: "carry" }] }` ' +
|
|
47
47
|
'(same entry came back: copy the earlier row into the new one — see rootr_update_column); any property may carry ' +
|
|
48
48
|
'`validate: { kind: "pattern"|"range"|"date"|"birthdate", pattern?, flags?, negate?, min?, max?, message? }` (flags a wrong value on every new write, never blocks); ' +
|
|
49
49
|
'a relation property may carry `relationFilter: [{ propId, op: "is"|"is_not_empty", values? }]` (which linked rows may be picked). ' +
|
|
@@ -54,6 +54,7 @@ const ACCESS_HINT =
|
|
|
54
54
|
'Any property may carry `align: "left"|"center"|"right"` and `wrap: true`; a date property `highlightOverdue: true` ' +
|
|
55
55
|
'(past dates red — off by default), `allowTime: false` (no time) and `allowRange: false` (no end date). A table view ' +
|
|
56
56
|
'config may carry `density: "compact"` and `frozenColumns: 1` (first N visible columns fixed while scrolling sideways). ' +
|
|
57
|
+
'A PAGE node may carry `page.analytics: true` (visit tracking — an "Analytics" folder with a visit LOG, a daily visits table and a dashboard is created next to it; records once published). ' +
|
|
57
58
|
'A DOCUMENT\'s `content` may embed a live ' +
|
|
58
59
|
'table/sheet/deck with a line `[[embed:<name of a node in this same tree>]]` or `[[embed:/absolute/path]]` — it is ' +
|
|
59
60
|
'linked to the real node after the whole tree is built (an unknown name rolls the whole tree back).';
|
|
@@ -236,6 +237,68 @@ export function registerWorkspaceTools(server) {
|
|
|
236
237
|
},
|
|
237
238
|
);
|
|
238
239
|
|
|
240
|
+
server.registerTool(
|
|
241
|
+
'rootr_workspace_members',
|
|
242
|
+
{
|
|
243
|
+
title: 'List members of a Rootr workspace',
|
|
244
|
+
description:
|
|
245
|
+
'List the members of a Rootr (루터) workspace — each with their user id, name, email, and role. Use the ' +
|
|
246
|
+
'id to address rootr_message_member, or to pass as assigneeId/userId elsewhere.',
|
|
247
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
248
|
+
inputSchema: {
|
|
249
|
+
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
async ({ workspace }) => {
|
|
253
|
+
try {
|
|
254
|
+
const ws = requireWorkspace(workspace);
|
|
255
|
+
const client = makeClient(ws);
|
|
256
|
+
const members = await client.listMembers(ws);
|
|
257
|
+
return jsonResult(
|
|
258
|
+
members.map((m) => ({
|
|
259
|
+
userId: m.userId ?? m.user?.id,
|
|
260
|
+
name: m.user?.name ?? '',
|
|
261
|
+
email: m.user?.email ?? '',
|
|
262
|
+
role: m.role,
|
|
263
|
+
})),
|
|
264
|
+
);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return errorResult(err);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
server.registerTool(
|
|
272
|
+
'rootr_message_member',
|
|
273
|
+
{
|
|
274
|
+
title: 'Send a direct message to a workspace member',
|
|
275
|
+
description:
|
|
276
|
+
'Send a direct notification-style message to another member of a Rootr (루터) workspace — not tied to ' +
|
|
277
|
+
'any document, issue, or comment. The message is posted as the person the ROOTR_API_KEY belongs to and ' +
|
|
278
|
+
'shows up for the recipient in their Rootr notification bell (and by email). Use rootr_workspace_members ' +
|
|
279
|
+
'first to find the recipient\'s userId.',
|
|
280
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
281
|
+
inputSchema: {
|
|
282
|
+
userId: z.string().describe('User id of the workspace member to message (see rootr_workspace_members)'),
|
|
283
|
+
body: z.string().describe('Message text'),
|
|
284
|
+
workspace: z
|
|
285
|
+
.string()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
async ({ userId, body, workspace }) => {
|
|
291
|
+
try {
|
|
292
|
+
const ws = requireWorkspace(workspace);
|
|
293
|
+
const client = makeClient(ws);
|
|
294
|
+
await client.messageMember(ws, userId, body);
|
|
295
|
+
return textResult(`${userId} 에게 메시지를 보냈습니다.`);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return errorResult(err);
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
239
302
|
server.registerTool(
|
|
240
303
|
'rootr_scaffold_plan',
|
|
241
304
|
{
|
package/lib/mcp.js
CHANGED
|
@@ -25,6 +25,7 @@ import { registerSignedDocTools } from './mcp-tools/signed.js';
|
|
|
25
25
|
import { registerFeedbackTools } from './mcp-tools/feedback.js';
|
|
26
26
|
import { registerMemoryTools } from './mcp-tools/memory.js';
|
|
27
27
|
import { registerDocMailTools } from './mcp-tools/doc-mail.js';
|
|
28
|
+
import { registerDomainTools } from './mcp-tools/domains.js';
|
|
28
29
|
import { withDocMailSink, withRateLimitSink } from './context.js';
|
|
29
30
|
import { makeClient } from './mcp-tools/shared.js';
|
|
30
31
|
import { t as tr } from './i18n.js';
|
|
@@ -330,6 +331,7 @@ export function buildMcpServer({ name, version }) {
|
|
|
330
331
|
registerFeedbackTools(server);
|
|
331
332
|
registerMemoryTools(server);
|
|
332
333
|
registerDocMailTools(server);
|
|
334
|
+
registerDomainTools(server);
|
|
333
335
|
|
|
334
336
|
return server;
|
|
335
337
|
}
|