rootr-cli 0.24.0 → 0.26.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 +48 -16
- package/README.md +54 -14
- package/bin/rootr.js +426 -34
- package/lib/client.js +118 -17
- package/lib/context.js +21 -0
- package/lib/mcp-tools/booking.js +11 -1
- package/lib/mcp-tools/databases.js +316 -8
- package/lib/mcp-tools/doc-mail.js +149 -27
- package/lib/mcp-tools/pages.js +23 -5
- package/lib/mcp-tools/public.js +8 -1
- package/lib/mcp-tools/workspaces.js +9 -1
- package/lib/mcp.js +43 -5
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* can render consistent, helpful messages.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { currentRateLimitSink } from './context.js';
|
|
9
|
+
import { currentDocMailSink, currentRateLimitSink } from './context.js';
|
|
10
10
|
import { t as tr } from './i18n.js';
|
|
11
11
|
|
|
12
12
|
/** Copy the API's X-RateLimit-* headers into the ambient per-call sink, if any. */
|
|
@@ -23,6 +23,25 @@ function captureRateLimit(res) {
|
|
|
23
23
|
sink.upgradeUrl = res.headers.get('x-ratelimit-upgrade-url') || undefined;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** DOC-MAIL-NOTIFY — 응답의 안 읽은 받은 문서 머리줄을 그릇에 모은다(그릇이 없으면 아무 일도 안 한다). */
|
|
27
|
+
function captureDocMail(res, sink) {
|
|
28
|
+
if (!sink) return;
|
|
29
|
+
const unread = Number(res.headers.get('x-rootr-doc-mail-unread'));
|
|
30
|
+
if (Number.isFinite(unread) && res.headers.get('x-rootr-doc-mail-unread') !== null) sink.unread = unread;
|
|
31
|
+
const raw = res.headers.get('x-rootr-doc-mail-new-items');
|
|
32
|
+
if (!raw) return;
|
|
33
|
+
try {
|
|
34
|
+
const items = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
|
|
35
|
+
sink.items = sink.items || [];
|
|
36
|
+
for (const it of Array.isArray(items) ? items : []) {
|
|
37
|
+
if (it && it.id && !sink.items.some((x) => x.id === it.id)) sink.items.push(it);
|
|
38
|
+
}
|
|
39
|
+
sink.newCount = Math.max(sink.newCount || 0, Number(res.headers.get('x-rootr-doc-mail-new')) || 0);
|
|
40
|
+
} catch {
|
|
41
|
+
/* 알림만 빠진다 */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
export class RootrApiError extends Error {
|
|
27
46
|
constructor(message, { status, body, hint } = {}) {
|
|
28
47
|
super(message);
|
|
@@ -154,6 +173,8 @@ export class RootrClient {
|
|
|
154
173
|
...this.defaultHeaders,
|
|
155
174
|
...(opts.headers || {}),
|
|
156
175
|
};
|
|
176
|
+
const docMailSink = currentDocMailSink();
|
|
177
|
+
if (docMailSink?.channel) headers['X-Rootr-Doc-Mail-Channel'] = docMailSink.channel;
|
|
157
178
|
|
|
158
179
|
let body = opts.body;
|
|
159
180
|
if (body !== undefined && typeof body !== 'string' && !(body instanceof Uint8Array)) {
|
|
@@ -173,6 +194,7 @@ export class RootrClient {
|
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
captureRateLimit(res);
|
|
197
|
+
if (res.ok) captureDocMail(res, docMailSink);
|
|
176
198
|
|
|
177
199
|
if (opts.returnResponse) {
|
|
178
200
|
if (!res.ok) {
|
|
@@ -1049,11 +1071,11 @@ export class RootrClient {
|
|
|
1049
1071
|
*/
|
|
1050
1072
|
async createDatabaseView(
|
|
1051
1073
|
databaseId,
|
|
1052
|
-
{ name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale } = {},
|
|
1074
|
+
{ name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns } = {},
|
|
1053
1075
|
) {
|
|
1054
1076
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/views`, {
|
|
1055
1077
|
method: 'POST',
|
|
1056
|
-
body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale },
|
|
1078
|
+
body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths, calendarScale, density, frozenColumns },
|
|
1057
1079
|
});
|
|
1058
1080
|
}
|
|
1059
1081
|
|
|
@@ -1220,31 +1242,78 @@ export class RootrClient {
|
|
|
1220
1242
|
|
|
1221
1243
|
async addColumn(
|
|
1222
1244
|
databaseId,
|
|
1223
|
-
{ name, type, options, relationDatabaseId, reverseName, formula, rollup, prefix, searchExcluded, dimensions } = {},
|
|
1245
|
+
{ name, type, options, relationDatabaseId, reverseName, formula, rollup, prefix, searchExcluded, dimensions, align, wrap, highlightOverdue, allowTime, allowRange } = {},
|
|
1224
1246
|
) {
|
|
1225
1247
|
return this.request(`/databases/${encodeURIComponent(databaseId)}/columns`, {
|
|
1226
1248
|
method: 'POST',
|
|
1227
1249
|
// 계산 칸(수식·롤업·번호)은 설정이 곧 그 칸이라 같이 보내야 한다.
|
|
1228
1250
|
// reverseName 은 상대 표에 짝 칸을 만든다(양방향 관계).
|
|
1229
1251
|
// searchExcluded 는 "검색 색인에서 제외"(표 관리자만).
|
|
1230
|
-
|
|
1252
|
+
// align·wrap·highlightOverdue·allowTime·allowRange 는 칸 표시 설정(정렬·줄바꿈·지난 날짜 빨강·시간·기간).
|
|
1253
|
+
body: { name, type, options, relationDatabaseId, reverseName, formula, rollup, prefix, searchExcluded, dimensions, align, wrap, highlightOverdue, allowTime, allowRange },
|
|
1231
1254
|
});
|
|
1232
1255
|
}
|
|
1233
1256
|
|
|
1234
1257
|
async updateColumn(
|
|
1235
1258
|
databaseId,
|
|
1236
1259
|
propertyId,
|
|
1237
|
-
{ name, options, optionRenames, formula, rollup, prefix, reverseName, searchExcluded, dedupe, dedupeShow, relationFilter } = {},
|
|
1260
|
+
{ name, options, optionRenames, formula, rollup, prefix, reverseName, searchExcluded, dedupe, dedupeShow, relationFilter, align, wrap, highlightOverdue, allowTime, allowRange, dedupeCarry, validate, identifyProps } = {},
|
|
1238
1261
|
) {
|
|
1239
1262
|
return this.request(
|
|
1240
1263
|
`/databases/${encodeURIComponent(databaseId)}/columns/${encodeURIComponent(propertyId)}`,
|
|
1241
1264
|
{
|
|
1242
1265
|
method: 'PATCH',
|
|
1243
|
-
|
|
1266
|
+
// identifyProps — 제목 칸만: 다른 표에서 이 표의 행을 고를 때 제목 옆에 붙일 칸(REL-PICK-IDENTIFY).
|
|
1267
|
+
body: { name, options, optionRenames, formula, rollup, prefix, reverseName, searchExcluded, dedupe, dedupeShow, relationFilter, align, wrap, highlightOverdue, allowTime, allowRange, dedupeCarry, validate, identifyProps },
|
|
1244
1268
|
},
|
|
1245
1269
|
);
|
|
1246
1270
|
}
|
|
1247
1271
|
|
|
1272
|
+
/** DEDUPE-CARRY(P1) — 같은 사람의 이전 행 값을 `rowId` 행으로 가져온다(`from` 없으면 같은 값의 가장 최근 행). 이전 행은 그대로. */
|
|
1273
|
+
async carryOver(databaseId, rowId, from) {
|
|
1274
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/${encodeURIComponent(rowId)}/carry-over`, {
|
|
1275
|
+
method: 'POST',
|
|
1276
|
+
body: from ? { from } : {},
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** VALUE-RULE(P2) — 지금 들어 있는 값 가운데 칸 검사에 안 맞는 것(고치지 않는다). */
|
|
1281
|
+
async ruleViolations(databaseId, { limit } = {}) {
|
|
1282
|
+
const q = limit ? `?limit=${encodeURIComponent(limit)}` : '';
|
|
1283
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/rule-violations${q}`);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** VALUE-RULE — 미리 채운 예시 꼴(제품이 정한 규칙이 아니라 고쳐 쓸 출발점). */
|
|
1287
|
+
async valueRulePresets() {
|
|
1288
|
+
return this.request('/value-rules/presets');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** VALUE-RULE — 규칙을 걸기 전에 시험 값으로 돌려 본다(아무것도 저장하지 않는다). */
|
|
1292
|
+
async testValueRule(rule, values, type) {
|
|
1293
|
+
return this.request('/value-rules/test', { method: 'POST', body: { rule, values, ...(type ? { type } : {}) } });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* COL-RENAME — 이 칸을 `name` 으로 바꾸면 함께 바뀌는 것(수식·버튼/자동 규칙 치환·다른 표 롤업·이어진 폼 질문·페이지 블록)과
|
|
1298
|
+
* 따라갈 수 없는 것(warnings). 아무것도 쓰지 않는다. 실제로 바꾸면 updateColumn 응답의 `renameCascade` 에 같은 모양으로 온다.
|
|
1299
|
+
*/
|
|
1300
|
+
async columnRenameImpact(databaseId, propertyId, name) {
|
|
1301
|
+
return this.request(
|
|
1302
|
+
`/databases/${encodeURIComponent(databaseId)}/columns/${encodeURIComponent(propertyId)}/rename-impact?name=${encodeURIComponent(name)}`,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* REL-PICK — 관계 칸에 이을 행을 표 전체에서 제목(과 구분 칸)으로 찾는다. `{ rows: [{ id, title, subtitle }], total, hasMore }`.
|
|
1308
|
+
* subtitle 은 그 표 제목 칸의 identifyProps(예: 연락처 끝 4자리 · 지원일) — 같은 이름을 가른다.
|
|
1309
|
+
*/
|
|
1310
|
+
async pickRows(databaseId, { q, filters, ids, limit } = {}) {
|
|
1311
|
+
return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/pick`, {
|
|
1312
|
+
method: 'POST',
|
|
1313
|
+
body: { q, filters, ids, limit },
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1248
1317
|
/**
|
|
1249
1318
|
* DEDUPE(F5) — 중복 감지 칸 값이 같은 기존 행. `rowId` 를 주면 그 행의 지금 값으로(그 행은 빠진다), 아니면 `values`
|
|
1250
1319
|
* ({칸 id 또는 이름: 값})로 찾는다. 기존 행마다 보일 칸(preview)과 이어진 행 최근 5건(history)이 함께 온다.
|
|
@@ -2051,15 +2120,16 @@ export class RootrClient {
|
|
|
2051
2120
|
|
|
2052
2121
|
// ---- Doc mail (DOC-MAIL — 문서 송수신) ---------------------------------
|
|
2053
2122
|
//
|
|
2054
|
-
// 노드 하나를
|
|
2055
|
-
//
|
|
2056
|
-
//
|
|
2123
|
+
// 노드 하나를 이메일 1~20명에게 **보낸다 = 원본을 그 이메일에 열어 준다**(복사본이 아니다 — DOC-MAIL-LIVE).
|
|
2124
|
+
// 받는 사람은 원본을 실시간으로 보고, 받은 권한(read 보기·comment 댓글·write 편집)만큼 쓴다. 계정이 없으면 메일 링크에서
|
|
2125
|
+
// 받은 이메일을 넣어 읽기 전용으로 열고, 그 이메일로 가입하면 문서함에서 원본이 열린다. 문서함은 사람(이메일)에 붙으므로
|
|
2126
|
+
// 워크스페이스 주소 밑이 아니다.
|
|
2057
2127
|
|
|
2058
|
-
/** 보내기 — WRITE 이상. 응답 `{batchId, items:[{id,to,emailed}]}`. */
|
|
2059
|
-
async sendNode(nodeId, { to, message } = {}) {
|
|
2128
|
+
/** 보내기 — 구성원 규칙으로 WRITE 이상. access: read|comment|write(기본 read). 응답 `{batchId, access, items:[{id,to,emailed,resent}]}`. */
|
|
2129
|
+
async sendNode(nodeId, { to, message, access } = {}) {
|
|
2060
2130
|
return this.request(`/nodes/${encodeURIComponent(nodeId)}/send`, {
|
|
2061
2131
|
method: 'POST',
|
|
2062
|
-
body: { to, ...(message ? { message } : {}) },
|
|
2132
|
+
body: { to, ...(message ? { message } : {}), ...(access ? { access } : {}) },
|
|
2063
2133
|
});
|
|
2064
2134
|
}
|
|
2065
2135
|
|
|
@@ -2074,16 +2144,47 @@ export class RootrClient {
|
|
|
2074
2144
|
return this.request(`/doc-mail${s ? `?${s}` : ''}`);
|
|
2075
2145
|
}
|
|
2076
2146
|
|
|
2077
|
-
/**
|
|
2078
|
-
|
|
2079
|
-
|
|
2147
|
+
/**
|
|
2148
|
+
* 한 통 열기 — 원본의 **지금** 내용(읽기 전용: 문서면 contentMd, 그 밖은 payload)·받은 권한(access)·원본 주소(openUrl).
|
|
2149
|
+
* 폴더를 받았으면 children 이 오고, node 로 그 아래 것을 골라 읽는다. 받는 사람이 열면 읽음으로 바뀐다.
|
|
2150
|
+
*/
|
|
2151
|
+
async getDocMail(id, { node } = {}) {
|
|
2152
|
+
return this.request(`/doc-mail/${encodeURIComponent(id)}${node ? `?node=${encodeURIComponent(node)}` : ''}`);
|
|
2080
2153
|
}
|
|
2081
2154
|
|
|
2082
|
-
/** 회수 — 보낸 사람만.
|
|
2155
|
+
/** 회수 — 보낸 사람만. 받는 사람의 원본 접근이 바로 닫힌다. */
|
|
2083
2156
|
async revokeDocMail(id) {
|
|
2084
2157
|
return this.request(`/doc-mail/${encodeURIComponent(id)}/revoke`, { method: 'POST', body: {} });
|
|
2085
2158
|
}
|
|
2086
2159
|
|
|
2160
|
+
// ---- Doc mail webhooks (DOC-MAIL-HOOK — 사람 단위) ---------------------
|
|
2161
|
+
/** 내 받은 문서 웹훅 만들기 — events: doc_mail.received|doc_mail.revoked|doc_mail.read(안 주면 셋 다). 비밀값은 이 응답에만. */
|
|
2162
|
+
async addDocMailWebhook({ url, events, name } = {}) {
|
|
2163
|
+
return this.request('/doc-mail-webhooks', { method: 'POST', body: { url, ...(events?.length ? { events } : {}), ...(name ? { name } : {}) } });
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
async listDocMailWebhooks() {
|
|
2167
|
+
return this.request('/doc-mail-webhooks');
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
async removeDocMailWebhook(id) {
|
|
2171
|
+
return this.request(`/doc-mail-webhooks/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/** 시험 전송 한 통(doc_mail.test). 결과는 docMailWebhookDeliveries(id). */
|
|
2175
|
+
async testDocMailWebhook(id) {
|
|
2176
|
+
return this.request(`/doc-mail-webhooks/${encodeURIComponent(id)}/test`, { method: 'POST', body: {} });
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
async docMailWebhookDeliveries(id) {
|
|
2180
|
+
return this.request(`/doc-mail-webhooks/${encodeURIComponent(id)}/deliveries`);
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/** 받은 사람의 권한 바꾸기 — 보낸 사람만, 내 수준까지. */
|
|
2184
|
+
async setDocMailAccess(id, access) {
|
|
2185
|
+
return this.request(`/doc-mail/${encodeURIComponent(id)}`, { method: 'PATCH', body: { access } });
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2087
2188
|
/** 내 문서함에서만 지우기(상대 화면은 그대로). box 로 나에게 보낸 것의 한쪽만 고를 수 있다. */
|
|
2088
2189
|
async hideDocMail(id, { box } = {}) {
|
|
2089
2190
|
return this.request(`/doc-mail/${encodeURIComponent(id)}${box ? `?box=${encodeURIComponent(box)}` : ''}`, {
|
package/lib/context.js
CHANGED
|
@@ -36,3 +36,24 @@ export function withRateLimitSink(sink, fn) {
|
|
|
36
36
|
export function currentRateLimitSink() {
|
|
37
37
|
return rateLimitStorage.getStore();
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* DOC-MAIL-NOTIFY — 안 읽은 받은 문서 알림을 모으는 그릇. `RootrClient.request` 가 그릇이 있으면 요청에
|
|
42
|
+
* `X-Rootr-Doc-Mail-Channel: <channel>` 을 붙이고, 응답의 `X-Rootr-Doc-Mail-Unread/New/New-Items` 를 여기 모은다(서버가
|
|
43
|
+
* 그 채널에 한 번만 알린다). MCP 는 도구 호출마다 async-local 그릇(channel 'mcp'), CLI 명령은 프로세스 전체 그릇(channel 'cli').
|
|
44
|
+
* 원격 MCP(API 프로세스 안)는 async-local 만 쓴다 — 전역 그릇은 CLI 실행 파일만 놓는다(여러 사람이 한 프로세스를 나눠 쓴다).
|
|
45
|
+
*/
|
|
46
|
+
const docMailStorage = new AsyncLocalStorage();
|
|
47
|
+
let globalDocMailSink = null;
|
|
48
|
+
|
|
49
|
+
export function withDocMailSink(sink, fn) {
|
|
50
|
+
return docMailStorage.run(sink, fn);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setGlobalDocMailSink(sink) {
|
|
54
|
+
globalDocMailSink = sink;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function currentDocMailSink() {
|
|
58
|
+
return docMailStorage.getStore() ?? globalDocMailSink;
|
|
59
|
+
}
|
package/lib/mcp-tools/booking.js
CHANGED
|
@@ -50,12 +50,22 @@ const GOOGLE_SCHEMA = z.object({
|
|
|
50
50
|
busyCalendarIds: z
|
|
51
51
|
.array(z.string())
|
|
52
52
|
.optional()
|
|
53
|
-
.describe(
|
|
53
|
+
.describe(
|
|
54
|
+
'Calendars whose busy times block bookings. Only busy times and the dates of all-day events are read — never titles or guests.',
|
|
55
|
+
),
|
|
54
56
|
writeCalendarId: z
|
|
55
57
|
.string()
|
|
56
58
|
.nullable()
|
|
57
59
|
.optional()
|
|
58
60
|
.describe('Calendar that receives confirmed bookings. null = do not write anything to Google.'),
|
|
61
|
+
allDayBusy: z
|
|
62
|
+
.boolean()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe(
|
|
65
|
+
'All-day events block the whole day (default true). Google marks all-day events "free" by default, so busy times alone miss ' +
|
|
66
|
+
'vacations, trips and holidays; a public holiday calendar in busyCalendarIds then closes holidays. Working-location, birthday ' +
|
|
67
|
+
'and declined events never block. false = only busy times block.',
|
|
68
|
+
),
|
|
59
69
|
meet: z
|
|
60
70
|
.boolean()
|
|
61
71
|
.optional()
|