rootr-cli 0.4.1 → 0.6.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 CHANGED
@@ -204,7 +204,7 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
204
204
 
205
205
  ## MCP 서버로 사용하기
206
206
 
207
- `rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 29종:
207
+ `rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 96종:
208
208
 
209
209
  노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
210
210
  `recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
@@ -282,6 +282,28 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
282
282
  | `rootr_create_webhook` | 웹훅 생성 — 응답의 secret은 **1회만 노출** |
283
283
  | `rootr_delete_webhook` | 웹훅 삭제 |
284
284
 
285
+ #### 표(DATABASE) — 행과 열 (docs:read/docs:write)
286
+
287
+ | 도구 | 설명 |
288
+ |---|---|
289
+ | `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** |
290
+ | `rootr_list_rows` | 행만 조회(스키마 제외) |
291
+ | `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로 지정) |
292
+ | `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값 변경) |
293
+ | `rootr_delete_row` | 행 삭제 |
294
+ | `rootr_add_column` | **열 추가** — 기존 열·행은 그대로 |
295
+ | `rootr_update_column` | 열 이름 변경 / select 선택지 교체 |
296
+ | `rootr_delete_column` | 열 삭제(파괴적) |
297
+
298
+ 열은 **한 번에 하나씩** 다룹니다. REST의 `PATCH /databases/:id`는 `properties`
299
+ 배열을 통째로 교체하기 때문에, 에이전트가 배열을 다시 만들다 빠뜨린 열이 그대로
300
+ 사라집니다. 위 세 도구는 서버가 병합하므로 나머지 열이 없어지지 않습니다.
301
+
302
+ - **열을 추가해도 기존 행 데이터는 건드리지 않습니다** — 새 열은 값이 채워지기 전까지 빈 칸으로 읽힙니다.
303
+ - **열의 `type`은 바꿀 수 없습니다**(모든 행의 값을 다시 써야 하므로). 새 열을 만들어 값을 옮기세요.
304
+ - `title` 열은 추가로 만들 수도, 삭제할 수도 없습니다.
305
+ - 삭제한 열의 값은 행 안에 남아 있지만 화면에는 나오지 않습니다 — 사용자 확인 후 호출하세요.
306
+
285
307
  #### 문서 버전 / 삭제
286
308
 
287
309
  | 도구 | 설명 |
@@ -289,10 +311,13 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
289
311
  | `rootr_document_versions` | 문서 버전 히스토리 조회 |
290
312
  | `rootr_delete_node` | 노드를 휴지통으로 이동(복구 가능) |
291
313
 
292
- #### 프레젠테이션 (현재 REST 전용 — 전용 MCP 도구는 예정)
314
+ #### 프레젠테이션
293
315
 
294
- 프레젠테이션(PRESENTATION) 노드는 아직 전용 MCP 도구가 없고, **같은 API 키로 REST를
295
- 직접 호출**해 저작합니다 (`x-api-key` 헤더, `docs:write`):
316
+ 프레젠테이션(PRESENTATION)은 전용 MCP 도구(`rootr_create_presentation`,
317
+ `rootr_read_presentation`, `rootr_append_presentation_slides`,
318
+ `rootr_update_presentation_slide`, `rootr_reorder_presentation_slides`,
319
+ `rootr_update_presentation`)로 저작합니다. 아래처럼 **같은 API 키로 REST를 직접
320
+ 호출**해도 됩니다 (`x-api-key` 헤더, `docs:write`):
296
321
 
297
322
  ```bash
298
323
  # 덱 생성 (슬라이드 포함 가능)
@@ -307,6 +332,33 @@ curl -X POST "$BASE/presentations/$ID/slides" -d '{"slides":[...]}' ...
307
332
  curl -X POST "$BASE/presentations/$ID/reorder" -d '{"order":["SLD-002","SLD-001"]}' ...
308
333
  ```
309
334
 
