rootr-cli 0.9.0 → 0.13.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.
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- import { jsonResult, errorResult, makeClient } from './shared.js';
3
+ import { jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
4
4
  import { resolveTarget, requireResolved } from '../resolve.js';
5
5
 
6
6
  /** Miscellaneous document/node tools: version history, trash. */
@@ -95,4 +95,87 @@ export function registerMiscTools(server) {
95
95
  }
96
96
  },
97
97
  );
98
+
99
+ // ── READ-ACK — 읽고 확인해야 하는 문서 ─────────────────────────────────
100
+ // 규정·공지처럼 "읽었다"를 남겨야 하는 것. 표·폴더에 걸면 그 안의 문서 전부에
101
+ // 걸리고, 얼마나 읽었는지 완료율로 본다.
102
+
103
+ server.registerTool(
104
+ 'rootr_read_requirement',
105
+ {
106
+ title: 'Read the read-acknowledgement status',
107
+ description:
108
+ 'Read the READ-ACK state of a Rootr (루터) node — who must read it, the due date, who has read or ' +
109
+ 'acknowledged, and (for a DATABASE or FOLDER) how far through the documents inside each person is. ' +
110
+ 'Use it to answer "누가 아직 안 읽었나" for a 규정/공지/교육자료. If the node itself has no requirement ' +
111
+ 'but an ancestor does, `ancestorRequirementId` names the node that carries it.',
112
+ annotations: { readOnlyHint: true, destructiveHint: false },
113
+ inputSchema: {
114
+ nodeId: z.string().describe(`Node id${NODE_TARGET_HINT}`),
115
+ },
116
+ },
117
+ async ({ nodeId }) => {
118
+ try {
119
+ const client = makeClient();
120
+ return jsonResult(await client.readRequirement(await resolveNodeId(client, nodeId)));
121
+ } catch (err) {
122
+ return errorResult(err);
123
+ }
124
+ },
125
+ );
126
+
127
+ server.registerTool(
128
+ 'rootr_set_read_requirement',
129
+ {
130
+ title: 'Require people to read a document',
131
+ description:
132
+ 'Make a Rootr (루터) node REQUIRED READING for the given people. Put it on a DOCUMENT for one notice, ' +
133
+ 'or on a DATABASE/FOLDER to require everything inside (완료율 rolls up per person). `dueAt` is an ISO ' +
134
+ 'date or null for no deadline; `requireExplicitAck: true` means opening it is not enough — the person ' +
135
+ 'must press 확인. Replaces the whole roster, so send the full `userIds` list. NOTE: this records a real ' +
136
+ 'person as the setter, so it needs a key that acts as a user (a PAT); a workspace connector key with no ' +
137
+ 'user attached is refused.',
138
+ annotations: { readOnlyHint: false, destructiveHint: false },
139
+ inputSchema: {
140
+ nodeId: z.string().describe(`Node id${NODE_TARGET_HINT}`),
141
+ userIds: z.array(z.string()).describe('Everyone who must read it (replaces the current roster)'),
142
+ dueAt: z.string().nullable().optional().describe('ISO date, or null for no deadline'),
143
+ requireExplicitAck: z
144
+ .boolean()
145
+ .optional()
146
+ .describe('true = opening is not enough, the person must press 확인'),
147
+ },
148
+ },
149
+ async ({ nodeId, userIds, dueAt, requireExplicitAck }) => {
150
+ try {
151
+ const client = makeClient();
152
+ const id = await resolveNodeId(client, nodeId);
153
+ return jsonResult(await client.setReadRequirement(id, { userIds, dueAt, requireExplicitAck }));
154
+ } catch (err) {
155
+ return errorResult(err);
156
+ }
157
+ },
158
+ );
159
+
160
+ server.registerTool(
161
+ 'rootr_clear_read_requirement',
162
+ {
163
+ title: 'Stop requiring a document to be read',
164
+ description:
165
+ 'Remove the READ-ACK requirement from a Rootr (루터) node. The record of who already read it is not ' +
166
+ 'shown any more; the node itself is untouched.',
167
+ annotations: { readOnlyHint: false, destructiveHint: true },
168
+ inputSchema: {
169
+ nodeId: z.string().describe(`Node id${NODE_TARGET_HINT}`),
170
+ },
171
+ },
172
+ async ({ nodeId }) => {
173
+ try {
174
+ const client = makeClient();
175
+ return jsonResult(await client.clearReadRequirement(await resolveNodeId(client, nodeId)));
176
+ } catch (err) {
177
+ return errorResult(err);
178
+ }
179
+ },
180
+ );
98
181
  }
