rootr-cli 0.19.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +45 -3
- package/README.md +41 -6
- package/bin/rootr.js +151 -8
- package/lib/client.js +100 -2
- package/lib/credentials.js +63 -24
- package/lib/mcp-tools/attachments.js +124 -6
- package/lib/mcp-tools/databases.js +289 -4
- package/lib/mcp-tools/forms.js +6 -1
- package/lib/mcp-tools/pages.js +29 -4
- package/lib/mcp-tools/workspaces.js +47 -2
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -211,6 +211,11 @@ rootr config # 등록된 키 목록 확인
|
|
|
211
211
|
워크스페이스가 바뀌고, 그 워크스페이스를 담당하는 키가 자동으로 선택·기억됩니다. 계정
|
|
212
212
|
키(PAT)를 쓰면 소속된 어느 워크스페이스로든 바로 전환됩니다.
|
|
213
213
|
|
|
214
|
+
MCP 서버를 `ROOTR_WORKSPACE` 환경변수로 띄웠다면(MCP 클라이언트 설정의 `env: {…}`), 재시작할 때마다
|
|
215
|
+
환경변수가 다시 이깁니다. `rootr_use_workspace` 전환은 그 서버 프로세스가 켜져 있는 동안만 적용되고,
|
|
216
|
+
전환 응답이 "재시작하면 어느 워크스페이스로 돌아가는지"를 알려 줍니다. 전환을 계속 유지하려면 그 설정의
|
|
217
|
+
`ROOTR_WORKSPACE` 를 바꾸거나 지우세요.
|
|
218
|
+
|
|
214
219
|
워크스페이스 키는 자기 워크스페이스 밖에서는 열리지 않습니다 — `rootr ws`(=`rootr_workspaces`)
|
|
215
220
|
목록에 보여도 접근은 안 되는 경우가 있어, 등록된 키가 워크스페이스 키뿐이면 목록에
|
|
216
221
|
`[등록된 키로는 접근 불가]`를 함께 표시합니다.
|
|
@@ -346,7 +351,7 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
346
351
|
|
|
347
352
|
## MCP 서버로 사용하기
|
|
348
353
|
|
|
349
|
-
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구
|
|
354
|
+
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 186종:
|
|
350
355
|
|
|
351
356
|
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
352
357
|
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
@@ -380,6 +385,8 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
380
385
|
|---|---|
|
|
381
386
|
| `rootr_upload_attachment` | 로컬 파일(`filePath`) 또는 base64(`base64Data`)를 업로드하고 `{id, url, markdown}` 반환 |
|
|
382
387
|
| `rootr_attach_image_to_document` | 위 업로드 + 지정 문서에 `rootr_append`로 바로 삽입 (가장 흔한 용법) |
|
|
388
|
+
| `rootr_create_upload_url` | 잠깐(5분) 쓰는 올리기 주소를 받는다 — 파일 바이트를 그 주소에 직접 `PUT`. 큰 파일, 그리고 `filePath` 를 못 쓰는 원격 커넥터용 |
|
|
389
|
+
| `rootr_complete_upload` | `PUT` 이 끝난 뒤 호출 — 올라간 것을 확인하고 `{id, url, markdown}` 반환 |
|
|
383
390
|
|
|
384
391
|
`url`은 `/attachments/{id}/raw` — 로그인 없이 렌더되는 공개 경로라 published(`/p/`) 문서에도
|
|
385
392
|
그대로 쓸 수 있습니다. CLI에서는 `rootr attach <file> [--doc <path|id>] [--heading "## X"] [--alt <text>]`.
|
|
@@ -535,6 +542,26 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
535
542
|
"steps": [{ "kind": "update_rows", "target": "selected", "values": { "오늘 할 일": true } }] }]
|
|
536
543
|
```
|
|
537
544
|
|
|
545
|
+
**같은 행이 이미 있으면 잇기.** 마스터 표에 등록하는 규칙(투입확정 → 사업장 마스터)은 기존 고객이면
|
|
546
|
+
이미 행이 있습니다. `add_row` 에 `match` 를 주면 만들기 전에 찾습니다 — 같은 이름이 **하나**면 새로
|
|
547
|
+
만들지 않고 그 행에 잇고, **없으면** 만들고, **여럿**이면 아무것도 안 만들고 요청 행에 "후보 여러 곳"
|
|
548
|
+
댓글을 남깁니다. 먼저 공백만 무시하고 똑같은 이름을 찾으므로 지점명까지 같으면 그 지점에 잇습니다.
|
|
549
|
+
관계 칸은 `"{배정 매니저}"` 처럼 칸 하나만 적으면(받는 칸이 같은 표를 가리킬 때) 행 자체가 옮겨집니다.
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
[{ "label": "투입확정 시 사업장 등록",
|
|
553
|
+
"when": [{ "property": "상태", "op": "changed_to", "value": "투입확정" },
|
|
554
|
+
{ "property": "사업장", "op": "is_empty" }],
|
|
555
|
+
"steps": [{ "kind": "add_row", "database": "/사업장 마스터",
|
|
556
|
+
"values": { "업체명": "{요청 건}", "담당 매니저": "{배정 매니저}" },
|
|
557
|
+
"link": { "relation": "채용 요청", "from": "selected" },
|
|
558
|
+
"match": { "key": "{요청 건}", "on": "업체명",
|
|
559
|
+
"normalize": ["spaces", "parens", "suffix:의원", "trailingDigits"], "fill": "empty" } }] }]
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`rootr db automations <표>` 는 규칙마다 "같은 행이 있으면 잇기"와 마지막 사유(후보 여러 곳·조건에 걸려
|
|
563
|
+
비운 칸)를 함께 보여 줍니다.
|
|
564
|
+
|
|
538
565
|
스캐폴드로 표를 지을 때도 `database.buttons` · `database.automations` 에 같은 모양을 넣으면
|
|
539
566
|
**표와 함께** 달립니다.
|
|
540
567
|
|
|
@@ -782,9 +809,24 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
782
809
|
`rootr_append_page_blocks`, `rootr_update_page`, `rootr_update_page_block`,
|
|
783
810
|
`rootr_read_page_live_data`)로 저작합니다.
|
|
784
811
|
|
|
785
|
-
HTML을 짜는 것이 아니라 **정해진 블록
|
|
812
|
+
HTML을 짜는 것이 아니라 **정해진 블록 15종을 골라 쌓습니다** — `hero` `text`
|
|
786
813
|
`features` `gallery` `pricing` `testimonials` `faq` `timeline` `cta` `footer` +
|
|
787
|
-
표에서 값을 끌어오는 `liveStats` `liveTable` `liveChart` + 신청을 받는 `form` +
|
|
814
|
+
표에서 값을 끌어오는 `liveStats` `liveTable` `liveChart` + 신청을 받는 `form` +
|
|
815
|
+
방문자가 시간을 잡는 `booking`(`{bookingNodeId, headline?, type?}` — 예약을 켜고 공개한 표의 노드 id) +
|
|
816
|
+
자유 블록 `custom`.
|
|
817
|
+
|
|
818
|
+
**랜딩 페이지**용으로 두 블록은 가운데 본문 기둥을 벗어나 화면 폭 전체를 쓸 수 있습니다.
|
|
819
|
+
`hero`에 `size: "screen"`을 주면 화면 한 장을 채우는 큰 제목(화면 폭 전체·화면 높이·큰 글자)이 되고,
|
|
820
|
+
`custom`에 `bleed: true`를 주면 좌우·위아래 여백 없이 화면 폭 전체를 써서 연달아 놓은 블록이
|
|
821
|
+
틈 없는 색 띠로 이어집니다. `custom`에 `minHeight: "screen"`을 주면 어느 기기에서나 틀 높이가
|
|
822
|
+
화면 한 장이 되고(숫자 `height`를 같이 주면 그 값이 하한), `bleed: true`와 함께 쓰면 화면을 꽉 채우는
|
|
823
|
+
섹션이 됩니다. 맨 위 이름 줄은 `config.navEnabled: false`로 끕니다.
|
|
824
|
+
|
|
825
|
+
**스크롤 등장 효과** — 어느 블록이나 `reveal: "fade"`(서서히) 또는 `"fade-up"`(아래에서 위로)을 주면
|
|
826
|
+
(기본 `"none"`), 발행한 페이지에서 그 블록이 화면에 들어올 때 한 번 나타납니다(큰 제목은 제목 → 부제 →
|
|
827
|
+
버튼 순서로). `custom`에 `reveal`을 주면 **틀 안의 CSS 애니메이션도 그 순간에 시작**합니다 — 틀을 블록이
|
|
828
|
+
화면에 들어올 때 만들기 때문입니다. `@keyframes`는 `@media (prefers-reduced-motion: no-preference)` 안에
|
|
829
|
+
적으세요. 움직임 줄이기를 켠 방문자에게는 효과 없이 바로 보입니다.
|
|
788
830
|
|
|
789
831
|
`custom`은 **격리된 방**입니다: HTML/CSS를 마음대로 쓰되 `sandbox` 아이프레임 안에서만
|
|
790
832
|
그려집니다(스크립트 없음·같은 출처 아님·바깥 통신 차단). 그래서 대담한 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, **192 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
|
|
|
@@ -117,6 +117,11 @@ After that, `rootr use` (and the `rootr_use_workspace` MCP tool) accepts a **nam
|
|
|
117
117
|
rootr.io URL**; the key that opens that workspace is selected and remembered automatically. An
|
|
118
118
|
account key (PAT) switches to any workspace you belong to.
|
|
119
119
|
|
|
120
|
+
If the MCP server was started with `ROOTR_WORKSPACE` set (the usual `env: {…}` block in an MCP client
|
|
121
|
+
config), the env var wins again on every restart: a `rootr_use_workspace` switch applies for as long as
|
|
122
|
+
that server process runs, and the reply says which workspace it returns to after a restart/reconnect.
|
|
123
|
+
To make the switch permanent, change (or remove) `ROOTR_WORKSPACE` in that config.
|
|
124
|
+
|
|
120
125
|
A workspace key does not open anything outside its own workspace. Because of that, `rootr ws`
|
|
121
126
|
marks entries your registered keys cannot open — **being listed and being reachable are not the
|
|
122
127
|
same thing**.
|
|
@@ -192,7 +197,13 @@ rootr db rows-new /projects/tasks --file rows.json # many at once — all or
|
|
|
192
197
|
rootr db rows-set /projects/tasks --ids r1,r2 --values '{"Status":"Done"}'
|
|
193
198
|
rootr db rows-each /projects/tasks --json '[{"rowId":"r1","values":{"Score":3}}]'
|
|
194
199
|
rootr db rows-rm /projects/tasks --ids r1,r2 --yes
|
|
200
|
+
rootr db duplicates /hr/applicants --row r9 # same person (dedupe column) + latest linked history
|
|
201
|
+
rootr db merge /hr/applicants r9 --into r1 # merge a duplicate into the existing row
|
|
202
|
+
rootr db keep-duplicate /hr/applicants r9 --from r1 # keep as a new row, link both
|
|
203
|
+
rootr db history /hr/applicants --row r1 # who changed what, when, how
|
|
204
|
+
rootr db restore /hr/applicants --at 2026-10-15T09:00:00+09:00 --yes # rows + columns/views back to that time
|
|
195
205
|
rootr db view /projects/tasks Calendar --scale month # views: table, board, calendar, timeline
|
|
206
|
+
rootr db view /hr/applicants Board --cards "Interview date,Phone" # fields shown on board cards
|
|
196
207
|
rootr db lock /projects/tasks --schema managers --rows own --locked "Approved,Score"
|
|
197
208
|
rootr db notify /projects/tasks --file rules.json # notification rules
|
|
198
209
|
rootr db buttons /projects/tasks --file buttons.json # buttons that do work when pressed
|
|
@@ -210,6 +221,13 @@ add `on: ["schedule"]` with `schedule: { at: "09:00" }` for a daily job (Korean
|
|
|
210
221
|
Prefer `changed_to` over `is`: `is` stays true on every later edit, so a rule that creates
|
|
211
222
|
rows would create them again each time the row is saved.
|
|
212
223
|
|
|
224
|
+
A rule that registers into a master table should not duplicate customers who are already
|
|
225
|
+
there: give the `add_row` a `match: { key: "{Request}", on: "Company", normalize: ["spaces",
|
|
226
|
+
"parens", "suffix:Clinic", "trailingDigits"], fill: "empty" }`. One matching row → linked
|
|
227
|
+
instead of created; none → created; several → nothing is created and the request row gets a
|
|
228
|
+
"several candidates" comment. A value that is exactly `"{Assigned manager}"` going into a
|
|
229
|
+
relation to the same table copies the linked rows, not their titles.
|
|
230
|
+
|
|
213
231
|
`--upsert-key` is what makes a migration or a sync safe to retry — without it, retrying
|
|
214
232
|
after a partial failure silently double-loads (a real incident: 903 duplicate rows).
|
|
215
233
|
|
|
@@ -389,12 +407,12 @@ is refused and says when the grace was used.
|
|
|
389
407
|
|
|
390
408
|
## MCP tools
|
|
391
409
|
|
|
392
|
-
`rootr mcp` speaks MCP over stdio.
|
|
410
|
+
`rootr mcp` speaks MCP over stdio. 192 tools:
|
|
393
411
|
|
|
394
412
|
| Group | Count | Tools |
|
|
395
413
|
|---|---|---|
|
|
396
414
|
| Documents | 22 | list, read, write, append, edit, search, comment, version history, commits & merges, publish, trash & restore |
|
|
397
|
-
| Databases |
|
|
415
|
+
| Databases | 32 | create, columns, views, rows (single & bulk), row pages, permissions, notifications, buttons, duplicates (find / merge / keep & link), history & point-in-time restore (row / whole table) |
|
|
398
416
|
| CRM | 18 | companies, contacts, deals (incl. pipeline moves), activities, tasks, CSV import |
|
|
399
417
|
| Signed documents & research notes | 14 | create, sign, revise, reject, reopen, approval lines, signing codes, ledger verification, research-note preset |
|
|
400
418
|
| Forms | 10 | create, read, update, responses, share links, collectors |
|
|
@@ -408,7 +426,7 @@ is refused and says when the grace was used.
|
|
|
408
426
|
| LOG datastores | 5 | create store, update fields, add entries, query, stats |
|
|
409
427
|
| Templates | 4 | list, apply, save as template, update |
|
|
410
428
|
| Whiteboards | 3 | create, read and update the shapes & edges scene |
|
|
411
|
-
| Attachments |
|
|
429
|
+
| Attachments | 4 | upload a file, attach an image straight into a document, upload by presigned URL (`rootr_create_upload_url` → PUT the bytes → `rootr_complete_upload`; for large files and the remote connector, where `filePath` is not available) |
|
|
412
430
|
| Agent memory | 3 | remember, recall, forget |
|
|
413
431
|
| Webhooks | 3 | list, create (secret shown once), delete |
|
|
414
432
|
| Visitor booking & Google Calendar | 6 | turn a published table into a booking page, rules, open slots, connect / list / disconnect Google Calendar |
|
|
@@ -446,9 +464,26 @@ signed-document, issue, CRM and LOG tools need `docs:read` / `docs:write`; `root
|
|
|
446
464
|
### Pages
|
|
447
465
|
|
|
448
466
|
A PAGE is **a screen for people outside the workspace** — an event announcement, a product page,
|
|
449
|
-
a sign-up. You do not write HTML: you stack **
|
|
467
|
+
a sign-up. You do not write HTML: you stack **15 predefined block types** — `hero`, `text`,
|
|
450
468
|
`features`, `gallery`, `pricing`, `testimonials`, `faq`, `timeline`, `cta`, `footer`, the
|
|
451
|
-
table-backed `liveStats`, `liveTable`, `liveChart`, the `form` block,
|
|
469
|
+
table-backed `liveStats`, `liveTable`, `liveChart`, the `form` block, the `booking` block
|
|
470
|
+
(a visitor scheduling calendar: `{bookingNodeId, headline?, type?}` — the node id of a published
|
|
471
|
+
table with visitor booking on) — plus the free-form `custom`.
|
|
472
|
+
|
|
473
|
+
For a **landing page**, two options let a block use the whole page width instead of the centred
|
|
474
|
+
text column: `hero` with `size: "screen"` is a full-screen hero (edge to edge, one viewport tall,
|
|
475
|
+
large headline), and `custom` with `bleed: true` spans the full width with no gap around it, so
|
|
476
|
+
consecutive bleed blocks read as seamless colour bands. `custom` with `minHeight: "screen"` is one
|
|
477
|
+
viewport tall on every device (a numeric `height` given with it is the floor), so
|
|
478
|
+
`bleed: true` + `minHeight: "screen"` is a section that fills the screen. Pair them with
|
|
479
|
+
`config.navEnabled: false`.
|
|
480
|
+
|
|
481
|
+
For **scroll-in animation**, any block takes `reveal: "fade"` or `"fade-up"` (default `"none"`): on
|
|
482
|
+
the published page it appears once, when it scrolls into view (a hero staggers title → subtitle →
|
|
483
|
+
button). On a `custom` block `reveal` also **starts the CSS animations inside the frame at that
|
|
484
|
+
moment** — the frame is created only when the block scrolls in — so put your `@keyframes` there and
|
|
485
|
+
wrap them in `@media (prefers-reduced-motion: no-preference)`. Visitors who ask for reduced motion
|
|
486
|
+
see everything at once.
|
|
452
487
|
|
|
453
488
|
`custom` is an **isolated room**: write any HTML/CSS you like, but it renders inside a `sandbox`
|
|
454
489
|
iframe (no scripts, not same-origin, no outbound calls), so even bold CSS cannot leak onto the
|
package/bin/rootr.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join, basename, extname } from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
import { loadConfig, saveConfig, keyForWorkspace, maskKey, upsertKey, CONFIG_PATH, DEFAULT_BASE_URL } from '../lib/config.js';
|
|
7
7
|
import { candidateClients, rememberWorkspace, resolveWorkspaceSwitch, surveyWorkspaces } from '../lib/credentials.js';
|
|
8
|
-
import { RootrClient, RootrApiError } from '../lib/client.js';
|
|
8
|
+
import { RootrClient, RootrApiError, imageMarkdown } from '../lib/client.js';
|
|
9
9
|
import { resolveTarget, requireResolved, resolveMoveDestination } from '../lib/resolve.js';
|
|
10
10
|
import { t as tr, lang } from '../lib/i18n.js';
|
|
11
11
|
|
|
@@ -38,13 +38,18 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
38
38
|
rootr db button-run <path|id> <buttonId> [--rows id,id] 버튼 한 번 누르기 (행 만들기·값 채우기·AI 채우기)
|
|
39
39
|
rootr db automations <path|id> [--file rules.json] [--clear] 자동 규칙 — "값이 이렇게 되면 이 일을 해라"·정해진 시각
|
|
40
40
|
rootr db import-sheets <파일.xlsx> --ws <id> [--dry] 시트 수백 장짜리 엑셀을 표 하나로 접어 가져오기
|
|
41
|
-
rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] 뷰 목록·달력 주/달/해
|
|
41
|
+
rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--cards "칸,칸"] 뷰 목록·달력 주/달/해 단위·보드 카드에 보일 칸
|
|
42
42
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
43
43
|
rootr db row-body <path|id> <rowId> [--file <f>] 행의 상세 내용(본문 메모) 쓰기 — 없으면 표준입력
|
|
44
44
|
rootr db rows-new <path|id> --json '[{…},…]' | --file <파일> 여러 행 한 번에 넣기 (하나라도 틀리면 전부 취소)
|
|
45
45
|
rootr db rows-set <path|id> --ids <id,id> --values '<json>' 고른 행 전부에 같은 값 (날짜 일괄 변경 등)
|
|
46
46
|
rootr db rows-each <path|id> --json '[{"rowId":…,"values":{…}},…]' 행마다 다른 값으로 한 번에 수정
|
|
47
47
|
rootr db rows-rm <path|id> --ids <id,id> [--yes] 고른 행 한 번에 삭제 — 하나라도 막히면 통째로 거절
|
|
48
|
+
rootr db duplicates <path|id> (--row <rowId> | --values '<json>') [--json] 같은 사람(중복 감지 칸 값이 같은 행)과 그 이력
|
|
49
|
+
rootr db merge <path|id> <rowId> --into <기존 rowId> 중복 행을 기존 행에 합치기(빈 칸만 채움, rowId 행은 지움)
|
|
50
|
+
rootr db keep-duplicate <path|id> <rowId> --from <이전 rowId> 새 행으로 두되 이전 행과 서로 링크
|
|
51
|
+
rootr db history <path|id> [--row <rowId>] [--limit N] [--json] 행·표 변경 이력(누가·언제·어떻게·전후 값)
|
|
52
|
+
rootr db restore <path|id> --at <ISO 시각> [--yes] 표 전체(행+칸·보기·버튼·규칙)를 그 시각으로 — --yes 없으면 미리보기만
|
|
48
53
|
rootr db row-page <rowId> [--type RECORDING] 이 행만 원하는 타입으로 상세 붙이기(녹음·스프레드시트·서명문서…)
|
|
49
54
|
rootr db row-page <rowId> --node <path|id> 이미 있는 문서를 이 행에 잇기 (옮기지 않고 가리킴)
|
|
50
55
|
rootr db row-page <rowId> --unlink 연결만 끊기 — 문서는 그대로 남음
|
|
@@ -146,6 +151,11 @@ usage:
|
|
|
146
151
|
rootr db rows-set <path|id> --ids <id,id> --values '<json>' same value on every chosen row (bulk date changes, …)
|
|
147
152
|
rootr db rows-each <path|id> --json '[{"rowId":…,"values":{…}},…]' a different value per row, in one call
|
|
148
153
|
rootr db rows-rm <path|id> --ids <id,id> [--yes] delete chosen rows at once — one blocked row rejects all
|
|
154
|
+
rootr db duplicates <path|id> (--row <rowId> | --values '<json>') [--json] rows of the same person (same dedupe value) with history
|
|
155
|
+
rootr db merge <path|id> <rowId> --into <existing rowId> merge a duplicate into the existing row (fills blanks, deletes rowId)
|
|
156
|
+
rootr db keep-duplicate <path|id> <rowId> --from <earlier rowId> keep as a new row but link both rows
|
|
157
|
+
rootr db history <path|id> [--row <rowId>] [--limit N] [--json] change history (who, when, how, before/after)
|
|
158
|
+
rootr db restore <path|id> --at <ISO time> [--yes] restore the whole table (rows + columns/views/buttons/rules) — preview without --yes
|
|
149
159
|
rootr db row-page <rowId> [--type RECORDING] give this row a typed detail page (recording, spreadsheet, signed doc…)
|
|
150
160
|
rootr db row-page <rowId> --node <path|id> attach an existing document to this row (points at it, does not move it)
|
|
151
161
|
rootr db row-page <rowId> --unlink unlink only — the document stays
|
|
@@ -576,7 +586,7 @@ async function cmdAttach(args) {
|
|
|
576
586
|
|
|
577
587
|
const { id } = await client.uploadAttachment(workspace, { filename, mimeType, nodeId, data });
|
|
578
588
|
const url = client.attachmentRawUrl(id);
|
|
579
|
-
const markdown =
|
|
589
|
+
const markdown = imageMarkdown(args.alt || filename, url);
|
|
580
590
|
|
|
581
591
|
if (nodeId) {
|
|
582
592
|
const result = await client.appendDocument(nodeId, { content: markdown, underHeading: args.heading });
|
|
@@ -1361,11 +1371,24 @@ async function cmdDbAutomations(args) {
|
|
|
1361
1371
|
if (a.when) console.log(` ${tr('조건', `if`)}: ${a.when.property} ${a.when.op} ${a.when.value ?? ''}`);
|
|
1362
1372
|
for (const [i, st] of (a.steps ?? []).entries()) {
|
|
1363
1373
|
const where = st.database ? ` → ${st.database}` : '';
|
|
1364
|
-
console.log(` ${i + 1}. ${st.kind}${where}`);
|
|
1374
|
+
console.log(` ${i + 1}. ${st.kind}${where}${matchNote(st)}`);
|
|
1365
1375
|
}
|
|
1376
|
+
// AUTO-MATCH · AUTO-REL-COPY — 후보 여러 곳·조건에 걸려 비운 칸도 여기(사유 자리)에 남는다.
|
|
1377
|
+
if (a.lastError?.message) console.log(` ${tr('사유', `note`)}: ${a.lastError.message}`);
|
|
1366
1378
|
}
|
|
1367
1379
|
}
|
|
1368
1380
|
|
|
1381
|
+
/** AUTO-MATCH — `add_row.match` 가 있으면 "같은 행이 있으면 잇기"를 한 줄로. */
|
|
1382
|
+
function matchNote(st) {
|
|
1383
|
+
if (st.kind !== 'add_row' || !st.match) return '';
|
|
1384
|
+
const norm = st.match.normalize?.length ? ` [${st.match.normalize.join(', ')}]` : '';
|
|
1385
|
+
const fill = st.match.fill === 'empty' ? tr(', 빈 칸 채움', `, fill blanks`) : '';
|
|
1386
|
+
return tr(
|
|
1387
|
+
` · 같은 행이 있으면 잇기(${st.match.on} = ${st.match.key}${norm}${fill})`,
|
|
1388
|
+
` · link to an existing row if one matches (${st.match.on} = ${st.match.key}${norm}${fill})`,
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1369
1392
|
/**
|
|
1370
1393
|
* XLSX-FOLD — 시트가 수백 장인 엑셀을 **표 하나로 접어** 가져온다.
|
|
1371
1394
|
*
|
|
@@ -1544,6 +1567,111 @@ async function cmdDbRowsRm(args) {
|
|
|
1544
1567
|
console.log(tr(`${res.deleted}개 행을 지웠습니다.`, `${res.deleted} rows deleted.`));
|
|
1545
1568
|
}
|
|
1546
1569
|
|
|
1570
|
+
/**
|
|
1571
|
+
* DEDUPE(F5) — `rootr db duplicates <표> --row <rowId>` 또는 `--values '{"연락처":"010-…"}'`.
|
|
1572
|
+
* 같은 사람(중복 감지 칸 값이 같은 행)마다 보일 칸과 이어진 행 최근 5건(연락 이력)을 보인다.
|
|
1573
|
+
*/
|
|
1574
|
+
async function cmdDbDuplicates(args) {
|
|
1575
|
+
const target = args._[1];
|
|
1576
|
+
if (!target || (typeof args.row !== 'string' && typeof args.values !== 'string')) {
|
|
1577
|
+
throw new Error(usage(`rootr db duplicates <path|id> (--row <rowId> | --values '<json>') [--json]`));
|
|
1578
|
+
}
|
|
1579
|
+
let values;
|
|
1580
|
+
if (typeof args.values === 'string') {
|
|
1581
|
+
try {
|
|
1582
|
+
values = JSON.parse(args.values);
|
|
1583
|
+
} catch {
|
|
1584
|
+
throw new Error(tr('--values 를 JSON 으로 읽을 수 없습니다.', 'Could not read --values as JSON.'));
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const client = makeClient();
|
|
1588
|
+
const id = await resolveDatabaseId(client, target);
|
|
1589
|
+
const res = await client.findDuplicates(id, { rowId: typeof args.row === 'string' ? args.row : undefined, values });
|
|
1590
|
+
if (args.json) return console.log(JSON.stringify(res, null, 2));
|
|
1591
|
+
if (!res.matches.length) return console.log(tr('같은 값의 기존 행이 없습니다.', 'No existing row has the same value.'));
|
|
1592
|
+
for (const m of res.matches) {
|
|
1593
|
+
console.log(tr(`「${m.propName}」 ${m.value} 같은 행:`, `Rows with ${m.propName} = ${m.value}:`));
|
|
1594
|
+
for (const r of m.rows) {
|
|
1595
|
+
console.log(`- ${r.title || '(제목 없음)'}\t${r.id}`);
|
|
1596
|
+
for (const p of r.preview) console.log(` ${p.name}: ${p.value}`);
|
|
1597
|
+
for (const h of r.history ?? []) {
|
|
1598
|
+
console.log(` ${h.propName} ${h.total}${tr('건', '')}`);
|
|
1599
|
+
for (const x of h.rows) console.log(` · ${[x.title, ...x.fields.map((f) => f.value)].filter(Boolean).join(' · ')}`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/** DEDUPE — `rootr db merge <표> <rowId> --into <기존 rowId>` */
|
|
1606
|
+
async function cmdDbMerge(args) {
|
|
1607
|
+
const [, target, rowId] = args._;
|
|
1608
|
+
if (!target || !rowId || typeof args.into !== 'string') throw new Error(usage(`rootr db merge <path|id> <rowId> --into <rowId>`));
|
|
1609
|
+
const client = makeClient();
|
|
1610
|
+
const id = await resolveDatabaseId(client, target);
|
|
1611
|
+
const res = await client.mergeRows(id, rowId, args.into);
|
|
1612
|
+
console.log(
|
|
1613
|
+
tr(
|
|
1614
|
+
`합쳤습니다 → ${res.into} (빈 칸 채움: ${res.filled.join(', ') || '없음'}). ${rowId} 행은 지웠습니다.`,
|
|
1615
|
+
`Merged into ${res.into} (filled: ${res.filled.join(', ') || 'none'}). Row ${rowId} was deleted.`,
|
|
1616
|
+
),
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/** ROW-HISTORY — `rootr db history <표> [--row <rowId>] [--limit N] [--json]` */
|
|
1621
|
+
async function cmdDbHistory(args) {
|
|
1622
|
+
const target = args._[1];
|
|
1623
|
+
if (!target) throw new Error(usage(`rootr db history <path|id> [--row <rowId>] [--limit N] [--json]`));
|
|
1624
|
+
const client = makeClient();
|
|
1625
|
+
const id = await resolveDatabaseId(client, target);
|
|
1626
|
+
const res = await client.databaseHistory(id, { rowId: typeof args.row === 'string' ? args.row : undefined, limit: args.limit ? Number(args.limit) : 50 });
|
|
1627
|
+
if (args.json) return console.log(JSON.stringify(res, null, 2));
|
|
1628
|
+
for (const c of res.changes) {
|
|
1629
|
+
const cols = [...new Set([...Object.keys(c.before ?? {}), ...Object.keys(c.after ?? {})])];
|
|
1630
|
+
console.log(`${c.at}\t${c.kind}\t${c.rowId}\t${c.actor?.name ?? '-'}${c.via ? ` (${c.via})` : ''}\t${cols.join(',')}\t${c.id}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/** ROW-RESTORE — `rootr db restore <표> --at <ISO> [--yes]` (--yes 없으면 미리보기) */
|
|
1635
|
+
async function cmdDbRestore(args) {
|
|
1636
|
+
const target = args._[1];
|
|
1637
|
+
if (!target || typeof args.at !== 'string') throw new Error(usage(`rootr db restore <path|id> --at <ISO time> [--yes]`));
|
|
1638
|
+
const client = makeClient();
|
|
1639
|
+
const id = await resolveDatabaseId(client, target);
|
|
1640
|
+
const preview = await client.restoreDatabase(id, args.at, { dryRun: true });
|
|
1641
|
+
const sc = preview.schema ?? {};
|
|
1642
|
+
console.log(
|
|
1643
|
+
tr(
|
|
1644
|
+
`고칠 행 ${preview.updates} · 되살릴 행 ${preview.creates} · 지울 행 ${preview.deletes}` +
|
|
1645
|
+
(sc.restorable ? (sc.differs ? ` · 표 정의: 되살릴 칸 ${sc.propsAdded}, 뺄 칸 ${sc.propsRemoved}, 바뀌는 칸 ${sc.propsChanged}${sc.views ? ', 보기' : ''}${sc.buttons ? ', 버튼' : ''}${sc.automations ? ', 자동 규칙' : ''}` : ' · 표 정의는 같음') : ' · 그 시각의 표 정의 기록 없음(행만)'),
|
|
1646
|
+
`rows to edit ${preview.updates} · revive ${preview.creates} · delete ${preview.deletes}` +
|
|
1647
|
+
(sc.restorable ? (sc.differs ? ` · definition: ${sc.propsAdded} column(s) back, ${sc.propsRemoved} removed, ${sc.propsChanged} changed${sc.views ? ', views' : ''}${sc.buttons ? ', buttons' : ''}${sc.automations ? ', automations' : ''}` : ' · definition unchanged') : ' · no definition record at that time (rows only)'),
|
|
1648
|
+
),
|
|
1649
|
+
);
|
|
1650
|
+
if (!args.yes) return console.log(tr('미리보기입니다. 되돌리려면 --yes 를 붙이세요.', 'Preview only. Add --yes to restore.'));
|
|
1651
|
+
const res = await client.restoreDatabase(id, args.at);
|
|
1652
|
+
if (res.job) {
|
|
1653
|
+
let job = { done: 0, total: res.job.total, state: 'running' };
|
|
1654
|
+
while (job.state !== 'done') {
|
|
1655
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1656
|
+
job = await client.restoreJob(id, res.job.id);
|
|
1657
|
+
process.stderr.write(`\r${job.done}/${job.total}`);
|
|
1658
|
+
}
|
|
1659
|
+
process.stderr.write('\n');
|
|
1660
|
+
if (job.failed) console.log(tr(`되돌리지 못한 행 ${job.failed}개 — 같은 시각으로 다시 실행하세요.`, `${job.failed} row(s) failed — run the same restore again.`));
|
|
1661
|
+
}
|
|
1662
|
+
console.log(tr('되돌렸습니다.', 'Restored.'));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/** DEDUPE-KEEP — `rootr db keep-duplicate <표> <rowId> --from <이전 rowId>` */
|
|
1666
|
+
async function cmdDbKeepDuplicate(args) {
|
|
1667
|
+
const [, target, rowId] = args._;
|
|
1668
|
+
if (!target || !rowId || typeof args.from !== 'string') throw new Error(usage(`rootr db keep-duplicate <path|id> <rowId> --from <rowId>`));
|
|
1669
|
+
const client = makeClient();
|
|
1670
|
+
const id = await resolveDatabaseId(client, target);
|
|
1671
|
+
await client.keepDuplicate(id, rowId, args.from);
|
|
1672
|
+
console.log(tr(`새 행으로 두고 ${args.from} 행과 서로 링크를 남겼습니다.`, `Kept as a new row and linked with ${args.from}.`));
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1547
1675
|
/**
|
|
1548
1676
|
* PUBLIC-ANY — `rootr publish <노드> [--children|--off|--status]`
|
|
1549
1677
|
*
|
|
@@ -2161,6 +2289,11 @@ async function cmdDb(args) {
|
|
|
2161
2289
|
if (sub === 'rows-each') return cmdDbRowsEach(args);
|
|
2162
2290
|
if (sub === 'rows-set') return cmdDbRowsSet(args);
|
|
2163
2291
|
if (sub === 'rows-rm') return cmdDbRowsRm(args);
|
|
2292
|
+
if (sub === 'duplicates') return cmdDbDuplicates(args);
|
|
2293
|
+
if (sub === 'merge') return cmdDbMerge(args);
|
|
2294
|
+
if (sub === 'keep-duplicate') return cmdDbKeepDuplicate(args);
|
|
2295
|
+
if (sub === 'history') return cmdDbHistory(args);
|
|
2296
|
+
if (sub === 'restore') return cmdDbRestore(args);
|
|
2164
2297
|
if (sub === 'row-page') return cmdDbRowPage(args);
|
|
2165
2298
|
if (sub === 'row-page-type') return cmdDbRowPageType(args);
|
|
2166
2299
|
if (sub === 'lock') return cmdDbLock(args);
|
|
@@ -2172,7 +2305,7 @@ async function cmdDb(args) {
|
|
|
2172
2305
|
if (sub === 'view') return cmdDbView(args);
|
|
2173
2306
|
if (sub !== 'create') {
|
|
2174
2307
|
throw new Error(
|
|
2175
|
-
usage(tr(`rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|row-page|row-page-type|lock|notify|buttons|button-run|automations|import-sheets|view … (자세한 건 rootr --help)`, `rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|row-page|row-page-type|lock|notify|buttons|button-run|view … (see rootr --help)`)),
|
|
2308
|
+
usage(tr(`rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|duplicates|merge|keep-duplicate|history|restore|row-page|row-page-type|lock|notify|buttons|button-run|automations|import-sheets|view … (자세한 건 rootr --help)`, `rootr db create|rows|row-add|rows-new|row-body|rows-set|rows-each|rows-rm|duplicates|merge|keep-duplicate|history|restore|row-page|row-page-type|lock|notify|buttons|button-run|view … (see rootr --help)`)),
|
|
2176
2309
|
);
|
|
2177
2310
|
}
|
|
2178
2311
|
const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
|
|
@@ -2295,7 +2428,7 @@ async function cmdDbNotify(args) {
|
|
|
2295
2428
|
*/
|
|
2296
2429
|
async function cmdDbView(args) {
|
|
2297
2430
|
const target = args._[1];
|
|
2298
|
-
if (!target) throw new Error(usage(tr(`rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--json]`, `rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] [--json]`)));
|
|
2431
|
+
if (!target) throw new Error(usage(tr(`rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--cards "칸,칸"] [--json]`, `rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] [--cards "field,field"] [--json]`)));
|
|
2299
2432
|
|
|
2300
2433
|
const client = makeClient();
|
|
2301
2434
|
const id = await resolveDatabaseId(client, target);
|
|
@@ -2329,8 +2462,13 @@ async function cmdDbView(args) {
|
|
|
2329
2462
|
patch.calendarScale = scale;
|
|
2330
2463
|
}
|
|
2331
2464
|
if (typeof args.name === 'string') patch.name = args.name;
|
|
2465
|
+
// CARD-PROPS — 보드 카드에 보일 칸(이름 또는 id, 쉼표로). "" 이면 기본으로.
|
|
2466
|
+
if (typeof args.cards === 'string') {
|
|
2467
|
+
const cards = args.cards.split(',').map((c) => c.trim()).filter(Boolean);
|
|
2468
|
+
patch.cardProps = cards.length ? cards : null;
|
|
2469
|
+
}
|
|
2332
2470
|
if (!Object.keys(patch).length) {
|
|
2333
|
-
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year 또는 --
|
|
2471
|
+
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year, --name 새이름 또는 --cards "칸,칸"', `Give something to change: --scale week|month|year, --name <new name>, or --cards "field,field"`));
|
|
2334
2472
|
}
|
|
2335
2473
|
|
|
2336
2474
|
const view = await client.updateDatabaseView(id, viewRef, patch);
|
|
@@ -2410,7 +2548,7 @@ async function cmdDbButtons(args) {
|
|
|
2410
2548
|
: st.values
|
|
2411
2549
|
? ' · ' + Object.entries(st.values).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
2412
2550
|
: '';
|
|
2413
|
-
console.log(` ${i + 1}. ${what}${where}${detail}`);
|
|
2551
|
+
console.log(` ${i + 1}. ${what}${where}${detail}${matchNote(st)}`);
|
|
2414
2552
|
}
|
|
2415
2553
|
}
|
|
2416
2554
|
if (myLevel && myLevel !== 'MANAGE' && myLevel !== 'FULL') {
|
|
@@ -2448,6 +2586,9 @@ async function cmdDbButtonRun(args) {
|
|
|
2448
2586
|
if (madeCount) parts.push(tr(`행 ${madeCount}개 만듦`, `${madeCount} rows created`));
|
|
2449
2587
|
if (r.updated) parts.push(tr(`행 ${r.updated}개 고침`, `${r.updated} rows changed`));
|
|
2450
2588
|
if (r.filled) parts.push(tr(`AI로 칸 ${r.filled}개 채움`, `${r.filled} columns filled by AI`));
|
|
2589
|
+
// AUTO-MATCH — 새로 만들지 않고 있던 행에 이은 것 / 후보가 여럿이라 멈춘 것.
|
|
2590
|
+
if (r.matched?.linked) parts.push(tr(`있던 행 ${r.matched.linked}개에 이음`, `${r.matched.linked} linked to existing rows`));
|
|
2591
|
+
if (r.matched?.ambiguous) parts.push(tr(`후보 여러 곳이라 멈춤 ${r.matched.ambiguous}건(행 댓글 확인)`, `${r.matched.ambiguous} stopped — several candidates (see the row comment)`));
|
|
2451
2592
|
console.log(parts.length ? parts.join(' · ') : tr('바뀐 것이 없습니다.', `Nothing changed.`));
|
|
2452
2593
|
if (r.openRowId) console.log(tr(`열 행: ${r.openRowId}`, `Row to open: ${r.openRowId}`));
|
|
2453
2594
|
// AI가 못 채워도 버튼은 계속 돈다 — 조용히 넘어가면 사람이 왜 빈칸인지 모른다.
|
|
@@ -2458,6 +2599,8 @@ async function cmdDbButtonRun(args) {
|
|
|
2458
2599
|
`${r.aiFailed} columns the AI could not fill${r.aiError ? ` — ${r.aiError}` : ''}`,
|
|
2459
2600
|
),
|
|
2460
2601
|
);
|
|
2602
|
+
for (const n of r.notes ?? []) console.log(`· ${n}`);
|
|
2603
|
+
if (r.linkWarning) console.log(`· ${r.linkWarning}`);
|
|
2461
2604
|
}
|
|
2462
2605
|
|
|
2463
2606
|
|
package/lib/client.js
CHANGED
|
@@ -1221,13 +1221,74 @@ export class RootrClient {
|
|
|
1221
1221
|
});
|
|
1222
1222
|
}
|
|
1223
1223
|
|
|
1224
|
-
async updateColumn(
|
|
1224
|
+
async updateColumn(
|
|
1225
|
+
databaseId,
|
|
1226
|
+
propertyId,
|
|
1227
|
+
{ name, options, optionRenames, formula, rollup, prefix, reverseName, searchExcluded, dedupe, dedupeShow, relationFilter } = {},
|
|
1228
|
+
) {
|
|
1225
1229
|
return this.request(
|
|
1226
1230
|
`/databases/${encodeURIComponent(databaseId)}/columns/${encodeURIComponent(propertyId)}`,
|
|
1227
|
-
{
|
|
1231
|
+
{
|
|
1232
|
+
method: 'PATCH',
|
|
1233
|
+
body: { name, options, optionRenames, formula, rollup, prefix, reverseName, searchExcluded, dedupe, dedupeShow, relationFilter },
|
|
1234
|
+
},
|
|
1228
1235
|
);
|
|
1229
1236
|
}
|
|
1230
1237
|
|
|
1238
|
+
/**
|
|
1239
|
+
* DEDUPE(F5) — 중복 감지 칸 값이 같은 기존 행. `rowId` 를 주면 그 행의 지금 값으로(그 행은 빠진다), 아니면 `values`
|
|
1240
|
+
* ({칸 id 또는 이름: 값})로 찾는다. 기존 행마다 보일 칸(preview)과 이어진 행 최근 5건(history)이 함께 온다.
|
|
1241
|
+
*/
|
|
1242
|
+
async findDuplicates(databaseId, { rowId, values } = {}) {
|
|
1243
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/duplicates`, {
|
|
1244
|
+
method: 'POST',
|
|
1245
|
+
body: rowId ? { rowId } : { values: values ?? {} },
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** DEDUPE — `rowId` 행을 `into` 행에 합친다(빈 칸만 채우고 "재지원" 댓글, `rowId` 행은 지운다). */
|
|
1250
|
+
async mergeRows(databaseId, rowId, into) {
|
|
1251
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/${encodeURIComponent(rowId)}/merge`, {
|
|
1252
|
+
method: 'POST',
|
|
1253
|
+
body: { into },
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** ROW-HISTORY(F6) — 표(또는 한 행)의 변경 이력, 최신부터. `before`+`beforeId` 로 이어 읽는다. */
|
|
1258
|
+
async databaseHistory(databaseId, { rowId, before, beforeId, limit } = {}) {
|
|
1259
|
+
const q = new URLSearchParams();
|
|
1260
|
+
if (rowId) q.set('rowId', rowId);
|
|
1261
|
+
if (before) q.set('before', before);
|
|
1262
|
+
if (beforeId) q.set('beforeId', beforeId);
|
|
1263
|
+
if (limit) q.set('limit', String(limit));
|
|
1264
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/history${q.toString() ? `?${q}` : ''}`);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** ROW-RESTORE(F7) — 행 하나를 그 변경의 전(`before`)/후(`after`) 모습으로. 지운 행도 되살린다. */
|
|
1268
|
+
async restoreRow(databaseId, rowId, changeId, side = 'after') {
|
|
1269
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/${encodeURIComponent(rowId)}/restore`, {
|
|
1270
|
+
method: 'POST',
|
|
1271
|
+
body: { changeId, side },
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/** ROW-RESTORE(F7·G3) — 표 전체(행 + 칸·보기·버튼·자동 규칙)를 그 시각으로. `dryRun` 이면 미리보기만. 큰 표는 `job` 이 온다. */
|
|
1276
|
+
async restoreDatabase(databaseId, at, { dryRun } = {}) {
|
|
1277
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/restore`, { method: 'POST', body: { at, dryRun: dryRun === true } });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async restoreJob(databaseId, jobId) {
|
|
1281
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/restore/${encodeURIComponent(jobId)}`);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** DEDUPE-KEEP — 새 행으로 두되 같은 사람의 이전 행(`from`)과 서로 링크 댓글을 남긴다. */
|
|
1285
|
+
async keepDuplicate(databaseId, rowId, from) {
|
|
1286
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/${encodeURIComponent(rowId)}/keep-duplicate`, {
|
|
1287
|
+
method: 'POST',
|
|
1288
|
+
body: { from },
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1231
1292
|
// DB-VECTOR — 에이전트용 벡터 칸(칸 id 또는 이름).
|
|
1232
1293
|
async putVectors(databaseId, column, items) {
|
|
1233
1294
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/vectors/${encodeURIComponent(column)}`, {
|
|
@@ -1652,6 +1713,24 @@ export class RootrClient {
|
|
|
1652
1713
|
return `${this.publicBaseUrl}/attachments/${encodeURIComponent(attachmentId)}/raw`;
|
|
1653
1714
|
}
|
|
1654
1715
|
|
|
1716
|
+
/**
|
|
1717
|
+
* ISSUE-222 — presigned-URL upload (option 1 from the feature request): the
|
|
1718
|
+
* caller PUTs the file bytes straight to S3 from wherever it has local disk
|
|
1719
|
+
* access, instead of inlining them into an MCP tool call as base64Data.
|
|
1720
|
+
* Mirrors the editor's presign→PUT→complete flow (`/attachments/presign`).
|
|
1721
|
+
*/
|
|
1722
|
+
async presignAttachment(workspaceId, { filename, mimeType, size, nodeId } = {}) {
|
|
1723
|
+
return this.request('/attachments/presign', {
|
|
1724
|
+
method: 'POST',
|
|
1725
|
+
body: { workspaceId, filename, mimeType, size, nodeId },
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/** Marks a presigned attachment READY after the bytes have been PUT to S3. */
|
|
1730
|
+
async completeAttachment(attachmentId) {
|
|
1731
|
+
return this.request(`/attachments/${encodeURIComponent(attachmentId)}/complete`, { method: 'POST' });
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1655
1734
|
// ---- Spreadsheets ---------------------------------------------------------
|
|
1656
1735
|
|
|
1657
1736
|
async listSpreadsheets(workspace) {
|
|
@@ -2043,3 +2122,22 @@ export class RootrClient {
|
|
|
2043
2122
|
}
|
|
2044
2123
|
|
|
2045
2124
|
}
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
* `` — 문서에 그대로 붙여 넣을 그림 한 줄.
|
|
2128
|
+
*
|
|
2129
|
+
* alt 자리에는 파일 이름이 그대로 들어가는데, 이름에 `]` 가 있으면 거기서 대괄호가 닫혀 그림이
|
|
2130
|
+
* 아니라 깨진 글자가 되고(실측: 편집기에서 그림이 안 뜬다), `` 같은 이름은
|
|
2131
|
+
* 남의 주소의 그림을 문서에 끼워 넣는다. 역슬래시로 피하는 방법은 편집기의 해석기가 역슬래시를
|
|
2132
|
+
* 글자 그대로 보여 줘서 쓰지 않는다 — 대괄호는 소괄호로, 역슬래시는 빗금으로, 줄바꿈·탭은
|
|
2133
|
+
* 빈칸으로 바꾼다(alt 는 설명 글이라 뜻이 달라지지 않는다). 저장되는 파일 이름은 건드리지 않는다.
|
|
2134
|
+
*/
|
|
2135
|
+
export function imageMarkdown(alt, url) {
|
|
2136
|
+
const safe = String(alt ?? '')
|
|
2137
|
+
.replace(/\[/g, '(')
|
|
2138
|
+
.replace(/\]/g, ')')
|
|
2139
|
+
.replace(/\\/g, '/')
|
|
2140
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
2141
|
+
.trim();
|
|
2142
|
+
return ``;
|
|
2143
|
+
}
|