335
+ ##### 슬라이드에 다이어그램 그려 넣기
336
+
337
+ 구조도·업무 흐름도·순서도·데이터 흐름도·상태도는 손으로 SVG를 짜지 말고
338
+ `rootr_create_diagram_slide`에 **형식이 정해진 JSON 스펙**을 넘기세요. 서버가
339
+ 검증하고 그려서 슬라이드에 바로 꽂습니다 — 화면은 물론 PDF·PPTX 내보내기에도
340
+ 그대로 나옵니다. 슬라이드의 `diagram`(mermaid) 필드는 **뷰어가 그리지 않으므로**
341
+ 이 도구를 쓰는 것이 유일한 방법입니다.
342
+
343
+ 종류마다 스펙 모양이 다르니 **`rootr_diagram_schema`로 스키마와 예제를 먼저
344
+ 받아보세요.** 맞지 않는 스펙은 어느 항목이 왜 틀렸는지와 고치는 법을 달아 거절되니,
345
+ 그대로 고쳐 다시 부르면 됩니다.
346
+
347
+ ```bash
348
+ # 스펙 모양 보기 (architecture | workflow | sequence | dataflow | lifecycle)
349
+ curl "$BASE/diagram-types/workflow/schema" -H "x-api-key: $KEY"
350
+
351
+ # 그려서 슬라이드에 꽂기 (slideId를 주면 그 슬라이드를 다시 그린다)
352
+ curl -X POST "$BASE/presentations/$ID/diagram-slides" -H "x-api-key: $KEY" \
353
+ -H 'Content-Type: application/json' \
354
+ -d '{"type":"workflow","spec":{...},"title":"이슈는 검수를 통과해야만 main에 들어간다",
355
+ "blocks":[{"heading":"검수가 관문","body":"..."}]}'
356
+ ```
357
+
358
+ `quality`는 기본 `standard`이고, `showcase`는 손으로 다듬은 대표 그림용 엄격
359
+ 기준이라 `standard`가 통과시키는 스펙도 거절합니다. 그림 속 글자는 지식 그래프에
360
+ 들어가지 않으니 사실은 `title`·`blocks`·`notes`에도 남기세요.
361
+
310
362
  **고품질 덱을 만들 때는 슬라이드 `html` 필드에 1280×720 완결 HTML을 직접 구워
311
363
  넣는 방식을 권장**합니다 — 뷰어가 기본 템플릿 대신 그대로 렌더합니다(완전 자유
312
364
  디자인). 필수 규칙(외부 요청 차단이라 전부 인라인, 이미지=data URI 내장, 폰트
package/lib/client.js CHANGED
@@ -565,6 +565,31 @@ export class RootrClient {
565
565
  );
566
566
  }
567
567
 
568
+ // ---- DB-COL: one column at a time --------------------------------------
569
+ // PATCH /databases/:id replaces the whole properties array, so rebuilding it
570
+ // client-side drops any column left out. These merge server-side instead.
571
+
572
+ async addColumn(databaseId, { name, type, options } = {}) {
573
+ return this.request(`/databases/${encodeURIComponent(databaseId)}/columns`, {
574
+ method: 'POST',
575
+ body: { name, type, options },
576
+ });
577
+ }
578
+
579
+ async updateColumn(databaseId, propertyId, { name, options } = {}) {
580
+ return this.request(
581
+ `/databases/${encodeURIComponent(databaseId)}/columns/${encodeURIComponent(propertyId)}`,
582
+ { method: 'PATCH', body: { name, options } },
583
+ );
584
+ }
585
+
586
+ async deleteColumn(databaseId, propertyId) {
587
+ return this.request(
588
+ `/databases/${encodeURIComponent(databaseId)}/columns/${encodeURIComponent(propertyId)}`,
589
+ { method: 'DELETE' },
590
+ );
591
+ }
592
+
568
593
  // ---- CRM ----------------------------------------------------------------
569
594
 
570
595
  /** Create a CRM node. pipelines/fields/config optional (server fills sane defaults). */
@@ -852,6 +877,24 @@ export class RootrClient {
852
877
  });
853
878
  }
854
879
 
