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/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
- body: { name, type, options, relationDatabaseId, reverseName, formula, rollup, prefix, searchExcluded, dimensions },
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
- body: { name, options, optionRenames, formula, rollup, prefix, reverseName, searchExcluded, dedupe, dedupeShow, relationFilter },
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
- // 노드 하나를 **보낼 때의 복사본**으로 이메일 1~20명에게 보낸다. 받은 문서는 받는 사람의
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
- /** 한 통 열기(본문 payload 포함). 받는 사람이 열면 읽음으로 바뀐다. */
2078
- async getDocMail(id) {
2079
- return this.request(`/doc-mail/${encodeURIComponent(id)}`);
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
+ }
@@ -50,12 +50,22 @@ const GOOGLE_SCHEMA = z.object({
50
50
  busyCalendarIds: z
51
51
  .array(z.string())
52
52
  .optional()
53
- .describe('Calendars whose busy times block bookings. Only free/busy is read — never event titles.'),
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()