rootr-cli 0.23.3 → 0.24.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 +29 -1
- package/README.md +20 -2
- package/bin/rootr.js +138 -2
- package/lib/client.js +42 -0
- package/lib/mcp-tools/doc-mail.js +116 -0
- package/lib/mcp.js +6 -0
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -305,6 +305,22 @@ rootr sign ledger # 워크스페이스 원장 전체 재계산
|
|
|
305
305
|
원본은 `SUPERSEDED`로 남습니다 — 지침이 "고친 흔적과 원본이 모두 보여야 한다"를
|
|
306
306
|
요구하기 때문입니다.
|
|
307
307
|
|
|
308
|
+
### 문서 보내기 · 문서함 (send · mail)
|
|
309
|
+
|
|
310
|
+
폴더만 빼고 어떤 노드든 이메일 1~20명에게 **보낼 때의 복사본**으로 보낼 수 있습니다. 담기는 내용은
|
|
311
|
+
공개 페이지와 같아서 사람·첨부·관계 칸은 나가지 않습니다. 받는 사람은 메일 링크로 **루터 계정 없이**
|
|
312
|
+
열고, 그 이메일에 루터 계정이 있거나 나중에 가입하면 받은 문서함에도 그대로 있습니다. 보낸 뒤 원본을
|
|
313
|
+
고치거나 지워도 받은 문서는 그대로입니다. 보내려면 그 노드에 쓰기(WRITE) 권한이 있어야 합니다(공유와 같은 문턱).
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
rootr send "/보고서/3분기 실적" --to partner@example.com,cfo@example.com --message "말씀드린 자료입니다"
|
|
317
|
+
rootr mail # 받은 문서 (안 읽은 수 함께)
|
|
318
|
+
rootr mail sent # 보낸 문서 — 읽었는지, 메일 실패
|
|
319
|
+
rootr mail show <id> # 열기 (문서는 마크다운 본문, 그 밖은 --json 으로 전부)
|
|
320
|
+
rootr mail revoke <id> # 회수 — 받는 쪽에서 본문이 안 보이고 링크는 410
|
|
321
|
+
rootr mail hide <id> # 내 문서함에서만 지우기
|
|
322
|
+
```
|
|
323
|
+
|
|
308
324
|
### 제보 보내기 (feedback)
|
|
309
325
|
|
|
310
326
|
```bash
|
|
@@ -352,7 +368,7 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
|
352
368
|
|
|
353
369
|
## MCP 서버로 사용하기
|
|
354
370
|
|
|
355
|
-
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구
|
|
371
|
+
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 197종:
|
|
356
372
|
|
|
357
373
|
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
358
374
|
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
@@ -799,6 +815,18 @@ rootr db view <표> <뷰> --scale year # 달력 보기를 주·달·해
|
|
|
799
815
|
주세요 — 그 표는 행을 열 때마다 서명문서(연구노트)가 만들어집니다(실험 기록 대장,
|
|
800
816
|
점검 대장).
|
|
801
817
|
|
|
818
|
+
#### 문서 보내기 · 문서함 (docs:read/docs:write)
|
|
819
|
+
|
|
820
|
+
| 도구 | 설명 |
|
|
821
|
+
|---|---|
|
|
822
|
+
| `rootr_send_document` | 노드 하나를 이메일 1~20명에게 보낼 때의 복사본으로 보내기 (계정 없어도 메일 링크로 열림) |
|
|
823
|
+
| `rootr_list_doc_mail` | 내 문서함 — `box`: inbox(받은, 기본) / sent(보낸), 안 읽은 수 함께 |
|
|
824
|
+
| `rootr_read_doc_mail` | 한 통 열기 — 본문(`payload`), 받은 것은 읽음 처리, 회수된 것은 본문 없음 |
|
|
825
|
+
| `rootr_revoke_doc_mail` | 보낸 문서 회수 (보낸 사람만) |
|
|
826
|
+
|
|
827
|
+
문서함은 워크스페이스가 아니라 **사람(이메일)**에 붙습니다. 사람 없는 워크스페이스 키(원격 커넥터)는
|
|
828
|
+
그 키를 만든 구성원으로 동작합니다.
|
|
829
|
+
|
|
802
830
|
#### 문서 버전 / 삭제
|
|
803
831
|
|
|
804
832
|
| 도구 | 설명 |
|
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, **197 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
|
|
|
@@ -327,6 +327,23 @@ A signed document is never edited in place. You issue a **revision**
|
|
|
327
327
|
(`rootr sign revise <id> --note "…"`) and the original stays as `SUPERSEDED`, because the
|
|
328
328
|
regulations require both the original and the trace of the change to remain visible.
|
|
329
329
|
|
|
330
|
+
### Sending a document to someone (mailbox)
|
|
331
|
+
|
|
332
|
+
Any node except a folder can be emailed to 1–20 addresses as a **copy taken at send time** — the
|
|
333
|
+
same content a public page shows, so person / attachment / relation columns never go out. The
|
|
334
|
+
recipient opens it from the email link **without a Rootr account**; if that address has (or later
|
|
335
|
+
gets) a Rootr account, it is also in their inbox. Editing or deleting the original afterwards does
|
|
336
|
+
not change what they received. Sending needs WRITE on the node, the same bar as sharing.
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
rootr send "/reports/Q3 numbers" --to partner@example.com,cfo@example.com --message "As discussed"
|
|
340
|
+
rootr mail # received (with the unread count)
|
|
341
|
+
rootr mail sent # what you sent — read or not, email failures
|
|
342
|
+
rootr mail show <id> # open it (a document prints its markdown; --json for any type)
|
|
343
|
+
rootr mail revoke <id> # withdraw: the recipient no longer sees the content, the link answers 410
|
|
344
|
+
rootr mail hide <id> # remove from your own mailbox only
|
|
345
|
+
```
|
|
346
|
+
|
|
330
347
|
### Team memory
|
|
331
348
|
|
|
332
349
|
```bash
|
|
@@ -408,7 +425,7 @@ is refused and says when the grace was used.
|
|
|
408
425
|
|
|
409
426
|
## MCP tools
|
|
410
427
|
|
|
411
|
-
`rootr mcp` speaks MCP over stdio.
|
|
428
|
+
`rootr mcp` speaks MCP over stdio. 197 tools:
|
|
412
429
|
|
|
413
430
|
| Group | Count | Tools |
|
|
414
431
|
|---|---|---|
|
|
@@ -416,6 +433,7 @@ is refused and says when the grace was used.
|
|
|
416
433
|
| Databases | 32 | create, columns, views, rows (single & bulk), row pages, permissions, notifications, buttons, duplicates (find / merge / keep & link), history & point-in-time restore (row / whole table) |
|
|
417
434
|
| CRM | 18 | companies, contacts, deals (incl. pipeline moves), activities, tasks, CSV import |
|
|
418
435
|
| Signed documents & research notes | 14 | create, sign, revise, reject, reopen, approval lines, signing codes, ledger verification, research-note preset |
|
|
436
|
+
| Sending documents (mailbox) | 4 | send any node as a frozen copy to email addresses (no account needed to open), list received / sent, read, withdraw |
|
|
419
437
|
| Forms | 10 | create, read, update, responses, share links, collectors |
|
|
420
438
|
| Presentations & images | 10 | decks, slides, diagram slides, image generation and background removal |
|
|
421
439
|
| Workspaces & scaffolding | 10 | list, create, delete, switch, set the emblem (sidebar logo), plan / review / apply a document tree, health check |
|
package/bin/rootr.js
CHANGED
|
@@ -95,6 +95,11 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
95
95
|
rootr sign list [--pending] [--status <S>] [--mode <M>] [--json] 서명문서 목록 (--pending: 내 차례)
|
|
96
96
|
rootr sign bulk <id…> --code <6자리> 여러 건 한 번에 올리기+서명 (코드 하나로, 초안 포함)
|
|
97
97
|
rootr sign ledger [--json] 워크스페이스 원장(해시 체인) 전체 검증
|
|
98
|
+
rootr send <path|id> --to a@x.com[,b@y.com] [--message "..."] 문서(어떤 타입이든)를 이메일로 보내기 — 보낼 때의 복사본, 계정 없어도 열림
|
|
99
|
+
rootr mail [inbox|sent] [--limit N] [--q <찾기>] [--json] 문서함 — 받은 문서(기본)·보낸 문서
|
|
100
|
+
rootr mail show <id> [--json] 받은/보낸 문서 열기(문서면 본문, 그 밖은 요약) — 받은 것은 읽음 처리
|
|
101
|
+
rootr mail revoke <id> 보낸 문서 회수 — 받는 쪽에서 본문이 안 보임
|
|
102
|
+
rootr mail hide <id> [--box inbox|sent] 내 문서함에서만 지우기(상대 화면은 그대로)
|
|
98
103
|
rootr config [--api-key <k>] [--workspace <w>] [--base-url <u>] 설정 조회/저장
|
|
99
104
|
rootr config --add-key <k> [--label <name>] [--workspace <w>] 워크스페이스별 키 추가 등록
|
|
100
105
|
rootr feedback "<무엇이 안 되는지>" [--kind bug|friction|idea|praise] [--email <주소>]
|
|
@@ -108,7 +113,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
|
|
|
108
113
|
rootr memory add "<내용>" [--kind 지시|사람|프로젝트|참고] [--source <경로|id>] [--related a,b] 기억 저장
|
|
109
114
|
rootr memory edit <id> [--content "<내용>"] [--kind …] [--source …] [--related a,b] 기억 고치기(준 칸만)
|
|
110
115
|
rootr memory forget <id> [--hard] 기억 폐기(--hard면 행 삭제)
|
|
111
|
-
rootr mcp MCP stdio 서버 구동 (도구
|
|
116
|
+
rootr mcp MCP stdio 서버 구동 (도구 197종 — README 참고)
|
|
112
117
|
rootr --help 도움말
|
|
113
118
|
rootr --version 버전 출력
|
|
114
119
|
|
|
@@ -201,6 +206,11 @@ usage:
|
|
|
201
206
|
rootr sign list [--pending] [--status <S>] [--mode <M>] [--json] list signed documents (--pending: waiting on you)
|
|
202
207
|
rootr sign bulk <id…> --code <6 digits> submit+sign many at once (one code, drafts included)
|
|
203
208
|
rootr sign ledger [--json] verify the whole workspace ledger (hash chain)
|
|
209
|
+
rootr send <path|id> --to a@x.com[,b@y.com] [--message "..."] email any node as a frozen copy — opens without an account
|
|
210
|
+
rootr mail [inbox|sent] [--limit N] [--q <text>] [--json] your document mailbox — received (default) or sent
|
|
211
|
+
rootr mail show <id> [--json] open a received/sent document (body for docs, a summary otherwise); marks received ones read
|
|
212
|
+
rootr mail revoke <id> withdraw a document you sent — the recipient no longer sees the content
|
|
213
|
+
rootr mail hide <id> [--box inbox|sent] remove from your own mailbox only (the other side keeps it)
|
|
204
214
|
rootr config [--api-key <k>] [--workspace <w>] [--base-url <u>] read/save configuration
|
|
205
215
|
rootr config --add-key <k> [--label <name>] [--workspace <w>] register an extra per-workspace key
|
|
206
216
|
rootr feedback "<what is not working>" [--kind bug|friction|idea|praise] [--email <address>]
|
|
@@ -214,7 +224,7 @@ usage:
|
|
|
214
224
|
rootr memory add "<content>" [--kind instruction|person|project|reference] [--source <path|id>] [--related a,b] save a memory
|
|
215
225
|
rootr memory edit <id> [--content "<content>"] [--kind …] [--source …] [--related a,b] edit a memory (only the fields you pass)
|
|
216
226
|
rootr memory forget <id> [--hard] retire a memory (--hard deletes the row)
|
|
217
|
-
rootr mcp run the MCP stdio server (
|
|
227
|
+
rootr mcp run the MCP stdio server (197 tools — see the README)
|
|
218
228
|
rootr --help this help
|
|
219
229
|
rootr --version print the version
|
|
220
230
|
|
|
@@ -3061,6 +3071,128 @@ async function cmdSign(args) {
|
|
|
3061
3071
|
}
|
|
3062
3072
|
}
|
|
3063
3073
|
|
|
3074
|
+
// ---- doc mail (DOC-MAIL — 문서 송수신) ------------------------------------
|
|
3075
|
+
|
|
3076
|
+
const MAIL_USAGE =
|
|
3077
|
+
usage(`rootr mail [inbox|sent] | show <id> | revoke <id> | hide <id>\n`) +
|
|
3078
|
+
tr(' (자세한 건 rootr --help)', ` (see rootr --help)`);
|
|
3079
|
+
|
|
3080
|
+
const NODE_TYPE_KO = {
|
|
3081
|
+
DOCUMENT: tr('문서', 'document'),
|
|
3082
|
+
DATABASE: tr('표', 'table'),
|
|
3083
|
+
SPREADSHEET: tr('스프레드시트', 'spreadsheet'),
|
|
3084
|
+
WHITEBOARD: tr('화이트보드', 'whiteboard'),
|
|
3085
|
+
PRESENTATION: tr('발표자료', 'deck'),
|
|
3086
|
+
FORM: tr('폼', 'form'),
|
|
3087
|
+
ISSUE_TRACKER: tr('이슈', 'issues'),
|
|
3088
|
+
LOG: tr('로그', 'log'),
|
|
3089
|
+
CRM: 'CRM',
|
|
3090
|
+
RECORDING: tr('녹음', 'recording'),
|
|
3091
|
+
PAGE: tr('페이지', 'page'),
|
|
3092
|
+
SIGNED: tr('서명문서', 'signed document'),
|
|
3093
|
+
};
|
|
3094
|
+
|
|
3095
|
+
async function cmdSend(args) {
|
|
3096
|
+
const target = args._[0];
|
|
3097
|
+
const toArg = typeof args.to === 'string' ? args.to : '';
|
|
3098
|
+
const to = toArg.split(/[,;\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
3099
|
+
if (!target || !to.length) {
|
|
3100
|
+
throw new Error(usage(tr(`rootr send <path|id> --to a@x.com[,b@y.com] [--message "..."]`, `rootr send <path|id> --to a@x.com[,b@y.com] [--message "..."]`)));
|
|
3101
|
+
}
|
|
3102
|
+
const client = makeClient();
|
|
3103
|
+
const nodeId = requireResolved(await resolveTarget(client, target));
|
|
3104
|
+
const out = await client.sendNode(nodeId, { to, message: typeof args.message === 'string' ? args.message : undefined });
|
|
3105
|
+
if (args.json) return console.log(JSON.stringify(out, null, 2));
|
|
3106
|
+
const items = out.items ?? [];
|
|
3107
|
+
console.log(tr(`${items.length}명에게 보냈습니다 (묶음 ${out.batchId})`, `Sent to ${items.length} ${items.length === 1 ? 'person' : 'people'} (batch ${out.batchId})`));
|
|
3108
|
+
for (const it of items) {
|
|
3109
|
+
console.log(
|
|
3110
|
+
` ${it.to}\t${it.id}\t` +
|
|
3111
|
+
(it.emailed ? tr('메일 보냄', 'emailed') : tr(`메일 실패${it.error ? `: ${it.error}` : ''} — 문서는 받은 문서함에 있습니다`, `email failed${it.error ? `: ${it.error}` : ''} — the copy is still in their inbox`)),
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
function mailWhen(iso) {
|
|
3117
|
+
return iso ? String(iso).replace('T', ' ').slice(0, 16) : '';
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
async function cmdMail(args) {
|
|
3121
|
+
const sub = args._[0];
|
|
3122
|
+
const client = makeClient();
|
|
3123
|
+
if (!sub || sub === 'inbox' || sub === 'sent' || sub === 'list') {
|
|
3124
|
+
const box = sub === 'sent' || args.box === 'sent' ? 'sent' : 'inbox';
|
|
3125
|
+
const limit = args.limit ? Number(args.limit) : undefined;
|
|
3126
|
+
const out = await client.listDocMail({ box, limit, q: typeof args.q === 'string' ? args.q : undefined, cursor: typeof args.cursor === 'string' ? args.cursor : undefined });
|
|
3127
|
+
if (args.json) return console.log(JSON.stringify(out, null, 2));
|
|
3128
|
+
if (box === 'inbox') console.log(tr(`받은 문서 — 안 읽은 것 ${out.unread}개`, `Received — ${out.unread} unread`));
|
|
3129
|
+
else console.log(tr('보낸 문서', 'Sent'));
|
|
3130
|
+
if (!out.items?.length) return console.log(tr(' (없음)', ' (none)'));
|
|
3131
|
+
for (const m of out.items) {
|
|
3132
|
+
const type = NODE_TYPE_KO[m.nodeType] ?? m.nodeType;
|
|
3133
|
+
const state = m.revoked
|
|
3134
|
+
? tr('[회수됨]', '[withdrawn]')
|
|
3135
|
+
: box === 'inbox'
|
|
3136
|
+
? m.readAt ? '' : tr('[새 문서]', '[new]')
|
|
3137
|
+
: m.readAt ? tr(`[읽음 ${mailWhen(m.readAt)}]`, `[read ${mailWhen(m.readAt)}]`) : tr('[안 읽음]', '[unread]');
|
|
3138
|
+
const who = box === 'inbox' ? `${m.senderName} <${m.senderEmail}>` : `→ ${m.recipientEmail}`;
|
|
3139
|
+
const mailFail = box === 'sent' && m.emailed === false ? tr(' [메일 실패]', ' [email failed]') : '';
|
|
3140
|
+
console.log(`${m.id}\t${mailWhen(m.createdAt)}\t${type}\t${m.title}\t${who}${state ? ` ${state}` : ''}${mailFail}`);
|
|
3141
|
+
if (m.message) console.log(` ${String(m.message).split('\n')[0].slice(0, 100)}`);
|
|
3142
|
+
}
|
|
3143
|
+
if (out.nextCursor) console.log(tr(`\n다음 쪽: rootr mail ${box} --cursor ${out.nextCursor}`, `\nNext page: rootr mail ${box} --cursor ${out.nextCursor}`));
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
const id = args._[1];
|
|
3147
|
+
if (!id) throw new Error(MAIL_USAGE);
|
|
3148
|
+
switch (sub) {
|
|
3149
|
+
case 'show':
|
|
3150
|
+
case 'read': {
|
|
3151
|
+
const m = await client.getDocMail(id);
|
|
3152
|
+
if (args.json) return console.log(JSON.stringify(m, null, 2));
|
|
3153
|
+
console.log(`${m.title} (${NODE_TYPE_KO[m.nodeType] ?? m.nodeType})`);
|
|
3154
|
+
console.log(tr(`보낸 사람: ${m.senderName} <${m.senderEmail}> · ${m.workspaceName}`, `From: ${m.senderName} <${m.senderEmail}> · ${m.workspaceName}`));
|
|
3155
|
+
console.log(tr(`받는 사람: ${m.recipientEmail} · 보낸 때 ${mailWhen(m.createdAt)}${m.readAt ? ` · 읽음 ${mailWhen(m.readAt)}` : ''}`, `To: ${m.recipientEmail} · sent ${mailWhen(m.createdAt)}${m.readAt ? ` · read ${mailWhen(m.readAt)}` : ''}`));
|
|
3156
|
+
if (m.message) console.log(tr(`메시지: ${m.message}`, `Message: ${m.message}`));
|
|
3157
|
+
if (m.revoked && !m.payload) return console.log(tr('\n보낸 사람이 이 문서를 회수했습니다.', '\nThe sender withdrew this document.'));
|
|
3158
|
+
if (m.revoked) console.log(tr('(회수함 — 받는 쪽에서는 본문이 안 보입니다)', '(withdrawn — the recipient no longer sees the content)'));
|
|
3159
|
+
const p = m.payload ?? {};
|
|
3160
|
+
console.log('');
|
|
3161
|
+
if (p.kind === 'doc') return console.log(p.content ?? '');
|
|
3162
|
+
if (p.kind === 'database') {
|
|
3163
|
+
const props = p.properties ?? [];
|
|
3164
|
+
const rows = p.rows ?? [];
|
|
3165
|
+
console.log(props.map((c) => c.name).join('\t'));
|
|
3166
|
+
for (const r of rows) {
|
|
3167
|
+
console.log(props.map((c) => {
|
|
3168
|
+
const v = r.values?.[c.id];
|
|
3169
|
+
return v === null || v === undefined ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v);
|
|
3170
|
+
}).join('\t'));
|
|
3171
|
+
}
|
|
3172
|
+
if (p.truncated) console.log(tr(`… ${p.truncated.total}행 중 ${p.truncated.shown}행만 담겼습니다`, `… ${p.truncated.shown} of ${p.truncated.total} rows were included`));
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
const keys = Object.keys(p).filter((k) => k !== 'kind');
|
|
3176
|
+
console.log(tr(`[${p.kind}] 담긴 것: ${keys.join(', ')} — 전부 보려면 --json`, `[${p.kind}] contains: ${keys.join(', ')} — use --json for everything`));
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
case 'revoke': {
|
|
3180
|
+
const out = await client.revokeDocMail(id);
|
|
3181
|
+
if (args.json) return console.log(JSON.stringify(out, null, 2));
|
|
3182
|
+
return console.log(tr(`회수했습니다 (${mailWhen(out.revokedAt)}) — 받는 쪽에서 본문이 안 보입니다.`, `Withdrawn (${mailWhen(out.revokedAt)}) — the recipient no longer sees the content.`));
|
|
3183
|
+
}
|
|
3184
|
+
case 'hide':
|
|
3185
|
+
case 'rm': {
|
|
3186
|
+
const box = args.box === 'inbox' || args.box === 'sent' ? args.box : undefined;
|
|
3187
|
+
const out = await client.hideDocMail(id, { box });
|
|
3188
|
+
if (args.json) return console.log(JSON.stringify(out, null, 2));
|
|
3189
|
+
return console.log(tr(`내 문서함에서 지웠습니다 (${out.hidden}). 상대 화면은 그대로입니다.`, `Removed from your mailbox (${out.hidden}). The other side still has it.`));
|
|
3190
|
+
}
|
|
3191
|
+
default:
|
|
3192
|
+
throw new Error(MAIL_USAGE);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3064
3196
|
async function main() {
|
|
3065
3197
|
const argv = process.argv.slice(2);
|
|
3066
3198
|
const args = parseArgs(argv);
|
|
@@ -3126,6 +3258,10 @@ async function main() {
|
|
|
3126
3258
|
return cmdAccess(args);
|
|
3127
3259
|
case 'sign':
|
|
3128
3260
|
return cmdSign(args);
|
|
3261
|
+
case 'send':
|
|
3262
|
+
return cmdSend(args);
|
|
3263
|
+
case 'mail':
|
|
3264
|
+
return cmdMail(args);
|
|
3129
3265
|
case 'feedback':
|
|
3130
3266
|
return cmdFeedback(args);
|
|
3131
3267
|
case 'scaffold':
|
package/lib/client.js
CHANGED
|
@@ -2049,6 +2049,48 @@ export class RootrClient {
|
|
|
2049
2049
|
return this.request(`/workspaces/${encodeURIComponent(workspaceId)}/ledger/verify`);
|
|
2050
2050
|
}
|
|
2051
2051
|
|
|
2052
|
+
// ---- Doc mail (DOC-MAIL — 문서 송수신) ---------------------------------
|
|
2053
|
+
//
|
|
2054
|
+
// 노드 하나를 **보낼 때의 복사본**으로 이메일 1~20명에게 보낸다. 받은 문서는 받는 사람의
|
|
2055
|
+
// 이메일에 붙는다 — 계정이 없어도 메일 링크로 열리고, 나중에 그 이메일로 가입하면 문서함에
|
|
2056
|
+
// 그대로 있다. 문서함은 사람(이메일)에 붙으므로 워크스페이스 주소 밑이 아니다.
|
|
2057
|
+
|
|
2058
|
+
/** 보내기 — WRITE 이상. 응답 `{batchId, items:[{id,to,emailed}]}`. */
|
|
2059
|
+
async sendNode(nodeId, { to, message } = {}) {
|
|
2060
|
+
return this.request(`/nodes/${encodeURIComponent(nodeId)}/send`, {
|
|
2061
|
+
method: 'POST',
|
|
2062
|
+
body: { to, ...(message ? { message } : {}) },
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/** 문서함 목록 — box: inbox(받은, 기본) | sent(보낸). 본문 없이 요약 + 안 읽은 수(unread). */
|
|
2067
|
+
async listDocMail({ box, limit, cursor, q } = {}) {
|
|
2068
|
+
const qs = new URLSearchParams();
|
|
2069
|
+
if (box) qs.set('box', box);
|
|
2070
|
+
if (limit) qs.set('limit', String(limit));
|
|
2071
|
+
if (cursor) qs.set('cursor', cursor);
|
|
2072
|
+
if (q) qs.set('q', q);
|
|
2073
|
+
const s = qs.toString();
|
|
2074
|
+
return this.request(`/doc-mail${s ? `?${s}` : ''}`);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/** 한 통 열기(본문 payload 포함). 받는 사람이 열면 읽음으로 바뀐다. */
|
|
2078
|
+
async getDocMail(id) {
|
|
2079
|
+
return this.request(`/doc-mail/${encodeURIComponent(id)}`);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/** 회수 — 보낸 사람만. 이후 받는 쪽에서 본문이 안 보인다. */
|
|
2083
|
+
async revokeDocMail(id) {
|
|
2084
|
+
return this.request(`/doc-mail/${encodeURIComponent(id)}/revoke`, { method: 'POST', body: {} });
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/** 내 문서함에서만 지우기(상대 화면은 그대로). box 로 나에게 보낸 것의 한쪽만 고를 수 있다. */
|
|
2088
|
+
async hideDocMail(id, { box } = {}) {
|
|
2089
|
+
return this.request(`/doc-mail/${encodeURIComponent(id)}${box ? `?box=${encodeURIComponent(box)}` : ''}`, {
|
|
2090
|
+
method: 'DELETE',
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2052
2094
|
/**
|
|
2053
2095
|
* FEEDBACK — 제보를 보낸다. 다른 메서드와 달리 `request()`를 쓰지 않고 직접
|
|
2054
2096
|
* fetch 한다: **API 키가 없어도, 키가 틀렸어도 보낼 수 있어야** 하기 때문이다.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DOC-MAIL — 문서 송수신(창업자 지시 2026-10-20).
|
|
7
|
+
*
|
|
8
|
+
* 어떤 타입이든(문서·표·시트·화이트보드·덱·폼·이슈·로그·CRM·녹음·페이지·서명문서) 노드 하나를
|
|
9
|
+
* **보낼 때의 복사본**으로 특정 이메일에게 보낸다. 받은 문서는 받는 사람의 **이메일 주소**에 붙는다 —
|
|
10
|
+
* 계정이 없어도 메일 링크로 열리고, 나중에 그 이메일로 가입하면 받은 문서함에 그대로 있다.
|
|
11
|
+
* 보낸 뒤 원본을 고치거나 지워도 받은 문서는 그대로다(살아 있는 공유가 필요하면 권한/공개를 쓴다).
|
|
12
|
+
*/
|
|
13
|
+
export function registerDocMailTools(server) {
|
|
14
|
+
server.registerTool(
|
|
15
|
+
'rootr_send_document',
|
|
16
|
+
{
|
|
17
|
+
title: 'Send a Rootr node to email addresses',
|
|
18
|
+
description:
|
|
19
|
+
'Send a SNAPSHOT of any Rootr node (document, table, spreadsheet, whiteboard, deck, form, issues, log, CRM, ' +
|
|
20
|
+
'recording, page, signed document — anything but a folder) to 1–20 email addresses. Each recipient gets an ' +
|
|
21
|
+
'email with a link that opens the copy WITHOUT a Rootr account; if they have (or later create) a Rootr account ' +
|
|
22
|
+
'with that email, it also sits in their inbox (rootr_list_doc_mail). The copy is frozen at send time — later ' +
|
|
23
|
+
'edits or deletion of the original do not change it; use permissions/publishing for a live share instead. ' +
|
|
24
|
+
'Needs WRITE on the node (the same bar as sharing). What never goes out: the same things a public page hides ' +
|
|
25
|
+
'(person/attachment/relation columns, row documents, form responses, signer emails). ' +
|
|
26
|
+
'The result lists each recipient with `emailed` (false = the notification email failed; the copy still exists). ' +
|
|
27
|
+
'Only send to addresses the user asked for — this emails real people.',
|
|
28
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
29
|
+
inputSchema: {
|
|
30
|
+
node: z.string().describe(`보낼 노드 id${NODE_TARGET_HINT}`),
|
|
31
|
+
to: z.array(z.string()).min(1).max(20).describe('받는 사람 이메일 주소들(1~20개). 대소문자만 다른 주소는 같은 사람'),
|
|
32
|
+
message: z.string().max(2000).optional().describe('보내며 함께 적을 말(메일과 문서함에 보인다, 2000자까지)'),
|
|
33
|
+
workspace: z.string().optional().describe('경로로 줄 때 찾을 워크스페이스 id (생략하면 설정값)'),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async ({ node, to, message, workspace }) => {
|
|
37
|
+
try {
|
|
38
|
+
const client = makeClient(workspace);
|
|
39
|
+
return jsonResult(await client.sendNode(await resolveNodeId(client, node), { to, message }));
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return errorResult(err);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
server.registerTool(
|
|
47
|
+
'rootr_list_doc_mail',
|
|
48
|
+
{
|
|
49
|
+
title: 'List my received or sent Rootr documents',
|
|
50
|
+
description:
|
|
51
|
+
"List the caller's document mailbox: box=inbox (documents sent TO the caller's account email — default) or " +
|
|
52
|
+
'box=sent (documents the caller sent). Summaries only (title, type, sender, recipient, message, sentAt, ' +
|
|
53
|
+
'readAt, revoked); open one with rootr_read_doc_mail. `unread` is the number of unread received documents. ' +
|
|
54
|
+
'Page with `cursor` = the previous `nextCursor`. Belongs to the person (their email), not to a workspace.',
|
|
55
|
+
annotations: { readOnlyHint: true },
|
|
56
|
+
inputSchema: {
|
|
57
|
+
box: z.enum(['inbox', 'sent']).optional().describe('inbox=받은 문서(기본), sent=보낸 문서'),
|
|
58
|
+
limit: z.number().int().min(1).max(100).optional().describe('한 번에 몇 개(기본 30)'),
|
|
59
|
+
cursor: z.string().optional().describe('다음 쪽 — 앞 응답의 nextCursor'),
|
|
60
|
+
q: z.string().optional().describe('제목·보낸 사람·받는 사람으로 찾기'),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
async ({ box, limit, cursor, q }) => {
|
|
64
|
+
try {
|
|
65
|
+
return jsonResult(await makeClient().listDocMail({ box, limit, cursor, q }));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return errorResult(err);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
server.registerTool(
|
|
73
|
+
'rootr_read_doc_mail',
|
|
74
|
+
{
|
|
75
|
+
title: 'Read a received or sent Rootr document',
|
|
76
|
+
description:
|
|
77
|
+
'Open one document from the mailbox (id from rootr_list_doc_mail). Returns the summary plus `payload` — the ' +
|
|
78
|
+
'snapshot taken at send time, in the same shape a public page uses: `kind` says what it is (doc → `content` ' +
|
|
79
|
+
'markdown; database → `properties` + `rows`; spreadsheet → `data`; …). Opening a received one marks it read. ' +
|
|
80
|
+
'If the sender withdrew it, `revoked` is true and `payload` is null.',
|
|
81
|
+
annotations: { readOnlyHint: false },
|
|
82
|
+
inputSchema: {
|
|
83
|
+
id: z.string().describe('문서함 항목 id (rootr_list_doc_mail 의 items[].id)'),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
async ({ id }) => {
|
|
87
|
+
try {
|
|
88
|
+
return jsonResult(await makeClient().getDocMail(id));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return errorResult(err);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.registerTool(
|
|
96
|
+
'rootr_revoke_doc_mail',
|
|
97
|
+
{
|
|
98
|
+
title: 'Withdraw a sent Rootr document',
|
|
99
|
+
description:
|
|
100
|
+
'Withdraw a document the caller sent (id from rootr_list_doc_mail box=sent). From then on the recipient sees ' +
|
|
101
|
+
'only the title and "withdrawn" — no content — and the email link answers 410. Only the sender can do this; ' +
|
|
102
|
+
'it cannot be undone (send it again instead). Copies the recipient already saved elsewhere are out of reach.',
|
|
103
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
104
|
+
inputSchema: {
|
|
105
|
+
id: z.string().describe('보낸 문서함 항목 id'),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
async ({ id }) => {
|
|
109
|
+
try {
|
|
110
|
+
return jsonResult(await makeClient().revokeDocMail(id));
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return errorResult(err);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
package/lib/mcp.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerAttachmentTools } from './mcp-tools/attachments.js';
|
|
|
24
24
|
import { registerSignedDocTools } from './mcp-tools/signed.js';
|
|
25
25
|
import { registerFeedbackTools } from './mcp-tools/feedback.js';
|
|
26
26
|
import { registerMemoryTools } from './mcp-tools/memory.js';
|
|
27
|
+
import { registerDocMailTools } from './mcp-tools/doc-mail.js';
|
|
27
28
|
import { withRateLimitSink } from './context.js';
|
|
28
29
|
import { makeClient } from './mcp-tools/shared.js';
|
|
29
30
|
import { t as tr } from './i18n.js';
|
|
@@ -224,6 +225,10 @@ export function buildMcpServer({ name, version }) {
|
|
|
224
225
|
'rootr_request_signing_code first and ASK THE HUMAN for the 6 digits that arrive in their mail — ' +
|
|
225
226
|
'that code is the only evidence a person (not an agent) pressed sign; ' +
|
|
226
227
|
'FOLDER = grouping; LOG store = typed event table whose relation fields auto-build a lineage graph (metrics, incidents, pipeline events — use the rootr_*_log_* tools).\n\n' +
|
|
228
|
+
'Sending a document to someone by email (문서 보내기 · 문서함): rootr_send_document mails a frozen COPY of any ' +
|
|
229
|
+
'node (not a folder) to 1-20 addresses — no Rootr account needed to open it, and it lands in their inbox if ' +
|
|
230
|
+
'that email has an account; rootr_list_doc_mail (box inbox|sent) / rootr_read_doc_mail / rootr_revoke_doc_mail ' +
|
|
231
|
+
'cover the mailbox. Only send to addresses the user gave you.\n\n' +
|
|
227
232
|
'Scaffolding discipline — treat "set up a workspace for X" exactly like "build me this app": you ask a ' +
|
|
228
233
|
'few clarifying questions, write it, compile it, run the tests, leave a README, and report. The user ' +
|
|
229
234
|
'should only have to type that one sentence; everything else finishes inside the conversation. Never ask ' +
|
|
@@ -286,6 +291,7 @@ export function buildMcpServer({ name, version }) {
|
|
|
286
291
|
registerSignedDocTools(server);
|
|
287
292
|
registerFeedbackTools(server);
|
|
288
293
|
registerMemoryTools(server);
|
|
294
|
+
registerDocMailTools(server);
|
|
289
295
|
|
|
290
296
|
return server;
|
|
291
297
|
}
|