880
+ /** Diagram families the server can draw, for discovery. */
881
+ async diagramTypes() {
882
+ return this.request('/diagram-types');
883
+ }
884
+
885
+ /** JSON Schema + a worked example for one diagram family. */
886
+ async diagramSchema(type) {
887
+ return this.request(`/diagram-types/${encodeURIComponent(type)}/schema`);
888
+ }
889
+
890
+ /** Render a typed diagram spec onto a slide (appends, or replaces slideId). */
891
+ async createDiagramSlide(presentationId, payload) {
892
+ return this.request(`/presentations/${encodeURIComponent(presentationId)}/diagram-slides`, {
893
+ method: 'POST',
894
+ body: payload,
895
+ });
896
+ }
897
+
855
898
  async updatePresentationSlide(presentationId, slideId, slide) {
856
899
  return this.request(
857
900
  `/presentations/${encodeURIComponent(presentationId)}/slides/${encodeURIComponent(slideId)}`,
@@ -171,4 +171,102 @@ export function registerDatabaseTools(server) {
171
171
  }
172
172
  },
173
173
  );
174
+
175
+ // ---- DB-COL: columns ----------------------------------------------------
176
+ // Row tools above cover the data; these cover the SHAPE. Deliberately one
177
+ // column per call: the underlying PATCH /databases/:id takes the entire
178
+ // properties array, and a model rebuilding that array drops whatever it
179
+ // forgets. Here the server merges, so nothing else in the table can be lost.
180
+
181
+ server.registerTool(
182
+ 'rootr_add_column',
183
+ {
184
+ title: 'Add a column to a database',
185
+ description:
186
+ 'Add ONE column to a Rootr (루터) DATABASE. The existing columns and every row are left untouched — ' +
187
+ 'rows simply read empty for the new column until you fill them with rootr_update_row. ' +
188
+ 'type ∈ text | number | select | multi_select | date | checkbox | url | person ' +
189
+ '("title" is not allowed: a database already has exactly one title column). ' +
190
+ 'For select/multi_select pass options: [{ name, color }]. Returns the created column WITH ITS id.',
191
+ annotations: { readOnlyHint: false, destructiveHint: false },
192
+ inputSchema: {
193
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
194
+ name: z.string().describe('Column header, e.g. "담당자"'),
195
+ type: z
196
+ .enum(['text', 'number', 'select', 'multi_select', 'date', 'checkbox', 'url', 'person'])
197
+ .describe('Column type'),
198
+ options: z
199
+ .array(z.object({ name: z.string(), color: z.string() }))
200
+ .optional()
201
+ .describe('select/multi_select only: the choices, e.g. [{ name: "진행중", color: "blue" }]'),
202
+ },
203
+ },
204
+ async ({ databaseId, name, type, options }) => {
205
+ try {
206
+ const client = makeClient();
207
+ const id = await resolveNodeId(client, databaseId);
208
+ const property = await client.addColumn(id, { name, type, options });
209
+ return jsonResult({ property });
210
+ } catch (err) {
211
+ return errorResult(err);
212
+ }
213
+ },
214
+ );
215
+
216
+ server.registerTool(
217
+ 'rootr_update_column',
218
+ {
219
+ title: 'Rename a column / set its options',
220
+ description:
221
+ 'Rename ONE column of a Rootr (루터) DATABASE, and/or replace the choices of a select/multi_select column. ' +
222
+ 'The column TYPE cannot be changed — that would rewrite every row — so add a new column and move the values instead. ' +
223
+ 'Call rootr_read_database first to get the exact propertyId.',
224
+ annotations: { readOnlyHint: false, destructiveHint: false },
225
+ inputSchema: {
226
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
227
+ propertyId: z.string().describe('Column id from rootr_read_database (NOT the header text)'),
228
+ name: z.string().optional().describe('New header text'),
229
+ options: z
230
+ .array(z.object({ name: z.string(), color: z.string() }))
231
+ .optional()
232
+ .describe('select/multi_select only: REPLACES the full choice list'),
233
+ },
234
+ },
235
+ async ({ databaseId, propertyId, name, options }) => {
236
+ try {
237
+ const client = makeClient();
238
+ const id = await resolveNodeId(client, databaseId);
239
+ const property = await client.updateColumn(id, propertyId, { name, options });
240
+ return jsonResult({ property });
241
+ } catch (err) {
242
+ return errorResult(err);
243
+ }
244
+ },
245
+ );
246
+
247
+ server.registerTool(
248
+ 'rootr_delete_column',
249
+ {
250
+ title: 'Delete a column from a database',
251
+ description:
252
+ 'Remove ONE column from a Rootr (루터) DATABASE. The values stored under it are NOT erased from the rows, ' +
253
+ 'so re-adding a column is not enough to bring them back in the UI — confirm with the user before calling this. ' +
254
+ 'The title column cannot be deleted. Views grouped/dated by the column are cleared automatically.',
255
+ annotations: { readOnlyHint: false, destructiveHint: true },
256
+ inputSchema: {
257
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
258
+ propertyId: z.string().describe('Column id from rootr_read_database'),
259
+ },
260
+ },
261
+ async ({ databaseId, propertyId }) => {
262
+ try {
263
+ const client = makeClient();
264
+ const id = await resolveNodeId(client, databaseId);
265
+ const property = await client.deleteColumn(id, propertyId);
266
+ return jsonResult({ deleted: true, property });
267
+ } catch (err) {
268
+ return errorResult(err);
269
+ }
270
+ },
271
+ );
174
272
  }
