rootr-cli 0.8.1 → 0.10.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.md +106 -1
- package/bin/rootr.js +532 -2
- package/lib/client.js +271 -7
- package/lib/mcp-tools/databases.js +421 -18
- package/lib/mcp-tools/documents.js +230 -3
- package/lib/mcp-tools/forms.js +87 -2
- package/lib/mcp-tools/misc.js +40 -0
- package/lib/mcp-tools/signed.js +467 -0
- package/lib/mcp-tools/workspaces.js +8 -1
- package/lib/mcp.js +13 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import { RootrApiError } from '../client.js';
|
|
4
4
|
import { resolveTarget, requireResolved, resolveMoveDestination } from '../resolve.js';
|
|
5
|
-
import { textResult, jsonResult, errorResult, makeClient } from './shared.js';
|
|
5
|
+
import { textResult, jsonResult, errorResult, makeClient, NODE_TARGET_HINT } from './shared.js';
|
|
6
6
|
|
|
7
7
|
/** Registers the original 6 document tools: list/read/append/edit/write/search. */
|
|
8
8
|
export function registerDocumentTools(server) {
|
|
@@ -304,6 +304,24 @@ export function registerDocumentTools(server) {
|
|
|
304
304
|
.union([z.string(), z.null()])
|
|
305
305
|
.optional()
|
|
306
306
|
.describe('ISO date to auto-close the public page, or null to clear an existing expiry'),
|
|
307
|
+
// PUBLIC-LOCK / PUBLIC-ACCESS — 노션 공개 페이지에 없는 것들.
|
|
308
|
+
password: z
|
|
309
|
+
.union([z.string(), z.null()])
|
|
310
|
+
.optional()
|
|
311
|
+
.describe(
|
|
312
|
+
'Lock the page so only readers who know the link AND this password can read it (4+ chars); ' +
|
|
313
|
+
'null or "" removes the lock. Team plan and up. Locking also turns search exposure off, and it ' +
|
|
314
|
+
'cannot be turned back on while the page stays locked.',
|
|
315
|
+
),
|
|
316
|
+
allowClone: z
|
|
317
|
+
.boolean()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe('Whether visitors may copy the page into their own workspace (turn off for quotes/contracts)'),
|
|
320
|
+
allowComments: z.boolean().optional().describe('Whether visitors may comment on the public page'),
|
|
321
|
+
noIndex: z
|
|
322
|
+
.boolean()
|
|
323
|
+
.optional()
|
|
324
|
+
.describe('true hides the page from search engines (Team plan and up)'),
|
|
307
325
|
skin: z
|
|
308
326
|
.enum([
|
|
309
327
|
'classic',
|
|
@@ -336,11 +354,104 @@ export function registerDocumentTools(server) {
|
|
|
336
354
|
.describe('Layout options; only the sub-fields you pass are changed'),
|
|
337
355
|
},
|
|
338
356
|
},
|
|
339
|
-
async ({ target, isPublic, allowCommits, expiresAt, skin, layout }) => {
|
|
357
|
+
async ({ target, isPublic, allowCommits, expiresAt, skin, layout, password, allowClone, allowComments, noIndex }) => {
|
|
358
|
+
try {
|
|
359
|
+
const client = makeClient();
|
|
360
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
361
|
+
// 주지 않은 값은 보내지 않는다 — 서버가 "안 보낸 것은 그대로"로 다루므로,
|
|
362
|
+
// undefined를 그대로 실어 보내면 다른 설정을 건드릴 위험이 있다.
|
|
363
|
+
const body = {};
|
|
364
|
+
for (const [k, v] of Object.entries({
|
|
365
|
+
isPublic,
|
|
366
|
+
allowCommits,
|
|
367
|
+
expiresAt,
|
|
368
|
+
skin,
|
|
369
|
+
layout,
|
|
370
|
+
password,
|
|
371
|
+
allowClone,
|
|
372
|
+
allowComments,
|
|
373
|
+
noIndex,
|
|
374
|
+
})) {
|
|
375
|
+
if (v !== undefined) body[k] = v;
|
|
376
|
+
}
|
|
377
|
+
return jsonResult(await client.setDocumentPublic(id, body));
|
|
378
|
+
} catch (err) {
|
|
379
|
+
return errorResult(err);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// PUBLIC-DOCS — 밖에서 온 변경 제안을 에이전트가 보고 처리할 수 있게. 노션에 없다.
|
|
385
|
+
server.registerTool(
|
|
386
|
+
'rootr_list_document_commits',
|
|
387
|
+
{
|
|
388
|
+
title: 'List change proposals on a Rootr document',
|
|
389
|
+
description:
|
|
390
|
+
'List the git-style change proposals people sent to a published Rootr (루터) document. Shows who proposed ' +
|
|
391
|
+
'what, whether the document moved on since (`stale`) and how many places clash (`conflicts`).',
|
|
392
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
393
|
+
inputSchema: {
|
|
394
|
+
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
395
|
+
status: z.enum(['OPEN', 'MERGED', 'REJECTED']).optional().describe('Defaults to OPEN'),
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
async ({ target, status }) => {
|
|
399
|
+
try {
|
|
400
|
+
const client = makeClient();
|
|
401
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
402
|
+
return jsonResult(await client.listDocumentCommits(id, status ?? 'OPEN'));
|
|
403
|
+
} catch (err) {
|
|
404
|
+
return errorResult(err);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
server.registerTool(
|
|
410
|
+
'rootr_merge_document_commit',
|
|
411
|
+
{
|
|
412
|
+
title: 'Merge a change proposal',
|
|
413
|
+
description:
|
|
414
|
+
'Apply one change proposal to the document. It is a three-way MERGE, not an overwrite: only the proposer\'s ' +
|
|
415
|
+
'edits land on top of the current text, so anything the owner changed meanwhile survives. If both sides ' +
|
|
416
|
+
'touched the same lines the call is refused with the conflict count — read it, then call again with ' +
|
|
417
|
+
'force: true to take the proposal for those lines. Owner-level action (MANAGE).',
|
|
418
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
419
|
+
inputSchema: {
|
|
420
|
+
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
421
|
+
commitId: z.string().describe('Proposal id from rootr_list_document_commits'),
|
|
422
|
+
force: z.boolean().optional().describe('Merge even though the same lines clash (proposal wins there)'),
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
async ({ target, commitId, force }) => {
|
|
340
426
|
try {
|
|
341
427
|
const client = makeClient();
|
|
342
428
|
const id = requireResolved(await resolveTarget(client, target));
|
|
343
|
-
return jsonResult(await client.
|
|
429
|
+
return jsonResult(await client.mergeDocumentCommit(id, commitId, force === true));
|
|
430
|
+
} catch (err) {
|
|
431
|
+
return errorResult(err);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
server.registerTool(
|
|
437
|
+
'rootr_reject_document_commit',
|
|
438
|
+
{
|
|
439
|
+
title: 'Reject a change proposal',
|
|
440
|
+
description:
|
|
441
|
+
'Turn down one change proposal. The reason (if given) is delivered to whoever proposed it — a rejection ' +
|
|
442
|
+
'with no explanation reads as being ignored. Owner-level action (MANAGE).',
|
|
443
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
444
|
+
inputSchema: {
|
|
445
|
+
target: z.string().describe('Document path (starting with "/") or node id, or a full rootr.io URL'),
|
|
446
|
+
commitId: z.string().describe('Proposal id from rootr_list_document_commits'),
|
|
447
|
+
reason: z.string().optional().describe('Sent to the proposer'),
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
async ({ target, commitId, reason }) => {
|
|
451
|
+
try {
|
|
452
|
+
const client = makeClient();
|
|
453
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
454
|
+
return jsonResult(await client.rejectDocumentCommit(id, commitId, reason));
|
|
344
455
|
} catch (err) {
|
|
345
456
|
return errorResult(err);
|
|
346
457
|
}
|
|
@@ -421,4 +532,120 @@ export function registerDocumentTools(server) {
|
|
|
421
532
|
}
|
|
422
533
|
},
|
|
423
534
|
);
|
|
535
|
+
|
|
536
|
+
// ── 공개범위·권한 (VIS-001) ─────────────────────────────────────────
|
|
537
|
+
// 화면에만 있던 "공개범위"/"권한" 창을 에이전트에게도 연다. 이게 없으면
|
|
538
|
+
// 에이전트가 문서는 만들면서 "서로 못 보게"는 사람이 따로 해야 한다.
|
|
539
|
+
|
|
540
|
+
server.registerTool(
|
|
541
|
+
'rootr_read_node_access',
|
|
542
|
+
{
|
|
543
|
+
title: "Read a node's visibility and who can access it",
|
|
544
|
+
description:
|
|
545
|
+
"Read a Rootr (루터) node's access settings: its explicit `visibility` (PUBLIC/PRIVATE/null=inherit), the " +
|
|
546
|
+
'effective value it resolves to, where an inherited value comes from, plus the permission overrides set ' +
|
|
547
|
+
'directly on it and on its ancestor folders. Requires MANAGE on the node — a caller who cannot change ' +
|
|
548
|
+
'access cannot read this either. Call before rootr_set_node_visibility / rootr_grant_node_permission.',
|
|
549
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
550
|
+
inputSchema: {
|
|
551
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
async ({ target }) => {
|
|
555
|
+
try {
|
|
556
|
+
const client = makeClient();
|
|
557
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
558
|
+
return jsonResult(await client.getNodeAccess(id));
|
|
559
|
+
} catch (err) {
|
|
560
|
+
return errorResult(err);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
server.registerTool(
|
|
566
|
+
'rootr_set_node_visibility',
|
|
567
|
+
{
|
|
568
|
+
title: "Set a node's visibility (private / inherit)",
|
|
569
|
+
description:
|
|
570
|
+
"Set a Rootr (루터) node's explicit visibility. PRIVATE hides the node and everything under it from " +
|
|
571
|
+
"workspace members' role-default access — only the creator, workspace OWNER/ADMIN, and people granted an " +
|
|
572
|
+
'explicit permission keep it. PUBLIC opens it to the workspace. Pass `visibility: null` to clear the ' +
|
|
573
|
+
'explicit value and inherit from the nearest ancestor folder / workspace default. Existing permission ' +
|
|
574
|
+
'overrides survive, so you cannot lock yourself out. This is the API behind the "공개범위" menu, and it is ' +
|
|
575
|
+
'NOT the same as publishing to the web (that is rootr_set_document_public).',
|
|
576
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
577
|
+
inputSchema: {
|
|
578
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
579
|
+
visibility: z
|
|
580
|
+
.enum(['PUBLIC', 'PRIVATE'])
|
|
581
|
+
.nullable()
|
|
582
|
+
.describe('PRIVATE = restrict to the creator/admins/explicit grants, PUBLIC = open to the workspace, null = inherit'),
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
async ({ target, visibility }) => {
|
|
586
|
+
try {
|
|
587
|
+
const client = makeClient();
|
|
588
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
589
|
+
return jsonResult(await client.setNodeVisibility(id, visibility ?? null));
|
|
590
|
+
} catch (err) {
|
|
591
|
+
return errorResult(err);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
server.registerTool(
|
|
597
|
+
'rootr_grant_node_permission',
|
|
598
|
+
{
|
|
599
|
+
title: 'Give one person or group access to a node',
|
|
600
|
+
description:
|
|
601
|
+
'Grant a workspace member (or group) an explicit permission on a Rootr (루터) node and everything under ' +
|
|
602
|
+
'it: READ (open it), WRITE (edit it), or MANAGE (also change its sharing/permissions). Use this together ' +
|
|
603
|
+
'with rootr_set_node_visibility to build "private folder, these three people" — set the folder PRIVATE, ' +
|
|
604
|
+
'then grant each person. Re-granting the same subject just changes their level. The subject must already ' +
|
|
605
|
+
'be a member of this workspace.',
|
|
606
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
607
|
+
inputSchema: {
|
|
608
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
609
|
+
subjectId: z.string().describe('User id (or group id when subjectType is GROUP)'),
|
|
610
|
+
level: z.enum(['READ', 'WRITE', 'MANAGE']).describe('Access level to grant'),
|
|
611
|
+
subjectType: z.enum(['USER', 'GROUP']).optional().describe('Defaults to USER'),
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
async ({ target, subjectId, level, subjectType }) => {
|
|
615
|
+
try {
|
|
616
|
+
const client = makeClient();
|
|
617
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
618
|
+
return jsonResult(await client.grantNodePermission(id, { subjectType, subjectId, level }));
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return errorResult(err);
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
server.registerTool(
|
|
626
|
+
'rootr_revoke_node_permission',
|
|
627
|
+
{
|
|
628
|
+
title: 'Take away one person or group\'s access to a node',
|
|
629
|
+
description:
|
|
630
|
+
'Remove an explicit permission override from a Rootr (루터) node. The subject falls back to whatever they ' +
|
|
631
|
+
'would get anyway — which on a PRIVATE node is no access, and on an inherited/PUBLIC node is their normal ' +
|
|
632
|
+
'workspace role. Call rootr_read_node_access first to see which overrides exist.',
|
|
633
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
634
|
+
inputSchema: {
|
|
635
|
+
target: z.string().describe(`Node path (starting with "/") or node id${NODE_TARGET_HINT}`),
|
|
636
|
+
subjectId: z.string().describe('User id (or group id when subjectType is GROUP)'),
|
|
637
|
+
subjectType: z.enum(['USER', 'GROUP']).optional().describe('Defaults to USER'),
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
async ({ target, subjectId, subjectType }) => {
|
|
641
|
+
try {
|
|
642
|
+
const client = makeClient();
|
|
643
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
644
|
+
await client.revokeNodePermission(id, { subjectType, subjectId });
|
|
645
|
+
return jsonResult({ revoked: true, nodeId: id, subjectId });
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return errorResult(err);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
);
|
|
424
651
|
}
|
package/lib/mcp-tools/forms.js
CHANGED
|
@@ -126,13 +126,20 @@ export function registerFormTools(server) {
|
|
|
126
126
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
127
127
|
inputSchema: {
|
|
128
128
|
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
129
|
+
latestPerRespondent: z
|
|
130
|
+
.boolean()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe(
|
|
133
|
+
'Keep only the newest response per signed-in submitter (`submitterUserId`), dropping their earlier ' +
|
|
134
|
+
'drafts. Anonymous share-link submissions have no submitter and are never merged. Default false.',
|
|
135
|
+
),
|
|
129
136
|
},
|
|
130
137
|
},
|
|
131
|
-
async ({ formId }) => {
|
|
138
|
+
async ({ formId, latestPerRespondent }) => {
|
|
132
139
|
try {
|
|
133
140
|
const client = makeClient();
|
|
134
141
|
formId = await resolveNodeId(client, formId);
|
|
135
|
-
const result = await client.listFormResponses(formId);
|
|
142
|
+
const result = await client.listFormResponses(formId, { latestPerRespondent });
|
|
136
143
|
return jsonResult({ responses: result });
|
|
137
144
|
} catch (err) {
|
|
138
145
|
return errorResult(err);
|
|
@@ -166,6 +173,84 @@ export function registerFormTools(server) {
|
|
|
166
173
|
},
|
|
167
174
|
);
|
|
168
175
|
|
|
176
|
+
server.registerTool(
|
|
177
|
+
'rootr_create_form_collector',
|
|
178
|
+
{
|
|
179
|
+
title: 'Create a table or spreadsheet that collects this form\'s responses',
|
|
180
|
+
description:
|
|
181
|
+
'Create a destination for a Rootr (루터) FORM\'s responses and link it in one call. ' +
|
|
182
|
+
'`target: "database"` builds a DATABASE whose columns come straight from the form\'s questions ' +
|
|
183
|
+
'(same names, matching types, select options carried over) — this is the right way to answer ' +
|
|
184
|
+
'"collect the answers in a table", because a hand-built table only mirrors when every column name ' +
|
|
185
|
+
'matches a question exactly. `target: "spreadsheet"` creates a SPREADSHEET whose sheet is bound to ' +
|
|
186
|
+
'the form, so its rows ARE the responses. Both replace any previous destination — a form collects ' +
|
|
187
|
+
'into one place. To link an already-built table instead, use rootr_update_form with targetDatabaseId.',
|
|
188
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
189
|
+
inputSchema: {
|
|
190
|
+
formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
|
|
191
|
+
target: z
|
|
192
|
+
.enum(['database', 'spreadsheet'])
|
|
193
|
+
.describe('database = typed columns/views (filters, boards); spreadsheet = one sheet of rows for formulas'),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
async ({ formId, target }) => {
|
|
197
|
+
try {
|
|
198
|
+
const client = makeClient();
|
|
199
|
+
formId = await resolveNodeId(client, formId);
|
|
200
|
+
return jsonResult(await client.createFormCollector(formId, target));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return errorResult(err);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
server.registerTool(
|
|
208
|
+
'rootr_update_form_response',
|
|
209
|
+
{
|
|
210
|
+
title: 'Fix an already-submitted Rootr form response',
|
|
211
|
+
description:
|
|
212
|
+
'Correct one response that was already submitted to a Rootr (루터) FORM. `values` REPLACES the whole ' +
|
|
213
|
+
'answer set and is re-checked against the form\'s current fields (required/type rules), so send every ' +
|
|
214
|
+
'answer you want to keep — call rootr_list_form_responses first. Use this instead of deleting and ' +
|
|
215
|
+
're-submitting when you want the original submitter and submission time preserved.',
|
|
216
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
217
|
+
inputSchema: {
|
|
218
|
+
responseId: z.string().describe('Response id from rootr_list_form_responses'),
|
|
219
|
+
values: z.record(z.any()).describe('Full replacement answers, keyed by field id'),
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
async ({ responseId, values }) => {
|
|
223
|
+
try {
|
|
224
|
+
const result = await makeClient().updateFormResponse(responseId, values);
|
|
225
|
+
return jsonResult(result);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
return errorResult(err);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
server.registerTool(
|
|
233
|
+
'rootr_delete_form_response',
|
|
234
|
+
{
|
|
235
|
+
title: 'Delete one Rootr form response',
|
|
236
|
+
description:
|
|
237
|
+
'Permanently delete a single response submitted to a Rootr (루터) FORM. This cannot be undone — the ' +
|
|
238
|
+
'answers are gone. Use rootr_update_form_response to correct a response instead of deleting it.',
|
|
239
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
240
|
+
inputSchema: {
|
|
241
|
+
responseId: z.string().describe('Response id from rootr_list_form_responses'),
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
async ({ responseId }) => {
|
|
245
|
+
try {
|
|
246
|
+
await makeClient().deleteFormResponse(responseId);
|
|
247
|
+
return jsonResult({ deleted: true, responseId });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
return errorResult(err);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
169
254
|
server.registerTool(
|
|
170
255
|
'rootr_create_form_share_link',
|
|
171
256
|
{
|
package/lib/mcp-tools/misc.js
CHANGED
|
@@ -31,6 +31,46 @@ export function registerMiscTools(server) {
|
|
|
31
31
|
},
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
server.registerTool(
|
|
35
|
+
'rootr_update_node',
|
|
36
|
+
{
|
|
37
|
+
title: 'Rename / re-icon / move any node',
|
|
38
|
+
description:
|
|
39
|
+
'Change a Rootr (루터) node\'s NAME, ICON, TAGS, or PARENT — works for every node type ' +
|
|
40
|
+
'(DOCUMENT, FOLDER, DATABASE, SPREADSHEET, FORM, …). Until now icons could only be set when a node ' +
|
|
41
|
+
'was CREATED, so matching a migrated tree to its Notion icons was impossible without recreating ' +
|
|
42
|
+
'everything. Only the fields you pass change. `parentId: null` moves the node to the workspace root; ' +
|
|
43
|
+
'a node cannot be moved inside its own subtree.',
|
|
44
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
45
|
+
inputSchema: {
|
|
46
|
+
target: z.string().describe('Node path (starting with "/") or node id'),
|
|
47
|
+
name: z.string().optional().describe('New name'),
|
|
48
|
+
icon: z.string().optional().describe('New emoji icon, e.g. "📋". Empty string clears it.'),
|
|
49
|
+
tags: z.array(z.string()).optional().describe('Replaces the tag list'),
|
|
50
|
+
parentId: z
|
|
51
|
+
.string()
|
|
52
|
+
.nullable()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Move under this folder/document/database (id or path). null = workspace root.'),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async ({ target, name, icon, tags, parentId }) => {
|
|
58
|
+
try {
|
|
59
|
+
const client = makeClient();
|
|
60
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
61
|
+
// 부모도 경로로 줄 수 있어야 한다 — id를 미리 아는 쪽이 오히려 드물다.
|
|
62
|
+
let parent = parentId;
|
|
63
|
+
if (typeof parentId === 'string' && parentId) {
|
|
64
|
+
parent = requireResolved(await resolveTarget(client, parentId));
|
|
65
|
+
}
|
|
66
|
+
const node = await client.updateNode(id, { name, icon, tags, parentId: parent });
|
|
67
|
+
return jsonResult(node);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return errorResult(err);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
34
74
|
server.registerTool(
|
|
35
75
|
'rootr_delete_node',
|
|
36
76
|
{
|