rootr-cli 0.31.0 → 0.33.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 +9 -2
- package/README.md +19 -3
- package/bin/rootr.js +179 -8
- package/lib/client.js +63 -8
- package/lib/mcp-tools/databases.js +66 -12
- package/lib/mcp-tools/feedback.js +4 -1
- package/lib/mcp-tools/forms.js +3 -0
- package/lib/mcp-tools/issues.js +144 -2
- package/lib/mcp-tools/pages.js +2 -0
- package/lib/mcp-tools/public.js +20 -4
- package/lib/mcp-tools/signed.js +14 -3
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -405,7 +405,7 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
405
405
|
|
|
406
406
|
## MCP 서버로 사용하기
|
|
407
407
|
|
|
408
|
-
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구
|
|
408
|
+
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 220종:
|
|
409
409
|
|
|
410
410
|
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
411
411
|
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
@@ -487,6 +487,13 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
487
487
|
| `rootr_get_issue` | 이슈 상세 조회 |
|
|
488
488
|
| `rootr_update_issue` | 이슈 갱신(닫기: state=CLOSED + stateReason) |
|
|
489
489
|
| `rootr_comment_issue` | 이슈에 댓글 추가 |
|
|
490
|
+
| `rootr_update_issue_tracker` | 트래커 설정 — **외부 참여**(`read` 보기만 · `login` 로그인한 사람만 작성 · `anyone` 누구나 작성), 라벨·유형 |
|
|
491
|
+
| `rootr_submit_public_issue` | **웹게시한** 트래커에 밖에서 이슈 올리기(공개 주소로, `asGuest`+`guestName` 이면 손님으로, 페이지 블록은 `blockId`) |
|
|
492
|
+
| `rootr_comment_public_issue` | 웹게시한 트래커의 이슈에 댓글(`read:true` 면 공개 댓글 읽기) |
|
|
493
|
+
|
|
494
|
+
CLI: `rootr issue tracker <트래커> --participation anyone` · `rootr issue list|create|comment …` ·
|
|
495
|
+
`rootr issue submit <공개 주소> --title "…" --name <이름>` · `rootr issue reply <공개 주소> <#번호> "<댓글>"`.
|
|
496
|
+
외부 참여는 **트래커 설정**이라 페이지(PAGE)에 `issues` 블록으로 끼운 트래커도 같은 규칙을 따릅니다.
|
|
490
497
|
|
|
491
498
|
#### 웹훅 (webhooks:manage 스코프)
|
|
492
499
|
|
|
@@ -713,7 +720,7 @@ rootr db column <표> 특이사항 --wrap on --align center # 긴 글 줄바꿈
|
|
|
713
720
|
| 도구 | 설명 |
|
|
714
721
|
|---|---|
|
|
715
722
|
| `rootr_publish_node` | **아무 노드나** 공개 — 문서·표·폼·시트·화이트보드·덱·페이지, 하위까지. **페이지(PAGE)는 Team 플랜부터**(무료는 403 `PLAN_UPGRADE_REQUIRED`, 하위 포함 시 페이지는 건너뛰고 개수를 알려 줌) |
|
|
716
|
-
| `rootr_set_node_public` | 이미 공개한 것의 설정 변경(하위 포함 여부·검색
|
|
723
|
+
| `rootr_set_node_public` | 이미 공개한 것의 설정 변경(하위 포함 여부·검색 노출·만료·비밀번호, 이슈트래커는 `participation` 외부 참여) |
|
|
717
724
|
| `rootr_node_public_status` | 지금 공개돼 있는지, 주소는 무엇인지 |
|
|
718
725
|
| `rootr_publish_document` | 문서 전용 발행(구형 — 새로 짤 때는 `rootr_publish_node`) |
|
|
719
726
|
| `rootr_set_document_public` | 문서 전용 공개 설정 변경 |
|
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, **220 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
|
|
|
@@ -296,6 +296,22 @@ rootr publish /handbook --status # is it public, at what URL, chi
|
|
|
296
296
|
rootr publish /projects/tasks --layout board --skin paper # 9 layouts × 6 skins
|
|
297
297
|
rootr publish /handbook/policy.md --comments --expires 2026-12-31T00:00:00Z
|
|
298
298
|
rootr publish /handbook --off
|
|
299
|
+
rootr publish /support/feedback --participation anyone # issue tracker: visitors file issues & comment with just a name
|
|
300
|
+
rootr publish /support/feedback --password s3cret # lock the address (Team plan) — writing is locked too
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Issue trackers take outside participation** (외부 참여): `read` (default, read-only), `login`
|
|
304
|
+
(any signed-in Rootr account files issues and comments) or `anyone` (no account — just a name, shown as
|
|
305
|
+
"name (guest)"). It is the tracker's own setting, so a tracker placed on a published page (`issues`
|
|
306
|
+
block) follows the same rule.
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
rootr issue tracker /support/feedback --participation login # change it (MANAGE)
|
|
310
|
+
rootr issue list /support/feedback --state ALL
|
|
311
|
+
rootr issue create /support/feedback --title "Export is slow" --body "…"
|
|
312
|
+
rootr issue comment /support/feedback 12 "Fixed in 1.4"
|
|
313
|
+
rootr issue submit https://rootr.io/ko/p/AbCd123 --title "Button does nothing" --name "Kim" # from outside, as a guest
|
|
314
|
+
rootr issue reply https://rootr.io/ko/p/AbCd123 12 "Same here" --name "Lee"
|
|
299
315
|
```
|
|
300
316
|
|
|
301
317
|
Every node type publishes on the Free plan **except PAGE** (the block-built web page): publishing a
|
|
@@ -517,7 +533,7 @@ is refused and says when the grace was used.
|
|
|
517
533
|
|
|
518
534
|
## MCP tools
|
|
519
535
|
|
|
520
|
-
`rootr mcp` speaks MCP over stdio.
|
|
536
|
+
`rootr mcp` speaks MCP over stdio. 220 tools:
|
|
521
537
|
|
|
522
538
|
| Group | Count | Tools |
|
|
523
539
|
|---|---|---|
|
|
@@ -532,7 +548,7 @@ is refused and says when the grace was used.
|
|
|
532
548
|
| Pages | 7 | HTML page nodes: create, read, update, append and patch blocks, live data, visit tracking (GA-style visits / scroll / conversions stored as a LOG, a daily table and a dashboard) |
|
|
533
549
|
| Spreadsheets | 7 | create, read, update, per-sheet create/update/delete, patch cells & formulas |
|
|
534
550
|
| Recordings | 7 | create, upload & download audio, transcribe, summarize, rename speakers |
|
|
535
|
-
| Issue tracker |
|
|
551
|
+
| Issue tracker | 9 | create tracker, change tracker settings (outside participation, labels, types), list / create / get / update issue, comment, file an issue / comment on a PUBLISHED tracker from outside |
|
|
536
552
|
| Permissions & visibility | 10 | grant / revoke node permissions, read access, visibility, public status, read requirements |
|
|
537
553
|
| LOG datastores | 5 | create store, update fields, add entries, query, stats |
|
|
538
554
|
| Templates | 4 | list, apply, save as template, update |
|
package/bin/rootr.js
CHANGED
|
@@ -43,8 +43,8 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
43
43
|
rootr db button-run <path|id> <buttonId> [--rows id,id] [--input 글|--input-file f|--input -] 버튼 한 번 누르기 (행 만들기·값 채우기·AI 채우기 · 글 받는 버튼이면 --input)
|
|
44
44
|
rootr db automations <path|id> [--file rules.json] [--clear] 자동 규칙 — "값이 이렇게 되면 이 일을 해라"·정해진 시각
|
|
45
45
|
rootr db import-sheets <파일.xlsx> --ws <id> [--dry] 시트 수백 장짜리 엑셀을 표 하나로 접어 가져오기
|
|
46
|
-
rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--cards "칸,칸"] 뷰 목록·달력 주/달/해 단위·보드 카드에 보일 칸 ·[--density compact|normal] [--freeze 0~3] 촘촘하게·왼쪽 칸
|
|
47
|
-
rootr db view <path|id> <뷰이름> [--row-hidden on|off] [--quick "칸,칸"] [--entry-row on|off] [--day-pager 날짜칸|off] 행 열면 감춘 칸 보이기·필터 맨 위 고정 칸·맨 아래 입력 줄·하루씩 보기
|
|
46
|
+
rootr db view <path|id> [뷰이름] [--scale week|month|year] [--name 새이름] [--cards "칸,칸"] 뷰 목록·달력 주/달/해 단위·보드 카드에 보일 칸 ·[--density compact|normal] [--freeze 0~3] [--lines rows|all|none] [--line-strength light|strong] 촘촘하게·왼쪽 칸 고정·칸 선
|
|
47
|
+
rootr db view <path|id> <뷰이름> [--row-hidden on|off] [--quick "칸,칸"] [--entry-row on|off|top|bottom] [--personal-filters on|off] [--day-pager 날짜칸|off] 행 열면 감춘 칸 보이기·필터 맨 위 고정 칸·맨 아래 입력 줄·하루씩 보기
|
|
48
48
|
rootr db column <path|id> <칸> [--align left|center|right] [--wrap on|off] [--overdue on|off] [--time on|off] [--range on|off] 칸 정렬·줄바꿈·지난 날짜 빨강·시간·기간
|
|
49
49
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
50
50
|
rootr db row-body <path|id> <rowId> [--file <f>] 행의 상세 내용(본문 메모) 쓰기 — 없으면 표준입력
|
|
@@ -75,6 +75,15 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
75
75
|
rootr publish <표> --layout board --skin paper 공개한 표를 무슨 꼴로 보일지 (배치 9종 × 분위기 6종)
|
|
76
76
|
rootr publish <문서> --comments [false] [--expires <ISO>] [--skin <분위기>] 문서 전용 — 댓글(편집 제안)·만료일·스킨 16종
|
|
77
77
|
rootr publish <path|id> --description "<글>" --cover <그림|id|none> 검색 설명문·링크 미리보기 대표 이미지 (검색엔진에 바로 알림)
|
|
78
|
+
rootr publish <path|id> --password <비번> | --no-password 공개 주소 잠그기(Team 부터)·풀기 — 이슈트래커는 쓰기도 같이 잠김
|
|
79
|
+
rootr publish <이슈트래커> --participation read|login|anyone 외부 참여 — 보기만·로그인한 사람만 작성·누구나 작성(이름만 적고)
|
|
80
|
+
rootr issue tracker <트래커> [--participation read|login|anyone] [--json] 트래커 설정 보기·외부 참여 바꾸기(MANAGE)
|
|
81
|
+
rootr issue list <트래커> [--state OPEN|CLOSED|ALL] [--q <검색어>] [--json] 이슈 목록
|
|
82
|
+
rootr issue create <트래커> --title "<제목>" [--body "<본문>"] 이슈 올리기(내 계정으로)
|
|
83
|
+
rootr issue comment <트래커> <#번호> "<댓글>" 이슈에 댓글
|
|
84
|
+
rootr issue submit <공개 주소> --title "<제목>" [--body …] [--name <이름>] [--block <블록 id>] [--password <비번>]
|
|
85
|
+
웹게시한 트래커에 밖에서 이슈 올리기(--name 이면 키 없이 손님으로, 페이지 블록은 --block)
|
|
86
|
+
rootr issue reply <공개 주소> <#번호> "<댓글>" [--name <이름>] [--block <블록 id>] [--password <비번>] 웹게시한 트래커 이슈에 댓글
|
|
78
87
|
rootr booking show <표> 방문자 일정 등록(예약) 설정 보기
|
|
79
88
|
rootr booking set <표> --on --days 1,2,3,4,5 --hours 10:00-18:00 --duration 30 예약 켜기·규칙
|
|
80
89
|
rootr booking set <표> --ask "연락처:required" --approval manual 방문자에게 더 묻기 · 승인 후 확정
|
|
@@ -146,7 +155,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
146
155
|
rootr memory add "<내용>" [--kind 지시|사람|프로젝트|참고] [--source <경로|id>] [--related a,b] 기억 저장
|
|
147
156
|
rootr memory edit <id> [--content "<내용>"] [--kind …] [--source …] [--related a,b] 기억 고치기(준 칸만)
|
|
148
157
|
rootr memory forget <id> [--hard] 기억 폐기(--hard면 행 삭제)
|
|
149
|
-
rootr mcp MCP stdio 서버 구동 (도구
|
|
158
|
+
rootr mcp MCP stdio 서버 구동 (도구 220종 — README 참고)
|
|
150
159
|
rootr --help 도움말
|
|
151
160
|
rootr --version 버전 출력
|
|
152
161
|
|
|
@@ -186,8 +195,8 @@ usage:
|
|
|
186
195
|
rootr db button-run <path|id> <buttonId> [--rows id,id] [--input text|--input-file f|--input -] press a button once (create rows, fill values, AI fill · --input for a button that asks for text)
|
|
187
196
|
rootr db automations <path|id> [--file rules.json] [--clear] automatic rules — "when a value becomes X, do Y", or at a set time
|
|
188
197
|
rootr db import-sheets <file.xlsx> --ws <id> [--dry] fold a one-sheet-per-record workbook into ONE table
|
|
189
|
-
rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] list views; calendar week/month/year [--density compact|normal] [--freeze 0-3] compact rows · frozen left columns
|
|
190
|
-
rootr db view <path|id> <view name> [--row-hidden on|off] [--quick "field,field"] [--entry-row on|off] [--day-pager <date field>|off] hidden fields shown in the row page · pinned filters · entry row · one day at a time
|
|
198
|
+
rootr db view <path|id> [view name] [--scale week|month|year] [--name <new name>] list views; calendar week/month/year [--density compact|normal] [--freeze 0-3] [--lines rows|all|none] [--line-strength light|strong] compact rows · frozen left columns · grid lines
|
|
199
|
+
rootr db view <path|id> <view name> [--row-hidden on|off] [--quick "field,field"] [--entry-row on|off|top|bottom] [--personal-filters on|off] [--day-pager <date field>|off] hidden fields shown in the row page · pinned filters · entry row · one day at a time
|
|
191
200
|
rootr db column <path|id> <column> [--align left|center|right] [--wrap on|off] [--overdue on|off] [--time on|off] [--range on|off] column alignment · wrapping · overdue red · time · end date
|
|
192
201
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] add a row (with its body)
|
|
193
202
|
rootr db row-body <path|id> <rowId> [--file <f>] write a row's body — reads stdin if no file
|
|
@@ -220,6 +229,15 @@ usage:
|
|
|
220
229
|
rootr publish <table> --layout board --skin paper how a published table looks (9 layouts × 6 skins)
|
|
221
230
|
rootr publish <doc> --comments [false] [--expires <ISO>] [--skin <skin>] documents only — comments, expiry, 16 skins
|
|
222
231
|
rootr publish <path|id> --description "<text>" --cover <image|id|none> search description & link-preview cover image (search engines pinged)
|
|
232
|
+
rootr publish <path|id> --password <pw> | --no-password lock (Team plan) / unlock the public address — on a tracker writing is locked too
|
|
233
|
+
rootr publish <issue tracker> --participation read|login|anyone outside participation — read-only · signed-in users write · anyone writes (just a name)
|
|
234
|
+
rootr issue tracker <tracker> [--participation read|login|anyone] [--json] show tracker settings / change outside participation (MANAGE)
|
|
235
|
+
rootr issue list <tracker> [--state OPEN|CLOSED|ALL] [--q <text>] [--json] list issues
|
|
236
|
+
rootr issue create <tracker> --title "<title>" [--body "<body>"] file an issue (as your account)
|
|
237
|
+
rootr issue comment <tracker> <#number> "<comment>" comment on an issue
|
|
238
|
+
rootr issue submit <public URL> --title "<title>" [--body …] [--name <name>] [--block <block id>] [--password <pw>]
|
|
239
|
+
file an issue on a PUBLISHED tracker from outside (--name = as a guest, no key; --block for a tracker on a page)
|
|
240
|
+
rootr issue reply <public URL> <#number> "<comment>" [--name <name>] [--block <block id>] [--password <pw>] comment on an issue of a published tracker
|
|
223
241
|
rootr booking show <table> show visitor booking settings
|
|
224
242
|
rootr booking set <table> --on --days 1,2,3,4,5 --hours 10:00-18:00 --duration 30 turn on + rules
|
|
225
243
|
rootr booking set <table> --ask "Phone:required" --approval manual ask more · confirm after approval
|
|
@@ -287,7 +305,7 @@ usage:
|
|
|
287
305
|
rootr memory add "<content>" [--kind instruction|person|project|reference] [--source <path|id>] [--related a,b] save a memory
|
|
288
306
|
rootr memory edit <id> [--content "<content>"] [--kind …] [--source …] [--related a,b] edit a memory (only the fields you pass)
|
|
289
307
|
rootr memory forget <id> [--hard] retire a memory (--hard deletes the row)
|
|
290
|
-
rootr mcp run the MCP stdio server (
|
|
308
|
+
rootr mcp run the MCP stdio server (220 tools — see the README)
|
|
291
309
|
rootr --help this help
|
|
292
310
|
rootr --version print the version
|
|
293
311
|
|
|
@@ -2161,6 +2179,130 @@ function flagToBool(v) {
|
|
|
2161
2179
|
return v !== 'false' && v !== '0';
|
|
2162
2180
|
}
|
|
2163
2181
|
|
|
2182
|
+
/** ISSUE-PUBLIC — 외부 참여(서버 issue-schema.ts PUBLIC_PARTICIPATION_MODES 와 같은 값). */
|
|
2183
|
+
const PARTICIPATION_MODES = ['read', 'login', 'anyone'];
|
|
2184
|
+
|
|
2185
|
+
function participationLabel(mode) {
|
|
2186
|
+
return {
|
|
2187
|
+
read: tr('보기만', 'read only'),
|
|
2188
|
+
login: tr('로그인한 사람만 작성', 'signed-in users write'),
|
|
2189
|
+
anyone: tr('누구나 작성(이름만 적고)', 'anyone writes (just a name)'),
|
|
2190
|
+
}[mode] ?? mode;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/** 공개 주소(rootr.io/…/p/{slug}) 또는 slug 그대로. */
|
|
2194
|
+
function publicSlugOf(target) {
|
|
2195
|
+
const m = /\/p\/([^/?#\s]+)/.exec(String(target || ''));
|
|
2196
|
+
return m ? decodeURIComponent(m[1]) : String(target || '').trim();
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
/** GET /issue-trackers/:id/issues 는 `{ issues: [...] }` 로 답한다(배열이 아니다 — 3100 실측에서 `find is not a function`). */
|
|
2200
|
+
function issuesOf(res) {
|
|
2201
|
+
return Array.isArray(res) ? res : Array.isArray(res?.issues) ? res.issues : [];
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
function issueNumberOf(raw) {
|
|
2205
|
+
const n = Number(String(raw ?? '').replace(/^#/, ''));
|
|
2206
|
+
if (!Number.isSafeInteger(n) || n < 1) throw new Error(tr(`이슈 번호가 아닙니다: ${raw}`, `Not an issue number: ${raw}`));
|
|
2207
|
+
return n;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* ISSUE-PUBLIC — 이슈트래커 명령(창업자 지시 2026-11-02 "api cli mcp scaffold 에도 적용"). 예전엔 CLI 에 이슈 명령이 아예 없었다.
|
|
2212
|
+
* · 안에서: tracker(설정·외부 참여) · list · create · comment
|
|
2213
|
+
* · 밖에서(웹게시한 트래커): submit · reply — 키가 있으면 그 계정으로, --name 이면 키 없이 손님으로
|
|
2214
|
+
*/
|
|
2215
|
+
async function cmdIssue(args) {
|
|
2216
|
+
const sub = args._[0];
|
|
2217
|
+
const client = makeClient();
|
|
2218
|
+
if (sub === 'tracker') {
|
|
2219
|
+
const target = args._[1];
|
|
2220
|
+
if (!target) throw new Error(usage('rootr issue tracker <path|id> [--participation read|login|anyone] [--json]'));
|
|
2221
|
+
const id = await resolveNodeId(client, target);
|
|
2222
|
+
let t;
|
|
2223
|
+
if (args.participation !== undefined) {
|
|
2224
|
+
if (!PARTICIPATION_MODES.includes(args.participation)) {
|
|
2225
|
+
throw new Error(tr(`--participation 은 다음 중 하나여야 합니다: ${PARTICIPATION_MODES.join(', ')}`, `--participation must be one of: ${PARTICIPATION_MODES.join(', ')}`));
|
|
2226
|
+
}
|
|
2227
|
+
t = await client.updateIssueTracker(id, { publicParticipation: args.participation });
|
|
2228
|
+
} else {
|
|
2229
|
+
t = await client.getIssueTracker(id);
|
|
2230
|
+
}
|
|
2231
|
+
if (args.json) return console.log(JSON.stringify(t, null, 2));
|
|
2232
|
+
const mode = (t.config && t.config.publicParticipation) || 'read';
|
|
2233
|
+
console.log(`${t.name} (${t.path})`);
|
|
2234
|
+
console.log(tr(` 열림 ${t.openCount} · 닫힘 ${t.closedCount}`, ` open ${t.openCount} · closed ${t.closedCount}`));
|
|
2235
|
+
console.log(tr(` 외부 참여: ${participationLabel(mode)}`, ` outside participation: ${participationLabel(mode)}`));
|
|
2236
|
+
console.log(tr(` 라벨: ${(t.labels || []).map((l) => l.name).join(', ') || '없음'}`, ` labels: ${(t.labels || []).map((l) => l.name).join(', ') || 'none'}`));
|
|
2237
|
+
if (mode !== 'read') {
|
|
2238
|
+
const st = await client.nodePublicStatus(id);
|
|
2239
|
+
if (!st.isPublic) {
|
|
2240
|
+
console.log(tr(' ※ 아직 웹게시 전입니다 — `rootr publish <트래커>` 로 공개하거나 발행한 페이지에 issues 블록으로 넣어야 밖에서 씁니다.', ' Note: not published yet — `rootr publish <tracker>` or put it on a published page (issues block) so outsiders can write.'));
|
|
2241
|
+
} else {
|
|
2242
|
+
console.log(tr(` 공개 주소: /p/${st.slug}`, ` public address: /p/${st.slug}`));
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (sub === 'list') {
|
|
2248
|
+
const target = args._[1];
|
|
2249
|
+
if (!target) throw new Error(usage('rootr issue list <path|id> [--state OPEN|CLOSED|ALL] [--q <text>] [--json]'));
|
|
2250
|
+
const id = await resolveNodeId(client, target);
|
|
2251
|
+
const list = issuesOf(await client.listIssues(id, { state: args.state, q: typeof args.q === 'string' ? args.q : undefined }));
|
|
2252
|
+
if (args.json) return console.log(JSON.stringify(list, null, 2));
|
|
2253
|
+
if (!list.length) return console.log(tr('이슈가 없습니다.', 'No issues.'));
|
|
2254
|
+
for (const i of list) {
|
|
2255
|
+
const who = i.guestName ? tr(`${i.guestName} (외부)`, `${i.guestName} (guest)`) : i.author?.name ?? '';
|
|
2256
|
+
console.log(`#${i.number}\t${i.state}\t${i.title}${who ? `\t${who}` : ''}${i.commentCount ? `\t💬${i.commentCount}` : ''}`);
|
|
2257
|
+
}
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (sub === 'create') {
|
|
2261
|
+
const target = args._[1];
|
|
2262
|
+
if (!target || typeof args.title !== 'string') throw new Error(usage('rootr issue create <path|id> --title "<title>" [--body "<body>"]'));
|
|
2263
|
+
const id = await resolveNodeId(client, target);
|
|
2264
|
+
const out = await client.createIssue(id, { title: args.title, body: typeof args.body === 'string' ? args.body : undefined });
|
|
2265
|
+
console.log(tr(`올렸습니다: #${out.number} ${out.title}`, `Filed: #${out.number} ${out.title}`));
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
if (sub === 'comment') {
|
|
2269
|
+
const [, target, num, ...rest] = args._;
|
|
2270
|
+
const text = rest.join(' ').trim();
|
|
2271
|
+
if (!target || !num || !text) throw new Error(usage('rootr issue comment <path|id> <#number> "<comment>"'));
|
|
2272
|
+
const id = await resolveNodeId(client, target);
|
|
2273
|
+
const n = issueNumberOf(num);
|
|
2274
|
+
const hit = issuesOf(await client.listIssues(id, { state: 'ALL', q: `#${n}` })).find((i) => i.number === n);
|
|
2275
|
+
if (!hit) throw new Error(tr(`#${n} 이슈가 없습니다.`, `Issue #${n} not found.`));
|
|
2276
|
+
await client.commentIssue(id, hit.id, text);
|
|
2277
|
+
console.log(tr(`#${n} 에 댓글을 달았습니다.`, `Commented on #${n}.`));
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (sub === 'submit' || sub === 'reply') {
|
|
2281
|
+
const slug = publicSlugOf(args._[1]);
|
|
2282
|
+
if (!slug) throw new Error(usage(`rootr issue ${sub} <public URL> …`));
|
|
2283
|
+
const asGuest = typeof args.name === 'string';
|
|
2284
|
+
const opts = {
|
|
2285
|
+
blockId: typeof args.block === 'string' ? args.block : undefined,
|
|
2286
|
+
guestName: asGuest ? args.name : undefined,
|
|
2287
|
+
asGuest,
|
|
2288
|
+
pass: typeof args.password === 'string' ? await client.unlockPublicNode(slug, args.password) : undefined,
|
|
2289
|
+
};
|
|
2290
|
+
if (sub === 'submit') {
|
|
2291
|
+
if (typeof args.title !== 'string') throw new Error(usage('rootr issue submit <public URL> --title "<title>" [--body …] [--name <name>] [--block <id>]'));
|
|
2292
|
+
const out = await client.submitPublicIssue(slug, { ...opts, title: args.title, body: typeof args.body === 'string' ? args.body : undefined });
|
|
2293
|
+
console.log(out?.number ? tr(`올렸습니다: #${out.number}`, `Filed: #${out.number}`) : tr('올렸습니다.', 'Filed.'));
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
const [, , num, ...rest] = args._;
|
|
2297
|
+
const text = rest.join(' ').trim();
|
|
2298
|
+
if (!num || !text) throw new Error(usage('rootr issue reply <public URL> <#number> "<comment>" [--name <name>] [--block <id>]'));
|
|
2299
|
+
await client.commentPublicIssue(slug, issueNumberOf(num), { ...opts, body: text });
|
|
2300
|
+
console.log(tr('댓글을 달았습니다.', 'Comment posted.'));
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
throw new Error(usage('rootr issue tracker|list|create|comment|submit|reply …'));
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2164
2306
|
async function cmdPublish(args) {
|
|
2165
2307
|
const target = args._[0];
|
|
2166
2308
|
if (!target) {
|
|
@@ -2219,6 +2361,7 @@ async function cmdPublish(args) {
|
|
|
2219
2361
|
printSeo(st.publicConfig?.seo);
|
|
2220
2362
|
} else {
|
|
2221
2363
|
if (st.hasPassword) console.log(tr(' 비밀번호 걸려 있음', ` password protected`));
|
|
2364
|
+
if (st.participation) console.log(tr(` 외부 참여: ${participationLabel(st.participation)}`, ` outside participation: ${participationLabel(st.participation)}`));
|
|
2222
2365
|
console.log(tr(` 하위 문서도 공개: ${st.includeChildren ? '예' : '아니오'}`, ` children published too: ${st.includeChildren ? 'yes' : 'no'}`));
|
|
2223
2366
|
console.log(tr(` 복제 허용: ${st.allowClone === false ? '아니오' : '예'}`, ` cloning allowed: ${st.allowClone === false ? 'no' : 'yes'}`));
|
|
2224
2367
|
if (st.design && (st.design.layout || st.design.skin)) {
|
|
@@ -2274,12 +2417,22 @@ async function cmdPublish(args) {
|
|
|
2274
2417
|
if (design.skin && !DB_SKINS.includes(design.skin)) {
|
|
2275
2418
|
throw new Error(tr(`--skin 은 다음 중 하나여야 합니다: ${DB_SKINS.join(', ')}`, `--skin must be one of: ${DB_SKINS.join(', ')}`));
|
|
2276
2419
|
}
|
|
2420
|
+
// ISSUE-PUBLIC — 이슈트래커의 외부 참여(트래커 설정에 저장 — 페이지에 끼운 블록도 같은 값을 따른다)
|
|
2421
|
+
if (args.participation !== undefined && !PARTICIPATION_MODES.includes(args.participation)) {
|
|
2422
|
+
throw new Error(tr(`--participation 은 다음 중 하나여야 합니다: ${PARTICIPATION_MODES.join(', ')}`, `--participation must be one of: ${PARTICIPATION_MODES.join(', ')}`));
|
|
2423
|
+
}
|
|
2277
2424
|
const res = await client.publishNode(id, {
|
|
2278
2425
|
...(args.children ? { includeChildren: true } : {}),
|
|
2279
2426
|
...(typeof args.view === 'string' ? { view: args.view } : {}),
|
|
2280
2427
|
...(Object.keys(design).length ? { design } : {}),
|
|
2428
|
+
...(typeof args.participation === 'string' ? { participation: args.participation } : {}),
|
|
2281
2429
|
});
|
|
2282
2430
|
console.log(tr(`공개했습니다: /p/${res.slug}`, `Published: /p/${res.slug}`));
|
|
2431
|
+
if (res.participation) console.log(tr(` 외부 참여: ${participationLabel(res.participation)}`, ` outside participation: ${participationLabel(res.participation)}`));
|
|
2432
|
+
if (typeof args.password === 'string' || args['no-password']) {
|
|
2433
|
+
await client.setNodePublic(id, { password: typeof args.password === 'string' ? args.password : null });
|
|
2434
|
+
console.log(typeof args.password === 'string' ? tr(' 비밀번호를 걸었습니다(쓰기도 같이 잠김).', ' Locked with a password (writing too).') : tr(' 비밀번호를 풀었습니다.', ' Password removed.'));
|
|
2435
|
+
}
|
|
2283
2436
|
if (args.children) console.log(tr(' 이 문서 아래 전부(손자·증손자 포함)도 각자 주소로 공개됩니다.', ` Everything under this document (grandchildren included) gets its own public address.`));
|
|
2284
2437
|
if (res.skippedPagesRequireTeam) {
|
|
2285
2438
|
// PAGE-PUBLISH-TEAM — 무료 워크스페이스에서는 아래의 페이지(PAGE)는 따라 나가지 않는다.
|
|
@@ -2987,6 +3140,17 @@ async function cmdDbView(args) {
|
|
|
2987
3140
|
if (!['compact', 'normal'].includes(density)) throw new Error(tr('--density 는 compact · normal 중 하나여야 합니다.', `--density must be compact or normal.`));
|
|
2988
3141
|
patch.density = density;
|
|
2989
3142
|
}
|
|
3143
|
+
// VIEW-GRID-LINES — 칸 선(rows 가로선만 · all 칸마다 · none)·진하기(light · strong).
|
|
3144
|
+
if (args.lines !== undefined) {
|
|
3145
|
+
const lines = String(args.lines);
|
|
3146
|
+
if (!['rows', 'all', 'none'].includes(lines)) throw new Error(tr('--lines 는 rows · all · none 중 하나여야 합니다.', `--lines must be rows, all or none.`));
|
|
3147
|
+
patch.gridLines = lines;
|
|
3148
|
+
}
|
|
3149
|
+
if (args['line-strength'] !== undefined) {
|
|
3150
|
+
const strength = String(args['line-strength']);
|
|
3151
|
+
if (!['light', 'strong'].includes(strength)) throw new Error(tr('--line-strength 는 light · strong 중 하나여야 합니다.', `--line-strength must be light or strong.`));
|
|
3152
|
+
patch.lineStrength = strength;
|
|
3153
|
+
}
|
|
2990
3154
|
if (args.freeze !== undefined) {
|
|
2991
3155
|
const n = Number(args.freeze);
|
|
2992
3156
|
if (!Number.isInteger(n) || n < 0 || n > 3) throw new Error(tr('--freeze 는 0~3 (왼쪽에 붙여 둘 칸 수)', `--freeze must be 0-3 (columns kept on the left)`));
|
|
@@ -3001,7 +3165,11 @@ async function cmdDbView(args) {
|
|
|
3001
3165
|
throw new Error(tr(`--${flag} 는 on 또는 off`, `--${flag} must be on or off`));
|
|
3002
3166
|
};
|
|
3003
3167
|
if (args['row-hidden'] !== undefined) patch.rowPaneShowHidden = onOff('row-hidden', args['row-hidden']);
|
|
3004
|
-
if (args['
|
|
3168
|
+
if (args['personal-filters'] !== undefined) patch.filtersPersonalOnly = onOff('personal-filters', args['personal-filters']);
|
|
3169
|
+
if (args['entry-row'] !== undefined) {
|
|
3170
|
+
const v = String(args['entry-row']).toLowerCase();
|
|
3171
|
+
patch.entryRow = v === 'top' || v === 'bottom' ? v : onOff('entry-row', args['entry-row']);
|
|
3172
|
+
}
|
|
3005
3173
|
if (typeof args.quick === 'string') {
|
|
3006
3174
|
const quick = args.quick.split(',').map((c) => c.trim()).filter(Boolean);
|
|
3007
3175
|
patch.quickFilters = quick.length ? quick : null;
|
|
@@ -3011,7 +3179,7 @@ async function cmdDbView(args) {
|
|
|
3011
3179
|
patch.dayPager = !v || v === 'off' || v === 'false' ? null : { dateProp: v };
|
|
3012
3180
|
}
|
|
3013
3181
|
if (!Object.keys(patch).length) {
|
|
3014
|
-
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year, --name 새이름, --cards "칸,칸", --density compact|normal, --freeze 0~3, --row-hidden on|off, --quick "칸,칸", --entry-row on|off 또는 --day-pager 날짜칸|off', `Give something to change: --scale week|month|year, --name <new name>, --cards "field,field", --density compact|normal, --freeze 0-3, --row-hidden on|off, --quick "field,field", --entry-row on|off, or --day-pager <date field>|off`));
|
|
3182
|
+
throw new Error(tr('바꿀 것을 주세요: --scale week|month|year, --name 새이름, --cards "칸,칸", --density compact|normal, --lines rows|all|none, --line-strength light|strong, --freeze 0~3, --row-hidden on|off, --quick "칸,칸", --entry-row on|off|top|bottom, --personal-filters on|off 또는 --day-pager 날짜칸|off', `Give something to change: --scale week|month|year, --name <new name>, --cards "field,field", --density compact|normal, --lines rows|all|none, --line-strength light|strong, --freeze 0-3, --row-hidden on|off, --quick "field,field", --entry-row on|off|top|bottom, --personal-filters on|off, or --day-pager <date field>|off`));
|
|
3015
3183
|
}
|
|
3016
3184
|
|
|
3017
3185
|
const view = await client.updateDatabaseView(id, viewRef, patch);
|
|
@@ -4148,6 +4316,9 @@ async function main() {
|
|
|
4148
4316
|
return cmdForm(args);
|
|
4149
4317
|
case 'publish':
|
|
4150
4318
|
return cmdPublish(args);
|
|
4319
|
+
case 'issue':
|
|
4320
|
+
case 'issues':
|
|
4321
|
+
return cmdIssue(args);
|
|
4151
4322
|
case 'booking':
|
|
4152
4323
|
return cmdBooking(args);
|
|
4153
4324
|
case 'gcal':
|
package/lib/client.js
CHANGED
|
@@ -165,11 +165,12 @@ export class RootrClient {
|
|
|
165
165
|
* @param {boolean} [opts.returnResponse] - if true, return the raw Response (for header access)
|
|
166
166
|
*/
|
|
167
167
|
async request(path, opts = {}) {
|
|
168
|
-
|
|
168
|
+
// ISSUE-PUBLIC — `noAuth` 는 공개 주소에 **손님으로** 부를 때(키를 보내면 그 키 주인의 계정으로 쓰인다).
|
|
169
|
+
if (!opts.noAuth) this._requireApiKey();
|
|
169
170
|
|
|
170
171
|
const url = opts.url || `${this.baseUrl}${path}`;
|
|
171
172
|
const headers = {
|
|
172
|
-
'x-api-key': this.apiKey,
|
|
173
|
+
...(opts.noAuth ? {} : { 'x-api-key': this.apiKey }),
|
|
173
174
|
...this.defaultHeaders,
|
|
174
175
|
...(opts.headers || {}),
|
|
175
176
|
};
|
|
@@ -936,10 +937,10 @@ export class RootrClient {
|
|
|
936
937
|
|
|
937
938
|
// ---- Issues ---------------------------------------------------------------
|
|
938
939
|
|
|
939
|
-
async createIssueTracker(workspace, name) {
|
|
940
|
+
async createIssueTracker(workspace, name, extra = {}) {
|
|
940
941
|
return this.request(`/workspaces/${encodeURIComponent(workspace)}/issue-trackers`, {
|
|
941
942
|
method: 'POST',
|
|
942
|
-
body: { name },
|
|
943
|
+
body: { name, ...extra },
|
|
943
944
|
});
|
|
944
945
|
}
|
|
945
946
|
|
|
@@ -947,6 +948,60 @@ export class RootrClient {
|
|
|
947
948
|
return this.request(`/issue-trackers/${encodeURIComponent(trackerId)}`);
|
|
948
949
|
}
|
|
949
950
|
|
|
951
|
+
/**
|
|
952
|
+
* 트래커 설정을 고친다 — `{ publicParticipation }`(외부 참여: read·login·anyone)만 보내면 그 값만 바뀐다.
|
|
953
|
+
* `labels`·`types` 는 통째로 바꾼다. 외부 참여를 **바꾸는 것**은 MANAGE(웹게시와 같은 주인 몫)다.
|
|
954
|
+
*/
|
|
955
|
+
async updateIssueTracker(trackerId, patch = {}) {
|
|
956
|
+
return this.request(`/issue-trackers/${encodeURIComponent(trackerId)}`, {
|
|
957
|
+
method: 'PATCH',
|
|
958
|
+
body: patch,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// ── ISSUE-PUBLIC — 남의(또는 내) **웹게시한** 이슈트래커에 밖에서 쓰기 ──────────────
|
|
963
|
+
// 공개 주소(rootr.io/p/{slug}) 위의 길이다. 키를 보내면 그 키 주인(계정)으로 쓰이고, `asGuest` 면 키 없이 손님 이름으로.
|
|
964
|
+
// 페이지에 끼운 트래커는 페이지 주소 + `blockId` 로 닿는다.
|
|
965
|
+
|
|
966
|
+
/** 잠긴 공개 주소의 비밀번호 → 통과증(`?pass=`). */
|
|
967
|
+
async unlockPublicNode(slug, password) {
|
|
968
|
+
const out = await this.request(`/public/nodes/${encodeURIComponent(slug)}/unlock`, {
|
|
969
|
+
method: 'POST',
|
|
970
|
+
body: { password },
|
|
971
|
+
noAuth: true,
|
|
972
|
+
});
|
|
973
|
+
return out?.token;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
_publicIssuesBase(slug, blockId) {
|
|
977
|
+
const s = encodeURIComponent(slug);
|
|
978
|
+
return blockId ? `/public/nodes/${s}/issue-blocks/${encodeURIComponent(blockId)}/issues` : `/public/nodes/${s}/issues`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async submitPublicIssue(slug, { blockId, title, body, guestName, pass, asGuest } = {}) {
|
|
982
|
+
const qs = pass ? `?pass=${encodeURIComponent(pass)}` : '';
|
|
983
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}${qs}`, {
|
|
984
|
+
method: 'POST',
|
|
985
|
+
body: { title, body, ...(guestName ? { guestName } : {}) },
|
|
986
|
+
noAuth: Boolean(asGuest) || !this.apiKey,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async commentPublicIssue(slug, number, { blockId, body, guestName, pass, asGuest } = {}) {
|
|
991
|
+
const qs = pass ? `?pass=${encodeURIComponent(pass)}` : '';
|
|
992
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}/${encodeURIComponent(String(number))}/comments${qs}`, {
|
|
993
|
+
method: 'POST',
|
|
994
|
+
body: { body, ...(guestName ? { guestName } : {}) },
|
|
995
|
+
noAuth: Boolean(asGuest) || !this.apiKey,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** 공개 트래커(또는 페이지 블록)의 이슈 하나 — 댓글까지. */
|
|
1000
|
+
async readPublicIssue(slug, number, { blockId, pass } = {}) {
|
|
1001
|
+
const qs = pass ? `?pass=${encodeURIComponent(pass)}` : '';
|
|
1002
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}/${encodeURIComponent(String(number))}${qs}`, { noAuth: true });
|
|
1003
|
+
}
|
|
1004
|
+
|
|
950
1005
|
async listIssues(trackerId, { state, label, type, assigneeId, q } = {}) {
|
|
951
1006
|
const params = new URLSearchParams();
|
|
952
1007
|
if (state) params.set('state', state);
|
|
@@ -1133,11 +1188,11 @@ export class RootrClient {
|
|
|
1133
1188
|
*/
|
|
1134
1189
|
async createDatabaseView(
|
|
1135
1190
|
databaseId,
|
|
1136
|
-
{ name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, dayPager } = {},
|
|
1191
|
+
{ name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, gridLines, lineStrength, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager } = {},
|
|
1137
1192
|
) {
|
|
1138
1193
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/views`, {
|
|
1139
1194
|
method: 'POST',
|
|
1140
|
-
body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, dayPager },
|
|
1195
|
+
body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, gridLines, lineStrength, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager },
|
|
1141
1196
|
});
|
|
1142
1197
|
}
|
|
1143
1198
|
|
|
@@ -2126,10 +2181,10 @@ export class RootrClient {
|
|
|
2126
2181
|
}
|
|
2127
2182
|
|
|
2128
2183
|
/** 연구노트 프리셋: 기재자(나) → 점검자 두 단계. */
|
|
2129
|
-
async applyResearchNotePreset(nodeId, { reviewerUserId, noteMeta } = {}) {
|
|
2184
|
+
async applyResearchNotePreset(nodeId, { reviewerUserId, noteMeta, authorUserId } = {}) {
|
|
2130
2185
|
return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/research-note-preset`, {
|
|
2131
2186
|
method: 'POST',
|
|
2132
|
-
body: { reviewerUserId, noteMeta },
|
|
2187
|
+
body: { reviewerUserId, noteMeta, authorUserId },
|
|
2133
2188
|
});
|
|
2134
2189
|
}
|
|
2135
2190
|
|
|
@@ -74,12 +74,16 @@ const FILTER_DESC =
|
|
|
74
74
|
'still show them. A new row added while a filter is on is seeded with the filtered values so it does not ' +
|
|
75
75
|
'vanish the moment it is created.';
|
|
76
76
|
|
|
77
|
-
const
|
|
77
|
+
const SORT_ONE = z.object({
|
|
78
78
|
propId: z.string().describe('Column NAME or id to sort by'),
|
|
79
79
|
dir: z.enum(['asc', 'desc']).describe('asc = 오름차순, desc = 내림차순'),
|
|
80
80
|
});
|
|
81
|
+
const SORT_SHAPE = z.union([SORT_ONE, z.array(SORT_ONE).min(1).max(3)]);
|
|
81
82
|
|
|
82
|
-
const SORT_DESC =
|
|
83
|
+
const SORT_DESC =
|
|
84
|
+
'Row order in this view — one { propId, dir } or a list of up to 3 (the next one breaks ties of the previous, e.g. ' +
|
|
85
|
+
'[{ "propId": "지원일", "dir": "desc" }, { "propId": "등록일시", "dir": "desc" }]). Rows still tied are ordered by creation ' +
|
|
86
|
+
'time in the direction of the first criterion (descending → the newest row first).';
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
89
|
* VIEW-ROW-SHOW-HIDDEN · VIEW-QUICK-FILTER · VIEW-ENTRY-ROW · VIEW-DAY-PAGER — 표 보기 설정 넷. 만들기(rootr_create_view)와
|
|
@@ -105,14 +109,24 @@ const VIEW_EXTRA_SHAPE = {
|
|
|
105
109
|
'has value, relation → pick linked rows, date → that day. null or [] turns it off.',
|
|
106
110
|
),
|
|
107
111
|
entryRow: z
|
|
108
|
-
.boolean()
|
|
112
|
+
.union([z.boolean(), z.enum(['top', 'bottom'])])
|
|
109
113
|
.nullable()
|
|
110
114
|
.optional()
|
|
111
115
|
.describe(
|
|
112
|
-
'table view: keep one always-empty entry row
|
|
116
|
+
'table view: keep one always-empty entry row (spreadsheet-like) — true/"bottom" at the bottom, "top" above the rows ' +
|
|
117
|
+
'(for tables read newest-first; rows made there stick to the top). Typing a value into any of its cells ' +
|
|
113
118
|
'creates the row (pre-filled to match the view filters, like the "new row" button) and a fresh empty row appears. ' +
|
|
114
119
|
'Focus/click alone creates nothing. false/null = off.',
|
|
115
120
|
),
|
|
121
|
+
filtersPersonalOnly: z
|
|
122
|
+
.boolean()
|
|
123
|
+
.nullable()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe(
|
|
126
|
+
'table view: filters set in this view always apply only to the person setting them (no "share with everyone" switch) — ' +
|
|
127
|
+
'for views several people search at once. Conditions already shared stay. false/null = off. (Filters are just-me by default anyway; ' +
|
|
128
|
+
'this removes the switch.)',
|
|
129
|
+
),
|
|
116
130
|
dayPager: z
|
|
117
131
|
.union([z.object({ dateProp: z.string() }), z.string()])
|
|
118
132
|
.nullable()
|
|
@@ -168,7 +182,8 @@ export function registerDatabaseTools(server) {
|
|
|
168
182
|
title: 'Create a Rootr database (table / board / kanban)',
|
|
169
183
|
description:
|
|
170
184
|
'Create a DATABASE node — a typed table with columns, views and rows — in ONE call, optionally seeded with ' +
|
|
171
|
-
'its first rows.
|
|
185
|
+
'its first rows. Column names must be unique in the table (trimmed, case-insensitive) — duplicates are ' +
|
|
186
|
+
'rejected with a 400 so rows can be addressed by column name unambiguously. **Use this whenever you need a table**: moving a Notion database over, building a kanban ' +
|
|
172
187
|
'board, a task/requirement/risk tracker, anything with trackable rows. Do NOT fake a table with a markdown ' +
|
|
173
188
|
'document, and do NOT create an empty database and then add columns one by one — `properties` order IS the ' +
|
|
174
189
|
'column order, so one call keeps the columns in exactly the order you list them. ' +
|
|
@@ -376,6 +391,16 @@ export function registerDatabaseTools(server) {
|
|
|
376
391
|
.nullable()
|
|
377
392
|
.optional()
|
|
378
393
|
.describe('table view: "compact" = lower rows and smaller text so more rows fit on one screen. null/"normal" = default.'),
|
|
394
|
+
gridLines: z
|
|
395
|
+
.enum(['rows', 'all', 'none'])
|
|
396
|
+
.nullable()
|
|
397
|
+
.optional()
|
|
398
|
+
.describe('table view lines: "rows" = lines between rows only (default), "all" = a border around every cell (spreadsheet-like), "none" = no lines. null = default.'),
|
|
399
|
+
lineStrength: z
|
|
400
|
+
.enum(['light', 'strong'])
|
|
401
|
+
.nullable()
|
|
402
|
+
.optional()
|
|
403
|
+
.describe('table view: "strong" = darker grid lines so rows are easier to tell apart. null/"light" = default.'),
|
|
379
404
|
frozenColumns: z
|
|
380
405
|
.number()
|
|
381
406
|
.int()
|
|
@@ -390,7 +415,7 @@ export function registerDatabaseTools(server) {
|
|
|
390
415
|
...VIEW_EXTRA_SHAPE,
|
|
391
416
|
},
|
|
392
417
|
},
|
|
393
|
-
async ({ databaseId, name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, dayPager }) => {
|
|
418
|
+
async ({ databaseId, name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, gridLines, lineStrength, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager }) => {
|
|
394
419
|
try {
|
|
395
420
|
const client = makeClient();
|
|
396
421
|
const id = await resolveNodeId(client, databaseId);
|
|
@@ -405,10 +430,13 @@ export function registerDatabaseTools(server) {
|
|
|
405
430
|
colWidths,
|
|
406
431
|
calendarScale,
|
|
407
432
|
density,
|
|
433
|
+
gridLines,
|
|
434
|
+
lineStrength,
|
|
408
435
|
frozenColumns,
|
|
409
436
|
rowPaneShowHidden,
|
|
410
437
|
quickFilters,
|
|
411
438
|
entryRow,
|
|
439
|
+
filtersPersonalOnly,
|
|
412
440
|
dayPager,
|
|
413
441
|
});
|
|
414
442
|
return jsonResult(view);
|
|
@@ -455,8 +483,7 @@ export function registerDatabaseTools(server) {
|
|
|
455
483
|
'Pass an empty array to show everything again. Only the fields you pass change, and other views are untouched. ' +
|
|
456
484
|
'For an at-a-glance table also pass density "compact" (lower rows, smaller text) and frozenColumns N (first N visible ' +
|
|
457
485
|
'columns stay fixed while scrolling sideways). Data-entry helpers: rowPaneShowHidden (hidden columns still show when a row ' +
|
|
458
|
-
'is opened), quickFilters (columns pinned at the top of the filter panel, just type), entryRow (always-empty
|
|
459
|
-
'Excel-like) and dayPager {dateProp} (one day per page with ◀ ▶ / Today, new rows get that day).',
|
|
486
|
+
'is opened), quickFilters (columns pinned at the top of the filter panel, just type), entryRow (always-empty row, Excel-like; "top" puts it above the rows), filtersPersonalOnly (filters always just-me) and dayPager {dateProp} (one day per page with ◀ ▶ / Today, new rows get that day).',
|
|
460
487
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
461
488
|
inputSchema: {
|
|
462
489
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
@@ -515,6 +542,16 @@ export function registerDatabaseTools(server) {
|
|
|
515
542
|
.nullable()
|
|
516
543
|
.optional()
|
|
517
544
|
.describe('table view: "compact" = lower rows and smaller text so more rows fit on one screen. null/"normal" = default.'),
|
|
545
|
+
gridLines: z
|
|
546
|
+
.enum(['rows', 'all', 'none'])
|
|
547
|
+
.nullable()
|
|
548
|
+
.optional()
|
|
549
|
+
.describe('table view lines: "rows" = lines between rows only (default), "all" = a border around every cell (spreadsheet-like), "none" = no lines. null = default.'),
|
|
550
|
+
lineStrength: z
|
|
551
|
+
.enum(['light', 'strong'])
|
|
552
|
+
.nullable()
|
|
553
|
+
.optional()
|
|
554
|
+
.describe('table view: "strong" = darker grid lines so rows are easier to tell apart. null/"light" = default.'),
|
|
518
555
|
frozenColumns: z
|
|
519
556
|
.number()
|
|
520
557
|
.int()
|
|
@@ -544,10 +581,13 @@ export function registerDatabaseTools(server) {
|
|
|
544
581
|
cardProps,
|
|
545
582
|
rowColors,
|
|
546
583
|
density,
|
|
584
|
+
gridLines,
|
|
585
|
+
lineStrength,
|
|
547
586
|
frozenColumns,
|
|
548
587
|
rowPaneShowHidden,
|
|
549
588
|
quickFilters,
|
|
550
589
|
entryRow,
|
|
590
|
+
filtersPersonalOnly,
|
|
551
591
|
dayPager,
|
|
552
592
|
}) => {
|
|
553
593
|
try {
|
|
@@ -566,10 +606,13 @@ export function registerDatabaseTools(server) {
|
|
|
566
606
|
cardProps,
|
|
567
607
|
rowColors,
|
|
568
608
|
density,
|
|
609
|
+
gridLines,
|
|
610
|
+
lineStrength,
|
|
569
611
|
frozenColumns,
|
|
570
612
|
rowPaneShowHidden,
|
|
571
613
|
quickFilters,
|
|
572
614
|
entryRow,
|
|
615
|
+
filtersPersonalOnly,
|
|
573
616
|
dayPager,
|
|
574
617
|
});
|
|
575
618
|
return jsonResult({ view });
|
|
@@ -1376,7 +1419,9 @@ export function registerDatabaseTools(server) {
|
|
|
1376
1419
|
'sources?: "all" (default — per column take the most recent earlier row that HAS a value) | "latest" (only the latest earlier row); ' +
|
|
1377
1420
|
'strategy?: default per column, one of fill_empty (default) | prefer_new | prefer_old | latest | skip; columns?: { "<column>": ' +
|
|
1378
1421
|
'fill_empty | prefer_new | prefer_old | latest | skip | max | min | sum (number) | earliest | latest_date (date) | union ' +
|
|
1379
|
-
'(multi_select/relation/files/person) | { append: { sep?: " / ", datePrefix?: true
|
|
1422
|
+
'(multi_select/relation/files/person) | { append: { sep?: " / ", datePrefix?: true, unique?: true, emptyText?: "기존 메모 없음", ' +
|
|
1423
|
+
'history?: { format: "{date}지원", dateFormat?: "YY-MM-DD", sep?: ", ", only?: true } } } (text — unique keeps a piece once, ' +
|
|
1424
|
+
'emptyText stands in for an empty earlier note, history adds ONE line of earlier rows\' dates, only = just that line) } — a strategy on the wrong column ' +
|
|
1380
1425
|
'type is a 400; exclude = skip, overwrite = prefer_old; representative?: "latest" | "oldest", representativeWhen?: [{ propId, op, ' +
|
|
1381
1426
|
'values }] (first row matching wins). Every operation returns an opId (undo with rootr_dedupe_undo); preview with ' +
|
|
1382
1427
|
'rootr_dedupe_preview. ' +
|
|
@@ -1975,7 +2020,10 @@ export function registerDatabaseTools(server) {
|
|
|
1975
2020
|
'Remove ONE column from a Rootr (루터) DATABASE. The values stored under it are NOT erased from the rows, ' +
|
|
1976
2021
|
'so re-adding a column is not enough to bring them back in the UI — confirm with the user before calling this. ' +
|
|
1977
2022
|
'Exception: values of a vector column are erased (see below). ' +
|
|
1978
|
-
'The title column cannot be deleted. Views grouped/dated by the column are cleared automatically
|
|
2023
|
+
'The title column cannot be deleted. Views grouped/dated by the column are cleared automatically, and so are the ' +
|
|
2024
|
+
'value lines that point at it in buttons/automations of THIS and OTHER tables (an add_row into this table, an update_rows ' +
|
|
2025
|
+
'of its rows, an ai_fill of that column); automations whose condition watches it stop. The result lists them under ' +
|
|
2026
|
+
'`deleteCascade` — show it to the user. ' +
|
|
1979
2027
|
'For a vector column the stored vectors are deleted with it (not kept for re-adding — re-add the column and ' +
|
|
1980
2028
|
'store the vectors again with rootr_set_row_vectors).',
|
|
1981
2029
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
@@ -2053,11 +2101,17 @@ export function registerDatabaseTools(server) {
|
|
|
2053
2101
|
'· {kind:"update_rows", target:"created"|"selected", values} — set values on the rows this button just ' +
|
|
2054
2102
|
'made, or on the rows the person ticked in the table.\n' +
|
|
2055
2103
|
'· {kind:"ai_fill", target, propId, prompt} — let AI write ONE cell. The prompt may quote the row with ' +
|
|
2056
|
-
'{칸이름}, e.g. "{제목}과 {메모}를 한 문장으로 요약해". If the AI fails the button keeps going
|
|
2104
|
+
'{칸이름}, e.g. "{제목}과 {메모}를 한 문장으로 요약해". If the AI fails the button keeps going. ' +
|
|
2105
|
+
'Pass fields:[{propId, prompt?}] (≤20) instead of propId to fill SEVERAL columns with ONE AI call from the same prompt ' +
|
|
2106
|
+
'(e.g. split one pasted text into many columns in one step; missing values stay empty).\n' +
|
|
2057
2107
|
'· {kind:"open_row"} — open the row this button made (UI only; over the API it just reports the id).\n' +
|
|
2058
2108
|
'Values accept @today, @today+7 and @me — the server fills them in at press time, not now. A text value ' +
|
|
2059
2109
|
'may also quote the pressed row with {칸이름}, e.g. "{이름} {통화 건수+1}차": computed columns (rollup, ' +
|
|
2060
|
-
'formula) are read too, {칸이름+N} / {칸이름-N} adds to a numeric value
|
|
2110
|
+
'formula) are read too, {칸이름+N} / {칸이름-N} adds to a numeric value (on a DATE column: N days later/earlier, ' +
|
|
2111
|
+
'{투입예정일+3}); {칸:YY-MM-DD} formats a date; {칸+3|@today+3} or {메모|없음} gives the value used when the column is ' +
|
|
2112
|
+
'empty; {@today} / {@today+7} / {@today:YY-MM-DD} work inside text too. APPEND A LINE: a value { append: "{@today:YY-MM-DD} ' +
|
|
2113
|
+
'{상태}", sep?: "\\n", unique?: true } on a text column adds that line to the END of the cell at write time (same line already ' +
|
|
2114
|
+
'there → skipped) instead of rewriting the whole cell. When several rows are ' +
|
|
2061
2115
|
'selected each new row is filled from its own source row. A relation column resolves to the linked row: ' +
|
|
2062
2116
|
'{지원자} is its TITLE (not the id) and {지원자.통화 건수} reads one of its columns — EXCEPT when the value ' +
|
|
2063
2117
|
'is exactly "{관계 칸}" and the column it goes into is a relation to the SAME table: then the row ids are ' +
|
|
@@ -60,7 +60,10 @@ export function registerFeedbackTools(server) {
|
|
|
60
60
|
.string()
|
|
61
61
|
.email()
|
|
62
62
|
.optional()
|
|
63
|
-
.describe(
|
|
63
|
+
.describe(
|
|
64
|
+
'Only if the user asked for a reply and gave you their address. Without it, the "fixed" notice goes to the ' +
|
|
65
|
+
'signed-in user, or — for a workspace API key — to the person who created that key.',
|
|
66
|
+
),
|
|
64
67
|
reporter: z
|
|
65
68
|
.string()
|
|
66
69
|
.max(120)
|
package/lib/mcp-tools/forms.js
CHANGED
|
@@ -116,6 +116,9 @@ const FORM_CONFIG_DESC =
|
|
|
116
116
|
'`mirror: { fieldMap, defaults: { <표의 칸 이름 또는 id>: value } }` — `defaults` stamps extra values onto the ' +
|
|
117
117
|
'row a response creates, for columns the person did not answer; use it to start a 신청 at "상신 중" so a ' +
|
|
118
118
|
'manager has something to move to 승인/거절. What the person actually answered always wins. ' +
|
|
119
|
+
// FORM-DEFAULTS-SUBMITTER(이슈 #227/#228) — 적어 두지 않으면 에이전트는 "누가 냈는지"를 칸에 남기는 길을 모른다.
|
|
120
|
+
'A default of `"@me"` becomes the logged-in submitter\'s name (person column → [name], title/text → name; ' +
|
|
121
|
+
'left empty for anonymous submissions) — use it to record who applied; `"@today"`/`"@today+7"` for dates. ' +
|
|
119
122
|
'`publicSubmit` ("anon"|"login") — who may answer from outside the workspace. Publishing the form ' +
|
|
120
123
|
'(`rootr_publish_node`, or the 웹에 공개 switch) turns this on by itself, so you almost never set it by hand; ' +
|
|
121
124
|
'to stop taking answers use `closesAt`/`maxResponses`, or unpublish. ' +
|
package/lib/mcp-tools/issues.js
CHANGED
|
@@ -2,6 +2,27 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import { textResult, jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, NODE_URL_HINT } from './shared.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* ISSUE-PUBLIC — 외부 참여(웹게시한 트래커·페이지에 끼운 트래커에 밖의 사람이 무엇을 할 수 있나). 서버 값과 같아야 한다
|
|
7
|
+
* (issues/issue-schema.ts PUBLIC_PARTICIPATION_MODES).
|
|
8
|
+
*/
|
|
9
|
+
const PARTICIPATION = z
|
|
10
|
+
.enum(['read', 'login', 'anyone'])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(
|
|
13
|
+
'Outside participation (외부 참여) on the tracker\'s PUBLIC page and on any published PAGE that shows it (`issues` block): ' +
|
|
14
|
+
'"read" 보기만 (default) · "login" 로그인한 사람만 작성 (any Rootr account may file issues and comment) · ' +
|
|
15
|
+
'"anyone" 누구나 작성 (no account — visitors type a name, shown as "이름 (외부)"). Takes effect once the tracker ' +
|
|
16
|
+
'(or the page holding it) is published — rootr_publish_node.',
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/** 공개 주소 글자(slug · rootr.io/…/p/{slug} 주소)에서 slug 만. */
|
|
20
|
+
function slugOf(target) {
|
|
21
|
+
const t = String(target || '').trim();
|
|
22
|
+
const m = /\/p\/([^/?#\s]+)/.exec(t);
|
|
23
|
+
return m ? decodeURIComponent(m[1]) : t;
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
/** Issue tracker tools: create tracker, list/create/get/update issues, comment. */
|
|
6
27
|
export function registerIssueTools(server) {
|
|
7
28
|
server.registerTool(
|
|
@@ -19,13 +40,14 @@ export function registerIssueTools(server) {
|
|
|
19
40
|
.string()
|
|
20
41
|
.optional()
|
|
21
42
|
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
43
|
+
publicParticipation: PARTICIPATION,
|
|
22
44
|
},
|
|
23
45
|
},
|
|
24
|
-
async ({ name, workspace }) => {
|
|
46
|
+
async ({ name, workspace, publicParticipation }) => {
|
|
25
47
|
try {
|
|
26
48
|
const ws = requireWorkspace(workspace);
|
|
27
49
|
const client = makeClient(ws);
|
|
28
|
-
const result = await client.createIssueTracker(ws, name);
|
|
50
|
+
const result = await client.createIssueTracker(ws, name, publicParticipation ? { publicParticipation } : {});
|
|
29
51
|
return jsonResult(result);
|
|
30
52
|
} catch (err) {
|
|
31
53
|
return errorResult(err);
|
|
@@ -170,4 +192,124 @@ export function registerIssueTools(server) {
|
|
|
170
192
|
}
|
|
171
193
|
},
|
|
172
194
|
);
|
|
195
|
+
|
|
196
|
+
server.registerTool(
|
|
197
|
+
'rootr_update_issue_tracker',
|
|
198
|
+
{
|
|
199
|
+
title: 'Change an issue tracker\'s settings (outside participation, labels, types)',
|
|
200
|
+
description:
|
|
201
|
+
'Change settings of a Rootr (루터) issue tracker. `publicParticipation` decides what people OUTSIDE the workspace may ' +
|
|
202
|
+
'do on its public page (rootr.io/p/…) and on published PAGEs that show it: "read" 보기만 · "login" 로그인한 사람만 ' +
|
|
203
|
+
'작성 · "anyone" 누구나 작성 (just a name). Only the keys you send change; `labels`/`types` REPLACE the whole list ' +
|
|
204
|
+
'([{ id?, name, color? }] — read the current ones with rootr_list_issues / the tracker first). Changing ' +
|
|
205
|
+
'publicParticipation is an owner-level action (MANAGE, same as publishing) — use an account key (PAT). ' +
|
|
206
|
+
'To also publish in one step use rootr_publish_node with `participation`.',
|
|
207
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
208
|
+
inputSchema: {
|
|
209
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
210
|
+
publicParticipation: PARTICIPATION,
|
|
211
|
+
labels: z
|
|
212
|
+
.array(z.object({ id: z.string().optional(), name: z.string(), color: z.string().optional() }))
|
|
213
|
+
.optional()
|
|
214
|
+
.describe('Full label list (replaces the current one)'),
|
|
215
|
+
types: z
|
|
216
|
+
.array(z.object({ id: z.string().optional(), name: z.string(), color: z.string().optional() }))
|
|
217
|
+
.optional()
|
|
218
|
+
.describe('Full issue type list (replaces the current one)'),
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
async ({ trackerId, ...patch }) => {
|
|
222
|
+
try {
|
|
223
|
+
const client = makeClient();
|
|
224
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
225
|
+
const body = Object.fromEntries(Object.entries(patch).filter(([, v]) => v !== undefined));
|
|
226
|
+
return jsonResult(await client.updateIssueTracker(trackerId, body));
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return errorResult(err);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const PUBLIC_TARGET = {
|
|
234
|
+
url: z
|
|
235
|
+
.string()
|
|
236
|
+
.describe(
|
|
237
|
+
'The PUBLIC address of the tracker — "https://rootr.io/ko/p/<slug>" or just the slug. For a tracker shown on a ' +
|
|
238
|
+
'published PAGE, the page\'s address plus `blockId`.',
|
|
239
|
+
),
|
|
240
|
+
blockId: z.string().optional().describe('Only when the tracker sits on a published PAGE: that `issues` block\'s id'),
|
|
241
|
+
guestName: z
|
|
242
|
+
.string()
|
|
243
|
+
.max(40)
|
|
244
|
+
.optional()
|
|
245
|
+
.describe('Name to show when writing as a guest (누구나 작성 mode with asGuest). Ignored when writing as your account.'),
|
|
246
|
+
asGuest: z
|
|
247
|
+
.boolean()
|
|
248
|
+
.optional()
|
|
249
|
+
.describe(
|
|
250
|
+
'Write WITHOUT your API key, as a guest with `guestName` (only works when participation is "anyone"). ' +
|
|
251
|
+
'Default false: the key\'s owner account is the author (works for "login" and "anyone").',
|
|
252
|
+
),
|
|
253
|
+
password: z.string().optional().describe('If the public page is password-locked'),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
server.registerTool(
|
|
257
|
+
'rootr_submit_public_issue',
|
|
258
|
+
{
|
|
259
|
+
title: 'File an issue on a PUBLISHED tracker (outside participation)',
|
|
260
|
+
description:
|
|
261
|
+
'File an issue on someone\'s (or your own) PUBLISHED Rootr (루터) issue tracker through its public address — the ' +
|
|
262
|
+
'way an outside person or an external agent reports a bug/request. Works only when the tracker\'s participation ' +
|
|
263
|
+
'allows it: "login" (your account key counts as signed in) or "anyone". Errors: 403 PUBLIC_READ_ONLY (보기만), ' +
|
|
264
|
+
'401 LOGIN_REQUIRED, 400 GUEST_NAME_REQUIRED, 401 PUBLIC_DOC_PASSWORD (pass `password`). Text only — labels, ' +
|
|
265
|
+
'assignee and state cannot be set from outside, and @mentions are stripped. Returns { ok, number }. ' +
|
|
266
|
+
'For a tracker in YOUR workspace prefer rootr_create_issue (full fields).',
|
|
267
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
268
|
+
inputSchema: {
|
|
269
|
+
...PUBLIC_TARGET,
|
|
270
|
+
title: z.string().max(200).describe('Issue title (≤200)'),
|
|
271
|
+
body: z.string().max(20000).optional().describe('Issue body, markdown (≤20,000)'),
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
async ({ url, blockId, guestName, asGuest, password, title, body }) => {
|
|
275
|
+
try {
|
|
276
|
+
const client = makeClient();
|
|
277
|
+
const slug = slugOf(url);
|
|
278
|
+
const pass = password ? await client.unlockPublicNode(slug, password) : undefined;
|
|
279
|
+
return jsonResult(await client.submitPublicIssue(slug, { blockId, title, body, guestName, pass, asGuest }));
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return errorResult(err);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
server.registerTool(
|
|
287
|
+
'rootr_comment_public_issue',
|
|
288
|
+
{
|
|
289
|
+
title: 'Comment on an issue of a PUBLISHED tracker (outside participation)',
|
|
290
|
+
description:
|
|
291
|
+
'Comment on issue #number of a PUBLISHED Rootr (루터) issue tracker through its public address (same rules as ' +
|
|
292
|
+
'rootr_submit_public_issue). Pass `read: true` instead of `body` to just read that issue\'s public comments. ' +
|
|
293
|
+
'Returns { ok, id } (or { number, comments } when reading).',
|
|
294
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
295
|
+
inputSchema: {
|
|
296
|
+
...PUBLIC_TARGET,
|
|
297
|
+
number: z.number().int().positive().describe('Issue number (#N)'),
|
|
298
|
+
body: z.string().max(10000).optional().describe('Comment, markdown (≤10,000). Required unless read is true'),
|
|
299
|
+
read: z.boolean().optional().describe('Only read the public comments of this issue'),
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
async ({ url, blockId, guestName, asGuest, password, number, body, read }) => {
|
|
303
|
+
try {
|
|
304
|
+
const client = makeClient();
|
|
305
|
+
const slug = slugOf(url);
|
|
306
|
+
const pass = password ? await client.unlockPublicNode(slug, password) : undefined;
|
|
307
|
+
if (read) return jsonResult(await client.readPublicIssue(slug, number, { blockId, pass }));
|
|
308
|
+
if (!body) throw new Error('body is required (or pass read: true)');
|
|
309
|
+
return jsonResult(await client.commentPublicIssue(slug, number, { blockId, body, guestName, pass, asGuest }));
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return errorResult(err);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
);
|
|
173
315
|
}
|
package/lib/mcp-tools/pages.js
CHANGED
|
@@ -49,6 +49,8 @@ const BLOCK_GUIDE =
|
|
|
49
49
|
'· booking {bookingNodeId(DATABASE node id — a table that is published with visitor booking turned on, see rootr_set_booking), headline?, type?} — ' +
|
|
50
50
|
'a visitor scheduling calendar right on the page; `type` shows only one of the table\'s booking types. ' +
|
|
51
51
|
'bookingNodeId must be the node ID (names/paths are not resolved here); a table that is not published or has booking off renders as "not taking bookings"\n' +
|
|
52
|
+
'· issues {trackerNodeId(ISSUE_TRACKER id, unique name or "/path"), headline?} — an issue tracker on the page: visitors read issues and comments and, ' +
|
|
53
|
+
'when the tracker\'s publicParticipation (rootr_update_issue_tracker) is "login" or "anyone", file issues and comment right there — the tracker itself need not be published\n' +
|
|
52
54
|
'· custom {html, height?(40-4000, default 260), minHeight?:"none"|"screen", bleed?:boolean} — free HTML/CSS, rendered inside a locked-down sandboxed iframe ' +
|
|
53
55
|
'(no scripts, no same-origin, no network). Use it for a look the other blocks cannot make. Page theme colors are ' +
|
|
54
56
|
'available inside as var(--pg-bg|text|muted|faint|card|line|accent|on-accent) and var(--pg-font). Scripts and ' +
|
package/lib/mcp-tools/public.js
CHANGED
|
@@ -14,6 +14,16 @@ const DB_DESIGN_SCHEMA = z.object({
|
|
|
14
14
|
skin: z.enum(['clean', 'paper', 'dark', 'vivid', 'bento', 'brutal']).optional(),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
const PARTICIPATION = z
|
|
18
|
+
.enum(['read', 'login', 'anyone'])
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
'ISSUE_TRACKER only — who OUTSIDE the workspace may write on the public page (외부 참여): ' +
|
|
22
|
+
'"read" 보기만 (default), "login" 로그인한 사람만 작성 (any Rootr account files issues and comments), ' +
|
|
23
|
+
'"anyone" 누구나 작성 (no account — visitors type a name, shown as "이름 (외부)"). Stored on the tracker itself, ' +
|
|
24
|
+
'so a tracker placed on a published PAGE (`issues` block) follows the same setting. A password lock locks writing too.',
|
|
25
|
+
);
|
|
26
|
+
|
|
17
27
|
/**
|
|
18
28
|
* PUBLIC-ANY — **어떤 타입이든 공개 발행한다**(창업자 지시 2026-08-27).
|
|
19
29
|
*
|
|
@@ -45,7 +55,8 @@ export function registerPublicTools(server) {
|
|
|
45
55
|
'its rows, form responses, recording audio, CRM contacts and deal amounts, and signer names on a signed ' +
|
|
46
56
|
'document. Rows/entries are capped at 500 per page and the page says so when it truncates. ' +
|
|
47
57
|
'Owner-level action (MANAGE): use an ACCOUNT key (PAT), not a workspace-scoped/connector token. ' +
|
|
48
|
-
'Safe to call again — the slug never changes, so links already shared stay valid.'
|
|
58
|
+
'Safe to call again — the slug never changes, so links already shared stay valid. ' +
|
|
59
|
+
'ISSUE_TRACKER: pass `participation` ("anyone" = outside feedback box) to let visitors file issues and comment.',
|
|
49
60
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
50
61
|
inputSchema: {
|
|
51
62
|
target: z.string().describe(`Node id${NODE_TARGET_HINT}`),
|
|
@@ -64,13 +75,14 @@ export function registerPublicTools(server) {
|
|
|
64
75
|
'leaves the layout alone. Layouts that need a column (calendar/timeline need a date, board a ' +
|
|
65
76
|
'select/status, ranking a number, chart either) fall back to the table and say why.',
|
|
66
77
|
),
|
|
78
|
+
participation: PARTICIPATION,
|
|
67
79
|
},
|
|
68
80
|
},
|
|
69
|
-
async ({ target, includeChildren, view, design }) => {
|
|
81
|
+
async ({ target, includeChildren, view, design, participation }) => {
|
|
70
82
|
try {
|
|
71
83
|
const client = makeClient();
|
|
72
84
|
const id = await resolveNodeId(client, target);
|
|
73
|
-
return jsonResult(await client.publishNode(id, { includeChildren, view, design }));
|
|
85
|
+
return jsonResult(await client.publishNode(id, { includeChildren, view, design, participation }));
|
|
74
86
|
} catch (err) {
|
|
75
87
|
return errorResult(err);
|
|
76
88
|
}
|
|
@@ -97,6 +109,8 @@ export function registerPublicTools(server) {
|
|
|
97
109
|
'`{"seoDescription": "...", "seoImage": "<attachment id>"}` sets the search-result description and link-preview ' +
|
|
98
110
|
'image (search engines are pinged right away). ' +
|
|
99
111
|
'`{"isPublic": true}` on a PAGE needs the Team plan (Free → 403 PLAN_UPGRADE_REQUIRED); closing it works on any plan. ' +
|
|
112
|
+
'`{"participation": "anyone"}` (ISSUE_TRACKER only) opens the public page to outside issues/comments — ' +
|
|
113
|
+
'"read" | "login" | "anyone"; on a tracker a password lock also locks writing. ' +
|
|
100
114
|
'For DOCUMENT nodes use rootr_set_document_public instead — documents keep their own settings (skins, ' +
|
|
101
115
|
'edit proposals) on the older path. Owner-level action (MANAGE): use an account key (PAT).',
|
|
102
116
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
@@ -125,6 +139,7 @@ export function registerPublicTools(server) {
|
|
|
125
139
|
'board, timeline, calendar, chart, ranking. `skin` — clean, paper, dark, vivid, bento, brutal. ' +
|
|
126
140
|
'Only the keys you send change.',
|
|
127
141
|
),
|
|
142
|
+
participation: PARTICIPATION,
|
|
128
143
|
},
|
|
129
144
|
},
|
|
130
145
|
async ({ target, ...settings }) => {
|
|
@@ -152,7 +167,8 @@ export function registerPublicTools(server) {
|
|
|
152
167
|
'address is closed right now — do NOT hand that URL out; tell the user why and how to reopen it. ' +
|
|
153
168
|
'`closed: "PAGE_PUBLISH_REQUIRES_TEAM"` means a PAGE was published but its workspace is on the Free plan, so ' +
|
|
154
169
|
'visitors see a "not published" screen — upgrading to Team reopens the same URL without publishing again. ' +
|
|
155
|
-
'Call this before publishing so you do not tell the user a page is new when it already had a URL.'
|
|
170
|
+
'Call this before publishing so you do not tell the user a page is new when it already had a URL. ' +
|
|
171
|
+
'An ISSUE_TRACKER also returns `participation` (read 보기만 | login 로그인한 사람만 작성 | anyone 누구나 작성).',
|
|
156
172
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
157
173
|
inputSchema: {
|
|
158
174
|
target: z.string().describe(`Node id${NODE_TARGET_HINT}`),
|
package/lib/mcp-tools/signed.js
CHANGED
|
@@ -184,22 +184,33 @@ export function registerSignedDocTools(server) {
|
|
|
184
184
|
title: 'Turn a signed document into a research note',
|
|
185
185
|
description:
|
|
186
186
|
'Switch a SIGNED document to RESEARCH_NOTE mode and build the two-step line the Korean national R&D ' +
|
|
187
|
-
'guideline requires: 기재자
|
|
188
|
-
'metadata (과제번호·과제명·기관). Use this instead of hand-building the approval line for 연구노트.'
|
|
187
|
+
'guideline requires: 기재자 → 점검자(the reviewer/연구책임자). Optionally stores the cover ' +
|
|
188
|
+
'metadata (과제번호·과제명·기관). Use this instead of hand-building the approval line for 연구노트. ' +
|
|
189
|
+
'When calling with a session login, 기재자 defaults to the caller and authorUserId can be omitted. ' +
|
|
190
|
+
'When calling with a WORKSPACE API KEY (no logged-in user), authorUserId is REQUIRED — pass the ' +
|
|
191
|
+
'기재자의 userId explicitly (it may be the same person as reviewerUserId).',
|
|
189
192
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
190
193
|
inputSchema: {
|
|
191
194
|
nodeId: z.string().describe(`서명문서 노드 id${NODE_TARGET_HINT}`),
|
|
192
195
|
reviewerUserId: z.string().describe('점검자(연구책임자)의 userId'),
|
|
196
|
+
authorUserId: z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe(
|
|
200
|
+
'기재자의 userId. 세션 로그인이면 생략 시 호출자 본인이 들어간다. ' +
|
|
201
|
+
'API 키로 부를 때는 필수 — 점검자와 같은 사람이어도 된다.',
|
|
202
|
+
),
|
|
193
203
|
noteMeta: z.record(z.any()).optional().describe('표지 정보: 과제번호·과제명·연구책임자·기관 등'),
|
|
194
204
|
workspace: z.string().optional().describe('워크스페이스 id (생략하면 설정값)'),
|
|
195
205
|
},
|
|
196
206
|
},
|
|
197
|
-
async ({ nodeId, reviewerUserId, noteMeta, workspace }) => {
|
|
207
|
+
async ({ nodeId, reviewerUserId, authorUserId, noteMeta, workspace }) => {
|
|
198
208
|
try {
|
|
199
209
|
const client = makeClient(workspace);
|
|
200
210
|
return jsonResult(
|
|
201
211
|
await client.applyResearchNotePreset(await resolveNodeId(client, nodeId), {
|
|
202
212
|
reviewerUserId,
|
|
213
|
+
authorUserId,
|
|
203
214
|
noteMeta,
|
|
204
215
|
}),
|
|
205
216
|
);
|