rootr-cli 0.22.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 +14 -0
- package/README.md +20 -4
- package/bin/rootr.js +37 -5
- package/lib/client.js +10 -0
- package/lib/mcp-tools/attachments.js +58 -1
- package/lib/mcp-tools/booking.js +21 -1
- package/lib/mcp-tools/pages.js +6 -4
- package/lib/mcp-tools/presentations.js +19 -0
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -192,6 +192,7 @@ rootr rm /notes/todo.md --yes # 확인 없이 (스크립트용)
|
|
|
192
192
|
```bash
|
|
193
193
|
rootr ws # 목록 (현재 워크스페이스에 *, 못 여는 곳은 표시)
|
|
194
194
|
rootr use "영업팀" # 전환 — id · 이름 · rootr.io 주소 아무거나
|
|
195
|
+
rootr emblem ./logo.png # 워크스페이스 엠블럼(사이드바 로고, 관리자) — --clear 면 지움
|
|
195
196
|
```
|
|
196
197
|
|
|
197
198
|
`rootr ws` 출력은 `id\tname` 목록입니다. 등록된 키가 워크스페이스 전용 키뿐이면 목록에는
|
|
@@ -401,6 +402,7 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
401
402
|
| `rootr_create_workspace` | **계정 키(PAT) + workspaces:create** | 짓기 3단계 — 계획된 트리로 새 워크스페이스 생성(검사 결과도 같이 돌려준다) |
|
|
402
403
|
| `rootr_scaffold_apply` | docs:write | 짓기 3단계 — 기존 워크스페이스에 트리 적용(검사 결과도 같이 돌려준다) |
|
|
403
404
|
| `rootr_delete_workspace` | **계정 키(PAT) + OWNER** | 짓다 실패해 남은 **빈 워크스페이스 치우기**. 이름을 그대로 다시 적어야 지워진다(되돌릴 수 없음) |
|
|
405
|
+
| `rootr_set_workspace_emblem` | docs:write + **ADMIN** | 워크스페이스 **엠블럼**(사이드바 배지·전환기 로고) 바꾸기 — 이미지 파일·이미 올린 첨부 id·`clear` 중 하나 |
|
|
404
406
|
|
|
405
407
|
스캐폴딩은 **plan → 물어보기 → 설계 → review → 고치기 → apply** 한 바퀴입니다.
|
|
406
408
|
`review` 를 건너뛰면 "얇은 마크다운 파일 묶음"이 됩니다. CLI 로도 같은 세 단계를 쓸 수 있습니다:
|
|
@@ -653,6 +655,11 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
653
655
|
| `rootr_set_booking` | 예약 켜기/끄기와 규칙(요일·시간·길이·여유·며칠 전·하루 최대·쉬는 날·**막는 시간**·확정 방식·물어볼 칸·**결과 메일**·**구글 밋**·**방문자 직접 취소**·**방문자 직접 시간 변경**·**미리 알림**) |
|
|
654
656
|
| `rootr_booking_slots` | 방문자에게 실제로 보이는 빈 시간(규칙이 뜻대로 걸렸는지 확인) |
|
|
655
657
|
|
|
658
|
+
여유 시간은 둘입니다. `bufferMin`은 이 표로 잡힌 **예약** 앞뒤(CLI `--buffer`), `calendarBufferMin`은
|
|
659
|
+
연결한 구글 캘린더에 **이미 있던 일정** 앞뒤(CLI `--calendar-buffer 60`) — 밖에서 하는 미팅의 이동 시간
|
|
660
|
+
같은 것입니다. 예: 구글 쪽 60분·예약 0분이면 구글에 있는 14~15시 미팅 때문에 13~16시가 닫히고, 온라인
|
|
661
|
+
상담 예약끼리는 붙여 잡힙니다. 이 표가 구글에 적은 예약은 "이미 있던 일정"으로 세지 않습니다.
|
|
662
|
+
|
|
656
663
|
방문자 화면은 **방문자 자신의 시간대**로 빈 시간을 보여주고(고칠 수도 있습니다), 서버와
|
|
657
664
|
주고받는 값은 끝까지 주인 시간대입니다. 확정·이동 메일에는 **일정 초대장(.ics)** 이 붙어
|
|
658
665
|
방문자 캘린더에 바로 들어가고, 시간을 옮기면 그 일정이 고쳐지고 취소하면 사라집니다.
|
|
@@ -822,6 +829,13 @@ HTML을 짜는 것이 아니라 **정해진 블록 15종을 골라 쌓습니다*
|
|
|
822
829
|
화면 한 장이 되고(숫자 `height`를 같이 주면 그 값이 하한), `bleed: true`와 함께 쓰면 화면을 꽉 채우는
|
|
823
830
|
섹션이 됩니다. 맨 위 이름 줄은 `config.navEnabled: false`로 끕니다.
|
|
824
831
|
|
|
832
|
+
**메타(페이스북·인스타그램) 광고 성과 재기** — `config.metaPixelId`에 픽셀 ID(숫자)를 넣으면 발행한
|
|
833
|
+
페이지가 방문(`PageView`)과 `booking` 블록의 예약 완료(`Schedule`)를 그 픽셀로 보냅니다. 편집 화면에서는
|
|
834
|
+
싣지 않고, `""`를 보내면 꺼집니다. `config`는 고칠 때 통째로 바뀌니 다른 값도 같이 보내세요. 어느 광고에서
|
|
835
|
+
온 예약인지 표에서 바로 보려면 예약 표에 유입 경로 기록을 켭니다(`rootr booking set <표> --track-source on`,
|
|
836
|
+
MCP는 `rootr_set_booking`의 `trackSource: true`). "유입 경로" 칸에 방문자가 들어온 주소의 꼬리표가
|
|
837
|
+
`source=meta, medium=paid, campaign=ax, content=3-docs`처럼 적힙니다.
|
|
838
|
+
|
|
825
839
|
**스크롤 등장 효과** — 어느 블록이나 `reveal: "fade"`(서서히) 또는 `"fade-up"`(아래에서 위로)을 주면
|
|
826
840
|
(기본 `"none"`), 발행한 페이지에서 그 블록이 화면에 들어올 때 한 번 나타납니다(큰 제목은 제목 → 부제 →
|
|
827
841
|
버튼 순서로). `custom`에 `reveal`을 주면 **틀 안의 CSS 애니메이션도 그 순간에 시작**합니다 — 틀을 블록이
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ citations back to the exact paragraph.
|
|
|
11
11
|
*(한국어 문서: [README.ko.md](https://github.com/gwmage/rootr-cli/blob/main/README.ko.md))*
|
|
12
12
|
|
|
13
13
|
- `rootr` CLI — documents, databases, forms, signed documents, publishing, permissions, memory
|
|
14
|
-
- `rootr mcp` — stdio MCP server, **
|
|
14
|
+
- `rootr mcp` — stdio MCP server, **193 tools** (see [MCP tools](#mcp-tools))
|
|
15
15
|
|
|
16
16
|
Plain Node.js ESM, no build step. Node 18+ (uses the built-in `fetch`).
|
|
17
17
|
|
|
@@ -180,6 +180,7 @@ as-is. `write --if-match` returns 412 if the document changed in the meantime.
|
|
|
180
180
|
```bash
|
|
181
181
|
rootr ws # id<TAB>name
|
|
182
182
|
rootr use "Sales" # switch — id, name, or rootr.io URL
|
|
183
|
+
rootr emblem ./logo.png # workspace emblem (sidebar logo, admin) — --clear removes it
|
|
183
184
|
rootr ask "why did latency go up after last week's deploy?"
|
|
184
185
|
rootr ask "why did payment failures rise?" --workspace ws_123
|
|
185
186
|
```
|
|
@@ -407,7 +408,7 @@ is refused and says when the grace was used.
|
|
|
407
408
|
|
|
408
409
|
## MCP tools
|
|
409
410
|
|
|
410
|
-
`rootr mcp` speaks MCP over stdio.
|
|
411
|
+
`rootr mcp` speaks MCP over stdio. 193 tools:
|
|
411
412
|
|
|
412
413
|
| Group | Count | Tools |
|
|
413
414
|
|---|---|---|
|
|
@@ -417,7 +418,7 @@ is refused and says when the grace was used.
|
|
|
417
418
|
| Signed documents & research notes | 14 | create, sign, revise, reject, reopen, approval lines, signing codes, ledger verification, research-note preset |
|
|
418
419
|
| Forms | 10 | create, read, update, responses, share links, collectors |
|
|
419
420
|
| Presentations & images | 10 | decks, slides, diagram slides, image generation and background removal |
|
|
420
|
-
| Workspaces & scaffolding |
|
|
421
|
+
| Workspaces & scaffolding | 10 | list, create, delete, switch, set the emblem (sidebar logo), plan / review / apply a document tree, health check |
|
|
421
422
|
| Pages | 6 | HTML page nodes: create, read, update, append and patch blocks, live data |
|
|
422
423
|
| Spreadsheets | 7 | create, read, update, per-sheet create/update/delete, patch cells & formulas |
|
|
423
424
|
| Recordings | 7 | create, upload & download audio, transcribe, summarize, rename speakers |
|
|
@@ -433,6 +434,10 @@ is refused and says when the grace was used.
|
|
|
433
434
|
| Root-cause Q&A | 1 | ask the workspace knowledge graph, answer with citations |
|
|
434
435
|
| Feedback | 1 | send feedback to the Rootr team |
|
|
435
436
|
|
|
437
|
+
Two gaps keep breathing room: `bufferMin` around bookings made through the table (CLI `--buffer`) and
|
|
438
|
+
`calendarBufferMin` around events already on the connected Google Calendar, e.g. travel time to an outside
|
|
439
|
+
meeting (CLI `--calendar-buffer 60`). Bookings the table wrote to Google count as bookings, not existing events.
|
|
440
|
+
|
|
436
441
|
Open hours can be narrowed by blocked times — one afternoon, lunch every day, Monday mornings, or a whole
|
|
437
442
|
weekday — with `blockedTimes` on `rootr_set_booking` (CLI: `--block-time "daily 12:00-13:00, mon 09:00-11:00, fri"`).
|
|
438
443
|
|
|
@@ -478,6 +483,14 @@ viewport tall on every device (a numeric `height` given with it is the floor), s
|
|
|
478
483
|
`bleed: true` + `minHeight: "screen"` is a section that fills the screen. Pair them with
|
|
479
484
|
`config.navEnabled: false`.
|
|
480
485
|
|
|
486
|
+
To **measure Meta (Facebook/Instagram) ads**, set `config.metaPixelId` to the numeric Pixel ID: the
|
|
487
|
+
published page then reports `PageView` on load and `Schedule` when a visitor completes a booking in a
|
|
488
|
+
`booking` block. The editor never loads the pixel; send `""` to turn it off. `config` is replaced as a
|
|
489
|
+
whole on update, so resend the other keys with it. To see which ad each booking came from in the table
|
|
490
|
+
itself, turn on source tracking on the booking table (`rootr booking set <table> --track-source on`,
|
|
491
|
+
or `trackSource: true` in `rootr_set_booking`): a "유입 경로" column is filled from the visitor's
|
|
492
|
+
address, e.g. `source=meta, medium=paid, campaign=ax, content=3-docs`.
|
|
493
|
+
|
|
481
494
|
For **scroll-in animation**, any block takes `reveal: "fade"` or `"fade-up"` (default `"none"`): on
|
|
482
495
|
the published page it appears once, when it scrolls into view (a hero staggers title → subtitle →
|
|
483
496
|
button). On a `custom` block `reveal` also **starts the CSS animations inside the frame at that
|
|
@@ -536,7 +549,10 @@ Decks are authored with `rootr_create_presentation`, `rootr_read_presentation`,
|
|
|
536
549
|
`rootr_append_presentation_slides`, `rootr_update_presentation_slide`,
|
|
537
550
|
`rootr_reorder_presentation_slides` and `rootr_update_presentation`. Artwork can be produced with
|
|
538
551
|
`rootr_generate_image` (image from a sentence) and `rootr_remove_image_background` — both spend
|
|
539
|
-
AI credits.
|
|
552
|
+
AI credits. Slides may also carry `videos[]` (uploaded mp4/webm ≤75MB, https video files, YouTube/Vimeo links;
|
|
553
|
+
Team plan and above), and any element in slide `html` can animate with `data-anim="fade-up"` (also fade,
|
|
554
|
+
fade-down, fly-left, fly-right, zoom, pop, blur, and looping pulse/float/spin/blink/shake; timing via
|
|
555
|
+
`--anim-dur`/`--anim-delay` inline CSS variables; all plans). The same thing works over plain REST with the same API key (`x-api-key`,
|
|
540
556
|
`docs:write`):
|
|
541
557
|
|
|
542
558
|
```bash
|
package/bin/rootr.js
CHANGED
|
@@ -28,6 +28,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
28
28
|
rootr restore <id> 지운 것 되돌리기 (화면의 Ctrl+Z 와 같은 일 — 경로 아닌 id)
|
|
29
29
|
rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>] 파일 업로드(+문서에 삽입)
|
|
30
30
|
rootr ws 내 워크스페이스 목록 (id\tname, 열 수 없는 곳은 표시)
|
|
31
|
+
rootr emblem <이미지 파일> | --clear 워크스페이스 엠블럼(사이드바 로고) 바꾸기·지우기 (관리자)
|
|
31
32
|
rootr use <id|이름|주소> 활성 워크스페이스 전환 (그 워크스페이스용 키도 기억)
|
|
32
33
|
rootr ask <question> [--workspace <w>] GraphRAG 질의응답 (RCA), 인용 포함 출력
|
|
33
34
|
rootr db create [--file <spec.json>] [--parent <path|id>] 표(DATABASE) 생성 — 컬럼 순서 그대로, 행까지 한 번에
|
|
@@ -134,6 +135,7 @@ usage:
|
|
|
134
135
|
rootr restore <id> undo a delete (same as Ctrl+Z in the app — id, not path)
|
|
135
136
|
rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>] upload a file (+ insert it into a document)
|
|
136
137
|
rootr ws my workspaces (id\tname; unreachable ones are marked)
|
|
138
|
+
rootr emblem <image file> | --clear set or clear the workspace emblem (sidebar logo; admin)
|
|
137
139
|
rootr use <id|name|url> switch the active workspace (remembers its key too)
|
|
138
140
|
rootr ask <question> [--workspace <w>] GraphRAG question answering (RCA), prints citations
|
|
139
141
|
rootr db create [--file <spec.json>] [--parent <path|id>] create a DATABASE — columns in order, rows included
|
|
@@ -597,6 +599,30 @@ async function cmdAttach(args) {
|
|
|
597
599
|
console.log(markdown);
|
|
598
600
|
}
|
|
599
601
|
|
|
602
|
+
/** WS-EMBLEM — 설정 → 일반 → 엠블럼과 같은 일. 이미지를 올려 걸거나(--clear 면) 이름 첫 글자 배지로 되돌린다. */
|
|
603
|
+
async function cmdEmblem(args) {
|
|
604
|
+
const filePath = args._[0];
|
|
605
|
+
if (!filePath && !args.clear) throw new Error(usage(`rootr emblem <image file> | --clear`));
|
|
606
|
+
const cfg = loadConfig();
|
|
607
|
+
const workspace = args.workspace || cfg.workspace;
|
|
608
|
+
if (!workspace) {
|
|
609
|
+
throw new RootrApiError(tr('워크스페이스가 설정되지 않았습니다.', `No workspace is configured.`), {
|
|
610
|
+
hint: tr('--workspace <id>로 넘기거나 rootr config --workspace <id> 또는 ROOTR_WORKSPACE 환경변수를 지정하세요.', `Pass --workspace <id>, or set it with rootr config --workspace <id> or the ROOTR_WORKSPACE environment variable.`),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const client = new RootrClient({ ...cfg, workspace, apiKey: keyForWorkspace(cfg, workspace) });
|
|
614
|
+
let id = null;
|
|
615
|
+
if (filePath) {
|
|
616
|
+
const mimeType = args.mime || MIME_BY_EXT[extname(filePath).toLowerCase()];
|
|
617
|
+
if (!mimeType || !mimeType.startsWith('image/')) {
|
|
618
|
+
throw new RootrApiError(tr('엠블럼은 이미지 파일만 됩니다 (png·jpg·webp·svg·gif).', 'The emblem must be an image file (png, jpg, webp, svg, gif).'));
|
|
619
|
+
}
|
|
620
|
+
({ id } = await client.uploadAttachment(workspace, { filename: basename(filePath), mimeType, data: readFileSync(filePath) }));
|
|
621
|
+
}
|
|
622
|
+
await client.setWorkspaceEmblem(workspace, id);
|
|
623
|
+
console.log(id ? tr(`엠블럼을 바꿨습니다: ${client.attachmentRawUrl(id)}`, `Emblem set: ${client.attachmentRawUrl(id)}`) : tr('엠블럼을 지웠습니다 (이름 첫 글자 배지).', 'Emblem cleared (initial-letter badge).'));
|
|
624
|
+
}
|
|
625
|
+
|
|
600
626
|
async function cmdWs() {
|
|
601
627
|
const cfg = loadConfig();
|
|
602
628
|
const candidates = candidateClients(cfg);
|
|
@@ -1917,24 +1943,24 @@ async function cmdBooking(args) {
|
|
|
1917
1943
|
usage(tr(`rootr booking show <path|id>`, `rootr booking show <path|id>`)) +
|
|
1918
1944
|
tr(
|
|
1919
1945
|
`\n rootr booking set <path|id> [--on|--off] [--days 1,2,3,4,5] [--hours 10:00-18:00]\n` +
|
|
1920
|
-
` [--duration 30] [--buffer 10] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1946
|
+
` [--duration 30] [--buffer 10] [--calendar-buffer 60] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1921
1947
|
` [--block 2026-10-03,2026-10-09] [--approval instant|manual] [--tz Asia/Seoul]\n` +
|
|
1922
1948
|
` [--block-time "daily 12:00-13:00=점심, mon 09:00-11:00, fri"]\n` +
|
|
1923
1949
|
` [--meet on|off] [--notify-visitor on|off] [--notify-email a@b.com,c@d.com] [--self-cancel on|off]\n` +
|
|
1924
1950
|
` [--self-move on|off] [--remind 1d,1h (안 보내려면 --remind off)]\n` +
|
|
1925
|
-
` [--types "30분 상담=30m,데모=60m|화면 보며 설명"] [--guests on|off] [--max-guests 5]\n` +
|
|
1951
|
+
` [--types "30분 상담=30m,데모=60m|화면 보며 설명"] [--guests on|off] [--max-guests 5] [--track-source on|off]\n` +
|
|
1926
1952
|
` [--after https://example.com/thanks]\n` +
|
|
1927
1953
|
` [--hosts "김상담=a@acme.com,이데모=b@acme.com"] [--assign roundrobin|collective|off]\n` +
|
|
1928
1954
|
` [--ask "연락처:required,상담 종류"] [--message "안내 문구"]\n` +
|
|
1929
1955
|
` [--gcal-connection <id>] [--gcal-busy <캘린더id,...>] [--gcal-write <캘린더id|none>]\n` +
|
|
1930
1956
|
` rootr booking slots <path|id> [--from 2026-10-01] [--to 2026-10-31] [--type <종류 이름표>]\n`,
|
|
1931
1957
|
`\n rootr booking set <path|id> [--on|--off] [--days 1,2,3,4,5] [--hours 10:00-18:00]\n` +
|
|
1932
|
-
` [--duration 30] [--buffer 10] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1958
|
+
` [--duration 30] [--buffer 10] [--calendar-buffer 60] [--notice 2] [--ahead 60] [--per-day 4]\n` +
|
|
1933
1959
|
` [--block 2026-10-03,2026-10-09] [--approval instant|manual] [--tz Asia/Seoul]\n` +
|
|
1934
1960
|
` [--block-time "daily 12:00-13:00=lunch, mon 09:00-11:00, fri"]\n` +
|
|
1935
1961
|
` [--meet on|off] [--notify-visitor on|off] [--notify-email a@b.com,c@d.com] [--self-cancel on|off]\n` +
|
|
1936
1962
|
` [--self-move on|off] [--remind 1d,1h (--remind off to stop)]\n` +
|
|
1937
|
-
` [--types "Intro=30m,Demo=60m|walk through the product"] [--guests on|off] [--max-guests 5]\n` +
|
|
1963
|
+
` [--types "Intro=30m,Demo=60m|walk through the product"] [--guests on|off] [--max-guests 5] [--track-source on|off]\n` +
|
|
1938
1964
|
` [--after https://example.com/thanks]\n` +
|
|
1939
1965
|
` [--hosts "Sam=a@acme.com,Lee=b@acme.com"] [--assign roundrobin|collective|off]\n` +
|
|
1940
1966
|
` [--ask "Phone:required,Topic"] [--message "note for visitors"]\n` +
|
|
@@ -1958,7 +1984,7 @@ async function cmdBooking(args) {
|
|
|
1958
1984
|
const days = (b.weekdays || []).map((d) => DAY_NAMES[d]).join('·');
|
|
1959
1985
|
console.log(tr(`방문자 일정 등록: 켜짐`, `Visitor booking: on`));
|
|
1960
1986
|
console.log(tr(` 받는 때: ${days} ${b.startTime}~${b.endTime} (${b.timezone})`, ` when: ${days} ${b.startTime}-${b.endTime} (${b.timezone})`));
|
|
1961
|
-
console.log(tr(` 한 번 길이: ${b.durationMin}분 ·
|
|
1987
|
+
console.log(tr(` 한 번 길이: ${b.durationMin}분 · 예약 앞뒤 여유 ${b.bufferMin}분 · 구글 기존 일정 앞뒤 여유 ${b.calendarBufferMin ?? b.bufferMin}분`, ` slot: ${b.durationMin} min · gap around bookings ${b.bufferMin} min · gap around existing Google events ${b.calendarBufferMin ?? b.bufferMin} min`));
|
|
1962
1988
|
console.log(tr(` ${b.minNoticeHours}시간 전까지 · ${b.maxDaysAhead}일 뒤까지 · 하루 ${b.maxPerDay || '제한 없음'}`, ` ${b.minNoticeHours}h notice · up to ${b.maxDaysAhead} days · per day ${b.maxPerDay || 'unlimited'}`));
|
|
1963
1989
|
console.log(tr(` 확정: ${b.approval === 'manual' ? '승인 후' : '바로'}`, ` confirm: ${b.approval === 'manual' ? 'after approval' : 'instantly'}`));
|
|
1964
1990
|
if (b.blockedDates?.length) console.log(tr(` 쉬는 날: ${b.blockedDates.join(', ')}`, ` days off: ${b.blockedDates.join(', ')}`));
|
|
@@ -1976,6 +2002,7 @@ async function cmdBooking(args) {
|
|
|
1976
2002
|
if (b.allowGuests && b.maxGuests > 0) {
|
|
1977
2003
|
console.log(tr(` 손님 함께 부르기: 최대 ${b.maxGuests}명`, ` guests: up to ${b.maxGuests}`));
|
|
1978
2004
|
}
|
|
2005
|
+
if (b.trackSource) console.log(tr(' 유입 경로 기록: 켜짐', ' source tracking: on'));
|
|
1979
2006
|
if (b.redirectUrl) console.log(tr(` 잡은 뒤 보낼 곳: ${b.redirectUrl}`, ` after booking: ${b.redirectUrl}`));
|
|
1980
2007
|
if (b.allowVisitorCancel === false) console.log(tr(' 방문자 직접 취소: 꺼짐', ' visitor self-cancel: off'));
|
|
1981
2008
|
if (b.allowVisitorReschedule === false) console.log(tr(' 방문자 직접 시간 변경: 꺼짐', ' visitor reschedule: off'));
|
|
@@ -2043,6 +2070,8 @@ async function cmdBooking(args) {
|
|
|
2043
2070
|
}
|
|
2044
2071
|
if (args.duration !== undefined) body.durationMin = Number(args.duration);
|
|
2045
2072
|
if (args.buffer !== undefined) body.bufferMin = Number(args.buffer);
|
|
2073
|
+
// 구글 캘린더에 이미 있던 일정 앞뒤 여유(이동 시간 등). --buffer 는 이 표로 잡힌 예약 앞뒤.
|
|
2074
|
+
if (args['calendar-buffer'] !== undefined) body.calendarBufferMin = Number(args['calendar-buffer']);
|
|
2046
2075
|
if (args.notice !== undefined) body.minNoticeHours = Number(args.notice);
|
|
2047
2076
|
if (args.ahead !== undefined) body.maxDaysAhead = Number(args.ahead);
|
|
2048
2077
|
if (args['per-day'] !== undefined) body.maxPerDay = Number(args['per-day']);
|
|
@@ -2054,6 +2083,7 @@ async function cmdBooking(args) {
|
|
|
2054
2083
|
if (args['self-move'] !== undefined) body.allowVisitorReschedule = args['self-move'] !== 'off' && args['self-move'] !== false;
|
|
2055
2084
|
if (args['guests'] !== undefined) body.allowGuests = args['guests'] !== 'off' && args['guests'] !== false;
|
|
2056
2085
|
if (args['max-guests'] !== undefined) body.maxGuests = Number(args['max-guests']);
|
|
2086
|
+
if (args['track-source'] !== undefined) body.trackSource = args['track-source'] !== 'off' && args['track-source'] !== false;
|
|
2057
2087
|
if (args['after'] !== undefined) body.redirectUrl = String(args['after'] === true ? '' : args['after']).trim();
|
|
2058
2088
|
if (args['assign'] !== undefined) {
|
|
2059
2089
|
const a = String(args['assign']).trim().toLowerCase();
|
|
@@ -3072,6 +3102,8 @@ async function main() {
|
|
|
3072
3102
|
return cmdRestore(args);
|
|
3073
3103
|
case 'ws':
|
|
3074
3104
|
return cmdWs(args);
|
|
3105
|
+
case 'emblem':
|
|
3106
|
+
return cmdEmblem(args);
|
|
3075
3107
|
case 'ask':
|
|
3076
3108
|
return cmdAsk(args);
|
|
3077
3109
|
case 'config':
|
package/lib/client.js
CHANGED
|
@@ -757,6 +757,16 @@ export class RootrClient {
|
|
|
757
757
|
* 이름을 그대로 다시 적게 하는 것은 서버가 요구하는 안전장치다 — 지울 것을
|
|
758
758
|
* 정말 보고 있는지 확인한다.
|
|
759
759
|
*/
|
|
760
|
+
/**
|
|
761
|
+
* WS-EMBLEM — set (attachment id) or clear (null) the workspace emblem shown in the
|
|
762
|
+
* sidebar badge and workspace switcher. The server only accepts the relative raw path
|
|
763
|
+
* of an image attachment already uploaded to this same workspace.
|
|
764
|
+
*/
|
|
765
|
+
async setWorkspaceEmblem(workspaceId, attachmentId) {
|
|
766
|
+
const iconUrl = attachmentId ? `/api/v1/attachments/${encodeURIComponent(attachmentId)}/raw` : null;
|
|
767
|
+
return this.request(`/workspaces/${encodeURIComponent(workspaceId)}`, { method: 'PATCH', body: { iconUrl } });
|
|
768
|
+
}
|
|
769
|
+
|
|
760
770
|
async deleteWorkspace({ workspaceId, confirmName }) {
|
|
761
771
|
return this.request(`/workspaces/${workspaceId}`, {
|
|
762
772
|
method: 'DELETE',
|
|
@@ -82,7 +82,10 @@ export function registerAttachmentTools(server) {
|
|
|
82
82
|
'`markdown` is ready to paste into a document body (). For the common case of inserting an ' +
|
|
83
83
|
'image straight into a document, prefer rootr_attach_image_to_document instead. For a large file (or ' +
|
|
84
84
|
'over the remote connector, where filePath is not available), use rootr_create_upload_url → PUT → ' +
|
|
85
|
-
'rootr_complete_upload instead of inlining the bytes as base64Data.'
|
|
85
|
+
'rootr_complete_upload instead of inlining the bytes as base64Data. ' +
|
|
86
|
+
'Video files (mp4/webm/mov, up to 75MB) are allowed; to put one on a slide, upload it with nodeId = the deck and add it to the slide\'s ' +
|
|
87
|
+
'`videos[]` (Team plan and above for decks). Videos play via signed URLs (`vt=`) that the server adds when it returns the slide — a bare /raw link ' +
|
|
88
|
+
'opened from another website is blocked.',
|
|
86
89
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
87
90
|
inputSchema: uploadInputSchema,
|
|
88
91
|
},
|
|
@@ -154,6 +157,60 @@ export function registerAttachmentTools(server) {
|
|
|
154
157
|
},
|
|
155
158
|
);
|
|
156
159
|
|
|
160
|
+
// 엠블럼은 워크스페이스에 붙는 것이라 노드 칸(nodeId)은 받지 않는다.
|
|
161
|
+
const { nodeId: _unusedNodeId, ...emblemUploadSchema } = uploadInputSchema;
|
|
162
|
+
// WS-EMBLEM — 화면(설정 → 일반 → 엠블럼)에서만 되던 일을 에이전트도 하게. 새 파일을 주면
|
|
163
|
+
// 올린 뒤 걸고, 이미 올린 첨부 id 를 주면 그것을 걸고, clear 면 이름 첫 글자 배지로 되돌린다.
|
|
164
|
+
server.registerTool(
|
|
165
|
+
'rootr_set_workspace_emblem',
|
|
166
|
+
{
|
|
167
|
+
title: 'Set or clear the workspace emblem (logo)',
|
|
168
|
+
description:
|
|
169
|
+
'Set the Rootr (루터) workspace emblem — the small square logo shown in the sidebar badge and the ' +
|
|
170
|
+
'workspace switcher for every member. Give ONE of: an image file (filePath or base64Data, with filename ' +
|
|
171
|
+
'+ mimeType — it is uploaded to this workspace first), `attachmentId` of an image already uploaded to ' +
|
|
172
|
+
'this workspace (e.g. from rootr_upload_attachment / rootr_complete_upload), or `clear: true` to go back ' +
|
|
173
|
+
'to the default initial-letter badge. Requires ADMIN on the workspace. A square PNG/SVG/WebP of 256px+ ' +
|
|
174
|
+
'looks best; it is shown cropped to a rounded square.',
|
|
175
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
176
|
+
inputSchema: {
|
|
177
|
+
...emblemUploadSchema,
|
|
178
|
+
filename: emblemUploadSchema.filename.optional(),
|
|
179
|
+
mimeType: emblemUploadSchema.mimeType.optional(),
|
|
180
|
+
attachmentId: z.string().optional().describe('Id of an image attachment already in this workspace'),
|
|
181
|
+
clear: z.boolean().optional().describe('true = remove the emblem (back to the initial-letter badge)'),
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
async ({ filename, mimeType, filePath, base64Data, attachmentId, clear, workspace }) => {
|
|
185
|
+
try {
|
|
186
|
+
const ws = requireWorkspace(workspace);
|
|
187
|
+
const client = makeClient(ws);
|
|
188
|
+
const given = [Boolean(filePath || base64Data), Boolean(attachmentId), Boolean(clear)].filter(Boolean).length;
|
|
189
|
+
if (given !== 1) {
|
|
190
|
+
throw new RootrApiError('파일(filePath/base64Data), attachmentId, clear 중 하나만 주세요.');
|
|
191
|
+
}
|
|
192
|
+
let id = null;
|
|
193
|
+
if (attachmentId) {
|
|
194
|
+
if (!ATTACHMENT_ID.test(attachmentId)) throw new RootrApiError(`첨부 id 모양이 아닙니다: ${attachmentId}`);
|
|
195
|
+
id = attachmentId;
|
|
196
|
+
} else if (!clear) {
|
|
197
|
+
if (!filename || !mimeType) throw new RootrApiError('파일을 올릴 때는 filename 과 mimeType 이 필요합니다.');
|
|
198
|
+
if (!mimeType.startsWith('image/')) throw new RootrApiError('엠블럼은 이미지(image/*)만 됩니다.');
|
|
199
|
+
const data = readAttachmentBytes({ filePath, base64Data });
|
|
200
|
+
({ id } = await client.uploadAttachment(ws, { filename, mimeType, data }));
|
|
201
|
+
}
|
|
202
|
+
const updated = await client.setWorkspaceEmblem(ws, id);
|
|
203
|
+
return jsonResult({
|
|
204
|
+
workspaceId: ws,
|
|
205
|
+
iconUrl: updated?.iconUrl ?? null,
|
|
206
|
+
emblemUrl: id ? client.attachmentRawUrl(id) : null,
|
|
207
|
+
});
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return errorResult(err);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
157
214
|
// ISSUE-222 — rootr_upload_attachment/rootr_attach_image_to_document require
|
|
158
215
|
// the full file as base64Data over the remote connector (it can't read
|
|
159
216
|
// filePath), which breaks down for larger images. This gives the caller a
|
package/lib/mcp-tools/booking.js
CHANGED
|
@@ -119,7 +119,18 @@ export function registerBookingTools(server) {
|
|
|
119
119
|
.number()
|
|
120
120
|
.optional()
|
|
121
121
|
.describe('Length of one booking in minutes: 15, 20, 30, 45, 60, 90, 120, 180 or 240.'),
|
|
122
|
-
bufferMin: z
|
|
122
|
+
bufferMin: z
|
|
123
|
+
.number()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('Gap kept before AND after each booking made through this table, in minutes (0-240).'),
|
|
126
|
+
calendarBufferMin: z
|
|
127
|
+
.number()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe(
|
|
130
|
+
'Gap kept before AND after every event already on the connected Google Calendar (travel time etc.), in minutes (0-240). ' +
|
|
131
|
+
'Separate from bufferMin: e.g. 60 here and 0 in bufferMin blocks an hour around outside meetings while online bookings may sit back to back. ' +
|
|
132
|
+
'Bookings this table wrote to Google are not "existing events" — they get bufferMin. If never set, it follows bufferMin.',
|
|
133
|
+
),
|
|
123
134
|
minNoticeHours: z.number().optional().describe('No bookings closer than this many hours from now.'),
|
|
124
135
|
maxDaysAhead: z.number().optional().describe('How far ahead visitors may book, in days.'),
|
|
125
136
|
maxPerDay: z.number().optional().describe('Max bookings per day. 0 = unlimited.'),
|
|
@@ -209,6 +220,15 @@ export function registerBookingTools(server) {
|
|
|
209
220
|
.optional()
|
|
210
221
|
.describe('Let the visitor add other email addresses. Guests get the same emails and the same calendar invite.'),
|
|
211
222
|
maxGuests: z.number().int().optional().describe('How many guests a visitor may add (default 5). 0 turns it off.'),
|
|
223
|
+
trackSource: z
|
|
224
|
+
.boolean()
|
|
225
|
+
.optional()
|
|
226
|
+
.describe(
|
|
227
|
+
'Record where each booking came from. Adds a text column ("유입 경로" unless you pass sourceLabel) and fills it ' +
|
|
228
|
+
'from the address the visitor arrived on, e.g. "source=meta, medium=paid, campaign=ax, content=3-docs" ' +
|
|
229
|
+
'(utm_* first, else the ad click id, else the referring site). Left empty when nothing is known.',
|
|
230
|
+
),
|
|
231
|
+
sourceLabel: z.string().optional().describe('Name for the column trackSource creates (default "유입 경로").'),
|
|
212
232
|
redirectUrl: z
|
|
213
233
|
.string()
|
|
214
234
|
.optional()
|
package/lib/mcp-tools/pages.js
CHANGED
|
@@ -94,8 +94,10 @@ const BLOCK_GUIDE =
|
|
|
94
94
|
'for a document in the same workspace. Internal path strings are rejected. A node link renders as the in-app ' +
|
|
95
95
|
'document for members, as the public address on the published page, and is hidden entirely if that document is ' +
|
|
96
96
|
'not published — so link to nodes rather than pasting addresses.\n' +
|
|
97
|
-
'CONFIG: {theme, accentColor?, navEnabled?}. 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.
|
|
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. ' +
|
|
99
101
|
'Every published page carries the Rootr signature at the bottom.\n' +
|
|
100
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.';
|
|
101
103
|
|
|
@@ -129,7 +131,7 @@ export function registerPageTools(server) {
|
|
|
129
131
|
config: z
|
|
130
132
|
.record(z.unknown())
|
|
131
133
|
.optional()
|
|
132
|
-
.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'),
|
|
133
135
|
workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
134
136
|
},
|
|
135
137
|
},
|
|
@@ -211,7 +213,7 @@ export function registerPageTools(server) {
|
|
|
211
213
|
.array(z.record(z.unknown()))
|
|
212
214
|
.optional()
|
|
213
215
|
.describe('COMPLETE blocks array to set — any block you omit is deleted'),
|
|
214
|
-
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'),
|
|
215
217
|
},
|
|
216
218
|
},
|
|
217
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'),
|