rootr-cli 0.32.0 → 0.34.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 +12 -2
- package/README.md +26 -3
- package/bin/rootr.js +247 -7
- package/lib/client.js +135 -10
- package/lib/mcp-tools/databases.js +53 -8
- package/lib/mcp-tools/documents.js +4 -1
- package/lib/mcp-tools/feedback.js +4 -1
- package/lib/mcp-tools/issues.js +233 -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) 서버를 구동합니다. 제공 도구 221종:
|
|
409
409
|
|
|
410
410
|
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
411
411
|
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
@@ -487,6 +487,16 @@ 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`, `files:[{filename, contentBase64}]` 첨부) |
|
|
492
|
+
| `rootr_comment_public_issue` | 웹게시한 트래커의 이슈에 댓글(`files` 첨부, `read:true` 면 공개 댓글 읽기) |
|
|
493
|
+
| `rootr_search_public_issues` | 웹게시한 트래커를 밖에서 찾기 — `q`(제목·본문·#번호·쓴 사람), `labels`(모두 가진 것), `state`, `sort`(newest·oldest·comments) |
|
|
494
|
+
|
|
495
|
+
CLI: `rootr issue tracker <트래커> --participation anyone` · `rootr issue list|create|comment …` ·
|
|
496
|
+
`rootr issue list <공개 주소> --q "결제" --label 버그` · `rootr issue submit <공개 주소> --title "…" --name <이름> --file 화면.png --file 로그.txt` ·
|
|
497
|
+
`rootr issue reply <공개 주소> <#번호> "<댓글>" [--file <경로>]`.
|
|
498
|
+
첨부는 그림(png·jpg·gif·webp)·pdf·txt·csv·log·docx·xlsx·pptx·zip, 한 파일 10MB·글 하나에 10개까지입니다(확장자와 파일 첫 바이트가 맞아야 함).
|
|
499
|
+
외부 참여는 **트래커 설정**이라 페이지(PAGE)에 `issues` 블록으로 끼운 트래커도 같은 규칙을 따릅니다.
|
|
490
500
|
|
|
491
501
|
#### 웹훅 (webhooks:manage 스코프)
|
|
492
502
|
|
|
@@ -713,7 +723,7 @@ rootr db column <표> 특이사항 --wrap on --align center # 긴 글 줄바꿈
|
|
|
713
723
|
| 도구 | 설명 |
|
|
714
724
|
|---|---|
|
|
715
725
|
| `rootr_publish_node` | **아무 노드나** 공개 — 문서·표·폼·시트·화이트보드·덱·페이지, 하위까지. **페이지(PAGE)는 Team 플랜부터**(무료는 403 `PLAN_UPGRADE_REQUIRED`, 하위 포함 시 페이지는 건너뛰고 개수를 알려 줌) |
|
|
716
|
-
| `rootr_set_node_public` | 이미 공개한 것의 설정 변경(하위 포함 여부·검색
|
|
726
|
+
| `rootr_set_node_public` | 이미 공개한 것의 설정 변경(하위 포함 여부·검색 노출·만료·비밀번호, 이슈트래커는 `participation` 외부 참여) |
|
|
717
727
|
| `rootr_node_public_status` | 지금 공개돼 있는지, 주소는 무엇인지 |
|
|
718
728
|
| `rootr_publish_document` | 문서 전용 발행(구형 — 새로 짤 때는 `rootr_publish_node`) |
|
|
719
729
|
| `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, **221 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,8 +296,31 @@ 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
|
|
299
301
|
```
|
|
300
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 submit https://rootr.io/ko/p/AbCd123 --title "Crash" --file shot.png --file app.log --name "Kim" # with files
|
|
315
|
+
rootr issue reply https://rootr.io/ko/p/AbCd123 12 "Same here" --name "Lee"
|
|
316
|
+
rootr issue list https://rootr.io/ko/p/AbCd123 --q "payment" --label bug --sort comments # search a published tracker
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Outside attachments: images (png·jpg·gif·webp), pdf, txt·csv·log, docx·xlsx·pptx, zip — 10MB per file, 10 per
|
|
320
|
+
issue/comment; the extension and the file's first bytes must agree. MCP: `rootr_submit_public_issue` /
|
|
321
|
+
`rootr_comment_public_issue` take `files: [{ filename, contentBase64 }]` (contents, never a path), and
|
|
322
|
+
`rootr_search_public_issues` searches a published tracker the way a visitor does.
|
|
323
|
+
|
|
301
324
|
Every node type publishes on the Free plan **except PAGE** (the block-built web page): publishing a
|
|
302
325
|
page to the web needs the **Team plan**. On Free, `rootr publish <page>` fails with
|
|
303
326
|
`PLAN_UPGRADE_REQUIRED` and nothing changes; `--children` skips pages underneath and says how many.
|
|
@@ -517,7 +540,7 @@ is refused and says when the grace was used.
|
|
|
517
540
|
|
|
518
541
|
## MCP tools
|
|
519
542
|
|
|
520
|
-
`rootr mcp` speaks MCP over stdio.
|
|
543
|
+
`rootr mcp` speaks MCP over stdio. 221 tools:
|
|
521
544
|
|
|
522
545
|
| Group | Count | Tools |
|
|
523
546
|
|---|---|---|
|
|
@@ -532,7 +555,7 @@ is refused and says when the grace was used.
|
|
|
532
555
|
| 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
556
|
| Spreadsheets | 7 | create, read, update, per-sheet create/update/delete, patch cells & formulas |
|
|
534
557
|
| Recordings | 7 | create, upload & download audio, transcribe, summarize, rename speakers |
|
|
535
|
-
| Issue tracker |
|
|
558
|
+
| 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
559
|
| Permissions & visibility | 10 | grant / revoke node permissions, read access, visibility, public status, read requirements |
|
|
537
560
|
| LOG datastores | 5 | create store, update fields, add entries, query, stats |
|
|
538
561
|
| Templates | 4 | list, apply, save as template, update |
|
package/bin/rootr.js
CHANGED
|
@@ -5,7 +5,16 @@ 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 {
|
|
8
|
+
import {
|
|
9
|
+
RootrClient,
|
|
10
|
+
RootrApiError,
|
|
11
|
+
imageMarkdown,
|
|
12
|
+
publicIssueMimeOf,
|
|
13
|
+
withPublicAttachments,
|
|
14
|
+
PUBLIC_ISSUE_FILE_EXTENSIONS,
|
|
15
|
+
PUBLIC_ISSUE_FILE_MAX_BYTES,
|
|
16
|
+
PUBLIC_ISSUE_FILES_PER_POST,
|
|
17
|
+
} from '../lib/client.js';
|
|
9
18
|
import { setGlobalDocMailSink } from '../lib/context.js';
|
|
10
19
|
import { resolveTarget, requireResolved, resolveMoveDestination } from '../lib/resolve.js';
|
|
11
20
|
import { t as tr, lang } from '../lib/i18n.js';
|
|
@@ -43,7 +52,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
43
52
|
rootr db button-run <path|id> <buttonId> [--rows id,id] [--input 글|--input-file f|--input -] 버튼 한 번 누르기 (행 만들기·값 채우기·AI 채우기 · 글 받는 버튼이면 --input)
|
|
44
53
|
rootr db automations <path|id> [--file rules.json] [--clear] 자동 규칙 — "값이 이렇게 되면 이 일을 해라"·정해진 시각
|
|
45
54
|
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] 촘촘하게·왼쪽 칸
|
|
55
|
+
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
56
|
rootr db view <path|id> <뷰이름> [--row-hidden on|off] [--quick "칸,칸"] [--entry-row on|off|top|bottom] [--personal-filters on|off] [--day-pager 날짜칸|off] 행 열면 감춘 칸 보이기·필터 맨 위 고정 칸·맨 아래 입력 줄·하루씩 보기
|
|
48
57
|
rootr db column <path|id> <칸> [--align left|center|right] [--wrap on|off] [--overdue on|off] [--time on|off] [--range on|off] 칸 정렬·줄바꿈·지난 날짜 빨강·시간·기간
|
|
49
58
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
@@ -75,6 +84,17 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
75
84
|
rootr publish <표> --layout board --skin paper 공개한 표를 무슨 꼴로 보일지 (배치 9종 × 분위기 6종)
|
|
76
85
|
rootr publish <문서> --comments [false] [--expires <ISO>] [--skin <분위기>] 문서 전용 — 댓글(편집 제안)·만료일·스킨 16종
|
|
77
86
|
rootr publish <path|id> --description "<글>" --cover <그림|id|none> 검색 설명문·링크 미리보기 대표 이미지 (검색엔진에 바로 알림)
|
|
87
|
+
rootr publish <path|id> --password <비번> | --no-password 공개 주소 잠그기(Team 부터)·풀기 — 이슈트래커는 쓰기도 같이 잠김
|
|
88
|
+
rootr publish <이슈트래커> --participation read|login|anyone 외부 참여 — 보기만·로그인한 사람만 작성·누구나 작성(이름만 적고)
|
|
89
|
+
rootr issue tracker <트래커> [--participation read|login|anyone] [--json] 트래커 설정 보기·외부 참여 바꾸기(MANAGE)
|
|
90
|
+
rootr issue list <트래커|공개 주소> [--state OPEN|CLOSED|ALL] [--q <검색어>] [--label <라벨>]… [--sort newest|oldest|comments] [--json]
|
|
91
|
+
이슈 목록·검색(공개 주소면 밖에서 — 제목·본문·#번호·쓴 사람, 페이지 블록은 --block)
|
|
92
|
+
rootr issue create <트래커> --title "<제목>" [--body "<본문>"] 이슈 올리기(내 계정으로)
|
|
93
|
+
rootr issue comment <트래커> <#번호> "<댓글>" 이슈에 댓글
|
|
94
|
+
rootr issue submit <공개 주소> --title "<제목>" [--body …] [--file <경로>]… [--name <이름>] [--block <블록 id>] [--password <비번>]
|
|
95
|
+
웹게시한 트래커에 밖에서 이슈 올리기(--name 이면 키 없이 손님으로, 페이지 블록은 --block)
|
|
96
|
+
--file: 그림·pdf·txt·csv·log·docx·xlsx·pptx·zip, 한 파일 10MB·글 하나에 10개
|
|
97
|
+
rootr issue reply <공개 주소> <#번호> "<댓글>" [--file <경로>]… [--name <이름>] [--block <블록 id>] [--password <비번>] 웹게시한 트래커 이슈에 댓글
|
|
78
98
|
rootr booking show <표> 방문자 일정 등록(예약) 설정 보기
|
|
79
99
|
rootr booking set <표> --on --days 1,2,3,4,5 --hours 10:00-18:00 --duration 30 예약 켜기·규칙
|
|
80
100
|
rootr booking set <표> --ask "연락처:required" --approval manual 방문자에게 더 묻기 · 승인 후 확정
|
|
@@ -146,7 +166,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
146
166
|
rootr memory add "<내용>" [--kind 지시|사람|프로젝트|참고] [--source <경로|id>] [--related a,b] 기억 저장
|
|
147
167
|
rootr memory edit <id> [--content "<내용>"] [--kind …] [--source …] [--related a,b] 기억 고치기(준 칸만)
|
|
148
168
|
rootr memory forget <id> [--hard] 기억 폐기(--hard면 행 삭제)
|
|
149
|
-
rootr mcp MCP stdio 서버 구동 (도구
|
|
169
|
+
rootr mcp MCP stdio 서버 구동 (도구 220종 — README 참고)
|
|
150
170
|
rootr --help 도움말
|
|
151
171
|
rootr --version 버전 출력
|
|
152
172
|
|
|
@@ -186,7 +206,7 @@ usage:
|
|
|
186
206
|
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
207
|
rootr db automations <path|id> [--file rules.json] [--clear] automatic rules — "when a value becomes X, do Y", or at a set time
|
|
188
208
|
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
|
|
209
|
+
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
|
|
190
210
|
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
211
|
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
212
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] add a row (with its body)
|
|
@@ -220,6 +240,17 @@ usage:
|
|
|
220
240
|
rootr publish <table> --layout board --skin paper how a published table looks (9 layouts × 6 skins)
|
|
221
241
|
rootr publish <doc> --comments [false] [--expires <ISO>] [--skin <skin>] documents only — comments, expiry, 16 skins
|
|
222
242
|
rootr publish <path|id> --description "<text>" --cover <image|id|none> search description & link-preview cover image (search engines pinged)
|
|
243
|
+
rootr publish <path|id> --password <pw> | --no-password lock (Team plan) / unlock the public address — on a tracker writing is locked too
|
|
244
|
+
rootr publish <issue tracker> --participation read|login|anyone outside participation — read-only · signed-in users write · anyone writes (just a name)
|
|
245
|
+
rootr issue tracker <tracker> [--participation read|login|anyone] [--json] show tracker settings / change outside participation (MANAGE)
|
|
246
|
+
rootr issue list <tracker|public URL> [--state OPEN|CLOSED|ALL] [--q <text>] [--label <name>]… [--sort newest|oldest|comments] [--json]
|
|
247
|
+
list/search issues (a public URL searches a PUBLISHED tracker from outside — title, body, #number, author; --block for a page)
|
|
248
|
+
rootr issue create <tracker> --title "<title>" [--body "<body>"] file an issue (as your account)
|
|
249
|
+
rootr issue comment <tracker> <#number> "<comment>" comment on an issue
|
|
250
|
+
rootr issue submit <public URL> --title "<title>" [--body …] [--file <path>]… [--name <name>] [--block <block id>] [--password <pw>]
|
|
251
|
+
file an issue on a PUBLISHED tracker from outside (--name = as a guest, no key; --block for a tracker on a page)
|
|
252
|
+
--file: images·pdf·txt·csv·log·docx·xlsx·pptx·zip, 10MB per file, 10 per issue/comment
|
|
253
|
+
rootr issue reply <public URL> <#number> "<comment>" [--file <path>]… [--name <name>] [--block <block id>] [--password <pw>] comment on an issue of a published tracker
|
|
223
254
|
rootr booking show <table> show visitor booking settings
|
|
224
255
|
rootr booking set <table> --on --days 1,2,3,4,5 --hours 10:00-18:00 --duration 30 turn on + rules
|
|
225
256
|
rootr booking set <table> --ask "Phone:required" --approval manual ask more · confirm after approval
|
|
@@ -287,7 +318,7 @@ usage:
|
|
|
287
318
|
rootr memory add "<content>" [--kind instruction|person|project|reference] [--source <path|id>] [--related a,b] save a memory
|
|
288
319
|
rootr memory edit <id> [--content "<content>"] [--kind …] [--source …] [--related a,b] edit a memory (only the fields you pass)
|
|
289
320
|
rootr memory forget <id> [--hard] retire a memory (--hard deletes the row)
|
|
290
|
-
rootr mcp run the MCP stdio server (
|
|
321
|
+
rootr mcp run the MCP stdio server (220 tools — see the README)
|
|
291
322
|
rootr --help this help
|
|
292
323
|
rootr --version print the version
|
|
293
324
|
|
|
@@ -306,7 +337,8 @@ function usage(syntax) {
|
|
|
306
337
|
}
|
|
307
338
|
|
|
308
339
|
function parseArgs(argv) {
|
|
309
|
-
|
|
340
|
+
// `__all[key]` — 같은 깃발을 여러 번 준 값 전부(`--file a --file b`). 나머지 명령은 예전처럼 마지막 값(args[key])을 쓴다.
|
|
341
|
+
const args = { _: [], __all: {} };
|
|
310
342
|
for (let i = 0; i < argv.length; i++) {
|
|
311
343
|
const tok = argv[i];
|
|
312
344
|
if (tok.startsWith('--')) {
|
|
@@ -316,6 +348,7 @@ function parseArgs(argv) {
|
|
|
316
348
|
args[key] = true;
|
|
317
349
|
} else {
|
|
318
350
|
args[key] = next;
|
|
351
|
+
(args.__all[key] = args.__all[key] || []).push(next);
|
|
319
352
|
i++;
|
|
320
353
|
}
|
|
321
354
|
} else {
|
|
@@ -2161,6 +2194,188 @@ function flagToBool(v) {
|
|
|
2161
2194
|
return v !== 'false' && v !== '0';
|
|
2162
2195
|
}
|
|
2163
2196
|
|
|
2197
|
+
/** ISSUE-PUBLIC — 외부 참여(서버 issue-schema.ts PUBLIC_PARTICIPATION_MODES 와 같은 값). */
|
|
2198
|
+
const PARTICIPATION_MODES = ['read', 'login', 'anyone'];
|
|
2199
|
+
|
|
2200
|
+
function participationLabel(mode) {
|
|
2201
|
+
return {
|
|
2202
|
+
read: tr('보기만', 'read only'),
|
|
2203
|
+
login: tr('로그인한 사람만 작성', 'signed-in users write'),
|
|
2204
|
+
anyone: tr('누구나 작성(이름만 적고)', 'anyone writes (just a name)'),
|
|
2205
|
+
}[mode] ?? mode;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/** 공개 주소(rootr.io/…/p/{slug}) 또는 slug 그대로. */
|
|
2209
|
+
function publicSlugOf(target) {
|
|
2210
|
+
const m = /\/p\/([^/?#\s]+)/.exec(String(target || ''));
|
|
2211
|
+
return m ? decodeURIComponent(m[1]) : String(target || '').trim();
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/** GET /issue-trackers/:id/issues 는 `{ issues: [...] }` 로 답한다(배열이 아니다 — 3100 실측에서 `find is not a function`). */
|
|
2215
|
+
function issuesOf(res) {
|
|
2216
|
+
return Array.isArray(res) ? res : Array.isArray(res?.issues) ? res.issues : [];
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function issueNumberOf(raw) {
|
|
2220
|
+
const n = Number(String(raw ?? '').replace(/^#/, ''));
|
|
2221
|
+
if (!Number.isSafeInteger(n) || n < 1) throw new Error(tr(`이슈 번호가 아닙니다: ${raw}`, `Not an issue number: ${raw}`));
|
|
2222
|
+
return n;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/** `--file <경로>`(여러 번) → 올릴 파일들. 개수·크기·형식을 보내기 전에 거른다 — 서버 안전선(issue-public-attachments.ts)과 같은 값. */
|
|
2226
|
+
function publicIssueFilesOf(args) {
|
|
2227
|
+
const paths = args.__all.file || (typeof args.file === 'string' ? [args.file] : []);
|
|
2228
|
+
if (paths.length > PUBLIC_ISSUE_FILES_PER_POST) {
|
|
2229
|
+
throw new Error(tr(`파일은 글 하나에 ${PUBLIC_ISSUE_FILES_PER_POST}개까지 붙일 수 있습니다.`, `At most ${PUBLIC_ISSUE_FILES_PER_POST} files per issue/comment.`));
|
|
2230
|
+
}
|
|
2231
|
+
return paths.map((p) => {
|
|
2232
|
+
const filename = basename(p);
|
|
2233
|
+
const mimeType = publicIssueMimeOf(filename);
|
|
2234
|
+
if (!mimeType) {
|
|
2235
|
+
throw new Error(
|
|
2236
|
+
tr(`이 형식은 올릴 수 없습니다: ${filename} — ${PUBLIC_ISSUE_FILE_EXTENSIONS.join('·')} 만`, `File type not allowed: ${filename} — only ${PUBLIC_ISSUE_FILE_EXTENSIONS.join(', ')}`),
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
const data = readFileSync(p);
|
|
2240
|
+
if (data.length > PUBLIC_ISSUE_FILE_MAX_BYTES) {
|
|
2241
|
+
throw new Error(tr(`파일이 너무 큽니다: ${filename} — 한 파일에 10MB까지`, `File too large: ${filename} — 10MB per file`));
|
|
2242
|
+
}
|
|
2243
|
+
return { filename, mimeType, data };
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
/**
|
|
2248
|
+
* ISSUE-PUBLIC — 이슈트래커 명령(창업자 지시 2026-11-02 "api cli mcp scaffold 에도 적용"). 예전엔 CLI 에 이슈 명령이 아예 없었다.
|
|
2249
|
+
* · 안에서: tracker(설정·외부 참여) · list · create · comment
|
|
2250
|
+
* · 밖에서(웹게시한 트래커): submit · reply — 키가 있으면 그 계정으로, --name 이면 키 없이 손님으로
|
|
2251
|
+
*/
|
|
2252
|
+
async function cmdIssue(args) {
|
|
2253
|
+
const sub = args._[0];
|
|
2254
|
+
const client = makeClient();
|
|
2255
|
+
if (sub === 'tracker') {
|
|
2256
|
+
const target = args._[1];
|
|
2257
|
+
if (!target) throw new Error(usage('rootr issue tracker <path|id> [--participation read|login|anyone] [--json]'));
|
|
2258
|
+
const id = await resolveNodeId(client, target);
|
|
2259
|
+
let t;
|
|
2260
|
+
if (args.participation !== undefined) {
|
|
2261
|
+
if (!PARTICIPATION_MODES.includes(args.participation)) {
|
|
2262
|
+
throw new Error(tr(`--participation 은 다음 중 하나여야 합니다: ${PARTICIPATION_MODES.join(', ')}`, `--participation must be one of: ${PARTICIPATION_MODES.join(', ')}`));
|
|
2263
|
+
}
|
|
2264
|
+
t = await client.updateIssueTracker(id, { publicParticipation: args.participation });
|
|
2265
|
+
} else {
|
|
2266
|
+
t = await client.getIssueTracker(id);
|
|
2267
|
+
}
|
|
2268
|
+
if (args.json) return console.log(JSON.stringify(t, null, 2));
|
|
2269
|
+
const mode = (t.config && t.config.publicParticipation) || 'read';
|
|
2270
|
+
console.log(`${t.name} (${t.path})`);
|
|
2271
|
+
console.log(tr(` 열림 ${t.openCount} · 닫힘 ${t.closedCount}`, ` open ${t.openCount} · closed ${t.closedCount}`));
|
|
2272
|
+
console.log(tr(` 외부 참여: ${participationLabel(mode)}`, ` outside participation: ${participationLabel(mode)}`));
|
|
2273
|
+
console.log(tr(` 라벨: ${(t.labels || []).map((l) => l.name).join(', ') || '없음'}`, ` labels: ${(t.labels || []).map((l) => l.name).join(', ') || 'none'}`));
|
|
2274
|
+
if (mode !== 'read') {
|
|
2275
|
+
const st = await client.nodePublicStatus(id);
|
|
2276
|
+
if (!st.isPublic) {
|
|
2277
|
+
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.'));
|
|
2278
|
+
} else {
|
|
2279
|
+
console.log(tr(` 공개 주소: /p/${st.slug}`, ` public address: /p/${st.slug}`));
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (sub === 'list') {
|
|
2285
|
+
const target = args._[1];
|
|
2286
|
+
if (!target) throw new Error(usage('rootr issue list <path|id|공개 주소> [--state OPEN|CLOSED|ALL] [--q <text>] [--label <이름>]… [--sort newest|oldest|comments] [--json]'));
|
|
2287
|
+
// ISSUE-PUBLIC-SEARCH — 공개 주소(…/p/{slug})나 --block 이면 웹게시한 트래커를 **밖에서** 찾는다(키 없이, 서버 검색).
|
|
2288
|
+
if (/\/p\//.test(String(target)) || typeof args.block === 'string') {
|
|
2289
|
+
const slug = publicSlugOf(target);
|
|
2290
|
+
const pass = typeof args.password === 'string' ? await client.unlockPublicNode(slug, args.password) : undefined;
|
|
2291
|
+
const res = await client.searchPublicIssues(slug, {
|
|
2292
|
+
blockId: typeof args.block === 'string' ? args.block : undefined,
|
|
2293
|
+
q: typeof args.q === 'string' ? args.q : undefined,
|
|
2294
|
+
state: typeof args.state === 'string' ? args.state : undefined,
|
|
2295
|
+
labels: args.__all.label || [],
|
|
2296
|
+
sort: typeof args.sort === 'string' ? args.sort : undefined,
|
|
2297
|
+
pass,
|
|
2298
|
+
});
|
|
2299
|
+
if (args.json) return console.log(JSON.stringify(res, null, 2));
|
|
2300
|
+
const list = res?.issues || [];
|
|
2301
|
+
if (!list.length) return console.log(tr('조건에 맞는 이슈가 없습니다.', 'No matching issues.'));
|
|
2302
|
+
for (const i of list) {
|
|
2303
|
+
const who = i.author ? (i.author.guest ? tr(`${i.author.name} (외부)`, `${i.author.name} (guest)`) : i.author.name) : '';
|
|
2304
|
+
const labels = (i.labels || []).map((l) => l.name).join(',');
|
|
2305
|
+
console.log(`#${i.number}\t${i.state}\t${i.title}${labels ? `\t[${labels}]` : ''}${who ? `\t${who}` : ''}${i.commentCount ? `\t💬${i.commentCount}` : ''}`);
|
|
2306
|
+
}
|
|
2307
|
+
if (res.truncated) console.log(tr(`… ${res.truncated.total}건 중 ${res.truncated.shown}건`, `… ${res.truncated.shown} of ${res.truncated.total}`));
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const id = await resolveNodeId(client, target);
|
|
2311
|
+
const list = issuesOf(await client.listIssues(id, { state: args.state, q: typeof args.q === 'string' ? args.q : undefined }));
|
|
2312
|
+
if (args.json) return console.log(JSON.stringify(list, null, 2));
|
|
2313
|
+
if (!list.length) return console.log(tr('이슈가 없습니다.', 'No issues.'));
|
|
2314
|
+
for (const i of list) {
|
|
2315
|
+
const who = i.guestName ? tr(`${i.guestName} (외부)`, `${i.guestName} (guest)`) : i.author?.name ?? '';
|
|
2316
|
+
console.log(`#${i.number}\t${i.state}\t${i.title}${who ? `\t${who}` : ''}${i.commentCount ? `\t💬${i.commentCount}` : ''}`);
|
|
2317
|
+
}
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
if (sub === 'create') {
|
|
2321
|
+
const target = args._[1];
|
|
2322
|
+
if (!target || typeof args.title !== 'string') throw new Error(usage('rootr issue create <path|id> --title "<title>" [--body "<body>"]'));
|
|
2323
|
+
const id = await resolveNodeId(client, target);
|
|
2324
|
+
const out = await client.createIssue(id, { title: args.title, body: typeof args.body === 'string' ? args.body : undefined });
|
|
2325
|
+
console.log(tr(`올렸습니다: #${out.number} ${out.title}`, `Filed: #${out.number} ${out.title}`));
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
if (sub === 'comment') {
|
|
2329
|
+
const [, target, num, ...rest] = args._;
|
|
2330
|
+
const text = rest.join(' ').trim();
|
|
2331
|
+
if (!target || !num || !text) throw new Error(usage('rootr issue comment <path|id> <#number> "<comment>"'));
|
|
2332
|
+
const id = await resolveNodeId(client, target);
|
|
2333
|
+
const n = issueNumberOf(num);
|
|
2334
|
+
const hit = issuesOf(await client.listIssues(id, { state: 'ALL', q: `#${n}` })).find((i) => i.number === n);
|
|
2335
|
+
if (!hit) throw new Error(tr(`#${n} 이슈가 없습니다.`, `Issue #${n} not found.`));
|
|
2336
|
+
await client.commentIssue(id, hit.id, text);
|
|
2337
|
+
console.log(tr(`#${n} 에 댓글을 달았습니다.`, `Commented on #${n}.`));
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
if (sub === 'submit' || sub === 'reply') {
|
|
2341
|
+
const slug = publicSlugOf(args._[1]);
|
|
2342
|
+
if (!slug) throw new Error(usage(`rootr issue ${sub} <public URL> …`));
|
|
2343
|
+
const asGuest = typeof args.name === 'string';
|
|
2344
|
+
const opts = {
|
|
2345
|
+
blockId: typeof args.block === 'string' ? args.block : undefined,
|
|
2346
|
+
guestName: asGuest ? args.name : undefined,
|
|
2347
|
+
asGuest,
|
|
2348
|
+
pass: typeof args.password === 'string' ? await client.unlockPublicNode(slug, args.password) : undefined,
|
|
2349
|
+
};
|
|
2350
|
+
// ISSUE-PUBLIC-ATTACH — `--file <경로>` 여러 번. 보내기 전에 크기·형식을 걸러(서버가 다시 판정) 올리고, 받은 줄을 본문 끝에 붙인다.
|
|
2351
|
+
const files = publicIssueFilesOf(args);
|
|
2352
|
+
const attach = async () => {
|
|
2353
|
+
const lines = [];
|
|
2354
|
+
for (const f of files) {
|
|
2355
|
+
const out = await client.uploadPublicIssueFile(slug, { ...opts, filename: f.filename, mimeType: f.mimeType, data: f.data });
|
|
2356
|
+
console.log(tr(`첨부: ${out.filename} (${out.size}B)`, `attached: ${out.filename} (${out.size}B)`));
|
|
2357
|
+
lines.push(out.markdown);
|
|
2358
|
+
}
|
|
2359
|
+
return lines;
|
|
2360
|
+
};
|
|
2361
|
+
if (sub === 'submit') {
|
|
2362
|
+
if (typeof args.title !== 'string') throw new Error(usage('rootr issue submit <public URL> --title "<title>" [--body …] [--file <경로>]… [--name <name>] [--block <id>]'));
|
|
2363
|
+
const body = withPublicAttachments(typeof args.body === 'string' ? args.body : '', await attach());
|
|
2364
|
+
const out = await client.submitPublicIssue(slug, { ...opts, title: args.title, body: body || undefined });
|
|
2365
|
+
console.log(out?.number ? tr(`올렸습니다: #${out.number}`, `Filed: #${out.number}`) : tr('올렸습니다.', 'Filed.'));
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
const [, , num, ...rest] = args._;
|
|
2369
|
+
const text = rest.join(' ').trim();
|
|
2370
|
+
if (!num || (!text && !files.length)) throw new Error(usage('rootr issue reply <public URL> <#number> "<comment>" [--file <경로>]… [--name <name>] [--block <id>]'));
|
|
2371
|
+
const n = issueNumberOf(num);
|
|
2372
|
+
await client.commentPublicIssue(slug, n, { ...opts, body: withPublicAttachments(text, await attach()) });
|
|
2373
|
+
console.log(tr('댓글을 달았습니다.', 'Comment posted.'));
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
throw new Error(usage('rootr issue tracker|list|create|comment|submit|reply …'));
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2164
2379
|
async function cmdPublish(args) {
|
|
2165
2380
|
const target = args._[0];
|
|
2166
2381
|
if (!target) {
|
|
@@ -2219,6 +2434,7 @@ async function cmdPublish(args) {
|
|
|
2219
2434
|
printSeo(st.publicConfig?.seo);
|
|
2220
2435
|
} else {
|
|
2221
2436
|
if (st.hasPassword) console.log(tr(' 비밀번호 걸려 있음', ` password protected`));
|
|
2437
|
+
if (st.participation) console.log(tr(` 외부 참여: ${participationLabel(st.participation)}`, ` outside participation: ${participationLabel(st.participation)}`));
|
|
2222
2438
|
console.log(tr(` 하위 문서도 공개: ${st.includeChildren ? '예' : '아니오'}`, ` children published too: ${st.includeChildren ? 'yes' : 'no'}`));
|
|
2223
2439
|
console.log(tr(` 복제 허용: ${st.allowClone === false ? '아니오' : '예'}`, ` cloning allowed: ${st.allowClone === false ? 'no' : 'yes'}`));
|
|
2224
2440
|
if (st.design && (st.design.layout || st.design.skin)) {
|
|
@@ -2274,12 +2490,22 @@ async function cmdPublish(args) {
|
|
|
2274
2490
|
if (design.skin && !DB_SKINS.includes(design.skin)) {
|
|
2275
2491
|
throw new Error(tr(`--skin 은 다음 중 하나여야 합니다: ${DB_SKINS.join(', ')}`, `--skin must be one of: ${DB_SKINS.join(', ')}`));
|
|
2276
2492
|
}
|
|
2493
|
+
// ISSUE-PUBLIC — 이슈트래커의 외부 참여(트래커 설정에 저장 — 페이지에 끼운 블록도 같은 값을 따른다)
|
|
2494
|
+
if (args.participation !== undefined && !PARTICIPATION_MODES.includes(args.participation)) {
|
|
2495
|
+
throw new Error(tr(`--participation 은 다음 중 하나여야 합니다: ${PARTICIPATION_MODES.join(', ')}`, `--participation must be one of: ${PARTICIPATION_MODES.join(', ')}`));
|
|
2496
|
+
}
|
|
2277
2497
|
const res = await client.publishNode(id, {
|
|
2278
2498
|
...(args.children ? { includeChildren: true } : {}),
|
|
2279
2499
|
...(typeof args.view === 'string' ? { view: args.view } : {}),
|
|
2280
2500
|
...(Object.keys(design).length ? { design } : {}),
|
|
2501
|
+
...(typeof args.participation === 'string' ? { participation: args.participation } : {}),
|
|
2281
2502
|
});
|
|
2282
2503
|
console.log(tr(`공개했습니다: /p/${res.slug}`, `Published: /p/${res.slug}`));
|
|
2504
|
+
if (res.participation) console.log(tr(` 외부 참여: ${participationLabel(res.participation)}`, ` outside participation: ${participationLabel(res.participation)}`));
|
|
2505
|
+
if (typeof args.password === 'string' || args['no-password']) {
|
|
2506
|
+
await client.setNodePublic(id, { password: typeof args.password === 'string' ? args.password : null });
|
|
2507
|
+
console.log(typeof args.password === 'string' ? tr(' 비밀번호를 걸었습니다(쓰기도 같이 잠김).', ' Locked with a password (writing too).') : tr(' 비밀번호를 풀었습니다.', ' Password removed.'));
|
|
2508
|
+
}
|
|
2283
2509
|
if (args.children) console.log(tr(' 이 문서 아래 전부(손자·증손자 포함)도 각자 주소로 공개됩니다.', ` Everything under this document (grandchildren included) gets its own public address.`));
|
|
2284
2510
|
if (res.skippedPagesRequireTeam) {
|
|
2285
2511
|
// PAGE-PUBLISH-TEAM — 무료 워크스페이스에서는 아래의 페이지(PAGE)는 따라 나가지 않는다.
|
|
@@ -2987,6 +3213,17 @@ async function cmdDbView(args) {
|
|
|
2987
3213
|
if (!['compact', 'normal'].includes(density)) throw new Error(tr('--density 는 compact · normal 중 하나여야 합니다.', `--density must be compact or normal.`));
|
|
2988
3214
|
patch.density = density;
|
|
2989
3215
|
}
|
|
3216
|
+
// VIEW-GRID-LINES — 칸 선(rows 가로선만 · all 칸마다 · none)·진하기(light · strong).
|
|
3217
|
+
if (args.lines !== undefined) {
|
|
3218
|
+
const lines = String(args.lines);
|
|
3219
|
+
if (!['rows', 'all', 'none'].includes(lines)) throw new Error(tr('--lines 는 rows · all · none 중 하나여야 합니다.', `--lines must be rows, all or none.`));
|
|
3220
|
+
patch.gridLines = lines;
|
|
3221
|
+
}
|
|
3222
|
+
if (args['line-strength'] !== undefined) {
|
|
3223
|
+
const strength = String(args['line-strength']);
|
|
3224
|
+
if (!['light', 'strong'].includes(strength)) throw new Error(tr('--line-strength 는 light · strong 중 하나여야 합니다.', `--line-strength must be light or strong.`));
|
|
3225
|
+
patch.lineStrength = strength;
|
|
3226
|
+
}
|
|
2990
3227
|
if (args.freeze !== undefined) {
|
|
2991
3228
|
const n = Number(args.freeze);
|
|
2992
3229
|
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)`));
|
|
@@ -3015,7 +3252,7 @@ async function cmdDbView(args) {
|
|
|
3015
3252
|
patch.dayPager = !v || v === 'off' || v === 'false' ? null : { dateProp: v };
|
|
3016
3253
|
}
|
|
3017
3254
|
if (!Object.keys(patch).length) {
|
|
3018
|
-
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|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, --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`));
|
|
3255
|
+
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`));
|
|
3019
3256
|
}
|
|
3020
3257
|
|
|
3021
3258
|
const view = await client.updateDatabaseView(id, viewRef, patch);
|
|
@@ -4152,6 +4389,9 @@ async function main() {
|
|
|
4152
4389
|
return cmdForm(args);
|
|
4153
4390
|
case 'publish':
|
|
4154
4391
|
return cmdPublish(args);
|
|
4392
|
+
case 'issue':
|
|
4393
|
+
case 'issues':
|
|
4394
|
+
return cmdIssue(args);
|
|
4155
4395
|
case 'booking':
|
|
4156
4396
|
return cmdBooking(args);
|
|
4157
4397
|
case 'gcal':
|
package/lib/client.js
CHANGED
|
@@ -115,6 +115,43 @@ function hintFor(status, body) {
|
|
|
115
115
|
return undefined;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* ISSUE-PUBLIC-ATTACH — 공개 이슈트래커 첨부의 안전선(서버 issue-public-attachments.ts 와 같은 값). 서버가 다시 판정하지만, 보내기 전에
|
|
120
|
+
* 걸러 큰 파일을 헛되이 싣지 않게 한다.
|
|
121
|
+
*/
|
|
122
|
+
export const PUBLIC_ISSUE_FILE_MAX_BYTES = 10 * 1024 * 1024;
|
|
123
|
+
export const PUBLIC_ISSUE_FILES_PER_POST = 10;
|
|
124
|
+
const PUBLIC_ISSUE_MIME = {
|
|
125
|
+
png: 'image/png',
|
|
126
|
+
jpg: 'image/jpeg',
|
|
127
|
+
jpeg: 'image/jpeg',
|
|
128
|
+
gif: 'image/gif',
|
|
129
|
+
webp: 'image/webp',
|
|
130
|
+
pdf: 'application/pdf',
|
|
131
|
+
txt: 'text/plain',
|
|
132
|
+
log: 'text/plain',
|
|
133
|
+
csv: 'text/csv',
|
|
134
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
135
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
136
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
137
|
+
zip: 'application/zip',
|
|
138
|
+
};
|
|
139
|
+
export const PUBLIC_ISSUE_FILE_EXTENSIONS = Object.keys(PUBLIC_ISSUE_MIME);
|
|
140
|
+
|
|
141
|
+
/** 파일 이름 → 보낼 mimeType(모르는 확장자면 null — 서버가 415 로 거절할 것을 미리 알린다). */
|
|
142
|
+
export function publicIssueMimeOf(filename) {
|
|
143
|
+
const ext = String(filename || '').split('.').pop().toLowerCase();
|
|
144
|
+
return PUBLIC_ISSUE_MIME[ext] || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 본문 끝에 첨부 줄(서버가 준 `markdown`)을 붙인다 — 화면(PublicIssueBoard)과 같은 모양. */
|
|
148
|
+
export function withPublicAttachments(body, markdowns) {
|
|
149
|
+
const lines = (markdowns || []).filter(Boolean);
|
|
150
|
+
if (!lines.length) return body || '';
|
|
151
|
+
const text = String(body || '').replace(/\s+$/, '');
|
|
152
|
+
return `${text}${text ? '\n\n' : ''}${lines.join('\n\n')}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
export class RootrClient {
|
|
119
156
|
constructor({ apiKey, workspace, baseUrl, publicBaseUrl, defaultHeaders }) {
|
|
120
157
|
this.apiKey = apiKey;
|
|
@@ -165,11 +202,12 @@ export class RootrClient {
|
|
|
165
202
|
* @param {boolean} [opts.returnResponse] - if true, return the raw Response (for header access)
|
|
166
203
|
*/
|
|
167
204
|
async request(path, opts = {}) {
|
|
168
|
-
|
|
205
|
+
// ISSUE-PUBLIC — `noAuth` 는 공개 주소에 **손님으로** 부를 때(키를 보내면 그 키 주인의 계정으로 쓰인다).
|
|
206
|
+
if (!opts.noAuth) this._requireApiKey();
|
|
169
207
|
|
|
170
208
|
const url = opts.url || `${this.baseUrl}${path}`;
|
|
171
209
|
const headers = {
|
|
172
|
-
'x-api-key': this.apiKey,
|
|
210
|
+
...(opts.noAuth ? {} : { 'x-api-key': this.apiKey }),
|
|
173
211
|
...this.defaultHeaders,
|
|
174
212
|
...(opts.headers || {}),
|
|
175
213
|
};
|
|
@@ -936,10 +974,10 @@ export class RootrClient {
|
|
|
936
974
|
|
|
937
975
|
// ---- Issues ---------------------------------------------------------------
|
|
938
976
|
|
|
939
|
-
async createIssueTracker(workspace, name) {
|
|
977
|
+
async createIssueTracker(workspace, name, extra = {}) {
|
|
940
978
|
return this.request(`/workspaces/${encodeURIComponent(workspace)}/issue-trackers`, {
|
|
941
979
|
method: 'POST',
|
|
942
|
-
body: { name },
|
|
980
|
+
body: { name, ...extra },
|
|
943
981
|
});
|
|
944
982
|
}
|
|
945
983
|
|
|
@@ -947,6 +985,93 @@ export class RootrClient {
|
|
|
947
985
|
return this.request(`/issue-trackers/${encodeURIComponent(trackerId)}`);
|
|
948
986
|
}
|
|
949
987
|
|
|
988
|
+
/**
|
|
989
|
+
* 트래커 설정을 고친다 — `{ publicParticipation }`(외부 참여: read·login·anyone)만 보내면 그 값만 바뀐다.
|
|
990
|
+
* `labels`·`types` 는 통째로 바꾼다. 외부 참여를 **바꾸는 것**은 MANAGE(웹게시와 같은 주인 몫)다.
|
|
991
|
+
*/
|
|
992
|
+
async updateIssueTracker(trackerId, patch = {}) {
|
|
993
|
+
return this.request(`/issue-trackers/${encodeURIComponent(trackerId)}`, {
|
|
994
|
+
method: 'PATCH',
|
|
995
|
+
body: patch,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// ── ISSUE-PUBLIC — 남의(또는 내) **웹게시한** 이슈트래커에 밖에서 쓰기 ──────────────
|
|
1000
|
+
// 공개 주소(rootr.io/p/{slug}) 위의 길이다. 키를 보내면 그 키 주인(계정)으로 쓰이고, `asGuest` 면 키 없이 손님 이름으로.
|
|
1001
|
+
// 페이지에 끼운 트래커는 페이지 주소 + `blockId` 로 닿는다.
|
|
1002
|
+
|
|
1003
|
+
/** 잠긴 공개 주소의 비밀번호 → 통과증(`?pass=`). */
|
|
1004
|
+
async unlockPublicNode(slug, password) {
|
|
1005
|
+
const out = await this.request(`/public/nodes/${encodeURIComponent(slug)}/unlock`, {
|
|
1006
|
+
method: 'POST',
|
|
1007
|
+
body: { password },
|
|
1008
|
+
noAuth: true,
|
|
1009
|
+
});
|
|
1010
|
+
return out?.token;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
_publicIssuesBase(slug, blockId) {
|
|
1014
|
+
const s = encodeURIComponent(slug);
|
|
1015
|
+
return blockId ? `/public/nodes/${s}/issue-blocks/${encodeURIComponent(blockId)}/issues` : `/public/nodes/${s}/issues`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async submitPublicIssue(slug, { blockId, title, body, guestName, pass, asGuest } = {}) {
|
|
1019
|
+
const qs = pass ? `?pass=${encodeURIComponent(pass)}` : '';
|
|
1020
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}${qs}`, {
|
|
1021
|
+
method: 'POST',
|
|
1022
|
+
body: { title, body, ...(guestName ? { guestName } : {}) },
|
|
1023
|
+
noAuth: Boolean(asGuest) || !this.apiKey,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async commentPublicIssue(slug, number, { blockId, body, guestName, pass, asGuest } = {}) {
|
|
1028
|
+
const qs = pass ? `?pass=${encodeURIComponent(pass)}` : '';
|
|
1029
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}/${encodeURIComponent(String(number))}/comments${qs}`, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
body: { body, ...(guestName ? { guestName } : {}) },
|
|
1032
|
+
noAuth: Boolean(asGuest) || !this.apiKey,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* ISSUE-PUBLIC-SEARCH — 공개 트래커(또는 페이지 블록)의 목록을 서버에서 찾는다. `q` 는 제목·본문·번호(#12)·(이름을 싣는 모드면) 쓴 사람,
|
|
1038
|
+
* `labels` 는 이름(모두 가진 것), `state` OPEN|CLOSED, `sort` newest|oldest|comments. 키 없이 부른다(공개 읽기).
|
|
1039
|
+
*/
|
|
1040
|
+
async searchPublicIssues(slug, { blockId, q, state, labels, sort, pass } = {}) {
|
|
1041
|
+
const qs = new URLSearchParams();
|
|
1042
|
+
if (q) qs.set('q', q);
|
|
1043
|
+
if (state && String(state).toUpperCase() !== 'ALL') qs.set('state', String(state).toUpperCase());
|
|
1044
|
+
for (const l of [].concat(labels || [])) if (l) qs.append('label', l);
|
|
1045
|
+
if (sort) qs.set('sort', sort);
|
|
1046
|
+
if (pass) qs.set('pass', pass);
|
|
1047
|
+
const tail = qs.toString();
|
|
1048
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}${tail ? `?${tail}` : ''}`, { noAuth: true });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* ISSUE-PUBLIC-ATTACH — 공개 트래커(또는 페이지 블록)에 새 이슈·댓글용 파일을 올린다(본문 = 바이트). 10MB·허용 형식은 서버가 판정한다.
|
|
1053
|
+
* 돌려받은 `markdown` 을 이슈·댓글 본문 끝에 붙여 보낸다(`withPublicAttachments`).
|
|
1054
|
+
*/
|
|
1055
|
+
async uploadPublicIssueFile(slug, { blockId, filename, mimeType, data, guestName, pass, asGuest } = {}) {
|
|
1056
|
+
const s = encodeURIComponent(slug);
|
|
1057
|
+
const base = blockId ? `/public/nodes/${s}/issue-blocks/${encodeURIComponent(blockId)}/attachments` : `/public/nodes/${s}/issues/attachments`;
|
|
1058
|
+
const qs = new URLSearchParams({ filename, mimeType: mimeType || publicIssueMimeOf(filename) || 'application/octet-stream' });
|
|
1059
|
+
if (guestName) qs.set('guestName', guestName);
|
|
1060
|
+
if (pass) qs.set('pass', pass);
|
|
1061
|
+
return this.request(`${base}?${qs.toString()}`, {
|
|
1062
|
+
method: 'POST',
|
|
1063
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
1064
|
+
body: data,
|
|
1065
|
+
noAuth: Boolean(asGuest) || !this.apiKey,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** 공개 트래커(또는 페이지 블록)의 이슈 하나 — 댓글까지. */
|
|
1070
|
+
async readPublicIssue(slug, number, { blockId, pass } = {}) {
|
|
1071
|
+
const qs = pass ? `?pass=${encodeURIComponent(pass)}` : '';
|
|
1072
|
+
return this.request(`${this._publicIssuesBase(slug, blockId)}/${encodeURIComponent(String(number))}${qs}`, { noAuth: true });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
950
1075
|
async listIssues(trackerId, { state, label, type, assigneeId, q } = {}) {
|
|
951
1076
|
const params = new URLSearchParams();
|
|
952
1077
|
if (state) params.set('state', state);
|
|
@@ -1133,11 +1258,11 @@ export class RootrClient {
|
|
|
1133
1258
|
*/
|
|
1134
1259
|
async createDatabaseView(
|
|
1135
1260
|
databaseId,
|
|
1136
|
-
{ name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager } = {},
|
|
1261
|
+
{ name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, gridLines, lineStrength, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager } = {},
|
|
1137
1262
|
) {
|
|
1138
1263
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/views`, {
|
|
1139
1264
|
method: 'POST',
|
|
1140
|
-
body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager },
|
|
1265
|
+
body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, gridLines, lineStrength, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager },
|
|
1141
1266
|
});
|
|
1142
1267
|
}
|
|
1143
1268
|
|
|
@@ -1454,10 +1579,10 @@ export class RootrClient {
|
|
|
1454
1579
|
});
|
|
1455
1580
|
}
|
|
1456
1581
|
|
|
1457
|
-
async queryVectors(databaseId, column, { vector, topK, minScore } = {}) {
|
|
1582
|
+
async queryVectors(databaseId, column, { vector, topK, minScore, filters } = {}) {
|
|
1458
1583
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/vectors/${encodeURIComponent(column)}/query`, {
|
|
1459
1584
|
method: 'POST',
|
|
1460
|
-
body: { vector, topK, minScore },
|
|
1585
|
+
body: { vector, topK, minScore, ...(filters?.length ? { filters } : {}) },
|
|
1461
1586
|
});
|
|
1462
1587
|
}
|
|
1463
1588
|
|
|
@@ -2126,10 +2251,10 @@ export class RootrClient {
|
|
|
2126
2251
|
}
|
|
2127
2252
|
|
|
2128
2253
|
/** 연구노트 프리셋: 기재자(나) → 점검자 두 단계. */
|
|
2129
|
-
async applyResearchNotePreset(nodeId, { reviewerUserId, noteMeta } = {}) {
|
|
2254
|
+
async applyResearchNotePreset(nodeId, { reviewerUserId, noteMeta, authorUserId } = {}) {
|
|
2130
2255
|
return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/research-note-preset`, {
|
|
2131
2256
|
method: 'POST',
|
|
2132
|
-
body: { reviewerUserId, noteMeta },
|
|
2257
|
+
body: { reviewerUserId, noteMeta, authorUserId },
|
|
2133
2258
|
});
|
|
2134
2259
|
}
|
|
2135
2260
|
|
|
@@ -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)와
|
|
@@ -178,7 +182,8 @@ export function registerDatabaseTools(server) {
|
|
|
178
182
|
title: 'Create a Rootr database (table / board / kanban)',
|
|
179
183
|
description:
|
|
180
184
|
'Create a DATABASE node — a typed table with columns, views and rows — in ONE call, optionally seeded with ' +
|
|
181
|
-
'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 ' +
|
|
182
187
|
'board, a task/requirement/risk tracker, anything with trackable rows. Do NOT fake a table with a markdown ' +
|
|
183
188
|
'document, and do NOT create an empty database and then add columns one by one — `properties` order IS the ' +
|
|
184
189
|
'column order, so one call keeps the columns in exactly the order you list them. ' +
|
|
@@ -386,6 +391,16 @@ export function registerDatabaseTools(server) {
|
|
|
386
391
|
.nullable()
|
|
387
392
|
.optional()
|
|
388
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.'),
|
|
389
404
|
frozenColumns: z
|
|
390
405
|
.number()
|
|
391
406
|
.int()
|
|
@@ -400,7 +415,7 @@ export function registerDatabaseTools(server) {
|
|
|
400
415
|
...VIEW_EXTRA_SHAPE,
|
|
401
416
|
},
|
|
402
417
|
},
|
|
403
|
-
async ({ databaseId, name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager }) => {
|
|
418
|
+
async ({ databaseId, name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, gridLines, lineStrength, frozenColumns, rowPaneShowHidden, quickFilters, entryRow, filtersPersonalOnly, dayPager }) => {
|
|
404
419
|
try {
|
|
405
420
|
const client = makeClient();
|
|
406
421
|
const id = await resolveNodeId(client, databaseId);
|
|
@@ -415,6 +430,8 @@ export function registerDatabaseTools(server) {
|
|
|
415
430
|
colWidths,
|
|
416
431
|
calendarScale,
|
|
417
432
|
density,
|
|
433
|
+
gridLines,
|
|
434
|
+
lineStrength,
|
|
418
435
|
frozenColumns,
|
|
419
436
|
rowPaneShowHidden,
|
|
420
437
|
quickFilters,
|
|
@@ -525,6 +542,16 @@ export function registerDatabaseTools(server) {
|
|
|
525
542
|
.nullable()
|
|
526
543
|
.optional()
|
|
527
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.'),
|
|
528
555
|
frozenColumns: z
|
|
529
556
|
.number()
|
|
530
557
|
.int()
|
|
@@ -554,6 +581,8 @@ export function registerDatabaseTools(server) {
|
|
|
554
581
|
cardProps,
|
|
555
582
|
rowColors,
|
|
556
583
|
density,
|
|
584
|
+
gridLines,
|
|
585
|
+
lineStrength,
|
|
557
586
|
frozenColumns,
|
|
558
587
|
rowPaneShowHidden,
|
|
559
588
|
quickFilters,
|
|
@@ -577,6 +606,8 @@ export function registerDatabaseTools(server) {
|
|
|
577
606
|
cardProps,
|
|
578
607
|
rowColors,
|
|
579
608
|
density,
|
|
609
|
+
gridLines,
|
|
610
|
+
lineStrength,
|
|
580
611
|
frozenColumns,
|
|
581
612
|
rowPaneShowHidden,
|
|
582
613
|
quickFilters,
|
|
@@ -1989,7 +2020,10 @@ export function registerDatabaseTools(server) {
|
|
|
1989
2020
|
'Remove ONE column from a Rootr (루터) DATABASE. The values stored under it are NOT erased from the rows, ' +
|
|
1990
2021
|
'so re-adding a column is not enough to bring them back in the UI — confirm with the user before calling this. ' +
|
|
1991
2022
|
'Exception: values of a vector column are erased (see below). ' +
|
|
1992
|
-
'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. ' +
|
|
1993
2027
|
'For a vector column the stored vectors are deleted with it (not kept for re-adding — re-add the column and ' +
|
|
1994
2028
|
'store the vectors again with rootr_set_row_vectors).',
|
|
1995
2029
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
@@ -2418,7 +2452,10 @@ export function registerDatabaseTools(server) {
|
|
|
2418
2452
|
'query with the same model you used for rootr_set_row_vectors. `minScore` drops weak matches. Rows the caller ' +
|
|
2419
2453
|
'cannot see (own-rows-only tables) are never returned. Rows without a stored vector are skipped. ' +
|
|
2420
2454
|
'The column can hold at most 50,000 vectors — a bigger column is refused, not searched partially — and the scan has ' +
|
|
2421
|
-
'no index, so latency grows with the number of rows
|
|
2455
|
+
'no index, so latency grows with the number of rows compared (1536 dims: ~0.7s at 2,000 rows, ~3s at 10,000). ' +
|
|
2456
|
+
'Pass `filters` (same shape as rootr_create_view filters, columns by NAME or id) to compare only the rows that match — ' +
|
|
2457
|
+
'e.g. [{"propId":"상태","op":"is","values":["진행"]}]. When more than 5,000 vectors were compared without filters the ' +
|
|
2458
|
+
'answer carries a `hint` saying so; `scanned` is how many vectors were compared.',
|
|
2422
2459
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
2423
2460
|
inputSchema: {
|
|
2424
2461
|
databaseId: z.string().describe(DATABASE_TARGET_DESC),
|
|
@@ -2426,13 +2463,21 @@ export function registerDatabaseTools(server) {
|
|
|
2426
2463
|
vector: z.array(z.number()).describe('Query vector — same length as the column\'s dimensions'),
|
|
2427
2464
|
topK: z.number().int().min(1).max(100).optional().default(10).describe('How many rows (default 10, max 100)'),
|
|
2428
2465
|
minScore: z.number().min(-1).max(1).optional().describe('Only rows with cosine ≥ this'),
|
|
2466
|
+
filters: z
|
|
2467
|
+
.array(z.object({ propId: z.string(), op: z.string(), values: z.array(z.string()).optional() }))
|
|
2468
|
+
.optional()
|
|
2469
|
+
.describe(
|
|
2470
|
+
'Compare only rows matching EVERY condition (AND), checked before any vector is compared — the way to keep a big ' +
|
|
2471
|
+
'column fast. Same conditions as a view filter: propId = column NAME or id, op = is / is_not / contains / ' +
|
|
2472
|
+
'is_empty / is_not_empty / before / after / on / between / eq / gt / lt / checked / unchecked / today / this_week …',
|
|
2473
|
+
),
|
|
2429
2474
|
},
|
|
2430
2475
|
},
|
|
2431
|
-
async ({ databaseId, column, vector, topK, minScore }) => {
|
|
2476
|
+
async ({ databaseId, column, vector, topK, minScore, filters }) => {
|
|
2432
2477
|
try {
|
|
2433
2478
|
const client = makeClient();
|
|
2434
2479
|
const id = await resolveNodeId(client, databaseId);
|
|
2435
|
-
return jsonResult(await client.queryVectors(id, column, { vector, topK, minScore }));
|
|
2480
|
+
return jsonResult(await client.queryVectors(id, column, { vector, topK, minScore, filters }));
|
|
2436
2481
|
} catch (err) {
|
|
2437
2482
|
return errorResult(err);
|
|
2438
2483
|
}
|
|
@@ -693,7 +693,10 @@ export function registerDocumentTools(server) {
|
|
|
693
693
|
title: 'Duplicate a Rootr node (deep copy)',
|
|
694
694
|
description:
|
|
695
695
|
'Deep-copy a Rootr (루터) node — a document or a whole folder tree — next to the original, or into ' +
|
|
696
|
-
'another workspace the user can access (targetWorkspaceId). Returns the new root node.'
|
|
696
|
+
'another workspace the user can access (targetWorkspaceId). Returns the new root node. If an older table ' +
|
|
697
|
+
'(or one inside the copied folder) has two columns with the same name, the COPY gets the later one renamed ' +
|
|
698
|
+
'to "Name (2)" (values and views follow — columns are kept by id; the original is not changed) and the ' +
|
|
699
|
+
'result lists it in `renamedColumns`.',
|
|
697
700
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
698
701
|
inputSchema: {
|
|
699
702
|
target: z.string().describe('Node path (starting with "/") or node id to copy'),
|
|
@@ -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/issues.js
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
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
|
+
import {
|
|
5
|
+
RootrApiError,
|
|
6
|
+
publicIssueMimeOf,
|
|
7
|
+
withPublicAttachments,
|
|
8
|
+
PUBLIC_ISSUE_FILE_EXTENSIONS,
|
|
9
|
+
PUBLIC_ISSUE_FILE_MAX_BYTES,
|
|
10
|
+
PUBLIC_ISSUE_FILES_PER_POST,
|
|
11
|
+
} from '../client.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ISSUE-PUBLIC — 외부 참여(웹게시한 트래커·페이지에 끼운 트래커에 밖의 사람이 무엇을 할 수 있나). 서버 값과 같아야 한다
|
|
15
|
+
* (issues/issue-schema.ts PUBLIC_PARTICIPATION_MODES).
|
|
16
|
+
*/
|
|
17
|
+
const PARTICIPATION = z
|
|
18
|
+
.enum(['read', 'login', 'anyone'])
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
'Outside participation (외부 참여) on the tracker\'s PUBLIC page and on any published PAGE that shows it (`issues` block): ' +
|
|
22
|
+
'"read" 보기만 (default) · "login" 로그인한 사람만 작성 (any Rootr account may file issues and comment) · ' +
|
|
23
|
+
'"anyone" 누구나 작성 (no account — visitors type a name, shown as "이름 (외부)"). Takes effect once the tracker ' +
|
|
24
|
+
'(or the page holding it) is published — rootr_publish_node.',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** 공개 주소 글자(slug · rootr.io/…/p/{slug} 주소)에서 slug 만. */
|
|
28
|
+
function slugOf(target) {
|
|
29
|
+
const t = String(target || '').trim();
|
|
30
|
+
const m = /\/p\/([^/?#\s]+)/.exec(t);
|
|
31
|
+
return m ? decodeURIComponent(m[1]) : t;
|
|
32
|
+
}
|
|
4
33
|
|
|
5
34
|
/** Issue tracker tools: create tracker, list/create/get/update issues, comment. */
|
|
6
35
|
export function registerIssueTools(server) {
|
|
@@ -19,13 +48,14 @@ export function registerIssueTools(server) {
|
|
|
19
48
|
.string()
|
|
20
49
|
.optional()
|
|
21
50
|
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
51
|
+
publicParticipation: PARTICIPATION,
|
|
22
52
|
},
|
|
23
53
|
},
|
|
24
|
-
async ({ name, workspace }) => {
|
|
54
|
+
async ({ name, workspace, publicParticipation }) => {
|
|
25
55
|
try {
|
|
26
56
|
const ws = requireWorkspace(workspace);
|
|
27
57
|
const client = makeClient(ws);
|
|
28
|
-
const result = await client.createIssueTracker(ws, name);
|
|
58
|
+
const result = await client.createIssueTracker(ws, name, publicParticipation ? { publicParticipation } : {});
|
|
29
59
|
return jsonResult(result);
|
|
30
60
|
} catch (err) {
|
|
31
61
|
return errorResult(err);
|
|
@@ -170,4 +200,205 @@ export function registerIssueTools(server) {
|
|
|
170
200
|
}
|
|
171
201
|
},
|
|
172
202
|
);
|
|
203
|
+
|
|
204
|
+
server.registerTool(
|
|
205
|
+
'rootr_update_issue_tracker',
|
|
206
|
+
{
|
|
207
|
+
title: 'Change an issue tracker\'s settings (outside participation, labels, types)',
|
|
208
|
+
description:
|
|
209
|
+
'Change settings of a Rootr (루터) issue tracker. `publicParticipation` decides what people OUTSIDE the workspace may ' +
|
|
210
|
+
'do on its public page (rootr.io/p/…) and on published PAGEs that show it: "read" 보기만 · "login" 로그인한 사람만 ' +
|
|
211
|
+
'작성 · "anyone" 누구나 작성 (just a name). Only the keys you send change; `labels`/`types` REPLACE the whole list ' +
|
|
212
|
+
'([{ id?, name, color? }] — read the current ones with rootr_list_issues / the tracker first). Changing ' +
|
|
213
|
+
'publicParticipation is an owner-level action (MANAGE, same as publishing) — use an account key (PAT). ' +
|
|
214
|
+
'To also publish in one step use rootr_publish_node with `participation`.',
|
|
215
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
216
|
+
inputSchema: {
|
|
217
|
+
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
|
218
|
+
publicParticipation: PARTICIPATION,
|
|
219
|
+
labels: z
|
|
220
|
+
.array(z.object({ id: z.string().optional(), name: z.string(), color: z.string().optional() }))
|
|
221
|
+
.optional()
|
|
222
|
+
.describe('Full label list (replaces the current one)'),
|
|
223
|
+
types: z
|
|
224
|
+
.array(z.object({ id: z.string().optional(), name: z.string(), color: z.string().optional() }))
|
|
225
|
+
.optional()
|
|
226
|
+
.describe('Full issue type list (replaces the current one)'),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
async ({ trackerId, ...patch }) => {
|
|
230
|
+
try {
|
|
231
|
+
const client = makeClient();
|
|
232
|
+
trackerId = await resolveNodeId(client, trackerId);
|
|
233
|
+
const body = Object.fromEntries(Object.entries(patch).filter(([, v]) => v !== undefined));
|
|
234
|
+
return jsonResult(await client.updateIssueTracker(trackerId, body));
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return errorResult(err);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const PUBLIC_TARGET = {
|
|
242
|
+
url: z
|
|
243
|
+
.string()
|
|
244
|
+
.describe(
|
|
245
|
+
'The PUBLIC address of the tracker — "https://rootr.io/ko/p/<slug>" or just the slug. For a tracker shown on a ' +
|
|
246
|
+
'published PAGE, the page\'s address plus `blockId`.',
|
|
247
|
+
),
|
|
248
|
+
blockId: z.string().optional().describe('Only when the tracker sits on a published PAGE: that `issues` block\'s id'),
|
|
249
|
+
guestName: z
|
|
250
|
+
.string()
|
|
251
|
+
.max(40)
|
|
252
|
+
.optional()
|
|
253
|
+
.describe('Name to show when writing as a guest (누구나 작성 mode with asGuest). Ignored when writing as your account.'),
|
|
254
|
+
asGuest: z
|
|
255
|
+
.boolean()
|
|
256
|
+
.optional()
|
|
257
|
+
.describe(
|
|
258
|
+
'Write WITHOUT your API key, as a guest with `guestName` (only works when participation is "anyone"). ' +
|
|
259
|
+
'Default false: the key\'s owner account is the author (works for "login" and "anyone").',
|
|
260
|
+
),
|
|
261
|
+
password: z.string().optional().describe('If the public page is password-locked'),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* ISSUE-PUBLIC-ATTACH — 공개 이슈·댓글에 붙일 파일. **내용(base64)만** 받는다 — 파일 경로는 받지 않는다: 원격 커넥터는 이 도구를 API 서버
|
|
266
|
+
* 안에서 돌리므로 경로를 읽으면 서버 파일을 남의 공개 트래커로 내보내는 구멍이 된다(rootr-remote-mcp-runs-in-api-process). 로컬에서도
|
|
267
|
+
* 같은 모양이라 원격/로컬을 가를 필요가 없다. 크기 상한은 서버와 같은 10MB(base64 로 약 13.4MB), 글 하나에 10개.
|
|
268
|
+
*/
|
|
269
|
+
const PUBLIC_FILES = z
|
|
270
|
+
.array(
|
|
271
|
+
z.object({
|
|
272
|
+
filename: z.string().min(1).max(255).describe('File name with extension, e.g. "screenshot.png"'),
|
|
273
|
+
contentBase64: z
|
|
274
|
+
.string()
|
|
275
|
+
.max(Math.ceil(PUBLIC_ISSUE_FILE_MAX_BYTES / 3) * 4 + 4)
|
|
276
|
+
.describe('The file bytes, base64-encoded (≤10MB decoded)'),
|
|
277
|
+
mimeType: z.string().max(120).optional().describe('Optional — guessed from the extension'),
|
|
278
|
+
}),
|
|
279
|
+
)
|
|
280
|
+
.max(PUBLIC_ISSUE_FILES_PER_POST)
|
|
281
|
+
.optional()
|
|
282
|
+
.describe(
|
|
283
|
+
`Files to attach (≤${PUBLIC_ISSUE_FILES_PER_POST}, ≤10MB each): ${PUBLIC_ISSUE_FILE_EXTENSIONS.join(', ')}. ` +
|
|
284
|
+
'Images show inside the text, other files as a download link. svg/html/executables/video are refused, and the file\'s ' +
|
|
285
|
+
'first bytes must match its extension. Content only — no file paths.',
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
/** 파일들을 그 트래커에 올리고, 본문 끝에 붙일 줄들을 돌려준다. 보내기 전에 크기·형식을 거른다(서버가 다시 판정). */
|
|
289
|
+
async function uploadPublicFiles(client, slug, files, opts) {
|
|
290
|
+
const lines = [];
|
|
291
|
+
for (const f of files || []) {
|
|
292
|
+
const data = Buffer.from(f.contentBase64, 'base64');
|
|
293
|
+
if (!data.length) throw new RootrApiError(`빈 파일입니다: ${f.filename}`);
|
|
294
|
+
if (data.length > PUBLIC_ISSUE_FILE_MAX_BYTES) throw new RootrApiError(`파일이 너무 큽니다: ${f.filename} — 한 파일에 10MB까지`);
|
|
295
|
+
const mimeType = f.mimeType || publicIssueMimeOf(f.filename);
|
|
296
|
+
if (!mimeType) {
|
|
297
|
+
throw new RootrApiError(`이 형식은 올릴 수 없습니다: ${f.filename}`, { hint: `허용: ${PUBLIC_ISSUE_FILE_EXTENSIONS.join(', ')}` });
|
|
298
|
+
}
|
|
299
|
+
const out = await client.uploadPublicIssueFile(slug, { ...opts, filename: f.filename, mimeType, data });
|
|
300
|
+
lines.push(out.markdown);
|
|
301
|
+
}
|
|
302
|
+
return lines;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
server.registerTool(
|
|
306
|
+
'rootr_search_public_issues',
|
|
307
|
+
{
|
|
308
|
+
title: 'Search the issues of a PUBLISHED tracker (outside view)',
|
|
309
|
+
description:
|
|
310
|
+
'Search/filter the issues of someone\'s (or your own) PUBLISHED Rootr (루터) issue tracker through its public address — ' +
|
|
311
|
+
'what an outside visitor sees. `q` matches title, body, number ("#12" or "12") and, when the tracker shows authors ' +
|
|
312
|
+
'(participation login/anyone), the author name; `labels` = label names (issues having all of them); `state` OPEN|CLOSED; ' +
|
|
313
|
+
'`sort` newest (default) | oldest | comments. Returns { participation, issues:[{number,title,state,body,labels,createdAt,' +
|
|
314
|
+
'closedAt,commentCount,author}], truncated? } (≤500 per call). No key needed. For a tracker in YOUR workspace prefer ' +
|
|
315
|
+
'rootr_list_issues (internal fields).',
|
|
316
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
317
|
+
inputSchema: {
|
|
318
|
+
url: PUBLIC_TARGET.url,
|
|
319
|
+
blockId: PUBLIC_TARGET.blockId,
|
|
320
|
+
password: PUBLIC_TARGET.password,
|
|
321
|
+
q: z.string().max(200).optional().describe('Search text'),
|
|
322
|
+
state: z.enum(['OPEN', 'CLOSED', 'ALL']).optional().describe('Default ALL'),
|
|
323
|
+
labels: z.array(z.string().max(100)).max(20).optional().describe('Label names — issues having all of them'),
|
|
324
|
+
sort: z.enum(['newest', 'oldest', 'comments']).optional(),
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
async ({ url, blockId, password, q, state, labels, sort }) => {
|
|
328
|
+
try {
|
|
329
|
+
const client = makeClient();
|
|
330
|
+
const slug = slugOf(url);
|
|
331
|
+
const pass = password ? await client.unlockPublicNode(slug, password) : undefined;
|
|
332
|
+
return jsonResult(await client.searchPublicIssues(slug, { blockId, q, state, labels, sort, pass }));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return errorResult(err);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
server.registerTool(
|
|
340
|
+
'rootr_submit_public_issue',
|
|
341
|
+
{
|
|
342
|
+
title: 'File an issue on a PUBLISHED tracker (outside participation)',
|
|
343
|
+
description:
|
|
344
|
+
'File an issue on someone\'s (or your own) PUBLISHED Rootr (루터) issue tracker through its public address — the ' +
|
|
345
|
+
'way an outside person or an external agent reports a bug/request. Works only when the tracker\'s participation ' +
|
|
346
|
+
'allows it: "login" (your account key counts as signed in) or "anyone". Errors: 403 PUBLIC_READ_ONLY (보기만), ' +
|
|
347
|
+
'401 LOGIN_REQUIRED, 400 GUEST_NAME_REQUIRED, 401 PUBLIC_DOC_PASSWORD (pass `password`). Text and `files` only — labels, ' +
|
|
348
|
+
'assignee and state cannot be set from outside, and @mentions are stripped. `files` are uploaded to that tracker first ' +
|
|
349
|
+
'and appended to the body (415 ATTACHMENT_TYPE_NOT_ALLOWED / ATTACHMENT_CONTENT_MISMATCH, 413 ATTACHMENT_TOO_LARGE, ' +
|
|
350
|
+
'403 TRACKER_STORAGE_FULL). Returns { ok, number }. For a tracker in YOUR workspace prefer rootr_create_issue (full fields).',
|
|
351
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
352
|
+
inputSchema: {
|
|
353
|
+
...PUBLIC_TARGET,
|
|
354
|
+
title: z.string().max(200).describe('Issue title (≤200)'),
|
|
355
|
+
body: z.string().max(20000).optional().describe('Issue body, markdown (≤20,000)'),
|
|
356
|
+
files: PUBLIC_FILES,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
async ({ url, blockId, guestName, asGuest, password, title, body, files }) => {
|
|
360
|
+
try {
|
|
361
|
+
const client = makeClient();
|
|
362
|
+
const slug = slugOf(url);
|
|
363
|
+
const pass = password ? await client.unlockPublicNode(slug, password) : undefined;
|
|
364
|
+
const lines = await uploadPublicFiles(client, slug, files, { blockId, guestName, pass, asGuest });
|
|
365
|
+
const full = withPublicAttachments(body || '', lines);
|
|
366
|
+
return jsonResult(await client.submitPublicIssue(slug, { blockId, title, body: full || undefined, guestName, pass, asGuest }));
|
|
367
|
+
} catch (err) {
|
|
368
|
+
return errorResult(err);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
server.registerTool(
|
|
374
|
+
'rootr_comment_public_issue',
|
|
375
|
+
{
|
|
376
|
+
title: 'Comment on an issue of a PUBLISHED tracker (outside participation)',
|
|
377
|
+
description:
|
|
378
|
+
'Comment on issue #number of a PUBLISHED Rootr (루터) issue tracker through its public address (same rules as ' +
|
|
379
|
+
'rootr_submit_public_issue, `files` too). Pass `read: true` instead of `body` to just read that issue\'s public comments. ' +
|
|
380
|
+
'Returns { ok, id } (or { number, comments } when reading).',
|
|
381
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
382
|
+
inputSchema: {
|
|
383
|
+
...PUBLIC_TARGET,
|
|
384
|
+
number: z.number().int().positive().describe('Issue number (#N)'),
|
|
385
|
+
body: z.string().max(10000).optional().describe('Comment, markdown (≤10,000). Required unless read is true or files are given'),
|
|
386
|
+
files: PUBLIC_FILES,
|
|
387
|
+
read: z.boolean().optional().describe('Only read the public comments of this issue'),
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
async ({ url, blockId, guestName, asGuest, password, number, body, files, read }) => {
|
|
391
|
+
try {
|
|
392
|
+
const client = makeClient();
|
|
393
|
+
const slug = slugOf(url);
|
|
394
|
+
const pass = password ? await client.unlockPublicNode(slug, password) : undefined;
|
|
395
|
+
if (read) return jsonResult(await client.readPublicIssue(slug, number, { blockId, pass }));
|
|
396
|
+
if (!body && !(files && files.length)) throw new Error('body is required (or files, or read: true)');
|
|
397
|
+
const lines = await uploadPublicFiles(client, slug, files, { blockId, guestName, pass, asGuest });
|
|
398
|
+
return jsonResult(await client.commentPublicIssue(slug, number, { blockId, body: withPublicAttachments(body || '', lines), guestName, pass, asGuest }));
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return errorResult(err);
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
);
|
|
173
404
|
}
|
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
|
);
|