rootr-cli 0.4.0 → 0.5.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 +40 -3
- package/lib/client.js +18 -0
- package/lib/mcp-tools/attachments.js +3 -2
- package/lib/mcp-tools/crm.js +29 -15
- package/lib/mcp-tools/databases.js +20 -14
- package/lib/mcp-tools/documents.js +56 -6
- package/lib/mcp-tools/forms.js +19 -10
- package/lib/mcp-tools/issues.js +7 -4
- package/lib/mcp-tools/logs.js +11 -6
- package/lib/mcp-tools/presentations.js +103 -9
- package/lib/mcp-tools/recordings.js +15 -8
- package/lib/mcp-tools/shared.js +26 -0
- package/lib/mcp-tools/spreadsheets.js +15 -8
- package/lib/mcp-tools/webhooks.js +3 -2
- package/lib/mcp-tools/whiteboards.js +7 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -206,6 +206,13 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
206
206
|
|
|
207
207
|
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 29종:
|
|
208
208
|
|
|
209
|
+
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
210
|
+
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
211
|
+
받습니다 — CLI와 같은 규칙으로 `/`로 시작하면 경로로 해석합니다.
|
|
212
|
+
즉 `rootr_add_row`에 `databaseId: "/제작 공정/칸반"`을 그대로 넘길 수 있어,
|
|
213
|
+
경로만 아는 에이전트가 uuid를 찾아 헤맬 필요가 없습니다.
|
|
214
|
+
`rootr_list`·`rootr_search` 출력도 각 줄 끝에 `[노드 id]`를 함께 표시합니다.
|
|
215
|
+
|
|
209
216
|
#### 문서 (workspace 키, docs:read/docs:write)
|
|
210
217
|
|
|
211
218
|
| 도구 | 설명 |
|
|
@@ -282,10 +289,13 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
282
289
|
| `rootr_document_versions` | 문서 버전 히스토리 조회 |
|
|
283
290
|
| `rootr_delete_node` | 노드를 휴지통으로 이동(복구 가능) |
|
|
284
291
|
|
|
285
|
-
#### 프레젠테이션
|
|
292
|
+
#### 프레젠테이션
|
|
286
293
|
|
|
287
|
-
프레젠테이션(PRESENTATION)
|
|
288
|
-
|
|
294
|
+
프레젠테이션(PRESENTATION)은 전용 MCP 도구(`rootr_create_presentation`,
|
|
295
|
+
`rootr_read_presentation`, `rootr_append_presentation_slides`,
|
|
296
|
+
`rootr_update_presentation_slide`, `rootr_reorder_presentation_slides`,
|
|
297
|
+
`rootr_update_presentation`)로 저작합니다. 아래처럼 **같은 API 키로 REST를 직접
|
|
298
|
+
호출**해도 됩니다 (`x-api-key` 헤더, `docs:write`):
|
|
289
299
|
|
|
290
300
|
```bash
|
|
291
301
|
# 덱 생성 (슬라이드 포함 가능)
|
|
@@ -300,6 +310,33 @@ curl -X POST "$BASE/presentations/$ID/slides" -d '{"slides":[...]}' ...
|
|
|
300
310
|
curl -X POST "$BASE/presentations/$ID/reorder" -d '{"order":["SLD-002","SLD-001"]}' ...
|
|
301
311
|
```
|
|
302
312
|
|
|
313
|
+
##### 슬라이드에 다이어그램 그려 넣기
|
|
314
|
+
|
|
315
|
+
구조도·업무 흐름도·순서도·데이터 흐름도·상태도는 손으로 SVG를 짜지 말고
|
|
316
|
+
`rootr_create_diagram_slide`에 **형식이 정해진 JSON 스펙**을 넘기세요. 서버가
|
|
317
|
+
검증하고 그려서 슬라이드에 바로 꽂습니다 — 화면은 물론 PDF·PPTX 내보내기에도
|
|
318
|
+
그대로 나옵니다. 슬라이드의 `diagram`(mermaid) 필드는 **뷰어가 그리지 않으므로**
|
|
319
|
+
이 도구를 쓰는 것이 유일한 방법입니다.
|
|
320
|
+
|
|
321
|
+
종류마다 스펙 모양이 다르니 **`rootr_diagram_schema`로 스키마와 예제를 먼저
|
|
322
|
+
받아보세요.** 맞지 않는 스펙은 어느 항목이 왜 틀렸는지와 고치는 법을 달아 거절되니,
|
|
323
|
+
그대로 고쳐 다시 부르면 됩니다.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
# 스펙 모양 보기 (architecture | workflow | sequence | dataflow | lifecycle)
|
|
327
|
+
curl "$BASE/diagram-types/workflow/schema" -H "x-api-key: $KEY"
|
|
328
|
+
|
|
329
|
+
# 그려서 슬라이드에 꽂기 (slideId를 주면 그 슬라이드를 다시 그린다)
|
|
330
|
+
curl -X POST "$BASE/presentations/$ID/diagram-slides" -H "x-api-key: $KEY" \
|
|
331
|
+
-H 'Content-Type: application/json' \
|
|
332
|
+
-d '{"type":"workflow","spec":{...},"title":"이슈는 검수를 통과해야만 main에 들어간다",
|
|
333
|
+
"blocks":[{"heading":"검수가 관문","body":"..."}]}'
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`quality`는 기본 `standard`이고, `showcase`는 손으로 다듬은 대표 그림용 엄격
|
|
337
|
+
기준이라 `standard`가 통과시키는 스펙도 거절합니다. 그림 속 글자는 지식 그래프에
|
|
338
|
+
들어가지 않으니 사실은 `title`·`blocks`·`notes`에도 남기세요.
|
|
339
|
+
|
|
303
340
|
**고품질 덱을 만들 때는 슬라이드 `html` 필드에 1280×720 완결 HTML을 직접 구워
|
|
304
341
|
넣는 방식을 권장**합니다 — 뷰어가 기본 템플릿 대신 그대로 렌더합니다(완전 자유
|
|
305
342
|
디자인). 필수 규칙(외부 요청 차단이라 전부 인라인, 이미지=data URI 내장, 폰트
|
package/lib/client.js
CHANGED
|
@@ -852,6 +852,24 @@ export class RootrClient {
|
|
|
852
852
|
});
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
+
/** Diagram families the server can draw, for discovery. */
|
|
856
|
+
async diagramTypes() {
|
|
857
|
+
return this.request('/diagram-types');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** JSON Schema + a worked example for one diagram family. */
|
|
861
|
+
async diagramSchema(type) {
|
|
862
|
+
return this.request(`/diagram-types/${encodeURIComponent(type)}/schema`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Render a typed diagram spec onto a slide (appends, or replaces slideId). */
|
|
866
|
+
async createDiagramSlide(presentationId, payload) {
|
|
867
|
+
return this.request(`/presentations/${encodeURIComponent(presentationId)}/diagram-slides`, {
|
|
868
|
+
method: 'POST',
|
|
869
|
+
body: payload,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
855
873
|
async updatePresentationSlide(presentationId, slideId, slide) {
|
|
856
874
|
return this.request(
|
|
857
875
|
`/presentations/${encodeURIComponent(presentationId)}/slides/${encodeURIComponent(slideId)}`,
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
|
|
5
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveOptionalNodeId, NODE_TARGET_HINT } from './shared.js';
|
|
6
6
|
import { resolveTarget, requireResolved } from '../resolve.js';
|
|
7
7
|
import { RootrApiError } from '../client.js';
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ const uploadInputSchema = {
|
|
|
24
24
|
.string()
|
|
25
25
|
.optional()
|
|
26
26
|
.describe('Base64-encoded file bytes; use this when the caller has the file in memory instead of on disk'),
|
|
27
|
-
nodeId: z.string().optional().describe(
|
|
27
|
+
nodeId: z.string().optional().describe(`Document/node id${NODE_TARGET_HINT} to associate the attachment with (permission-checked)`),
|
|
28
28
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -48,6 +48,7 @@ export function registerAttachmentTools(server) {
|
|
|
48
48
|
try {
|
|
49
49
|
const client = makeClient();
|
|
50
50
|
const ws = requireWorkspace(workspace);
|
|
51
|
+
nodeId = await resolveOptionalNodeId(client, nodeId);
|
|
51
52
|
const data = readAttachmentBytes({ filePath, base64Data });
|
|
52
53
|
const { id } = await client.uploadAttachment(ws, { filename, mimeType, nodeId, data });
|
|
53
54
|
const url = client.attachmentRawUrl(id);
|
package/lib/mcp-tools/crm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Shared context repeated (shortened) across tool descriptions so an agent
|
|
@@ -61,7 +61,7 @@ export function registerCrmTools(server) {
|
|
|
61
61
|
inputSchema: {
|
|
62
62
|
name: z.string().optional().describe('CRM node name'),
|
|
63
63
|
path: z.string().optional().describe('Full tree path to create at, e.g. "/sales/crm"'),
|
|
64
|
-
parentId: z.string().optional().describe(
|
|
64
|
+
parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
|
|
65
65
|
icon: z.string().optional().describe('Emoji icon for the CRM node'),
|
|
66
66
|
pipelines: z
|
|
67
67
|
.array(pipelineShape)
|
|
@@ -81,6 +81,7 @@ export function registerCrmTools(server) {
|
|
|
81
81
|
try {
|
|
82
82
|
const ws = requireWorkspace(workspace);
|
|
83
83
|
const client = makeClient(ws);
|
|
84
|
+
parentId = await resolveOptionalNodeId(client, parentId);
|
|
84
85
|
const result = await client.createCrm(ws, {
|
|
85
86
|
name,
|
|
86
87
|
path,
|
|
@@ -111,7 +112,7 @@ export function registerCrmTools(server) {
|
|
|
111
112
|
CRM_CONTEXT,
|
|
112
113
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
113
114
|
inputSchema: {
|
|
114
|
-
crmId: z.string().describe(
|
|
115
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
115
116
|
includeSummary: z
|
|
116
117
|
.boolean()
|
|
117
118
|
.optional()
|
|
@@ -121,6 +122,7 @@ export function registerCrmTools(server) {
|
|
|
121
122
|
async ({ crmId, includeSummary }) => {
|
|
122
123
|
try {
|
|
123
124
|
const client = makeClient();
|
|
125
|
+
crmId = await resolveNodeId(client, crmId);
|
|
124
126
|
const crm = await client.getCrm(crmId);
|
|
125
127
|
if (includeSummary) {
|
|
126
128
|
const summary = await client.getCrmSummary(crmId);
|
|
@@ -144,7 +146,7 @@ export function registerCrmTools(server) {
|
|
|
144
146
|
CRM_CONTEXT,
|
|
145
147
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
146
148
|
inputSchema: {
|
|
147
|
-
crmId: z.string().describe(
|
|
149
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
148
150
|
pipelines: z
|
|
149
151
|
.array(pipelineShape)
|
|
150
152
|
.optional()
|
|
@@ -157,6 +159,7 @@ export function registerCrmTools(server) {
|
|
|
157
159
|
async ({ crmId, pipelines, fields, config, icon }) => {
|
|
158
160
|
try {
|
|
159
161
|
const client = makeClient();
|
|
162
|
+
crmId = await resolveNodeId(client, crmId);
|
|
160
163
|
const result = await client.updateCrm(crmId, {
|
|
161
164
|
pipelines: coerceJson(pipelines),
|
|
162
165
|
fields: coerceJson(fields),
|
|
@@ -182,7 +185,7 @@ export function registerCrmTools(server) {
|
|
|
182
185
|
CRM_CONTEXT,
|
|
183
186
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
184
187
|
inputSchema: {
|
|
185
|
-
crmId: z.string().describe(
|
|
188
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
186
189
|
q: z.string().optional().describe('Free-text search (name/domain/etc.)'),
|
|
187
190
|
ownerId: z.string().optional().describe('Filter by owning user id'),
|
|
188
191
|
tag: z.string().optional().describe('Filter by tag'),
|
|
@@ -194,6 +197,7 @@ export function registerCrmTools(server) {
|
|
|
194
197
|
async ({ crmId, q, ownerId, tag, sort, limit, offset }) => {
|
|
195
198
|
try {
|
|
196
199
|
const client = makeClient();
|
|
200
|
+
crmId = await resolveNodeId(client, crmId);
|
|
197
201
|
const result = await client.listCrmCompanies(crmId, { q, ownerId, tag, sort, limit, offset });
|
|
198
202
|
return jsonResult(result);
|
|
199
203
|
} catch (err) {
|
|
@@ -213,7 +217,7 @@ export function registerCrmTools(server) {
|
|
|
213
217
|
CRM_CONTEXT,
|
|
214
218
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
215
219
|
inputSchema: {
|
|
216
|
-
crmId: z.string().describe(
|
|
220
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
217
221
|
companyId: z.string().optional().describe('Existing company id to update/delete; omit to create a new company'),
|
|
218
222
|
delete: z.boolean().optional().describe('If true (with companyId), delete the company instead of updating'),
|
|
219
223
|
name: z.string().optional().describe('Company name (required when creating)'),
|
|
@@ -231,6 +235,7 @@ export function registerCrmTools(server) {
|
|
|
231
235
|
async ({ crmId, companyId, delete: doDelete, custom, ...rest }) => {
|
|
232
236
|
try {
|
|
233
237
|
const client = makeClient();
|
|
238
|
+
crmId = await resolveNodeId(client, crmId);
|
|
234
239
|
if (companyId && doDelete) {
|
|
235
240
|
await client.deleteCrmCompany(companyId);
|
|
236
241
|
return jsonResult({ deleted: true, companyId });
|
|
@@ -283,7 +288,7 @@ export function registerCrmTools(server) {
|
|
|
283
288
|
CRM_CONTEXT,
|
|
284
289
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
285
290
|
inputSchema: {
|
|
286
|
-
crmId: z.string().describe(
|
|
291
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
287
292
|
q: z.string().optional().describe('Free-text search (name/email/etc.)'),
|
|
288
293
|
companyId: z.string().optional().describe('Filter by company id'),
|
|
289
294
|
lifecycleStage: z.string().optional().describe('lead | mql | sql | opportunity | customer | evangelist'),
|
|
@@ -298,6 +303,7 @@ export function registerCrmTools(server) {
|
|
|
298
303
|
async ({ crmId, q, companyId, lifecycleStage, leadStatus, ownerId, tag, sort, limit, offset }) => {
|
|
299
304
|
try {
|
|
300
305
|
const client = makeClient();
|
|
306
|
+
crmId = await resolveNodeId(client, crmId);
|
|
301
307
|
const result = await client.listCrmContacts(crmId, {
|
|
302
308
|
q,
|
|
303
309
|
companyId,
|
|
@@ -327,7 +333,7 @@ export function registerCrmTools(server) {
|
|
|
327
333
|
CRM_CONTEXT,
|
|
328
334
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
329
335
|
inputSchema: {
|
|
330
|
-
crmId: z.string().describe(
|
|
336
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
331
337
|
contactId: z.string().optional().describe('Existing contact id to update/delete; omit to create a new contact'),
|
|
332
338
|
delete: z.boolean().optional().describe('If true (with contactId), delete the contact instead of updating'),
|
|
333
339
|
name: z.string().optional().describe('Contact name (required when creating)'),
|
|
@@ -353,6 +359,7 @@ export function registerCrmTools(server) {
|
|
|
353
359
|
async ({ crmId, contactId, delete: doDelete, custom, ...rest }) => {
|
|
354
360
|
try {
|
|
355
361
|
const client = makeClient();
|
|
362
|
+
crmId = await resolveNodeId(client, crmId);
|
|
356
363
|
if (contactId && doDelete) {
|
|
357
364
|
await client.deleteCrmContact(contactId);
|
|
358
365
|
return jsonResult({ deleted: true, contactId });
|
|
@@ -401,7 +408,7 @@ export function registerCrmTools(server) {
|
|
|
401
408
|
CRM_CONTEXT,
|
|
402
409
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
403
410
|
inputSchema: {
|
|
404
|
-
crmId: z.string().describe(
|
|
411
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
405
412
|
q: z.string().optional().describe('Free-text search (deal name/etc.)'),
|
|
406
413
|
pipelineId: z.string().optional().describe('Filter by pipeline id'),
|
|
407
414
|
stageId: z.string().optional().describe('Filter by stage id'),
|
|
@@ -418,6 +425,7 @@ export function registerCrmTools(server) {
|
|
|
418
425
|
async ({ crmId, q, pipelineId, stageId, status, ownerId, companyId, contactId, tag, sort, limit, offset }) => {
|
|
419
426
|
try {
|
|
420
427
|
const client = makeClient();
|
|
428
|
+
crmId = await resolveNodeId(client, crmId);
|
|
421
429
|
const result = await client.listCrmDeals(crmId, {
|
|
422
430
|
q,
|
|
423
431
|
pipelineId,
|
|
@@ -450,7 +458,7 @@ export function registerCrmTools(server) {
|
|
|
450
458
|
CRM_CONTEXT,
|
|
451
459
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
452
460
|
inputSchema: {
|
|
453
|
-
crmId: z.string().describe(
|
|
461
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
454
462
|
dealId: z.string().optional().describe('Existing deal id to update/delete; omit to create a new deal'),
|
|
455
463
|
delete: z.boolean().optional().describe('If true (with dealId), delete the deal instead of updating'),
|
|
456
464
|
name: z.string().optional().describe('Deal name (required when creating)'),
|
|
@@ -478,6 +486,7 @@ export function registerCrmTools(server) {
|
|
|
478
486
|
async ({ crmId, dealId, delete: doDelete, contacts, custom, ...rest }) => {
|
|
479
487
|
try {
|
|
480
488
|
const client = makeClient();
|
|
489
|
+
crmId = await resolveNodeId(client, crmId);
|
|
481
490
|
if (dealId && doDelete) {
|
|
482
491
|
await client.deleteCrmDeal(dealId);
|
|
483
492
|
return jsonResult({ deleted: true, dealId });
|
|
@@ -552,7 +561,7 @@ export function registerCrmTools(server) {
|
|
|
552
561
|
CRM_CONTEXT,
|
|
553
562
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
554
563
|
inputSchema: {
|
|
555
|
-
crmId: z.string().describe(
|
|
564
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
556
565
|
type: z.string().describe('note | call | email | meeting'),
|
|
557
566
|
subject: z.string().optional(),
|
|
558
567
|
body: z.string().optional().describe('Activity body/notes (markdown/plain text)'),
|
|
@@ -565,6 +574,7 @@ export function registerCrmTools(server) {
|
|
|
565
574
|
async ({ crmId, ...rest }) => {
|
|
566
575
|
try {
|
|
567
576
|
const client = makeClient();
|
|
577
|
+
crmId = await resolveNodeId(client, crmId);
|
|
568
578
|
const result = await client.createCrmActivity(crmId, rest);
|
|
569
579
|
return jsonResult(result);
|
|
570
580
|
} catch (err) {
|
|
@@ -582,7 +592,7 @@ export function registerCrmTools(server) {
|
|
|
582
592
|
CRM_CONTEXT,
|
|
583
593
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
584
594
|
inputSchema: {
|
|
585
|
-
crmId: z.string().describe(
|
|
595
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
586
596
|
contactId: z.string().optional(),
|
|
587
597
|
companyId: z.string().optional(),
|
|
588
598
|
dealId: z.string().optional(),
|
|
@@ -594,6 +604,7 @@ export function registerCrmTools(server) {
|
|
|
594
604
|
async ({ crmId, contactId, companyId, dealId, type, limit, offset }) => {
|
|
595
605
|
try {
|
|
596
606
|
const client = makeClient();
|
|
607
|
+
crmId = await resolveNodeId(client, crmId);
|
|
597
608
|
const result = await client.listCrmActivities(crmId, { contactId, companyId, dealId, type, limit, offset });
|
|
598
609
|
return jsonResult(result);
|
|
599
610
|
} catch (err) {
|
|
@@ -615,7 +626,7 @@ export function registerCrmTools(server) {
|
|
|
615
626
|
CRM_CONTEXT,
|
|
616
627
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
617
628
|
inputSchema: {
|
|
618
|
-
crmId: z.string().describe(
|
|
629
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT} (required when creating; ignored when updating)`),
|
|
619
630
|
taskId: z.string().optional().describe('Existing task id to update; omit to create a new task'),
|
|
620
631
|
title: z.string().optional().describe('Task title (required when creating)'),
|
|
621
632
|
type: z.string().optional().describe('todo | call | email | meeting'),
|
|
@@ -630,6 +641,7 @@ export function registerCrmTools(server) {
|
|
|
630
641
|
async ({ crmId, taskId, ...rest }) => {
|
|
631
642
|
try {
|
|
632
643
|
const client = makeClient();
|
|
644
|
+
crmId = await resolveNodeId(client, crmId);
|
|
633
645
|
const result = taskId ? await client.updateCrmTask(taskId, rest) : await client.createCrmTask(crmId, rest);
|
|
634
646
|
return jsonResult(result);
|
|
635
647
|
} catch (err) {
|
|
@@ -648,7 +660,7 @@ export function registerCrmTools(server) {
|
|
|
648
660
|
CRM_CONTEXT,
|
|
649
661
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
650
662
|
inputSchema: {
|
|
651
|
-
crmId: z.string().describe(
|
|
663
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
652
664
|
status: z.string().optional().describe('open | done | all'),
|
|
653
665
|
assigneeId: z.string().optional(),
|
|
654
666
|
due: z.string().optional().describe('overdue | today | upcoming'),
|
|
@@ -662,6 +674,7 @@ export function registerCrmTools(server) {
|
|
|
662
674
|
async ({ crmId, status, assigneeId, due, contactId, companyId, dealId, limit, offset }) => {
|
|
663
675
|
try {
|
|
664
676
|
const client = makeClient();
|
|
677
|
+
crmId = await resolveNodeId(client, crmId);
|
|
665
678
|
const result = await client.listCrmTasks(crmId, {
|
|
666
679
|
status,
|
|
667
680
|
assigneeId,
|
|
@@ -693,7 +706,7 @@ export function registerCrmTools(server) {
|
|
|
693
706
|
CRM_CONTEXT,
|
|
694
707
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
695
708
|
inputSchema: {
|
|
696
|
-
crmId: z.string().describe(
|
|
709
|
+
crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
|
|
697
710
|
entity: z.string().describe('contact | company | deal'),
|
|
698
711
|
rows: z.array(z.record(z.unknown())).describe('Array of row objects to import, matching the entity shape'),
|
|
699
712
|
},
|
|
@@ -701,6 +714,7 @@ export function registerCrmTools(server) {
|
|
|
701
714
|
async ({ crmId, entity, rows }) => {
|
|
702
715
|
try {
|
|
703
716
|
const client = makeClient();
|
|
717
|
+
crmId = await resolveNodeId(client, crmId);
|
|
704
718
|
const result = await client.importCrm(crmId, { entity, rows: coerceJson(rows) });
|
|
705
719
|
return jsonResult(result);
|
|
706
720
|
} catch (err) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
import { jsonResult, errorResult, makeClient } from './shared.js';
|
|
3
|
+
import { jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/** Human text for the `databaseId` param — accepts a path just like the document tools. */
|
|
6
|
+
const DATABASE_TARGET_DESC = `DATABASE node id${NODE_TARGET_HINT} (e.g. "/제작 공정/칸반")`;
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Resolve a `{propertyNameOrId: value}` map to `{propertyId: value}` using the
|
|
@@ -40,15 +43,16 @@ export function registerDatabaseTools(server) {
|
|
|
40
43
|
'property ids/names and, for board/kanban views, which select column holds the status. Only works on DATABASE nodes.',
|
|
41
44
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
42
45
|
inputSchema: {
|
|
43
|
-
databaseId: z.string().describe(
|
|
46
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
44
47
|
},
|
|
45
48
|
},
|
|
46
49
|
async ({ databaseId }) => {
|
|
47
50
|
try {
|
|
48
51
|
const client = makeClient();
|
|
52
|
+
const id = await resolveNodeId(client, databaseId);
|
|
49
53
|
const [database, rows] = await Promise.all([
|
|
50
|
-
client.getDatabase(
|
|
51
|
-
client.listRows(
|
|
54
|
+
client.getDatabase(id),
|
|
55
|
+
client.listRows(id),
|
|
52
56
|
]);
|
|
53
57
|
return jsonResult({ database, rows });
|
|
54
58
|
} catch (err) {
|
|
@@ -66,13 +70,13 @@ export function registerDatabaseTools(server) {
|
|
|
66
70
|
'Use rootr_read_database instead when you also need the property/column definitions.',
|
|
67
71
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
68
72
|
inputSchema: {
|
|
69
|
-
databaseId: z.string().describe(
|
|
73
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
70
74
|
},
|
|
71
75
|
},
|
|
72
76
|
async ({ databaseId }) => {
|
|
73
77
|
try {
|
|
74
78
|
const client = makeClient();
|
|
75
|
-
const rows = await client.listRows(databaseId);
|
|
79
|
+
const rows = await client.listRows(await resolveNodeId(client, databaseId));
|
|
76
80
|
return jsonResult({ rows });
|
|
77
81
|
} catch (err) {
|
|
78
82
|
return errorResult(err);
|
|
@@ -91,7 +95,7 @@ export function registerDatabaseTools(server) {
|
|
|
91
95
|
'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index.',
|
|
92
96
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
93
97
|
inputSchema: {
|
|
94
|
-
databaseId: z.string().describe(
|
|
98
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
95
99
|
values: z.record(z.unknown()).describe('{ propertyNameOrId: value } for the new row'),
|
|
96
100
|
position: z.number().optional().describe('0-based insert index; appended at the end if omitted'),
|
|
97
101
|
},
|
|
@@ -99,9 +103,10 @@ export function registerDatabaseTools(server) {
|
|
|
99
103
|
async ({ databaseId, values, position }) => {
|
|
100
104
|
try {
|
|
101
105
|
const client = makeClient();
|
|
102
|
-
const
|
|
106
|
+
const id = await resolveNodeId(client, databaseId);
|
|
107
|
+
const database = await client.getDatabase(id);
|
|
103
108
|
const resolved = resolveValueKeys(database.properties || [], values);
|
|
104
|
-
const row = await client.createRow(
|
|
109
|
+
const row = await client.createRow(id, { values: resolved, position });
|
|
105
110
|
return jsonResult(row);
|
|
106
111
|
} catch (err) {
|
|
107
112
|
return errorResult(err);
|
|
@@ -120,7 +125,7 @@ export function registerDatabaseTools(server) {
|
|
|
120
125
|
'Optionally set `position` to reorder the row.',
|
|
121
126
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
122
127
|
inputSchema: {
|
|
123
|
-
databaseId: z.string().describe(
|
|
128
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
124
129
|
rowId: z.string().describe('Row id (must belong to the database)'),
|
|
125
130
|
values: z.record(z.unknown()).optional().describe('{ propertyNameOrId: newValue | null } partial patch'),
|
|
126
131
|
position: z.number().optional().describe('New 0-based position for the row'),
|
|
@@ -129,12 +134,13 @@ export function registerDatabaseTools(server) {
|
|
|
129
134
|
async ({ databaseId, rowId, values, position }) => {
|
|
130
135
|
try {
|
|
131
136
|
const client = makeClient();
|
|
137
|
+
const id = await resolveNodeId(client, databaseId);
|
|
132
138
|
let resolved;
|
|
133
139
|
if (values !== undefined) {
|
|
134
|
-
const database = await client.getDatabase(
|
|
140
|
+
const database = await client.getDatabase(id);
|
|
135
141
|
resolved = resolveValueKeys(database.properties || [], values);
|
|
136
142
|
}
|
|
137
|
-
const row = await client.updateRow(
|
|
143
|
+
const row = await client.updateRow(id, rowId, { values: resolved, position });
|
|
138
144
|
return jsonResult(row);
|
|
139
145
|
} catch (err) {
|
|
140
146
|
return errorResult(err);
|
|
@@ -151,14 +157,14 @@ export function registerDatabaseTools(server) {
|
|
|
151
157
|
'Verify the row via rootr_read_database first — do not guess rowIds.',
|
|
152
158
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
153
159
|
inputSchema: {
|
|
154
|
-
databaseId: z.string().describe(
|
|
160
|
+
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
155
161
|
rowId: z.string().describe('Row id to delete (must belong to the database)'),
|
|
156
162
|
},
|
|
157
163
|
},
|
|
158
164
|
async ({ databaseId, rowId }) => {
|
|
159
165
|
try {
|
|
160
166
|
const client = makeClient();
|
|
161
|
-
await client.deleteRow(databaseId, rowId);
|
|
167
|
+
await client.deleteRow(await resolveNodeId(client, databaseId), rowId);
|
|
162
168
|
return jsonResult({ deleted: true, rowId });
|
|
163
169
|
} catch (err) {
|
|
164
170
|
return errorResult(err);
|
|
@@ -31,7 +31,12 @@ export function registerDocumentTools(server) {
|
|
|
31
31
|
const p = n.path || '';
|
|
32
32
|
return p === path || p.startsWith(path.endsWith('/') ? path : path + '/');
|
|
33
33
|
})
|
|
34
|
-
.map((n) =>
|
|
34
|
+
.map((n) => {
|
|
35
|
+
const label = n.path || n.name || n.id;
|
|
36
|
+
// Keep the id visible: tools that take a node id (databases, sheets,
|
|
37
|
+
// whiteboards, …) are otherwise unreachable from a listing.
|
|
38
|
+
return n.id && label !== n.id ? `${n.type || '?'} ${label} [${n.id}]` : `${n.type || '?'} ${label}`;
|
|
39
|
+
})
|
|
35
40
|
.sort();
|
|
36
41
|
return textResult(rows.length ? rows.join('\n') : '(no matching nodes)');
|
|
37
42
|
} catch (err) {
|
|
@@ -189,7 +194,11 @@ export function registerDocumentTools(server) {
|
|
|
189
194
|
try {
|
|
190
195
|
const client = makeClient();
|
|
191
196
|
const results = await client.search(query);
|
|
192
|
-
const rows = results.map((r) =>
|
|
197
|
+
const rows = results.map((r) => {
|
|
198
|
+
const label = r.path || r.name || r.nodeId;
|
|
199
|
+
const id = r.nodeId && label !== r.nodeId ? ` [${r.nodeId}]` : '';
|
|
200
|
+
return `${label}${id} — ${r.snippet || ''}`;
|
|
201
|
+
});
|
|
193
202
|
return textResult(rows.length ? rows.join('\n') : '(no results)');
|
|
194
203
|
} catch (err) {
|
|
195
204
|
return errorResult(err);
|
|
@@ -256,7 +265,9 @@ export function registerDocumentTools(server) {
|
|
|
256
265
|
'and propose git-style edit suggestions the owner can merge. Publishing is an owner-level action: it needs ' +
|
|
257
266
|
'MANAGE permission, which workspace-scoped keys (including remote-connector tokens) do not carry — use an ' +
|
|
258
267
|
'ACCOUNT key (PAT) of the owner, or ask the user to publish from the share dialog. ' +
|
|
259
|
-
'
|
|
268
|
+
'Safe to call again on an already-published document (republish): the public slug/URL never changes, so ' +
|
|
269
|
+
'links already shared out stay valid — republishing just creates a new published version from the current draft. ' +
|
|
270
|
+
'Returns the slug/URL. Check rootr_document_public_status first; unpublish or change settings via rootr_set_document_public.',
|
|
260
271
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
261
272
|
inputSchema: {
|
|
262
273
|
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
@@ -278,19 +289,58 @@ export function registerDocumentTools(server) {
|
|
|
278
289
|
{
|
|
279
290
|
title: 'Update public settings of a Rootr document',
|
|
280
291
|
description:
|
|
281
|
-
'
|
|
292
|
+
'Partially update a published Rootr (루터) document\'s public settings — only the fields you pass are ' +
|
|
293
|
+
'changed, everything else (including slug) is left as-is. Examples: {"isPublic": false} to unpublish; ' +
|
|
294
|
+
'{"allowCommits": true} to let public readers propose edits; {"skin": "tech-dark"} or ' +
|
|
295
|
+
'{"layout": {"cover": true}} to restyle the public page; {"expiresAt": "2026-12-31T00:00:00Z"} to set an ' +
|
|
296
|
+
'auto-close date, or {"expiresAt": null} to clear it. ' +
|
|
282
297
|
'Owner-level action (MANAGE): works with an account key (PAT), not with workspace-scoped/connector tokens.',
|
|
283
298
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
284
299
|
inputSchema: {
|
|
285
300
|
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
286
301
|
isPublic: z.boolean().optional().describe('false to unpublish, true to re-enable'),
|
|
302
|
+
allowCommits: z.boolean().optional().describe('Whether public readers can propose git-style edit suggestions'),
|
|
303
|
+
expiresAt: z
|
|
304
|
+
.union([z.string(), z.null()])
|
|
305
|
+
.optional()
|
|
306
|
+
.describe('ISO date to auto-close the public page, or null to clear an existing expiry'),
|
|
307
|
+
skin: z
|
|
308
|
+
.enum([
|
|
309
|
+
'classic',
|
|
310
|
+
'minimal',
|
|
311
|
+
'magazine',
|
|
312
|
+
'paper',
|
|
313
|
+
'tech-dark',
|
|
314
|
+
'vivid',
|
|
315
|
+
'manual',
|
|
316
|
+
'book',
|
|
317
|
+
'editorial',
|
|
318
|
+
'bento',
|
|
319
|
+
'glass',
|
|
320
|
+
'mesh',
|
|
321
|
+
'immersive',
|
|
322
|
+
'brutal',
|
|
323
|
+
'neumorph',
|
|
324
|
+
'y2k',
|
|
325
|
+
])
|
|
326
|
+
.optional()
|
|
327
|
+
.describe('Visual skin for the public page'),
|
|
328
|
+
layout: z
|
|
329
|
+
.object({
|
|
330
|
+
cover: z.boolean().optional().describe('Show a cover banner'),
|
|
331
|
+
toc: z.boolean().optional().describe('Show a table of contents'),
|
|
332
|
+
width: z.enum(['narrow', 'wide']).optional().describe('Body width'),
|
|
333
|
+
paging: z.boolean().optional().describe('Paginate long documents'),
|
|
334
|
+
})
|
|
335
|
+
.optional()
|
|
336
|
+
.describe('Layout options; only the sub-fields you pass are changed'),
|
|
287
337
|
},
|
|
288
338
|
},
|
|
289
|
-
async ({ target, isPublic }) => {
|
|
339
|
+
async ({ target, isPublic, allowCommits, expiresAt, skin, layout }) => {
|
|
290
340
|
try {
|
|
291
341
|
const client = makeClient();
|
|
292
342
|
const id = requireResolved(await resolveTarget(client, target));
|
|
293
|
-
return jsonResult(await client.setDocumentPublic(id, { isPublic }));
|
|
343
|
+
return jsonResult(await client.setDocumentPublic(id, { isPublic, allowCommits, expiresAt, skin, layout }));
|
|
294
344
|
} catch (err) {
|
|
295
345
|
return errorResult(err);
|
|
296
346
|
}
|