@@ -268,7 +268,11 @@ export function registerPresentationTools(server) {
268
268
  try {
269
269
  const client = makeClient();
270
270
  presentationId = await resolveNodeId(client, presentationId);
271
- const result = await client.updatePresentationSlide(presentationId, slideId, slide);
271
+ // 블록 id 는 이 도구도 채워 준다 — 스키마가 "생략하면 자동 생성"이라고
272
+ // 적어 두고서 이 길로만 400(blocks[].id is required)을 내던 것을 고친다
273
+ // (2026-08-29 실측). 슬라이드 id 는 서버가 어차피 slideId 로 못 박는다.
274
+ const patched = ensureSlideIds([{ ...slide, id: slideId }])[0];
275
+ const result = await client.updatePresentationSlide(presentationId, slideId, patched);
272
276
  return jsonResult(result);
273
277
  } catch (err) {
274
278
  return errorResult(err);
@@ -0,0 +1,148 @@
1
+ import { z } from 'zod';
2
+
3
+ import { jsonResult, errorResult, makeClient, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
4
+
5
+ /**
6
+ * PUBLIC-DB-DESIGN — 공개한 표를 **표 말고 다른 꼴로도** 보여준다
7
+ * (창업자 지시 2026-08-28: "꼭 표 형태일 필욘 없잖아").
8
+ * 값 목록은 서버(`public-nodes.dto.ts`)와 **같아야** 한다.
9
+ */
10
+ const DB_DESIGN_SCHEMA = z.object({
11
+ layout: z
12
+ .enum(['table', 'cards', 'gallery', 'list', 'board', 'timeline', 'calendar', 'chart', 'ranking'])
13
+ .optional(),
14
+ skin: z.enum(['clean', 'paper', 'dark', 'vivid', 'bento', 'brutal']).optional(),
15
+ });
16
+
17
+ /**
18
+ * PUBLIC-ANY — **어떤 타입이든 공개 발행한다**(창업자 지시 2026-08-27).
19
+ *
20
+ * 문서·덱만 되던 것을 표·시트·화이트보드·폼·이슈·로그·CRM·녹음·서명문서까지 열었다.
21
+ * 문서는 이 도구로 불러도 예전 발행 경로로 넘어가므로, **무엇을 공개하든 여기 하나만
22
+ * 쓰면 된다** — 타입마다 다른 도구를 찾을 필요가 없다.
23
+ */
24
+ export function registerPublicTools(server) {
25
+ server.registerTool(
26
+ 'rootr_publish_node',
27
+ {
28
+ title: 'Publish anything to a public URL',
29
+ description:
30
+ 'Publish ANY Rootr (루터) node at a public read-only URL (rootr.io/p/{slug}) — a DATABASE (visitors get a ' +
31
+ 'searchable table and can clone it into their own workspace), SPREADSHEET, WHITEBOARD, FORM (questions only), ' +
32
+ 'ISSUE_TRACKER, LOG, CRM, RECORDING (transcript only), SIGNED or DOCUMENT. Use this instead of ' +
33
+ 'rootr_publish_document unless you specifically want the document-only extras (skins, edit proposals). ' +
34
+ 'FOLDER cannot be published. ' +
35
+ 'Set `includeChildren: true` to publish everything underneath as well — the WHOLE subtree, grandchildren ' +
36
+ 'included, each getting its own URL and listed on the parent page. Leave it off and only this one node goes ' +
37
+ 'out (its sub-page names do not even appear). Turning it back off, closing the parent, moving a child out, ' +
38
+ 'or deleting the parent all close the pages that went out because of it — pages the owner published on their ' +
39
+ 'own are left alone. ' +
40
+ 'What is deliberately NOT exposed: a table\'s person/attachment/relation columns, the documents attached to ' +
41
+ 'its rows, form responses, recording audio, CRM contacts and deal amounts, and signer names on a signed ' +
42
+ 'document. Rows/entries are capped at 500 per page and the page says so when it truncates. ' +
43
+ 'Owner-level action (MANAGE): use an ACCOUNT key (PAT), not a workspace-scoped/connector token. ' +
44
+ 'Safe to call again — the slug never changes, so links already shared stay valid.',
45
+ annotations: { readOnlyHint: false, destructiveHint: false },
46
+ inputSchema: {
47
+ target: z.string().describe(`Node id${NODE_TARGET_HINT}`),
48
+ includeChildren: z
49
+ .boolean()
50
+ .optional()
51
+ .describe('Also publish everything underneath (whole subtree). Default false.'),
52
+ view: z
53
+ .string()
54
+ .optional()
55
+ .describe('DATABASE only: which saved view the public page opens with (view id).'),
56
+ design: DB_DESIGN_SCHEMA.optional().describe(
57
+ 'DATABASE only: how the public page presents the table. `layout` is one of ' +
58
+ 'table, cards, gallery, list, board, timeline, calendar, chart, ranking; `skin` is one of ' +
59
+ 'clean, paper, dark, vivid, bento, brutal. Only the keys you send change — setting a skin ' +
60
+ 'leaves the layout alone. Layouts that need a column (calendar/timeline need a date, board a ' +
61
+ 'select/status, ranking a number, chart either) fall back to the table and say why.',
62
+ ),
63
+ },
64
+ },
65
+ async ({ target, includeChildren, view, design }) => {
66
+ try {
67
+ const client = makeClient();
68
+ const id = await resolveNodeId(client, target);
69
+ return jsonResult(await client.publishNode(id, { includeChildren, view, design }));
70
+ } catch (err) {
71
+ return errorResult(err);
72
+ }
73
+ },
74
+ );
75
+
76
+ server.registerTool(
77
+ 'rootr_set_node_public',
78
+ {
79
+ title: 'Update public settings of anything published',
80
+ description:
81
+ 'Partially update a published Rootr (루터) node\'s public settings — only the fields you pass change, and ' +
82
+ 'the slug is never touched, so shared links stay valid. ' +
83
+ '`{"isPublic": false}` closes it (and closes whatever went out under it); ' +
84
+ '`{"includeChildren": false}` closes just the sub-pages; ' +
85
+ '`{"allowClone": false}` hides the "clone this" button AND refuses the clone API; ' +
86
+ '`{"password": "..."}` locks the page (the content is not sent at all until the password is entered — it is ' +
87
+ 'not hidden in the browser), `{"password": null}` unlocks; ' +
88
+ '`{"expiresAt": "2026-12-31T00:00:00Z"}` auto-closes on that date, `null` clears it; ' +
89
+ '`{"noIndex": true}` asks search engines to stay away; ' +
90
+ '`{"design": {"layout": "board", "skin": "paper"}}` changes how a published TABLE looks — it does not ' +
91
+ 'have to be a table (cards, gallery, list, board, timeline, calendar, chart, ranking). ' +
92
+ 'For DOCUMENT nodes use rootr_set_document_public instead — documents keep their own settings (skins, ' +
93
+ 'edit proposals) on the older path. Owner-level action (MANAGE): use an account key (PAT).',
94
+ annotations: { readOnlyHint: false, destructiveHint: false },
95
+ inputSchema: {
96
+ target: z.string().describe(`Node id${NODE_TARGET_HINT}`),
97
+ isPublic: z.boolean().optional(),
98
+ includeChildren: z.boolean().optional(),
99
+ allowClone: z.boolean().optional(),
100
+ noIndex: z.boolean().optional(),
101
+ expiresAt: z.string().nullable().optional().describe('ISO date, or null to clear'),
102
+ password: z.string().nullable().optional().describe('null or "" removes the lock'),
103
+ view: z.string().optional().describe('DATABASE only: view id the public page opens with'),
104
+ design: DB_DESIGN_SCHEMA.optional().describe(
105
+ 'DATABASE only: how the public page presents the table. `layout` — table, cards, gallery, list, ' +
106
+ 'board, timeline, calendar, chart, ranking. `skin` — clean, paper, dark, vivid, bento, brutal. ' +
107
+ 'Only the keys you send change.',
108
+ ),
109
+ },
110
+ },
111
+ async ({ target, ...settings }) => {
112
+ try {
113
+ const client = makeClient();
114
+ const id = await resolveNodeId(client, target);
115
+ const body = Object.fromEntries(Object.entries(settings).filter(([, v]) => v !== undefined));
116
+ return jsonResult(await client.setNodePublic(id, body));
117
+ } catch (err) {
118
+ return errorResult(err);
119
+ }
120
+ },
121
+ );
122
+
123
+ server.registerTool(
124
+ 'rootr_node_public_status',
125
+ {
126
+ title: 'Is this published, and at what URL',
127
+ description:
128
+ 'Check whether a Rootr (루터) node is published and how: its public slug (rootr.io/p/{slug}), whether it is ' +
129
+ 'currently open, when it expires, whether a password is set (the password itself is never returned), whether ' +
130
+ 'cloning is allowed, and whether the pages underneath went out with it. ' +
131
+ '`publishable: false` means this type cannot be published at all (FOLDER). ' +
132
+ 'Call this before publishing so you do not tell the user a page is new when it already had a URL.',
133
+ annotations: { readOnlyHint: true, destructiveHint: false },
134
+ inputSchema: {
135
+ target: z.string().describe(`Node id${NODE_TARGET_HINT}`),
136
+ },
137
+ },
138
+ async ({ target }) => {
139
+ try {
140
+ const client = makeClient();
141
+ const id = await resolveNodeId(client, target);
142
+ return jsonResult(await client.nodePublicStatus(id));
143
+ } catch (err) {
144
+ return errorResult(err);
145
+ }
146
+ },
147
+ );
148
+ }