rootr-cli 0.2.0 → 0.4.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.
@@ -38,6 +38,7 @@ export function registerDatabaseTools(server) {
38
38
  'Read a Rootr (루터) DATABASE node — its schema (properties with ids/types and select options) plus all rows ' +
39
39
  '(each with its rowId and values). ALWAYS call this before rootr_add_row / rootr_update_row so you know the exact ' +
40
40
  'property ids/names and, for board/kanban views, which select column holds the status. Only works on DATABASE nodes.',
41
+ annotations: { readOnlyHint: true, destructiveHint: false },
41
42
  inputSchema: {
42
43
  databaseId: z.string().describe('DATABASE node id'),
43
44
  },
@@ -63,6 +64,7 @@ export function registerDatabaseTools(server) {
63
64
  description:
64
65
  'List the rows of a Rootr (루터) DATABASE (id + values + position), without the schema. ' +
65
66
  'Use rootr_read_database instead when you also need the property/column definitions.',
67
+ annotations: { readOnlyHint: true, destructiveHint: false },
66
68
  inputSchema: {
67
69
  databaseId: z.string().describe('DATABASE node id'),
68
70
  },
@@ -87,6 +89,7 @@ export function registerDatabaseTools(server) {
87
89
  'Value shapes by type: title/text/url/select/person → string; number → number; multi_select → string[]; ' +
88
90
  'checkbox → boolean; date → { start: "YYYY-MM-DD", end? }. A select value is the OPTION NAME. ' +
89
91
  'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index.',
92
+ annotations: { readOnlyHint: false, destructiveHint: false },
90
93
  inputSchema: {
91
94
  databaseId: z.string().describe('DATABASE node id'),
92
95
  values: z.record(z.unknown()).describe('{ propertyNameOrId: value } for the new row'),
@@ -115,6 +118,7 @@ export function registerDatabaseTools(server) {
115
118
  '`values` maps property NAME (or id) → new value; only the passed keys change, `null` clears a key, omitted keys ' +
116
119
  'are left unchanged. To MOVE a kanban card to another column, set its select/status property to the new option name. ' +
117
120
  'Optionally set `position` to reorder the row.',
121
+ annotations: { readOnlyHint: false, destructiveHint: false },
118
122
  inputSchema: {
119
123
  databaseId: z.string().describe('DATABASE node id'),
120
124
  rowId: z.string().describe('Row id (must belong to the database)'),
@@ -145,6 +149,7 @@ export function registerDatabaseTools(server) {
145
149
  description:
146
150
  'Delete ONE row of a Rootr (루터) DATABASE by rowId (its linked page, if any, is moved to the trash). ' +
147
151
  'Verify the row via rootr_read_database first — do not guess rowIds.',
152
+ annotations: { readOnlyHint: false, destructiveHint: true },
148
153
  inputSchema: {
149
154
  databaseId: z.string().describe('DATABASE node id'),
150
155
  rowId: z.string().describe('Row id to delete (must belong to the database)'),
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
 
3
3
  import { RootrApiError } from '../client.js';
4
4
  import { resolveTarget, requireResolved } from '../resolve.js';
5
- import { textResult, errorResult, makeClient } from './shared.js';
5
+ import { textResult, jsonResult, errorResult, makeClient } from './shared.js';
6
6
 
7
7
  /** Registers the original 6 document tools: list/read/append/edit/write/search. */
8
8
  export function registerDocumentTools(server) {
@@ -13,6 +13,7 @@ export function registerDocumentTools(server) {
13
13
  description:
14
14
  'List documents and folders in the connected Rootr (루터) workspace, optionally filtered to a path prefix. ' +
15
15
  'Use this first to discover what exists before reading or writing.',
16
+ annotations: { readOnlyHint: true, destructiveHint: false },
16
17
  inputSchema: {
17
18
  path: z
18
19
  .string()
@@ -46,6 +47,7 @@ export function registerDocumentTools(server) {
46
47
  description:
47
48
  'Read the full markdown content of a Rootr (루터) document by path (starting with "/") or by node id. ' +
48
49
  'Read a document before editing or appending to it so you know its current structure.',
50
+ annotations: { readOnlyHint: true, destructiveHint: false },
49
51
  inputSchema: {
50
52
  target: z.string().describe('Document path (e.g. "/notes/todo.md") or node id'),
51
53
  },
@@ -71,6 +73,7 @@ export function registerDocumentTools(server) {
71
73
  'Append content to a Rootr document, optionally under a given heading. This is a conflict-free, ' +
72
74
  'additive operation — it never overwrites existing content, so PREFER this over rootr_write whenever ' +
73
75
  'you just need to add new information (log entries, notes, findings) to an existing document.',
76
+ annotations: { readOnlyHint: false, destructiveHint: false },
74
77
  inputSchema: {
75
78
  target: z.string().describe('Document path (starting with "/") or node id'),
76
79
  content: z.string().describe('Markdown content to append'),
@@ -105,6 +108,7 @@ export function registerDocumentTools(server) {
105
108
  'Fails with a clear error if the snippet is missing or ambiguous (not unique), so read the document ' +
106
109
  'first to copy the exact text to find. Good for precise, targeted edits; use rootr_append instead when ' +
107
110
  'you are only adding new content.',
111
+ annotations: { readOnlyHint: false, destructiveHint: false },
108
112
  inputSchema: {
109
113
  target: z.string().describe('Document path (starting with "/") or node id'),
110
114
  find: z.string().describe('Exact existing text to find (must be unique unless replaceAll is set)'),
@@ -133,6 +137,7 @@ export function registerDocumentTools(server) {
133
137
  'Replace the ENTIRE content of a Rootr document, or create a new document at a path if it does not exist. ' +
134
138
  'This is DESTRUCTIVE — it discards whatever is currently there. Read the document first and pass its etag ' +
135
139
  'as ifMatch to avoid clobbering concurrent changes. Prefer rootr_append or rootr_edit for incremental changes.',
140
+ annotations: { readOnlyHint: false, destructiveHint: true },
136
141
  inputSchema: {
137
142
  target: z.string().describe('Document path (starting with "/", created if missing) or node id'),
138
143
  content: z.string().describe('Full new markdown content for the document'),
@@ -175,6 +180,7 @@ export function registerDocumentTools(server) {
175
180
  description:
176
181
  'Full-text search across the connected Rootr (루터) workspace. Use this to find relevant documents before ' +
177
182
  'reading or editing them.',
183
+ annotations: { readOnlyHint: true, destructiveHint: false },
178
184
  inputSchema: {
179
185
  query: z.string().describe('Search query text'),
180
186
  },
@@ -190,4 +196,150 @@ export function registerDocumentTools(server) {
190
196
  }
191
197
  },
192
198
  );
199
+
200
+ server.registerTool(
201
+ 'rootr_comment_document',
202
+ {
203
+ title: 'Comment on a Rootr document',
204
+ description:
205
+ 'Add a comment to a Rootr (루터) document (optionally anchored to a block, or as a reply via parentId). ' +
206
+ 'Comments carry a user identity, so this requires an ACCOUNT key (PAT) — a workspace-scoped key ' +
207
+ '(including remote-connector tokens) gets a 403; fall back to rootr_append in that case.',
208
+ annotations: { readOnlyHint: false, destructiveHint: false },
209
+ inputSchema: {
210
+ target: z.string().describe('Document path (starting with "/") or node id'),
211
+ body: z.string().describe('Comment text (markdown)'),
212
+ blockId: z.string().optional().describe('Block id to anchor the comment to'),
213
+ parentId: z.string().optional().describe('Parent comment id, to reply in a thread'),
214
+ },
215
+ },
216
+ async ({ target, body, blockId, parentId }) => {
217
+ try {
218
+ const client = makeClient();
219
+ const id = requireResolved(await resolveTarget(client, target));
220
+ return jsonResult(await client.createComment(id, { body, blockId, parentId }));
221
+ } catch (err) {
222
+ return errorResult(err);
223
+ }
224
+ },
225
+ );
226
+
227
+ server.registerTool(
228
+ 'rootr_list_document_comments',
229
+ {
230
+ title: 'List comments on a Rootr document',
231
+ description:
232
+ 'List the comment threads on a Rootr (루터) document, optionally filtered to one block. Works with any key.',
233
+ annotations: { readOnlyHint: true, destructiveHint: false },
234
+ inputSchema: {
235
+ target: z.string().describe('Document path (starting with "/") or node id'),
236
+ blockId: z.string().optional().describe('Only comments anchored to this block'),
237
+ },
238
+ },
239
+ async ({ target, blockId }) => {
240
+ try {
241
+ const client = makeClient();
242
+ const id = requireResolved(await resolveTarget(client, target));
243
+ return jsonResult(await client.listComments(id, { blockId }));
244
+ } catch (err) {
245
+ return errorResult(err);
246
+ }
247
+ },
248
+ );
249
+
250
+ server.registerTool(
251
+ 'rootr_publish_document',
252
+ {
253
+ title: 'Publish a Rootr document to a public URL',
254
+ description:
255
+ 'Publish a Rootr (루터) document at a public read-only URL (rootr.io/p/{slug}) — outsiders can read it ' +
256
+ 'and propose git-style edit suggestions the owner can merge. Publishing is an owner-level action: it needs ' +
257
+ 'MANAGE permission, which workspace-scoped keys (including remote-connector tokens) do not carry — use an ' +
258
+ 'ACCOUNT key (PAT) of the owner, or ask the user to publish from the share dialog. ' +
259
+ 'Returns the slug/URL. Check rootr_document_public_status first; unpublish via rootr_set_document_public.',
260
+ annotations: { readOnlyHint: false, destructiveHint: false },
261
+ inputSchema: {
262
+ target: z.string().describe('Document path (starting with "/") or node id'),
263
+ },
264
+ },
265
+ async ({ target }) => {
266
+ try {
267
+ const client = makeClient();
268
+ const id = requireResolved(await resolveTarget(client, target));
269
+ return jsonResult(await client.publishDocument(id));
270
+ } catch (err) {
271
+ return errorResult(err);
272
+ }
273
+ },
274
+ );
275
+
276
+ server.registerTool(
277
+ 'rootr_set_document_public',
278
+ {
279
+ title: 'Update public settings of a Rootr document',
280
+ description:
281
+ 'Update a published Rootr (루터) document\'s public settings — e.g. {"isPublic": false} to unpublish. ' +
282
+ 'Owner-level action (MANAGE): works with an account key (PAT), not with workspace-scoped/connector tokens.',
283
+ annotations: { readOnlyHint: false, destructiveHint: false },
284
+ inputSchema: {
285
+ target: z.string().describe('Document path (starting with "/") or node id'),
286
+ isPublic: z.boolean().optional().describe('false to unpublish, true to re-enable'),
287
+ },
288
+ },
289
+ async ({ target, isPublic }) => {
290
+ try {
291
+ const client = makeClient();
292
+ const id = requireResolved(await resolveTarget(client, target));
293
+ return jsonResult(await client.setDocumentPublic(id, { isPublic }));
294
+ } catch (err) {
295
+ return errorResult(err);
296
+ }
297
+ },
298
+ );
299
+
300
+ server.registerTool(
301
+ 'rootr_document_public_status',
302
+ {
303
+ title: 'Public status of a Rootr document',
304
+ description: 'Whether a Rootr (루터) document is published, and at which public slug/URL.',
305
+ annotations: { readOnlyHint: true, destructiveHint: false },
306
+ inputSchema: {
307
+ target: z.string().describe('Document path (starting with "/") or node id'),
308
+ },
309
+ },
310
+ async ({ target }) => {
311
+ try {
312
+ const client = makeClient();
313
+ const id = requireResolved(await resolveTarget(client, target));
314
+ return jsonResult(await client.getDocumentPublicStatus(id));
315
+ } catch (err) {
316
+ return errorResult(err);
317
+ }
318
+ },
319
+ );
320
+
321
+ server.registerTool(
322
+ 'rootr_duplicate_node',
323
+ {
324
+ title: 'Duplicate a Rootr node (deep copy)',
325
+ description:
326
+ 'Deep-copy a Rootr (루터) node — a document or a whole folder tree — next to the original, or into ' +
327
+ 'another workspace the user can access (targetWorkspaceId). Returns the new root node.',
328
+ annotations: { readOnlyHint: false, destructiveHint: false },
329
+ inputSchema: {
330
+ target: z.string().describe('Node path (starting with "/") or node id to copy'),
331
+ deep: z.boolean().optional().describe('Copy children too (default true)'),
332
+ targetWorkspaceId: z.string().optional().describe('Destination workspace id (defaults to same workspace)'),
333
+ },
334
+ },
335
+ async ({ target, deep, targetWorkspaceId }) => {
336
+ try {
337
+ const client = makeClient();
338
+ const id = requireResolved(await resolveTarget(client, target));
339
+ return jsonResult(await client.duplicateNode(id, { deep, targetWorkspaceId }));
340
+ } catch (err) {
341
+ return errorResult(err);
342
+ }
343
+ },
344
+ );
193
345
  }
@@ -0,0 +1,206 @@
1
+ import { z } from 'zod';
2
+
3
+ import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
4
+
5
+ const fieldShape = z
6
+ .object({
7
+ id: z.string().optional().describe('Stable field id (also the key under response `values`). Server-generated if omitted.'),
8
+ name: z.string().describe('Field label shown to respondents'),
9
+ type: z
10
+ .enum(['text', 'textarea', 'number', 'select', 'multi_select', 'checkbox', 'date'])
11
+ .describe('Field input type'),
12
+ required: z.boolean().optional().describe('Whether the field must be filled in'),
13
+ options: z
14
+ .array(z.object({ name: z.string(), color: z.string() }))
15
+ .optional()
16
+ .describe('Choices for select/multi_select fields'),
17
+ })
18
+ .describe('One form field definition.');
19
+
20
+ const FIELDS_GUIDE =
21
+ 'Fields are typed inputs shown to respondents: text | textarea | number | select | multi_select | checkbox | date; ' +
22
+ 'select/multi_select need `options` ({name,color}[]). Read the form first (rootr_read_form) before updating so ' +
23
+ 'you keep each field\'s `id` stable — response values are keyed by field id, so renaming/dropping an id orphans ' +
24
+ 'past responses\' answers for that field.';
25
+
26
+ /** FORM node tools: create/read/update the field definitions, list/submit responses, manage public share links. */
27
+ export function registerFormTools(server) {
28
+ server.registerTool(
29
+ 'rootr_create_form',
30
+ {
31
+ title: 'Create a Rootr form',
32
+ description:
33
+ 'Create a new FORM node (fillable intake) in a Rootr (루터) workspace, optionally seeded with field ' +
34
+ 'definitions. Use FORM for surveys/request queues collected from people outside the tree — set ' +
35
+ '`targetDatabaseId` to also mirror each response into a DATABASE\'s rows. ' +
36
+ FIELDS_GUIDE,
37
+ annotations: { readOnlyHint: false, destructiveHint: false },
38
+ inputSchema: {
39
+ name: z.string().optional().describe('Form name/title'),
40
+ parentId: z.string().optional().describe('Parent folder/node id to create it under'),
41
+ icon: z.string().optional().describe('Emoji icon for the node'),
42
+ fields: z.array(fieldShape).optional().describe('Initial field definitions; defaults to an empty form'),
43
+ targetDatabaseId: z.string().optional().describe('DATABASE node id whose rows should mirror this form\'s responses'),
44
+ config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
45
+ workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
46
+ },
47
+ },
48
+ async ({ name, parentId, icon, fields, targetDatabaseId, config, workspace }) => {
49
+ try {
50
+ const ws = requireWorkspace(workspace);
51
+ const client = makeClient(ws);
52
+ const result = await client.createForm(ws, { name, parentId, icon, fields, targetDatabaseId, config });
53
+ return jsonResult(result);
54
+ } catch (err) {
55
+ return errorResult(err);
56
+ }
57
+ },
58
+ );
59
+
60
+ server.registerTool(
61
+ 'rootr_read_form',
62
+ {
63
+ title: 'Read a Rootr form',
64
+ description:
65
+ 'Read a Rootr (루터) FORM node — its field definitions, targetDatabaseId, and config. Call this before ' +
66
+ 'rootr_update_form so you keep existing field ids stable, or before rootr_list_form_responses to know ' +
67
+ 'which field ids the response `values` are keyed by.',
68
+ annotations: { readOnlyHint: true, destructiveHint: false },
69
+ inputSchema: {
70
+ formId: z.string().describe('FORM node id'),
71
+ },
72
+ },
73
+ async ({ formId }) => {
74
+ try {
75
+ const client = makeClient();
76
+ const result = await client.getForm(formId);
77
+ return jsonResult(result);
78
+ } catch (err) {
79
+ return errorResult(err);
80
+ }
81
+ },
82
+ );
83
+
84
+ server.registerTool(
85
+ 'rootr_update_form',
86
+ {
87
+ title: 'Update a Rootr form (fields/targetDatabaseId/config)',
88
+ description:
89
+ 'Merge-patch a Rootr (루터) FORM: if you pass `fields`, it REPLACES the whole fields array — call ' +
90
+ 'rootr_read_form first and resend fields you want to keep (with their existing ids) so past responses ' +
91
+ 'stay attributable. Pass `targetDatabaseId: null` to unlink the mirrored DATABASE. ' +
92
+ FIELDS_GUIDE,
93
+ annotations: { readOnlyHint: false, destructiveHint: false },
94
+ inputSchema: {
95
+ formId: z.string().describe('FORM node id'),
96
+ fields: z.array(fieldShape).optional().describe('COMPLETE fields array to set — omitted existing fields will be dropped'),
97
+ targetDatabaseId: z.string().nullable().optional().describe('DATABASE node id to mirror responses into, or null to unlink'),
98
+ config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
99
+ icon: z.string().optional().describe('Emoji icon for the node'),
100
+ },
101
+ },
102
+ async ({ formId, fields, targetDatabaseId, config, icon }) => {
103
+ try {
104
+ const client = makeClient();
105
+ const result = await client.updateForm(formId, { fields, targetDatabaseId, config, icon });
106
+ return jsonResult(result);
107
+ } catch (err) {
108
+ return errorResult(err);
109
+ }
110
+ },
111
+ );
112
+
113
+ server.registerTool(
114
+ 'rootr_list_form_responses',
115
+ {
116
+ title: 'List a Rootr form\'s submitted responses',
117
+ description:
118
+ 'List all responses submitted to a Rootr (루터) FORM, most recent first. Each response has `values` keyed ' +
119
+ 'by field id — call rootr_read_form first to map ids to field names. This is what lets an AI analyze ' +
120
+ 'responses collected from a form.',
121
+ annotations: { readOnlyHint: true, destructiveHint: false },
122
+ inputSchema: {
123
+ formId: z.string().describe('FORM node id'),
124
+ },
125
+ },
126
+ async ({ formId }) => {
127
+ try {
128
+ const client = makeClient();
129
+ const result = await client.listFormResponses(formId);
130
+ return jsonResult({ responses: result });
131
+ } catch (err) {
132
+ return errorResult(err);
133
+ }
134
+ },
135
+ );
136
+
137
+ server.registerTool(
138
+ 'rootr_submit_form_response',
139
+ {
140
+ title: 'Submit a response to a Rootr form',
141
+ description:
142
+ 'Submit one response to a Rootr (루터) FORM on behalf of the caller (authenticated submission, not the ' +
143
+ 'public/anonymous share-link flow). `values` is keyed by field id — call rootr_read_form first to get the ' +
144
+ 'exact field ids and types.',
145
+ annotations: { readOnlyHint: false, destructiveHint: false },
146
+ inputSchema: {
147
+ formId: z.string().describe('FORM node id'),
148
+ values: z.record(z.unknown()).describe('Response values keyed by field id'),
149
+ },
150
+ },
151
+ async ({ formId, values }) => {
152
+ try {
153
+ const client = makeClient();
154
+ const result = await client.createFormResponse(formId, values);
155
+ return jsonResult(result);
156
+ } catch (err) {
157
+ return errorResult(err);
158
+ }
159
+ },
160
+ );
161
+
162
+ server.registerTool(
163
+ 'rootr_create_form_share_link',
164
+ {
165
+ title: 'Issue a public share link for a Rootr form',
166
+ description:
167
+ 'Issue a new public share link/token for a Rootr (루터) FORM so people outside the workspace can submit ' +
168
+ 'responses (via the /f/{token} page). The form\'s public-submit mode itself is controlled separately via ' +
169
+ 'rootr_update_form\'s config.publicSubmit ("off" | "login" | "anon").',
170
+ annotations: { readOnlyHint: false, destructiveHint: false },
171
+ inputSchema: {
172
+ formId: z.string().describe('FORM node id'),
173
+ },
174
+ },
175
+ async ({ formId }) => {
176
+ try {
177
+ const client = makeClient();
178
+ const result = await client.createFormShareLink(formId);
179
+ return jsonResult(result);
180
+ } catch (err) {
181
+ return errorResult(err);
182
+ }
183
+ },
184
+ );
185
+
186
+ server.registerTool(
187
+ 'rootr_list_form_share_links',
188
+ {
189
+ title: 'List a Rootr form\'s public share links',
190
+ description: 'List existing public share links/tokens issued for a Rootr (루터) FORM.',
191
+ annotations: { readOnlyHint: true, destructiveHint: false },
192
+ inputSchema: {
193
+ formId: z.string().describe('FORM node id'),
194
+ },
195
+ },
196
+ async ({ formId }) => {
197
+ try {
198
+ const client = makeClient();
199
+ const result = await client.listFormShareLinks(formId);
200
+ return jsonResult({ links: result });
201
+ } catch (err) {
202
+ return errorResult(err);
203
+ }
204
+ },
205
+ );
206
+ }
@@ -11,6 +11,7 @@ export function registerIssueTools(server) {
11
11
  description:
12
12
  'Create a new GitHub-style issue tracker document in a Rootr (루터) workspace. Returns the tracker with ' +
13
13
  'its id, plus its default labels/types — use rootr_create_issue afterward to add issues to it.',
14
+ annotations: { readOnlyHint: false, destructiveHint: false },
14
15
  inputSchema: {
15
16
  name: z.string().describe('Name of the issue tracker'),
16
17
  workspace: z
@@ -38,6 +39,7 @@ export function registerIssueTools(server) {
38
39
  description:
39
40
  'List issues in a Rootr (루터) issue tracker, optionally filtered by state (OPEN/CLOSED/ALL), label id, ' +
40
41
  'type id, or a text query. Use rootr_get_issue for full details of one issue.',
42
+ annotations: { readOnlyHint: true, destructiveHint: false },
41
43
  inputSchema: {
42
44
  trackerId: z.string().describe('Issue tracker node id'),
43
45
  state: z.string().optional().describe('OPEN | CLOSED | ALL'),
@@ -65,6 +67,7 @@ export function registerIssueTools(server) {
65
67
  'Create a new issue in a Rootr (루터) issue tracker. labels is an array of label ids, type is a type id ' +
66
68
  '(both from the tracker\'s custom type/label definitions — see rootr_read on the tracker or its metadata). ' +
67
69
  'parentIssueId nests this issue under another issue (sub-issue).',
70
+ annotations: { readOnlyHint: false, destructiveHint: false },
68
71
  inputSchema: {
69
72
  trackerId: z.string().describe('Issue tracker node id'),
70
73
  title: z.string().describe('Issue title'),
@@ -90,6 +93,7 @@ export function registerIssueTools(server) {
90
93
  {
91
94
  title: 'Get an issue',
92
95
  description: 'Fetch full details of a single Rootr (루터) issue by id.',
96
+ annotations: { readOnlyHint: true, destructiveHint: false },
93
97
  inputSchema: {
94
98
  issueId: z.string().describe('Issue id'),
95
99
  },
@@ -112,6 +116,7 @@ export function registerIssueTools(server) {
112
116
  description:
113
117
  'Update arbitrary fields of a Rootr (루터) issue. To close an issue, set state to "CLOSED" and stateReason ' +
114
118
  'to one of completed | not_planned | duplicate.',
119
+ annotations: { readOnlyHint: false, destructiveHint: false },
115
120
  inputSchema: {
116
121
  issueId: z.string().describe('Issue id'),
117
122
  title: z.string().optional(),
@@ -143,6 +148,7 @@ export function registerIssueTools(server) {
143
148
  description:
144
149
  'Add a comment to a Rootr (루터) issue within its tracker. Comments carry a user identity, ' +
145
150
  'so this requires an ACCOUNT key (PAT) as ROOTR_API_KEY — a workspace key gets a 403.',
151
+ annotations: { readOnlyHint: false, destructiveHint: false },
146
152
  inputSchema: {
147
153
  trackerId: z.string().describe('Issue tracker node id'),
148
154
  issueId: z.string().describe('Issue id to comment on'),
@@ -13,6 +13,7 @@ export function registerLogTools(server) {
13
13
  'Each field has a name/type; type is one of string|int|float|bool|datetime|json|enum|level|relation. ' +
14
14
  'Relation fields additionally need target (a node id to relate to) and optional many/relation flags. ' +
15
15
  'Use rootr_add_log_entries afterward to insert rows, and rootr_query_log_entries / rootr_log_stats to read them back.',
16
+ annotations: { readOnlyHint: false, destructiveHint: false },
16
17
  inputSchema: {
17
18
  name: z.string().describe('Name of the LOG store'),
18
19
  fields: z
@@ -48,6 +49,7 @@ export function registerLogTools(server) {
48
49
  'Update the field schema of an existing LOG store. WARNING: this REPLACES THE ENTIRE FIELD SET — ' +
49
50
  'you must resend every field you want to keep, not just the ones you are changing/adding. ' +
50
51
  'Read the LOG store first (e.g. via rootr_read or a get-log call) to see its current fields before calling this.',
52
+ annotations: { readOnlyHint: false, destructiveHint: true },
51
53
  inputSchema: {
52
54
  logId: z.string().describe('LOG store node id'),
53
55
  fields: z
@@ -73,6 +75,7 @@ export function registerLogTools(server) {
73
75
  description:
74
76
  'Append one or more entries (rows) to a LOG store. Each entry may carry ts, source, level, message, data ' +
75
77
  '(shaped per the store\'s field schema). This is additive — existing entries are untouched.',
78
+ annotations: { readOnlyHint: false, destructiveHint: false },
76
79
  inputSchema: {
77
80
  logId: z.string().describe('LOG store node id'),
78
81
  entries: z
@@ -98,6 +101,7 @@ export function registerLogTools(server) {
98
101
  description:
99
102
  'Query/filter entries from a LOG store by time range, source, level, and/or anomaly flag. ' +
100
103
  'Use this to inspect recent activity or find anomalous entries before asking rootr_ask for root-cause analysis.',
104
+ annotations: { readOnlyHint: true, destructiveHint: false },
101
105
  inputSchema: {
102
106
  logId: z.string().describe('LOG store node id'),
103
107
  from: z.string().optional().describe('ISO timestamp lower bound'),
@@ -136,6 +140,7 @@ export function registerLogTools(server) {
136
140
  'Compute aggregate statistics over a LOG store\'s entries, grouped by hour/day/source/level, with a ' +
137
141
  'metric of count/avg/max/min/sum (avg/max/min/sum require a numeric `field`). Useful for trend/volume ' +
138
142
  'questions without pulling raw entries.',
143
+ annotations: { readOnlyHint: true, destructiveHint: false },
139
144
  inputSchema: {
140
145
  logId: z.string().describe('LOG store node id'),
141
146
  groupBy: z.string().optional().describe('hour | day | source | level'),
@@ -12,6 +12,7 @@ export function registerMiscTools(server) {
12
12
  description:
13
13
  'List saved versions of a Rootr (루터) document, most recent first. Useful for seeing what changed and ' +
14
14
  'when, or to recover an older revision.',
15
+ annotations: { readOnlyHint: true, destructiveHint: false },
15
16
  inputSchema: {
16
17
  target: z.string().describe('Document path (starting with "/") or node id'),
17
18
  limit: z.number().optional().describe('Max number of versions to return'),
@@ -37,6 +38,7 @@ export function registerMiscTools(server) {
37
38
  description:
38
39
  'Delete a Rootr (루터) document or folder by moving it to the trash — this is recoverable, not a ' +
39
40
  'permanent delete. Use rootr_read/rootr_list first to confirm you have the right target.',
41
+ annotations: { readOnlyHint: false, destructiveHint: true },
40
42
  inputSchema: {
41
43
  target: z.string().describe('Document/folder path (starting with "/") or node id'),
42
44
  },