rootr-cli 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/bin/rootr.js +289 -1
- package/lib/client.js +136 -5
- package/lib/mcp-tools/databases.js +314 -16
- package/lib/mcp-tools/documents.js +230 -3
- package/lib/mcp-tools/forms.js +87 -2
- package/lib/mcp-tools/misc.js +40 -0
- package/lib/mcp-tools/workspaces.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -304,6 +304,39 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
304
304
|
| `rootr_add_column` | **열 추가** — 기존 열·행은 그대로 |
|
|
305
305
|
| `rootr_update_column` | 열 이름 변경 / select 선택지 교체 |
|
|
306
306
|
| `rootr_delete_column` | 열 삭제(파괴적) |
|
|
307
|
+
| `rootr_read_database_permissions` | 표에 걸린 자물쇠 보기 |
|
|
308
|
+
| `rootr_set_database_permissions` | 표 잠그기 — 칸·뷰 잠금 / 본인 행만 / 관리자만 고칠 칸 |
|
|
309
|
+
| `rootr_read_database_notifications` | 표 알림 규칙 보기 |
|
|
310
|
+
| `rootr_set_database_notifications` | 표 알림 규칙 저장 — 누구에게·언제·어떤 것만 |
|
|
311
|
+
| `rootr_list_document_commits` | 공개 문서에 온 변경 제안 목록 |
|
|
312
|
+
| `rootr_merge_document_commit` | 변경 제안 반영(세 벌 병합 — 덮어쓰기 아님) |
|
|
313
|
+
| `rootr_reject_document_commit` | 변경 제안 거절(사유는 제안자에게 전달) |
|
|
314
|
+
| `rootr_read_node_access` | 이 문서·폴더를 누가 볼 수 있는지(공개범위 + 따로 준 권한) |
|
|
315
|
+
| `rootr_set_node_visibility` | 비공개로 잠그기 / 상위 설정 따르기 |
|
|
316
|
+
| `rootr_grant_node_permission` | 한 사람(또는 그룹)에게 READ·WRITE·MANAGE 주기 |
|
|
317
|
+
| `rootr_revoke_node_permission` | 준 권한 거두기 |
|
|
318
|
+
| `rootr_update_form_response` | 이미 들어온 폼 응답 고치기(폼 검사 규칙 다시 통과) |
|
|
319
|
+
| `rootr_delete_form_response` | 폼 응답 한 건 삭제 |
|
|
320
|
+
| `rootr_create_form_collector` | 응답 모을 표·스프레드시트를 **만들어서** 연결(표는 질문 그대로 칸 생성) |
|
|
321
|
+
|
|
322
|
+
**사람 칸은 여러 명을 담습니다.** 회의 참석자·공동 담당처럼 여럿이 들어가는 자리가
|
|
323
|
+
흔해서, 값은 이름 목록(`["김대표","이팀장"]`)입니다. 이름 하나만 보내도 받아 주고
|
|
324
|
+
목록으로 맞춰 돌려줍니다. 새로 들어온 사람에게만 알림이 갑니다.
|
|
325
|
+
|
|
326
|
+
**"만들면서 같이 잠그기".** 공개범위는 스캐폴드 트리에서도 노드마다 지정할 수 있습니다 —
|
|
327
|
+
`{ name: "채용", visibility: "PRIVATE", access: [{ subjectId: "<userId>", level: "READ" }] }`.
|
|
328
|
+
폴더는 만들어 주면서 잠그는 건 사람이 따로 해야 하는 상태가 되지 않도록, 한 번에 갑니다.
|
|
329
|
+
명령줄에서는 `rootr access <경로|id> --private`, `--grant <userId> --level READ`,
|
|
330
|
+
`--revoke <userId>` 입니다. (웹에 공개하는 것과는 다릅니다 — 그건 `rootr_set_document_public`.)
|
|
331
|
+
|
|
332
|
+
응답 모을 곳은 `rootr form collect <폼> --to db|sheet`로 **만들면서** 연결합니다 —
|
|
333
|
+
표로 만들면 폼 질문 그대로 칸이 생기므로, 표를 미리 만들고 칸 이름을 맞출 필요가 없습니다
|
|
334
|
+
(이름이 하나라도 어긋나면 그 칸은 영영 빈 칸으로 남습니다). 스캐폴드 트리에서는 폼 노드에
|
|
335
|
+
`form.collectInto: "database" | "spreadsheet"`로 같은 일을 합니다.
|
|
336
|
+
|
|
337
|
+
폼 응답은 `rootr form responses <폼> [--latest]`로 보고, `rootr form response-edit <응답id>
|
|
338
|
+
--values '{"필드id":"값"}'`로 고치고, `rootr form response-rm <응답id> --yes`로 지웁니다.
|
|
339
|
+
`--latest`는 같은 사람이 여러 번 낸 것 중 마지막 한 건만 남깁니다(익명 제출은 합치지 않습니다).
|
|
307
340
|
|
|
308
341
|
행의 **상세 내용(본문 메모)** — 화면에서 칸반 카드를 열면 속성 아래에 적는 그 글 — 은
|
|
309
342
|
`body`로 오갑니다. 실체는 표 밑에 달리는 문서 한 장이고, 없으면 쓸 때 만들어집니다.
|
package/bin/rootr.js
CHANGED
|
@@ -19,6 +19,7 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
19
19
|
rootr write <path|id> [--file <f>] [--if-match <etag>] 문서 전체 교체(파일 없으면 생성)
|
|
20
20
|
rootr append <path|id> [<text>] [--heading "## X"] [--file <f>] 문서 끝(또는 헤딩 아래)에 안전하게 추가
|
|
21
21
|
rootr edit <path|id> --find <text> --replace <text> [--all] 본문 내 텍스트 치환
|
|
22
|
+
rootr comments <path|id> [--block <id>] [--json] 문서에 달린 댓글 읽기 (공개 페이지 댓글 포함)
|
|
22
23
|
rootr search <query> 워크스페이스 검색
|
|
23
24
|
rootr mv <path|id> <새 경로|폴더 id> 문서/폴더 이동 및/또는 이름 변경
|
|
24
25
|
rootr rm <path|id> [--yes] 문서/폴더를 휴지통으로 이동 (복구 가능)
|
|
@@ -28,8 +29,18 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
28
29
|
rootr ask <question> [--workspace <w>] GraphRAG 질의응답 (RCA), 인용 포함 출력
|
|
29
30
|
rootr db create [--file <spec.json>] [--parent <path|id>] 표(DATABASE) 생성 — 컬럼 순서 그대로, 행까지 한 번에
|
|
30
31
|
rootr db rows <path|id> [--body] [--json] 표의 행 목록 (--body: 상세 내용까지)
|
|
32
|
+
rootr db lock <path|id> [--schema managers] [--rows own] [--locked "승인,점수"] 표 자물쇠 보기·걸기
|
|
33
|
+
rootr db notify <path|id> [--file rules.json] [--clear] 표 알림 규칙 보기·저장
|
|
31
34
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
32
35
|
rootr db row-body <path|id> <rowId> [--file <f>] 행의 상세 내용(본문 메모) 쓰기 — 없으면 표준입력
|
|
36
|
+
rootr access <path|id> [--json] 공개범위·권한 보기 (관리 권한 필요)
|
|
37
|
+
rootr access <path|id> --private|--public|--inherit 공개범위 바꾸기
|
|
38
|
+
rootr access <path|id> --grant <userId> --level READ|WRITE|MANAGE 사람에게 권한 주기
|
|
39
|
+
rootr access <path|id> --revoke <userId> 준 권한 거두기
|
|
40
|
+
rootr form collect <path|id> --to db|sheet 응답 모을 곳을 새로 만들어 연결 (표는 질문 그대로 칸 생성)
|
|
41
|
+
rootr form responses <path|id> [--latest] [--json] 폼에 들어온 응답 보기 (--latest: 사람마다 최신 1건만)
|
|
42
|
+
rootr form response-edit <응답id> --values '<json>' 이미 낸 응답 고치기 (폼 검사 규칙 다시 통과)
|
|
43
|
+
rootr form response-rm <응답id> [--yes] 응답 한 건 삭제 (되돌릴 수 없음)
|
|
33
44
|
rootr config [--api-key <k>] [--workspace <w>] [--base-url <u>] 설정 조회/저장
|
|
34
45
|
rootr config --add-key <k> [--label <name>] [--workspace <w>] 워크스페이스별 키 추가 등록
|
|
35
46
|
rootr mcp MCP stdio 서버 구동 (도구 90종+ — README 참고)
|
|
@@ -115,6 +126,41 @@ async function cmdLs(args) {
|
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
/**
|
|
130
|
+
* `rootr comments <path|id>` — 그 문서에 달린 댓글을 읽는다.
|
|
131
|
+
*
|
|
132
|
+
* 2026-08-23 창업자 확인 요청: "댓글 써진 거 mcp/api/cli로 읽어올 수 있는 거 맞는지."
|
|
133
|
+
* API와 MCP에는 있었고 CLI에만 없었다. 공개 페이지에 달린 댓글도 같은 곳에 쌓이므로
|
|
134
|
+
* 여기서 함께 나온다.
|
|
135
|
+
*/
|
|
136
|
+
async function cmdComments(args) {
|
|
137
|
+
const target = args._[0];
|
|
138
|
+
if (!target) throw new Error('사용법: rootr comments <path|id> [--block <blockId>] [--json]');
|
|
139
|
+
|
|
140
|
+
const client = makeClient();
|
|
141
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
142
|
+
const threads = await client.listComments(id, { blockId: args.block });
|
|
143
|
+
|
|
144
|
+
if (args.json) {
|
|
145
|
+
console.log(JSON.stringify(threads, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!threads.length) {
|
|
149
|
+
console.log('댓글이 없습니다.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const t of threads) {
|
|
153
|
+
const when = String(t.createdAt ?? '').slice(0, 16).replace('T', ' ');
|
|
154
|
+
console.log(`- ${t.author?.name ?? '(알 수 없음)'} · ${when}`);
|
|
155
|
+
console.log(` ${String(t.body ?? '').replace(/\n/g, '\n ')}`);
|
|
156
|
+
for (const r of t.replies ?? []) {
|
|
157
|
+
const rWhen = String(r.createdAt ?? '').slice(0, 16).replace('T', ' ');
|
|
158
|
+
console.log(` ↳ ${r.author?.name ?? '(알 수 없음)'} · ${rWhen}`);
|
|
159
|
+
console.log(` ${String(r.body ?? '').replace(/\n/g, '\n ')}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
118
164
|
async function cmdRead(args) {
|
|
119
165
|
const target = args._[0];
|
|
120
166
|
if (!target) throw new Error('사용법: rootr read <path|id> [--json]');
|
|
@@ -445,14 +491,166 @@ async function cmdMcp() {
|
|
|
445
491
|
* 컬럼을 하나씩 붙이는 방식은 호출 순서가 곧 컬럼 순서가 되어 옮겨온 표가 뒤죽박죽이
|
|
446
492
|
* 됐다 — 그래서 CLI도 "한 번에" 쪽만 제공한다.
|
|
447
493
|
*/
|
|
494
|
+
/**
|
|
495
|
+
* `rootr access <path|id> [--private|--public|--inherit] [--grant <id> --level …] [--revoke <id>]`
|
|
496
|
+
*
|
|
497
|
+
* 화면의 "공개범위"·"권한" 창을 터미널에서. 인자 없이 부르면 지금 상태를 보여 준다.
|
|
498
|
+
* 관리 권한이 없으면 서버가 막는다(보기도 안 된다).
|
|
499
|
+
*/
|
|
500
|
+
async function cmdAccess(args) {
|
|
501
|
+
const target = args._[0];
|
|
502
|
+
if (!target) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
'사용법: rootr access <path|id> [--private|--public|--inherit] [--grant <userId> --level READ|WRITE|MANAGE] [--revoke <userId>]',
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const client = makeClient();
|
|
508
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
509
|
+
const group = args.group ? 'GROUP' : 'USER';
|
|
510
|
+
|
|
511
|
+
let changed = false;
|
|
512
|
+
if (args.private || args.public || args.inherit) {
|
|
513
|
+
const visibility = args.private ? 'PRIVATE' : args.public ? 'PUBLIC' : null;
|
|
514
|
+
await client.setNodeVisibility(id, visibility);
|
|
515
|
+
console.log(
|
|
516
|
+
`공개범위: ${visibility === 'PRIVATE' ? '비공개(만든 사람·관리자·따로 준 사람만)' : visibility === 'PUBLIC' ? '워크스페이스 전체' : '상위 폴더/워크스페이스 기본값을 따름'}`,
|
|
517
|
+
);
|
|
518
|
+
changed = true;
|
|
519
|
+
}
|
|
520
|
+
if (typeof args.grant === 'string') {
|
|
521
|
+
const level = String(args.level || '').toUpperCase();
|
|
522
|
+
if (!['READ', 'WRITE', 'MANAGE'].includes(level)) {
|
|
523
|
+
throw new Error('--grant 에는 --level READ|WRITE|MANAGE 를 함께 주세요.');
|
|
524
|
+
}
|
|
525
|
+
await client.grantNodePermission(id, { subjectType: group, subjectId: args.grant, level });
|
|
526
|
+
console.log(`권한 부여: ${args.grant} → ${level}`);
|
|
527
|
+
changed = true;
|
|
528
|
+
}
|
|
529
|
+
if (typeof args.revoke === 'string') {
|
|
530
|
+
await client.revokeNodePermission(id, { subjectType: group, subjectId: args.revoke });
|
|
531
|
+
console.log(`권한 회수: ${args.revoke}`);
|
|
532
|
+
changed = true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const info = await client.getNodeAccess(id);
|
|
536
|
+
if (args.json) {
|
|
537
|
+
console.log(JSON.stringify(info, null, 2));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (!changed) {
|
|
541
|
+
const eff = info.effectiveVisibility ?? info.visibility;
|
|
542
|
+
console.log(`공개범위: ${eff === 'PRIVATE' ? '비공개' : '워크스페이스 전체'}${info.visibility === null ? ' (상속받음)' : ''}`);
|
|
543
|
+
}
|
|
544
|
+
const direct = info.direct ?? [];
|
|
545
|
+
console.log(`따로 준 권한: ${direct.length ? '' : '없음'}`);
|
|
546
|
+
for (const p of direct) console.log(` - ${p.subjectType} ${p.subjectId} → ${p.level}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* `rootr form responses|response-edit|response-rm`
|
|
551
|
+
*
|
|
552
|
+
* 폼에 들어온 응답을 터미널에서 보고 고치고 지운다. 화면에서만 되면
|
|
553
|
+
* 에이전트에게는 없는 기능이라 여기까지 열어 둔다.
|
|
554
|
+
*/
|
|
555
|
+
async function cmdForm(args) {
|
|
556
|
+
const sub = args._[0];
|
|
557
|
+
if (sub === 'collect') return cmdFormCollect(args);
|
|
558
|
+
if (sub === 'responses') return cmdFormResponses(args);
|
|
559
|
+
if (sub === 'response-edit') return cmdFormResponseEdit(args);
|
|
560
|
+
if (sub === 'response-rm') return cmdFormResponseRm(args);
|
|
561
|
+
throw new Error('사용법: rootr form collect|responses|response-edit|response-rm … (자세한 건 rootr --help)');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* `rootr form collect <path|id> --to db|sheet`
|
|
566
|
+
*
|
|
567
|
+
* 응답 모을 곳을 새로 만들어 연결한다. 표로 만들면 폼 질문 그대로 칸이 생기므로,
|
|
568
|
+
* 표를 미리 만들고 칸 이름을 하나하나 맞출 필요가 없다.
|
|
569
|
+
*/
|
|
570
|
+
async function cmdFormCollect(args) {
|
|
571
|
+
const target = args._[1];
|
|
572
|
+
if (!target) throw new Error('사용법: rootr form collect <path|id> --to db|sheet');
|
|
573
|
+
const to = String(args.to || '').toLowerCase();
|
|
574
|
+
const map = { db: 'database', database: 'database', sheet: 'spreadsheet', spreadsheet: 'spreadsheet' };
|
|
575
|
+
if (!map[to]) throw new Error('--to 에 db 또는 sheet 를 주세요.');
|
|
576
|
+
|
|
577
|
+
const client = makeClient();
|
|
578
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
579
|
+
const made = await client.createFormCollector(id, map[to]);
|
|
580
|
+
if (args.json) {
|
|
581
|
+
console.log(JSON.stringify(made, null, 2));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
console.log(
|
|
585
|
+
made.kind === 'database'
|
|
586
|
+
? `표를 만들어 연결했습니다: ${made.nodeId} (폼 질문 그대로 칸이 생겼습니다)`
|
|
587
|
+
: `스프레드시트를 만들어 연결했습니다: ${made.nodeId}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function cmdFormResponses(args) {
|
|
592
|
+
const target = args._[1];
|
|
593
|
+
if (!target) throw new Error('사용법: rootr form responses <path|id> [--latest] [--json]');
|
|
594
|
+
const client = makeClient();
|
|
595
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
596
|
+
const rows = await client.listFormResponses(id, { latestPerRespondent: Boolean(args.latest) });
|
|
597
|
+
|
|
598
|
+
if (args.json) {
|
|
599
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!rows.length) {
|
|
603
|
+
console.log('아직 들어온 응답이 없습니다.');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
console.log(`응답 ${rows.length}건${args.latest ? ' (사람마다 최신 1건만)' : ''}`);
|
|
607
|
+
for (const r of rows) {
|
|
608
|
+
const who = r.submitterUserId ? r.submitterUserId : '익명';
|
|
609
|
+
console.log(`- ${r.id} ${new Date(r.createdAt).toISOString()} 제출자:${who}`);
|
|
610
|
+
console.log(` ${JSON.stringify(r.values)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function cmdFormResponseEdit(args) {
|
|
615
|
+
const responseId = args._[1];
|
|
616
|
+
if (!responseId) throw new Error("사용법: rootr form response-edit <응답id> --values '<json>'");
|
|
617
|
+
const raw = typeof args.values === 'string' ? args.values : await readStdin();
|
|
618
|
+
let values;
|
|
619
|
+
try {
|
|
620
|
+
values = JSON.parse(raw);
|
|
621
|
+
} catch {
|
|
622
|
+
throw new Error('--values 를 JSON으로 읽을 수 없습니다. {"필드id":"값"} 형태여야 합니다.');
|
|
623
|
+
}
|
|
624
|
+
// 통째로 바뀐다 — 남기고 싶은 답도 같이 보내야 한다.
|
|
625
|
+
const result = await makeClient().updateFormResponse(responseId, values);
|
|
626
|
+
if (args.json) {
|
|
627
|
+
console.log(JSON.stringify(result, null, 2));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
console.log(`응답 수정됨: ${result.id ?? responseId}`);
|
|
631
|
+
console.log(` ${JSON.stringify(result.values ?? values)}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function cmdFormResponseRm(args) {
|
|
635
|
+
const responseId = args._[1];
|
|
636
|
+
if (!responseId) throw new Error('사용법: rootr form response-rm <응답id> [--yes]');
|
|
637
|
+
if (!args.yes) {
|
|
638
|
+
throw new Error('응답 삭제는 되돌릴 수 없습니다. 정말 지우려면 --yes 를 붙이세요.');
|
|
639
|
+
}
|
|
640
|
+
await makeClient().deleteFormResponse(responseId);
|
|
641
|
+
console.log(`응답 삭제됨: ${responseId}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
448
644
|
async function cmdDb(args) {
|
|
449
645
|
const sub = args._[0];
|
|
450
646
|
if (sub === 'rows') return cmdDbRows(args);
|
|
451
647
|
if (sub === 'row-add') return cmdDbRowAdd(args);
|
|
452
648
|
if (sub === 'row-body') return cmdDbRowBody(args);
|
|
649
|
+
if (sub === 'lock') return cmdDbLock(args);
|
|
650
|
+
if (sub === 'notify') return cmdDbNotify(args);
|
|
453
651
|
if (sub !== 'create') {
|
|
454
652
|
throw new Error(
|
|
455
|
-
'사용법: rootr db create|rows|row-add|row-body … (자세한 건 rootr --help)',
|
|
653
|
+
'사용법: rootr db create|rows|row-add|row-body|lock|notify … (자세한 건 rootr --help)',
|
|
456
654
|
);
|
|
457
655
|
}
|
|
458
656
|
const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
|
|
@@ -482,6 +680,90 @@ async function cmdDb(args) {
|
|
|
482
680
|
}
|
|
483
681
|
|
|
484
682
|
/** 표의 행 목록. --body 를 주면 행마다 상세 내용(본문 메모)까지 가져온다. */
|
|
683
|
+
/**
|
|
684
|
+
* `rootr db lock <path|id> [--schema managers|open] [--rows own|all] [--locked <칸,칸>]`
|
|
685
|
+
*
|
|
686
|
+
* 표에 거는 자물쇠 — 노션에 없는 것이다. 인자 없이 부르면 지금 상태를 보여 준다.
|
|
687
|
+
*/
|
|
688
|
+
async function cmdDbLock(args) {
|
|
689
|
+
const target = args._[1];
|
|
690
|
+
if (!target) throw new Error('사용법: rootr db lock <path|id> [--schema managers|open] [--rows own|all] [--locked "승인,점수"]');
|
|
691
|
+
|
|
692
|
+
const client = makeClient();
|
|
693
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
694
|
+
|
|
695
|
+
const patch = {};
|
|
696
|
+
if (args.schema !== undefined) patch.schemaLock = args.schema;
|
|
697
|
+
if (args.rows !== undefined) patch.rowScope = args.rows;
|
|
698
|
+
if (args.locked !== undefined) {
|
|
699
|
+
patch.lockedProps = String(args.locked)
|
|
700
|
+
.split(',')
|
|
701
|
+
.map((x) => x.trim())
|
|
702
|
+
.filter(Boolean);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const result = Object.keys(patch).length
|
|
706
|
+
? await client.setDatabasePermissions(id, patch)
|
|
707
|
+
: await client.getDatabasePermissions(id);
|
|
708
|
+
|
|
709
|
+
if (args.json) {
|
|
710
|
+
console.log(JSON.stringify(result, null, 2));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
console.log(`칸·뷰 잠금: ${result.schemaLock === 'managers' ? '관리자만' : '누구나'}`);
|
|
714
|
+
console.log(`행 보기 : ${result.rowScope === 'own' ? '본인이 만든 행만' : '전부'}`);
|
|
715
|
+
console.log(`잠긴 칸 : ${(result.lockedProps ?? []).length ? result.lockedProps.join(', ') : '없음'}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* `rootr db notify <path|id> [--file rules.json]`
|
|
720
|
+
*
|
|
721
|
+
* 표에 무슨 일이 생기면 누구에게 알릴지. 파일 없이 부르면 지금 규칙을 보여 준다.
|
|
722
|
+
* 규칙 모양: [{ "userIds": ["…"], "events": ["created"], "filter": {"propId":"p1","op":"is","value":"긴급"} }]
|
|
723
|
+
*/
|
|
724
|
+
async function cmdDbNotify(args) {
|
|
725
|
+
const target = args._[1];
|
|
726
|
+
if (!target) throw new Error('사용법: rootr db notify <path|id> [--file rules.json] [--clear] [--json]');
|
|
727
|
+
|
|
728
|
+
const client = makeClient();
|
|
729
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
730
|
+
|
|
731
|
+
if (args.clear) {
|
|
732
|
+
const cleared = await client.setDatabaseNotifications(id, []);
|
|
733
|
+
console.log('알림 규칙을 모두 지웠습니다.');
|
|
734
|
+
if (args.json) console.log(JSON.stringify(cleared, null, 2));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (args.file) {
|
|
738
|
+
const raw = readFileSync(args.file, 'utf8');
|
|
739
|
+
const saved = await client.setDatabaseNotifications(id, JSON.parse(raw));
|
|
740
|
+
console.log(`알림 규칙 ${saved.watchers.length}개를 저장했습니다.`);
|
|
741
|
+
if (args.json) console.log(JSON.stringify(saved, null, 2));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const { watchers } = await client.getDatabaseNotifications(id);
|
|
746
|
+
if (args.json) {
|
|
747
|
+
console.log(JSON.stringify(watchers, null, 2));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!watchers.length) {
|
|
751
|
+
console.log('알림 규칙이 없습니다.');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const WHEN = { created: '행 추가', updated: '행 수정', deleted: '행 삭제' };
|
|
755
|
+
for (const w of watchers) {
|
|
756
|
+
const when = (w.events ?? []).map((e) => WHEN[e] ?? e).join(' · ');
|
|
757
|
+
const f = w.filter;
|
|
758
|
+
const only = f
|
|
759
|
+
? f.op === 'changed'
|
|
760
|
+
? ` (${f.propId} 칸이 바뀌었을 때만)`
|
|
761
|
+
: ` (${f.propId} ${f.op === 'is' ? '=' : '⊃'} ${f.value})`
|
|
762
|
+
: '';
|
|
763
|
+
console.log(`- ${when}${only} → ${(w.userIds ?? []).length}명`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
485
767
|
async function cmdDbRows(args) {
|
|
486
768
|
const target = args._[1];
|
|
487
769
|
if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--json]');
|
|
@@ -575,6 +857,8 @@ async function main() {
|
|
|
575
857
|
switch (cmd) {
|
|
576
858
|
case 'ls':
|
|
577
859
|
return cmdLs(args);
|
|
860
|
+
case 'comments':
|
|
861
|
+
return cmdComments(args);
|
|
578
862
|
case 'read':
|
|
579
863
|
return cmdRead(args);
|
|
580
864
|
case 'write':
|
|
@@ -601,6 +885,10 @@ async function main() {
|
|
|
601
885
|
return cmdUse(args);
|
|
602
886
|
case 'db':
|
|
603
887
|
return cmdDb(args);
|
|
888
|
+
case 'form':
|
|
889
|
+
return cmdForm(args);
|
|
890
|
+
case 'access':
|
|
891
|
+
return cmdAccess(args);
|
|
604
892
|
case 'mcp':
|
|
605
893
|
return cmdMcp(args);
|
|
606
894
|
default:
|
package/lib/client.js
CHANGED
|
@@ -348,6 +348,55 @@ export class RootrClient {
|
|
|
348
348
|
});
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// ── DB-PERM-001 / DB-WATCH — 표에 거는 자물쇠와 알림 규칙 ──────────────
|
|
352
|
+
//
|
|
353
|
+
// 노션에 없는 것들이라, 에이전트가 표를 만들어 놓고도 "지원서는 서로 못 보게" 같은
|
|
354
|
+
// 요구를 못 들어주는 일이 있었다. 만들 수 있으면 잠글 수도 있어야 한다.
|
|
355
|
+
|
|
356
|
+
async getDatabasePermissions(databaseId) {
|
|
357
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/permissions`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async setDatabasePermissions(databaseId, permissions) {
|
|
361
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/permissions`, {
|
|
362
|
+
method: 'PATCH',
|
|
363
|
+
body: permissions,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async getDatabaseNotifications(databaseId) {
|
|
368
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/notifications`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async setDatabaseNotifications(databaseId, watchers) {
|
|
372
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/notifications`, {
|
|
373
|
+
method: 'PUT',
|
|
374
|
+
body: { watchers },
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── PUBLIC-DOCS — 발행한 문서에 올라온 변경 제안 ────────────────────────
|
|
379
|
+
|
|
380
|
+
async listDocumentCommits(documentId, status = 'OPEN') {
|
|
381
|
+
return this.request(
|
|
382
|
+
`/documents/${encodeURIComponent(documentId)}/commits?status=${encodeURIComponent(status)}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async mergeDocumentCommit(documentId, commitId, force = false) {
|
|
387
|
+
return this.request(
|
|
388
|
+
`/documents/${encodeURIComponent(documentId)}/commits/${encodeURIComponent(commitId)}/merge`,
|
|
389
|
+
{ method: 'POST', body: force ? { force: true } : {} },
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async rejectDocumentCommit(documentId, commitId, reason) {
|
|
394
|
+
return this.request(
|
|
395
|
+
`/documents/${encodeURIComponent(documentId)}/commits/${encodeURIComponent(commitId)}/reject`,
|
|
396
|
+
{ method: 'POST', body: reason ? { reason } : {} },
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
351
400
|
async getDocumentPublicStatus(documentId) {
|
|
352
401
|
return this.request(`/documents/${encodeURIComponent(documentId)}/public-status`);
|
|
353
402
|
}
|
|
@@ -602,6 +651,25 @@ export class RootrClient {
|
|
|
602
651
|
});
|
|
603
652
|
}
|
|
604
653
|
|
|
654
|
+
/**
|
|
655
|
+
* 뷰를 하나 **덧붙인다**(기존 뷰는 그대로). 표를 만들 때만 뷰를 정할 수 있어서,
|
|
656
|
+
* 노션에서 뷰가 넷인 표를 옮기면 하나밖에 못 두던 문제를 푼다.
|
|
657
|
+
*/
|
|
658
|
+
async createDatabaseView(databaseId, { name, type, groupBy, dateProp, endDateProp } = {}) {
|
|
659
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/views`, {
|
|
660
|
+
method: 'POST',
|
|
661
|
+
body: { name, type, groupBy, dateProp, endDateProp },
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** 뷰 하나를 지운다. 마지막 하나는 못 지운다. 행 값은 안 지워진다. */
|
|
666
|
+
async deleteDatabaseView(databaseId, viewId) {
|
|
667
|
+
return this.request(
|
|
668
|
+
`/databases/${encodeURIComponent(databaseId)}/views/${encodeURIComponent(viewId)}`,
|
|
669
|
+
{ method: 'DELETE' },
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
605
673
|
/**
|
|
606
674
|
* 뷰 하나만 고친다(이름·감출 컬럼·그룹 기준). PATCH /databases/:id 는 views 배열을
|
|
607
675
|
* 통째로 갈아끼워서 빠뜨린 뷰가 사라지는데, 여기서는 서버가 그 뷰만 병합한다.
|
|
@@ -655,10 +723,11 @@ export class RootrClient {
|
|
|
655
723
|
// PATCH /databases/:id replaces the whole properties array, so rebuilding it
|
|
656
724
|
// client-side drops any column left out. These merge server-side instead.
|
|
657
725
|
|
|
658
|
-
async addColumn(databaseId, { name, type, options } = {}) {
|
|
726
|
+
async addColumn(databaseId, { name, type, options, relationDatabaseId, formula, rollup, prefix } = {}) {
|
|
659
727
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/columns`, {
|
|
660
728
|
method: 'POST',
|
|
661
|
-
|
|
729
|
+
// 계산 칸(수식·롤업·번호)은 설정이 곧 그 칸이라 같이 보내야 한다.
|
|
730
|
+
body: { name, type, options, relationDatabaseId, formula, rollup, prefix },
|
|
662
731
|
});
|
|
663
732
|
}
|
|
664
733
|
|
|
@@ -1136,13 +1205,75 @@ export class RootrClient {
|
|
|
1136
1205
|
});
|
|
1137
1206
|
}
|
|
1138
1207
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1208
|
+
// ── 문서·폴더 접근 권한(공개범위) ────────────────────────────────
|
|
1209
|
+
// 화면의 "공개범위"·"권한" 창이 하는 일을 그대로 연다. 관리 권한이 있는
|
|
1210
|
+
// 사람만 부를 수 있고, 없으면 서버가 403으로 막는다.
|
|
1211
|
+
|
|
1212
|
+
/** 이 노드의 공개범위와 직접·상속 권한을 함께 읽는다. */
|
|
1213
|
+
async getNodeAccess(nodeId) {
|
|
1214
|
+
return this.request(`/nodes/${encodeURIComponent(nodeId)}/permissions`);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** 공개범위를 정한다. null이면 상위 폴더/워크스페이스 기본값을 따른다. */
|
|
1218
|
+
async setNodeVisibility(nodeId, visibility) {
|
|
1219
|
+
return this.request(`/nodes/${encodeURIComponent(nodeId)}/visibility`, {
|
|
1220
|
+
method: 'POST',
|
|
1221
|
+
body: { visibility: visibility ?? null },
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/** 특정 사람/그룹에게 이 노드의 권한을 준다(이미 있으면 등급만 바뀐다). */
|
|
1226
|
+
async grantNodePermission(nodeId, { subjectType = 'USER', subjectId, level }) {
|
|
1227
|
+
return this.request(`/nodes/${encodeURIComponent(nodeId)}/permissions`, {
|
|
1228
|
+
method: 'POST',
|
|
1229
|
+
body: { subjectType, subjectId, level },
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/** 준 권한을 거둔다. */
|
|
1234
|
+
async revokeNodePermission(nodeId, { subjectType = 'USER', subjectId }) {
|
|
1235
|
+
return this.request(`/nodes/${encodeURIComponent(nodeId)}/permissions`, {
|
|
1236
|
+
method: 'DELETE',
|
|
1237
|
+
body: { subjectType, subjectId },
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Submitted responses, most recent first.
|
|
1243
|
+
* `latestPerRespondent`이면 같은 사람이 여러 번 낸 것 중 마지막 것만 남긴다
|
|
1244
|
+
* (누가 냈는지 모르는 익명 제출은 합치지 않는다 — 합칠 근거가 없다).
|
|
1245
|
+
*/
|
|
1246
|
+
async listFormResponses(formId, { latestPerRespondent } = {}) {
|
|
1247
|
+
const qs = latestPerRespondent ? '?latestPerRespondent=true' : '';
|
|
1248
|
+
const data = await this.request(`/forms/${encodeURIComponent(formId)}/responses${qs}`);
|
|
1142
1249
|
if (data && Array.isArray(data.responses)) return data.responses;
|
|
1143
1250
|
return [];
|
|
1144
1251
|
}
|
|
1145
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* 응답 모을 곳을 새로 만들어 연결한다. 'database'면 폼 질문 그대로 칸이 생기고,
|
|
1255
|
+
* 'spreadsheet'면 응답이 깔리는 시트가 한 장 생긴다. 미리 만들어 둘 필요가 없다.
|
|
1256
|
+
*/
|
|
1257
|
+
async createFormCollector(formId, target) {
|
|
1258
|
+
return this.request(`/forms/${encodeURIComponent(formId)}/collect`, {
|
|
1259
|
+
method: 'POST',
|
|
1260
|
+
body: { target },
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/** 이미 낸 응답의 내용을 고친다(폼의 검사 규칙을 그대로 다시 통과해야 한다). */
|
|
1265
|
+
async updateFormResponse(responseId, values) {
|
|
1266
|
+
return this.request(`/form-responses/${encodeURIComponent(responseId)}`, {
|
|
1267
|
+
method: 'PATCH',
|
|
1268
|
+
body: { values },
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** 응답 한 건을 지운다. */
|
|
1273
|
+
async deleteFormResponse(responseId) {
|
|
1274
|
+
return this.request(`/form-responses/${encodeURIComponent(responseId)}`, { method: 'DELETE' });
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1146
1277
|
async createFormResponse(formId, values) {
|
|
1147
1278
|
return this.request(`/forms/${encodeURIComponent(formId)}/responses`, {
|
|
1148
1279
|
method: 'POST',
|