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.
@@ -126,13 +126,20 @@ export function registerFormTools(server) {
126
126
  annotations: { readOnlyHint: true, destructiveHint: false },
127
127
  inputSchema: {
128
128
  formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
129
+ latestPerRespondent: z
130
+ .boolean()
131
+ .optional()
132
+ .describe(
133
+ 'Keep only the newest response per signed-in submitter (`submitterUserId`), dropping their earlier ' +
134
+ 'drafts. Anonymous share-link submissions have no submitter and are never merged. Default false.',
135
+ ),
129
136
  },
130
137
  },
131
- async ({ formId }) => {
138
+ async ({ formId, latestPerRespondent }) => {
132
139
  try {
133
140
  const client = makeClient();
134
141
  formId = await resolveNodeId(client, formId);
135
- const result = await client.listFormResponses(formId);
142
+ const result = await client.listFormResponses(formId, { latestPerRespondent });
136
143
  return jsonResult({ responses: result });
137
144
  } catch (err) {
138
145
  return errorResult(err);
@@ -166,6 +173,84 @@ export function registerFormTools(server) {
166
173
  },
167
174
  );
168
175
 
176
+ server.registerTool(
177
+ 'rootr_create_form_collector',
178
+ {
179
+ title: 'Create a table or spreadsheet that collects this form\'s responses',
180
+ description:
181
+ 'Create a destination for a Rootr (루터) FORM\'s responses and link it in one call. ' +
182
+ '`target: "database"` builds a DATABASE whose columns come straight from the form\'s questions ' +
183
+ '(same names, matching types, select options carried over) — this is the right way to answer ' +
184
+ '"collect the answers in a table", because a hand-built table only mirrors when every column name ' +
185
+ 'matches a question exactly. `target: "spreadsheet"` creates a SPREADSHEET whose sheet is bound to ' +
186
+ 'the form, so its rows ARE the responses. Both replace any previous destination — a form collects ' +
187
+ 'into one place. To link an already-built table instead, use rootr_update_form with targetDatabaseId.',
188
+ annotations: { readOnlyHint: false, destructiveHint: false },
189
+ inputSchema: {
190
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
191
+ target: z
192
+ .enum(['database', 'spreadsheet'])
193
+ .describe('database = typed columns/views (filters, boards); spreadsheet = one sheet of rows for formulas'),
194
+ },
195
+ },
196
+ async ({ formId, target }) => {
197
+ try {
198
+ const client = makeClient();
199
+ formId = await resolveNodeId(client, formId);
200
+ return jsonResult(await client.createFormCollector(formId, target));
201
+ } catch (err) {
202
+ return errorResult(err);
203
+ }
204
+ },
205
+ );
206
+
207
+ server.registerTool(
208
+ 'rootr_update_form_response',
209
+ {
210
+ title: 'Fix an already-submitted Rootr form response',
211
+ description:
212
+ 'Correct one response that was already submitted to a Rootr (루터) FORM. `values` REPLACES the whole ' +
213
+ 'answer set and is re-checked against the form\'s current fields (required/type rules), so send every ' +
214
+ 'answer you want to keep — call rootr_list_form_responses first. Use this instead of deleting and ' +
215
+ 're-submitting when you want the original submitter and submission time preserved.',
216
+ annotations: { readOnlyHint: false, destructiveHint: false },
217
+ inputSchema: {
218
+ responseId: z.string().describe('Response id from rootr_list_form_responses'),
219
+ values: z.record(z.any()).describe('Full replacement answers, keyed by field id'),
220
+ },
221
+ },
222
+ async ({ responseId, values }) => {
223
+ try {
224
+ const result = await makeClient().updateFormResponse(responseId, values);
225
+ return jsonResult(result);
226
+ } catch (err) {
227
+ return errorResult(err);
228
+ }
229
+ },
230
+ );
231
+
232
+ server.registerTool(
233
+ 'rootr_delete_form_response',
234
+ {
235
+ title: 'Delete one Rootr form response',
236
+ description:
237
+ 'Permanently delete a single response submitted to a Rootr (루터) FORM. This cannot be undone — the ' +
238
+ 'answers are gone. Use rootr_update_form_response to correct a response instead of deleting it.',
239
+ annotations: { readOnlyHint: false, destructiveHint: true },
240
+ inputSchema: {
241
+ responseId: z.string().describe('Response id from rootr_list_form_responses'),
242
+ },
243
+ },
244
+ async ({ responseId }) => {
245
+ try {
246
+ await makeClient().deleteFormResponse(responseId);
247
+ return jsonResult({ deleted: true, responseId });
248
+ } catch (err) {
249
+ return errorResult(err);
250
+ }
251
+ },
252
+ );
253
+
169
254
  server.registerTool(
170
255
  'rootr_create_form_share_link',
171
256
  {
@@ -31,6 +31,46 @@ export function registerMiscTools(server) {
31
31
  },
32
32
  );
33
33
 
34
+ server.registerTool(
35
+ 'rootr_update_node',
36
+ {
37
+ title: 'Rename / re-icon / move any node',
38
+ description:
39
+ 'Change a Rootr (루터) node\'s NAME, ICON, TAGS, or PARENT — works for every node type ' +
40
+ '(DOCUMENT, FOLDER, DATABASE, SPREADSHEET, FORM, …). Until now icons could only be set when a node ' +
41
+ 'was CREATED, so matching a migrated tree to its Notion icons was impossible without recreating ' +
42
+ 'everything. Only the fields you pass change. `parentId: null` moves the node to the workspace root; ' +
43
+ 'a node cannot be moved inside its own subtree.',
44
+ annotations: { readOnlyHint: false, destructiveHint: false },
45
+ inputSchema: {
46
+ target: z.string().describe('Node path (starting with "/") or node id'),
47
+ name: z.string().optional().describe('New name'),
48
+ icon: z.string().optional().describe('New emoji icon, e.g. "📋". Empty string clears it.'),
49
+ tags: z.array(z.string()).optional().describe('Replaces the tag list'),
50
+ parentId: z
51
+ .string()
52
+ .nullable()
53
+ .optional()
54
+ .describe('Move under this folder/document/database (id or path). null = workspace root.'),
55
+ },
56
+ },
57
+ async ({ target, name, icon, tags, parentId }) => {
58
+ try {
59
+ const client = makeClient();
60
+ const id = requireResolved(await resolveTarget(client, target));
61
+ // 부모도 경로로 줄 수 있어야 한다 — id를 미리 아는 쪽이 오히려 드물다.
62
+ let parent = parentId;
63
+ if (typeof parentId === 'string' && parentId) {
64
+ parent = requireResolved(await resolveTarget(client, parentId));
65
+ }
66
+ const node = await client.updateNode(id, { name, icon, tags, parentId: parent });
67
+ return jsonResult(node);
68
+ } catch (err) {
69
+ return errorResult(err);
70
+ }
71
+ },
72
+ );
73
+
34
74
  server.registerTool(
35
75
  'rootr_delete_node',
36
76
  {
@@ -14,7 +14,14 @@ import { parseRootrUrl } from '../resolve.js';
14
14
  * branch plus parseTree() keeps any client that still stringifies working.
15
15
  * Mirrors the typed-array pattern already used in logs.js.
16
16
  */
17
- const treeArg = (desc) => z.array(z.record(z.any())).or(z.string()).describe(desc);
17
+ const ACCESS_HINT =
18
+ ' Any node may also carry `visibility: "PRIVATE"` (only its creator, workspace admins, and people listed in ' +
19
+ '`access` can see it and everything under it) plus `access: [{ subjectId: "<userId>", level: "READ"|"WRITE"|' +
20
+ '"MANAGE" }]` — that pair is how "make a folder these three people can see" happens in one call instead of ' +
21
+ 'leaving the locking to the user afterwards. A DATABASE node may also carry `database.permissions` (칸/행 ' +
22
+ 'lock) and `database.notifications` (who to notify on row changes).';
23
+
24
+ const treeArg = (desc) => z.array(z.record(z.any())).or(z.string()).describe(desc + ACCESS_HINT);
18
25
 
19
26
  /** Accept either a real array (typed clients) or a JSON string (clients that stringify structured args). */
20
27
  function parseTree(tree) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Official Rootr CLI + MCP server — read, write, edit and search Rootr documents from the shell or from an AI agent.",
5
5
  "type": "module",
6
6
  "bin": {