@@ -265,7 +265,9 @@ export function registerDocumentTools(server) {
265
265
  'and propose git-style edit suggestions the owner can merge. Publishing is an owner-level action: it needs ' +
266
266
  'MANAGE permission, which workspace-scoped keys (including remote-connector tokens) do not carry — use an ' +
267
267
  'ACCOUNT key (PAT) of the owner, or ask the user to publish from the share dialog. ' +
268
- 'Returns the slug/URL. Check rootr_document_public_status first; unpublish via rootr_set_document_public.',
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.',
269
271
  annotations: { readOnlyHint: false, destructiveHint: false },
270
272
  inputSchema: {
271
273
  target: z.string().describe('Document path (starting with "/") or node id'),
@@ -287,19 +289,58 @@ export function registerDocumentTools(server) {
287
289
  {
288
290
  title: 'Update public settings of a Rootr document',
289
291
  description:
290
- 'Update a published Rootr (루터) document\'s public settings — e.g. {"isPublic": false} to unpublish. ' +
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. ' +
291
297
  'Owner-level action (MANAGE): works with an account key (PAT), not with workspace-scoped/connector tokens.',
292
298
  annotations: { readOnlyHint: false, destructiveHint: false },
293
299
  inputSchema: {
294
300
  target: z.string().describe('Document path (starting with "/") or node id'),
295
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'),
296
337
  },
297
338
  },
298
- async ({ target, isPublic }) => {
339
+ async ({ target, isPublic, allowCommits, expiresAt, skin, layout }) => {
299
340
  try {
300
341
  const client = makeClient();
301
342
  const id = requireResolved(await resolveTarget(client, target));
302
- return jsonResult(await client.setDocumentPublic(id, { isPublic }));
343
+ return jsonResult(await client.setDocumentPublic(id, { isPublic, allowCommits, expiresAt, skin, layout }));
303
344
  } catch (err) {
304
345
  return errorResult(err);
305
346
  }
@@ -361,4 +361,90 @@ export function registerPresentationTools(server) {
361
361
  }
362
362
  },
363
363
  );
364
+
365
+ server.registerTool(
366
+ 'rootr_diagram_schema',
367
+ {
368
+ title: 'Get the spec shape for a diagram type',
369
+ description:
370
+ 'Return the JSON Schema and a worked example for one Rootr (루터) diagram family: architecture (components, ' +
371
+ 'services, stores, boundaries), workflow (swimlane process with approvals/retries), sequence (call order ' +
372
+ 'between participants), dataflow (pipelines, lineage), or lifecycle (states, retries, outcomes). ' +
373
+ 'ALWAYS call this before rootr_create_diagram_slide for a family you have not authored yet — every family ' +
374
+ 'has a different shape (a workflow wants lanes/nodes/edges, an architecture wants components/connections), ' +
375
+ 'and the renderer rejects a spec that does not match. Reading the schema first turns a guess-and-fail into ' +
376
+ 'one correct call. Pass no type to list the available families.',
377
+ annotations: { readOnlyHint: true },
378
+ inputSchema: {
379
+ type: z
380
+ .enum(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'])
381
+ .optional()
382
+ .describe('Diagram family; omit to list the available families'),
383
+ },
384
+ },
385
+ async ({ type }) => {
386
+ try {
387
+ const client = makeClient();
388
+ return jsonResult(type ? await client.diagramSchema(type) : await client.diagramTypes());
389
+ } catch (err) {
390
+ return errorResult(err);
391
+ }
392
+ },
393
+ );
394
+
395
+ server.registerTool(
396
+ 'rootr_create_diagram_slide',
397
+ {
398
+ title: 'Draw a diagram onto a slide',
399
+ description:
400
+ 'Render a typed diagram spec into a Rootr (루터) presentation slide — architecture, workflow, sequence, ' +
401
+ 'dataflow, or lifecycle. The drawing is baked into the slide, so it shows on screen AND in the PDF/PPTX ' +
402
+ 'exports. Use this instead of the mermaid `diagram` field, which the slide viewer does not draw.\n' +
403
+ 'Call rootr_diagram_schema first to get the exact shape for the family you want — the spec is validated ' +
404
+ 'fail-closed and a mismatch is rejected with the offending field named and a suggested fix, which you ' +
405
+ 'should apply and retry.\n' +
406
+ 'Meaning must live in TEXT: `title` (one assertion-style claim, e.g. "Checkout fails when the cache is ' +
407
+ 'cold" — not a topic label like "Checkout"), `blocks[]`, and `notes` are what connects into the knowledge ' +
408
+ 'graph. The picture itself is NEVER indexed, so never put a fact only in the drawing.\n' +
409
+ 'Pass `slideId` to redraw an existing slide; omit it to append a new one. Quality defaults to "standard"; ' +
410
+ '"showcase" adds a strict composition bar (turn run-ups, legend fit) meant for a hand-tuned hero diagram ' +
411
+ 'and will reject specs that "standard" accepts.',
412
+ annotations: { readOnlyHint: false, destructiveHint: false },
413
+ inputSchema: {
414
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
415
+ type: z
416
+ .enum(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'])
417
+ .describe('Diagram family — see rootr_diagram_schema'),
418
+ spec: z.record(z.any()).describe('The typed diagram spec matching that family\'s JSON Schema'),
419
+ title: z.string().optional().describe('Assertion-style slide title — text spine, also drawn as the heading'),
420
+ subtitle: z.string().optional().describe('One supporting line drawn under the title'),
421
+ blocks: z
422
+ .array(z.object({ heading: z.string().optional(), body: z.string().optional() }).passthrough())
423
+ .optional()
424
+ .describe('Text spine — the facts the diagram shows, so they reach the knowledge graph'),
425
+ notes: z.string().optional().describe('Speaker notes — also part of the text spine'),
426
+ slideId: z.string().optional().describe('Redraw this slide instead of appending a new one'),
427
+ quality: z.enum(['standard', 'showcase']).optional().describe('Default "standard"'),
428
+ },
429
+ },
430
+ async ({ presentationId, type, spec, title, subtitle, blocks, notes, slideId, quality }) => {
431
+ try {
432
+ const client = makeClient();
433
+ presentationId = await resolveNodeId(client, presentationId);
434
+ const result = await client.createDiagramSlide(presentationId, {
435
+ type,
436
+ spec,
437
+ title,
438
+ subtitle,
439
+ blocks,
440
+ notes,
441
+ slideId,
442
+ quality,
443
+ });
444
+ return jsonResult(result);
445
+ } catch (err) {
446
+ return errorResult(err);
447
+ }
448
+ },
449
+ );
364
450
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Official Rootr CLI + MCP server — read, write, edit and search Rootr documents from the shell or from an AI agent.",
5
5
  "type": "module",
6
6
  "bin": {