rootr-cli 0.4.0 → 0.5.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, requireWorkspace } from './shared.js';
3
+ import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
4
4
 
5
5
  const fieldShape = z
6
6
  .object({
@@ -37,10 +37,10 @@ export function registerFormTools(server) {
37
37
  annotations: { readOnlyHint: false, destructiveHint: false },
38
38
  inputSchema: {
39
39
  name: z.string().optional().describe('Form name/title'),
40
- parentId: z.string().optional().describe('Parent folder/node id to create it under'),
40
+ parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
41
41
  icon: z.string().optional().describe('Emoji icon for the node'),
42
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'),
43
+ targetDatabaseId: z.string().optional().describe(`DATABASE node id${NODE_TARGET_HINT} whose rows mirror this form's responses`),
44
44
  config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
45
45
  workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
46
46
  },
@@ -49,6 +49,8 @@ export function registerFormTools(server) {
49
49
  try {
50
50
  const ws = requireWorkspace(workspace);
51
51
  const client = makeClient(ws);
52
+ targetDatabaseId = await resolveOptionalNodeId(client, targetDatabaseId);
53
+ parentId = await resolveOptionalNodeId(client, parentId);
52
54
  const result = await client.createForm(ws, { name, parentId, icon, fields, targetDatabaseId, config });
53
55
  return jsonResult(result);
54
56
  } catch (err) {
@@ -67,12 +69,13 @@ export function registerFormTools(server) {
67
69
  'which field ids the response `values` are keyed by.',
68
70
  annotations: { readOnlyHint: true, destructiveHint: false },
69
71
  inputSchema: {
70
- formId: z.string().describe('FORM node id'),
72
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
71
73
  },
72
74
  },
73
75
  async ({ formId }) => {
74
76
  try {
75
77
  const client = makeClient();
78
+ formId = await resolveNodeId(client, formId);
76
79
  const result = await client.getForm(formId);
77
80
  return jsonResult(result);
78
81
  } catch (err) {
@@ -92,9 +95,9 @@ export function registerFormTools(server) {
92
95
  FIELDS_GUIDE,
93
96
  annotations: { readOnlyHint: false, destructiveHint: false },
94
97
  inputSchema: {
95
- formId: z.string().describe('FORM node id'),
98
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
96
99
  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'),
100
+ targetDatabaseId: z.string().nullable().optional().describe(`DATABASE node id${NODE_TARGET_HINT} to mirror responses into, or null to unlink`),
98
101
  config: z.record(z.unknown()).optional().describe('Free-form form config object (e.g. publicSubmit mode)'),
99
102
  icon: z.string().optional().describe('Emoji icon for the node'),
100
103
  },
@@ -102,6 +105,8 @@ export function registerFormTools(server) {
102
105
  async ({ formId, fields, targetDatabaseId, config, icon }) => {
103
106
  try {
104
107
  const client = makeClient();
108
+ targetDatabaseId = await resolveOptionalNodeId(client, targetDatabaseId);
109
+ formId = await resolveNodeId(client, formId);
105
110
  const result = await client.updateForm(formId, { fields, targetDatabaseId, config, icon });
106
111
  return jsonResult(result);
107
112
  } catch (err) {
@@ -120,12 +125,13 @@ export function registerFormTools(server) {
120
125
  'responses collected from a form.',
121
126
  annotations: { readOnlyHint: true, destructiveHint: false },
122
127
  inputSchema: {
123
- formId: z.string().describe('FORM node id'),
128
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
124
129
  },
125
130
  },
126
131
  async ({ formId }) => {
127
132
  try {
128
133
  const client = makeClient();
134
+ formId = await resolveNodeId(client, formId);
129
135
  const result = await client.listFormResponses(formId);
130
136
  return jsonResult({ responses: result });
131
137
  } catch (err) {
@@ -144,13 +150,14 @@ export function registerFormTools(server) {
144
150
  'exact field ids and types.',
145
151
  annotations: { readOnlyHint: false, destructiveHint: false },
146
152
  inputSchema: {
147
- formId: z.string().describe('FORM node id'),
153
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
148
154
  values: z.record(z.unknown()).describe('Response values keyed by field id'),
149
155
  },
150
156
  },
151
157
  async ({ formId, values }) => {
152
158
  try {
153
159
  const client = makeClient();
160
+ formId = await resolveNodeId(client, formId);
154
161
  const result = await client.createFormResponse(formId, values);
155
162
  return jsonResult(result);
156
163
  } catch (err) {
@@ -169,12 +176,13 @@ export function registerFormTools(server) {
169
176
  'rootr_update_form\'s config.publicSubmit ("off" | "login" | "anon").',
170
177
  annotations: { readOnlyHint: false, destructiveHint: false },
171
178
  inputSchema: {
172
- formId: z.string().describe('FORM node id'),
179
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
173
180
  },
174
181
  },
175
182
  async ({ formId }) => {
176
183
  try {
177
184
  const client = makeClient();
185
+ formId = await resolveNodeId(client, formId);
178
186
  const result = await client.createFormShareLink(formId);
179
187
  return jsonResult(result);
180
188
  } catch (err) {
@@ -190,12 +198,13 @@ export function registerFormTools(server) {
190
198
  description: 'List existing public share links/tokens issued for a Rootr (루터) FORM.',
191
199
  annotations: { readOnlyHint: true, destructiveHint: false },
192
200
  inputSchema: {
193
- formId: z.string().describe('FORM node id'),
201
+ formId: z.string().describe(`FORM node id${NODE_TARGET_HINT}`),
194
202
  },
195
203
  },
196
204
  async ({ formId }) => {
197
205
  try {
198
206
  const client = makeClient();
207
+ formId = await resolveNodeId(client, formId);
199
208
  const result = await client.listFormShareLinks(formId);
200
209
  return jsonResult({ links: result });
201
210
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- import { textResult, jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
3
+ import { textResult, jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT } from './shared.js';
4
4
 
5
5
  /** Issue tracker tools: create tracker, list/create/get/update issues, comment. */
6
6
  export function registerIssueTools(server) {
@@ -41,7 +41,7 @@ export function registerIssueTools(server) {
41
41
  'type id, or a text query. Use rootr_get_issue for full details of one issue.',
42
42
  annotations: { readOnlyHint: true, destructiveHint: false },
43
43
  inputSchema: {
44
- trackerId: z.string().describe('Issue tracker node id'),
44
+ trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
45
45
  state: z.string().optional().describe('OPEN | CLOSED | ALL'),
46
46
  q: z.string().optional().describe('Free-text search within issues'),
47
47
  label: z.string().optional().describe('Label id to filter by'),
@@ -51,6 +51,7 @@ export function registerIssueTools(server) {
51
51
  async ({ trackerId, state, q, label, type }) => {
52
52
  try {
53
53
  const client = makeClient();
54
+ trackerId = await resolveNodeId(client, trackerId);
54
55
  const result = await client.listIssues(trackerId, { state, q, label, type });
55
56
  return jsonResult(result);
56
57
  } catch (err) {
@@ -69,7 +70,7 @@ export function registerIssueTools(server) {
69
70
  'parentIssueId nests this issue under another issue (sub-issue).',
70
71
  annotations: { readOnlyHint: false, destructiveHint: false },
71
72
  inputSchema: {
72
- trackerId: z.string().describe('Issue tracker node id'),
73
+ trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
73
74
  title: z.string().describe('Issue title'),
74
75
  body: z.string().optional().describe('Issue body (markdown)'),
75
76
  labels: z.array(z.string()).optional().describe('Label ids to attach'),
@@ -80,6 +81,7 @@ export function registerIssueTools(server) {
80
81
  async ({ trackerId, title, body, labels, type, parentIssueId }) => {
81
82
  try {
82
83
  const client = makeClient();
84
+ trackerId = await resolveNodeId(client, trackerId);
83
85
  const result = await client.createIssue(trackerId, { title, body, labels, type, parentIssueId });
84
86
  return jsonResult(result);
85
87
  } catch (err) {
@@ -150,7 +152,7 @@ export function registerIssueTools(server) {
150
152
  'so this requires an ACCOUNT key (PAT) as ROOTR_API_KEY — a workspace key gets a 403.',
151
153
  annotations: { readOnlyHint: false, destructiveHint: false },
152
154
  inputSchema: {
153
- trackerId: z.string().describe('Issue tracker node id'),
155
+ trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
154
156
  issueId: z.string().describe('Issue id to comment on'),
155
157
  body: z.string().describe('Comment body (markdown)'),
156
158
  },
@@ -158,6 +160,7 @@ export function registerIssueTools(server) {
158
160
  async ({ trackerId, issueId, body }) => {
159
161
  try {
160
162
  const client = makeClient();
163
+ trackerId = await resolveNodeId(client, trackerId);
161
164
  const result = await client.commentIssue(trackerId, issueId, body);
162
165
  return textResult(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
163
166
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
3
+ import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
4
4
 
5
5
  /** LOG datastore tools: create/update-fields/add-entries/query-entries/stats. */
6
6
  export function registerLogTools(server) {
@@ -22,7 +22,7 @@ export function registerLogTools(server) {
22
22
  'Field definitions: [{name, type, ...}]. type=string|int|float|bool|datetime|json|enum|level|relation. ' +
23
23
  'relation fields add target (node id), many?, relation?.',
24
24
  ),
25
- parentId: z.string().optional().describe('Parent node id to nest the LOG store under'),
25
+ parentId: z.string().optional().describe(`Parent node id${NODE_TARGET_HINT}`),
26
26
  workspace: z
27
27
  .string()
28
28
  .optional()
@@ -33,6 +33,7 @@ export function registerLogTools(server) {
33
33
  try {
34
34
  const ws = requireWorkspace(workspace);
35
35
  const client = makeClient(ws);
36
+ parentId = await resolveOptionalNodeId(client, parentId);
36
37
  const result = await client.createLogStore(ws, { name, fields, parentId });
37
38
  return jsonResult(result);
38
39
  } catch (err) {
@@ -51,7 +52,7 @@ export function registerLogTools(server) {
51
52
  'Read the LOG store first (e.g. via rootr_read or a get-log call) to see its current fields before calling this.',
52
53
  annotations: { readOnlyHint: false, destructiveHint: true },
53
54
  inputSchema: {
54
- logId: z.string().describe('LOG store node id'),
55
+ logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
55
56
  fields: z
56
57
  .array(z.record(z.any()))
57
58
  .describe('COMPLETE field list to set — omitted existing fields will be dropped'),
@@ -60,6 +61,7 @@ export function registerLogTools(server) {
60
61
  async ({ logId, fields }) => {
61
62
  try {
62
63
  const client = makeClient();
64
+ logId = await resolveNodeId(client, logId);
63
65
  const result = await client.updateLogFields(logId, fields);
64
66
  return jsonResult(result);
65
67
  } catch (err) {
@@ -77,7 +79,7 @@ export function registerLogTools(server) {
77
79
  '(shaped per the store\'s field schema). This is additive — existing entries are untouched.',
78
80
  annotations: { readOnlyHint: false, destructiveHint: false },
79
81
  inputSchema: {
80
- logId: z.string().describe('LOG store node id'),
82
+ logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
81
83
  entries: z
82
84
  .array(z.record(z.any()))
83
85
  .describe('Entries to add: [{ts?, source?, level?, message?, data?}, ...]'),
@@ -86,6 +88,7 @@ export function registerLogTools(server) {
86
88
  async ({ logId, entries }) => {
87
89
  try {
88
90
  const client = makeClient();
91
+ logId = await resolveNodeId(client, logId);
89
92
  const result = await client.addLogEntries(logId, entries);
90
93
  return jsonResult(result);
91
94
  } catch (err) {
@@ -103,7 +106,7 @@ export function registerLogTools(server) {
103
106
  'Use this to inspect recent activity or find anomalous entries before asking rootr_ask for root-cause analysis.',
104
107
  annotations: { readOnlyHint: true, destructiveHint: false },
105
108
  inputSchema: {
106
- logId: z.string().describe('LOG store node id'),
109
+ logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
107
110
  from: z.string().optional().describe('ISO timestamp lower bound'),
108
111
  to: z.string().optional().describe('ISO timestamp upper bound'),
109
112
  source: z.string().optional().describe('Filter by source'),
@@ -116,6 +119,7 @@ export function registerLogTools(server) {
116
119
  async ({ logId, from, to, source, level, anomalyOnly, limit, order }) => {
117
120
  try {
118
121
  const client = makeClient();
122
+ logId = await resolveNodeId(client, logId);
119
123
  const result = await client.queryLogEntries(logId, {
120
124
  from,
121
125
  to,
@@ -142,7 +146,7 @@ export function registerLogTools(server) {
142
146
  'questions without pulling raw entries.',
143
147
  annotations: { readOnlyHint: true, destructiveHint: false },
144
148
  inputSchema: {
145
- logId: z.string().describe('LOG store node id'),
149
+ logId: z.string().describe(`LOG store node id${NODE_TARGET_HINT}`),
146
150
  groupBy: z.string().optional().describe('hour | day | source | level'),
147
151
  metric: z.string().optional().describe('count | avg | max | min | sum'),
148
152
  field: z.string().optional().describe('Numeric field name, required for avg/max/min/sum'),
@@ -153,6 +157,7 @@ export function registerLogTools(server) {
153
157
  async ({ logId, groupBy, metric, field, from, to }) => {
154
158
  try {
155
159
  const client = makeClient();
160
+ logId = await resolveNodeId(client, logId);
156
161
  const result = await client.logStats(logId, { groupBy, metric, field, from, to });
157
162
  return jsonResult(result);
158
163
  } catch (err) {
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
 
3
- import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
3
+ import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
4
4
 
5
5
  /**
6
6
  * Authoring guide, repeated (shortened) across every tool description so an
@@ -127,7 +127,7 @@ export function registerPresentationTools(server) {
127
127
  annotations: { readOnlyHint: false, destructiveHint: false },
128
128
  inputSchema: {
129
129
  name: z.string().optional().describe('Deck name/title'),
130
- parentId: z.string().optional().describe('Parent folder/node id to create the deck under'),
130
+ parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
131
131
  theme: themeShape,
132
132
  slides: z.array(slideShape).optional().describe('Initial slides, in order'),
133
133
  config: z.record(z.unknown()).optional().describe('Free-form deck config/settings object'),
@@ -142,6 +142,7 @@ export function registerPresentationTools(server) {
142
142
  try {
143
143
  const ws = requireWorkspace(workspace);
144
144
  const client = makeClient(ws);
145
+ parentId = await resolveOptionalNodeId(client, parentId);
145
146
  const result = await client.createPresentation(ws, {
146
147
  name,
147
148
  parentId,
@@ -167,12 +168,13 @@ export function registerPresentationTools(server) {
167
168
  'know the current slide ids and content.',
168
169
  annotations: { readOnlyHint: true, destructiveHint: false },
169
170
  inputSchema: {
170
- presentationId: z.string().describe('PRESENTATION node id'),
171
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
171
172
  },
172
173
  },
173
174
  async ({ presentationId }) => {
174
175
  try {
175
176
  const client = makeClient();
177
+ presentationId = await resolveNodeId(client, presentationId);
176
178
  const result = await client.getPresentation(presentationId);
177
179
  return jsonResult(result);
178
180
  } catch (err) {
@@ -193,7 +195,7 @@ export function registerPresentationTools(server) {
193
195
  AUTHORING_GUIDE,
194
196
  annotations: { readOnlyHint: false, destructiveHint: false },
195
197
  inputSchema: {
196
- presentationId: z.string().describe('PRESENTATION node id'),
198
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
197
199
  theme: themeShape,
198
200
  slides: z
199
201
  .array(slideShape)
@@ -206,6 +208,7 @@ export function registerPresentationTools(server) {
206
208
  async ({ presentationId, theme, slides, config, icon }) => {
207
209
  try {
208
210
  const client = makeClient();
211
+ presentationId = await resolveNodeId(client, presentationId);
209
212
  const result = await client.updatePresentation(presentationId, {
210
213
  theme,
211
214
  slides: ensureSlideIds(slides),
@@ -229,13 +232,14 @@ export function registerPresentationTools(server) {
229
232
  AUTHORING_GUIDE,
230
233
  annotations: { readOnlyHint: false, destructiveHint: false },
231
234
  inputSchema: {
232
- presentationId: z.string().describe('PRESENTATION node id'),
235
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
233
236
  slides: z.array(slideShape).describe('Slides to append, in order'),
234
237
  },
235
238
  },
236
239
  async ({ presentationId, slides }) => {
237
240
  try {
238
241
  const client = makeClient();
242
+ presentationId = await resolveNodeId(client, presentationId);
239
243
  const result = await client.appendPresentationSlides(presentationId, ensureSlideIds(slides));
240
244
  return jsonResult(result);
241
245
  } catch (err) {
@@ -255,7 +259,7 @@ export function registerPresentationTools(server) {
255
259
  AUTHORING_GUIDE,
256
260
  annotations: { readOnlyHint: false, destructiveHint: false },
257
261
  inputSchema: {
258
- presentationId: z.string().describe('PRESENTATION node id'),
262
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
259
263
  slideId: z.string().describe('Slide id to update (must belong to the deck)'),
260
264
  slide: slideShape.describe('Partial slide fields to merge into the existing slide'),
261
265
  },
@@ -263,6 +267,7 @@ export function registerPresentationTools(server) {
263
267
  async ({ presentationId, slideId, slide }) => {
264
268
  try {
265
269
  const client = makeClient();
270
+ presentationId = await resolveNodeId(client, presentationId);
266
271
  const result = await client.updatePresentationSlide(presentationId, slideId, slide);
267
272
  return jsonResult(result);
268
273
  } catch (err) {
@@ -280,13 +285,14 @@ export function registerPresentationTools(server) {
280
285
  'in the new desired order — call rootr_read_presentation first to get the current ids.',
281
286
  annotations: { readOnlyHint: false, destructiveHint: false },
282
287
  inputSchema: {
283
- presentationId: z.string().describe('PRESENTATION node id'),
288
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
284
289
  order: z.array(z.string()).describe('Complete ordered array of slide ids'),
285
290
  },
286
291
  },
287
292
  async ({ presentationId, order }) => {
288
293
  try {
289
294
  const client = makeClient();
295
+ presentationId = await resolveNodeId(client, presentationId);
290
296
  const result = await client.reorderPresentationSlides(presentationId, order);
291
297
  return jsonResult(result);
292
298
  } catch (err) {
@@ -310,7 +316,7 @@ export function registerPresentationTools(server) {
310
316
  annotations: { readOnlyHint: false, destructiveHint: false },
311
317
  inputSchema: {
312
318
  prompt: z.string().describe('English image description; ask for "no text" in the image'),
313
- nodeId: z.string().optional().describe('Presentation/node id to attach the image to (permission-checked)'),
319
+ nodeId: z.string().optional().describe(`Presentation/node id${NODE_TARGET_HINT} to attach the image to (permission-checked)`),
314
320
  workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
315
321
  },
316
322
  },
@@ -318,6 +324,7 @@ export function registerPresentationTools(server) {
318
324
  try {
319
325
  const client = makeClient();
320
326
  const ws = requireWorkspace(workspace);
327
+ nodeId = await resolveOptionalNodeId(client, nodeId);
321
328
  const result = await client.generateImage(ws, { prompt, nodeId });
322
329
  return jsonResult(result);
323
330
  } catch (err) {
@@ -338,7 +345,7 @@ export function registerPresentationTools(server) {
338
345
  annotations: { readOnlyHint: false, destructiveHint: false },
339
346
  inputSchema: {
340
347
  attachmentId: z.string().describe('Source attachment id (from rootr_generate_image or an upload)'),
341
- nodeId: z.string().optional().describe('Presentation/node id to attach the result to (permission-checked)'),
348
+ nodeId: z.string().optional().describe(`Presentation/node id${NODE_TARGET_HINT} to attach the result to (permission-checked)`),
342
349
  workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
343
350
  },
344
351
  },
@@ -346,6 +353,7 @@ export function registerPresentationTools(server) {
346
353
  try {
347
354
  const client = makeClient();
348
355
  const ws = requireWorkspace(workspace);
356
+ nodeId = await resolveOptionalNodeId(client, nodeId);
349
357
  const result = await client.removeImageBackground(ws, { attachmentId, nodeId });
350
358
  return jsonResult(result);
351
359
  } catch (err) {
@@ -353,4 +361,90 @@ export function registerPresentationTools(server) {
353
361
  }
354
362
  },
355
363
  );
364
+
365
+ server.registerTool(
366
+ 'rootr_diagram_schema',
367
+ {
368
+ title: 'Get the spec shape for a diagram type',
369
+ description:
370
+ 'Return the JSON Schema and a worked example for one Rootr (루터) diagram family: architecture (components, ' +
371
+ 'services, stores, boundaries), workflow (swimlane process with approvals/retries), sequence (call order ' +
372
+ 'between participants), dataflow (pipelines, lineage), or lifecycle (states, retries, outcomes). ' +
373
+ 'ALWAYS call this before rootr_create_diagram_slide for a family you have not authored yet — every family ' +
374
+ 'has a different shape (a workflow wants lanes/nodes/edges, an architecture wants components/connections), ' +
375
+ 'and the renderer rejects a spec that does not match. Reading the schema first turns a guess-and-fail into ' +
376
+ 'one correct call. Pass no type to list the available families.',
377
+ annotations: { readOnlyHint: true },
378
+ inputSchema: {
379
+ type: z
380
+ .enum(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'])
381
+ .optional()
382
+ .describe('Diagram family; omit to list the available families'),
383
+ },
384
+ },
385
+ async ({ type }) => {
386
+ try {
387
+ const client = makeClient();
388
+ return jsonResult(type ? await client.diagramSchema(type) : await client.diagramTypes());
389
+ } catch (err) {
390
+ return errorResult(err);
391
+ }
392
+ },
393
+ );
394
+
395
+ server.registerTool(
396
+ 'rootr_create_diagram_slide',
397
+ {
398
+ title: 'Draw a diagram onto a slide',
399
+ description:
400
+ 'Render a typed diagram spec into a Rootr (루터) presentation slide — architecture, workflow, sequence, ' +
401
+ 'dataflow, or lifecycle. The drawing is baked into the slide, so it shows on screen AND in the PDF/PPTX ' +
402
+ 'exports. Use this instead of the mermaid `diagram` field, which the slide viewer does not draw.\n' +
403
+ 'Call rootr_diagram_schema first to get the exact shape for the family you want — the spec is validated ' +
404
+ 'fail-closed and a mismatch is rejected with the offending field named and a suggested fix, which you ' +
405
+ 'should apply and retry.\n' +
406
+ 'Meaning must live in TEXT: `title` (one assertion-style claim, e.g. "Checkout fails when the cache is ' +
407
+ 'cold" — not a topic label like "Checkout"), `blocks[]`, and `notes` are what connects into the knowledge ' +
408
+ 'graph. The picture itself is NEVER indexed, so never put a fact only in the drawing.\n' +
409
+ 'Pass `slideId` to redraw an existing slide; omit it to append a new one. Quality defaults to "standard"; ' +
410
+ '"showcase" adds a strict composition bar (turn run-ups, legend fit) meant for a hand-tuned hero diagram ' +
411
+ 'and will reject specs that "standard" accepts.',
412
+ annotations: { readOnlyHint: false, destructiveHint: false },
413
+ inputSchema: {
414
+ presentationId: z.string().describe(`PRESENTATION node id${NODE_TARGET_HINT}`),
415
+ type: z
416
+ .enum(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle'])
417
+ .describe('Diagram family — see rootr_diagram_schema'),
418
+ spec: z.record(z.any()).describe('The typed diagram spec matching that family\'s JSON Schema'),
419
+ title: z.string().optional().describe('Assertion-style slide title — text spine, also drawn as the heading'),
420
+ subtitle: z.string().optional().describe('One supporting line drawn under the title'),
421
+ blocks: z
422
+ .array(z.object({ heading: z.string().optional(), body: z.string().optional() }).passthrough())
423
+ .optional()
424
+ .describe('Text spine — the facts the diagram shows, so they reach the knowledge graph'),
425
+ notes: z.string().optional().describe('Speaker notes — also part of the text spine'),
426
+ slideId: z.string().optional().describe('Redraw this slide instead of appending a new one'),
427
+ quality: z.enum(['standard', 'showcase']).optional().describe('Default "standard"'),
428
+ },
429
+ },
430
+ async ({ presentationId, type, spec, title, subtitle, blocks, notes, slideId, quality }) => {
431
+ try {
432
+ const client = makeClient();
433
+ presentationId = await resolveNodeId(client, presentationId);
434
+ const result = await client.createDiagramSlide(presentationId, {
435
+ type,
436
+ spec,
437
+ title,
438
+ subtitle,
439
+ blocks,
440
+ notes,
441
+ slideId,
442
+ quality,
443
+ });
444
+ return jsonResult(result);
445
+ } catch (err) {
446
+ return errorResult(err);
447
+ }
448
+ },
449
+ );
356
450
  }
@@ -4,7 +4,7 @@ import { basename, extname, join, isAbsolute } from 'node:path';
4
4
 
5
5
  import { z } from 'zod';
6
6
 
7
- import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
7
+ import { jsonResult, errorResult, makeClient, requireWorkspace, resolveNodeId, NODE_TARGET_HINT, resolveOptionalNodeId } from './shared.js';
8
8
  import { RootrApiError } from '../client.js';
9
9
 
10
10
  /** 100MB — the backend's hard limit on a single audio upload. */
@@ -72,7 +72,7 @@ export function registerRecordingTools(server) {
72
72
  annotations: { readOnlyHint: false, destructiveHint: false },
73
73
  inputSchema: {
74
74
  name: z.string().optional().describe('Recording name/title, e.g. "2026-07-21 주간회의"'),
75
- parentId: z.string().optional().describe('Parent folder/node id to create the recording under'),
75
+ parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
76
76
  path: z
77
77
  .string()
78
78
  .optional()
@@ -101,6 +101,7 @@ export function registerRecordingTools(server) {
101
101
  try {
102
102
  const ws = requireWorkspace(workspace);
103
103
  const client = makeClient(ws);
104
+ parentId = await resolveOptionalNodeId(client, parentId);
104
105
  const result = await client.createRecording(ws, {
105
106
  name,
106
107
  parentId,
@@ -130,7 +131,7 @@ export function registerRecordingTools(server) {
130
131
  'rootr_transcribe_recording.',
131
132
  annotations: { readOnlyHint: false, destructiveHint: true },
132
133
  inputSchema: {
133
- recordingId: z.string().describe('RECORDING node id (create one with rootr_create_recording)'),
134
+ recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT} (create one with rootr_create_recording)`),
134
135
  filePath: z
135
136
  .string()
136
137
  .optional()
@@ -156,6 +157,7 @@ export function registerRecordingTools(server) {
156
157
  async ({ recordingId, filePath, base64Data, filename, mimeType, durationSec }) => {
157
158
  try {
158
159
  const client = makeClient();
160
+ recordingId = await resolveNodeId(client, recordingId);
159
161
  const data = readAudioBytes({ filePath, base64Data });
160
162
  if (data.length > MAX_AUDIO_BYTES) {
161
163
  throw new RootrApiError(
@@ -207,12 +209,13 @@ export function registerRecordingTools(server) {
207
209
  'TRANSCRIBING = speech-to-text still running (poll again later); ' +
208
210
  'READY = transcript available; FAILED = last transcription errored (see `error`).',
209
211
  inputSchema: {
210
- recordingId: z.string().describe('RECORDING node id'),
212
+ recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
211
213
  },
212
214
  },
213
215
  async ({ recordingId }) => {
214
216
  try {
215
217
  const client = makeClient();
218
+ recordingId = await resolveNodeId(client, recordingId);
216
219
  const result = await client.getRecording(recordingId);
217
220
  const segments = result?.transcript?.segments ?? [];
218
221
  const names = result?.speakerNames ?? {};
@@ -240,7 +243,7 @@ export function registerRecordingTools(server) {
240
243
  '{"S1":"김대표","S2":"이팀장"}. Merge-patch: only the keys you send change. The names apply to the ' +
241
244
  'transcript everywhere (web UI, rootr_read_recording, knowledge graph).',
242
245
  inputSchema: {
243
- recordingId: z.string().describe('RECORDING node id'),
246
+ recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
244
247
  speakerNames: z
245
248
  .record(z.string())
246
249
  .optional()
@@ -260,6 +263,7 @@ export function registerRecordingTools(server) {
260
263
  throw new RootrApiError('speakerNames 또는 expectedSpeakers 중 하나는 필요합니다.');
261
264
  }
262
265
  const client = makeClient();
266
+ recordingId = await resolveNodeId(client, recordingId);
263
267
  const current = await client.getRecording(recordingId);
264
268
  const merged = speakerNames
265
269
  ? { ...(current?.speakerNames ?? {}), ...speakerNames }
@@ -292,12 +296,13 @@ export function registerRecordingTools(server) {
292
296
  'take a few minutes).',
293
297
  annotations: { readOnlyHint: false, destructiveHint: true },
294
298
  inputSchema: {
295
- recordingId: z.string().describe('RECORDING node id'),
299
+ recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
296
300
  },
297
301
  },
298
302
  async ({ recordingId }) => {
299
303
  try {
300
304
  const client = makeClient();
305
+ recordingId = await resolveNodeId(client, recordingId);
301
306
  const result = await client.transcribeRecording(recordingId);
302
307
  return jsonResult({
303
308
  ...result,
@@ -326,7 +331,7 @@ export function registerRecordingTools(server) {
326
331
  'for Rootr\'s built-in summary.',
327
332
  annotations: { readOnlyHint: false, destructiveHint: false },
328
333
  inputSchema: {
329
- recordingId: z.string().describe('RECORDING node id (status must be READY)'),
334
+ recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT} (status must be READY)`),
330
335
  format: z
331
336
  .enum(['summary', 'onepager'])
332
337
  .optional()
@@ -343,6 +348,7 @@ export function registerRecordingTools(server) {
343
348
  async ({ recordingId, locale, format }) => {
344
349
  try {
345
350
  const client = makeClient();
351
+ recordingId = await resolveNodeId(client, recordingId);
346
352
  const result = await client.summarizeRecording(recordingId, { locale, format });
347
353
  return jsonResult(result);
348
354
  } catch (err) {
@@ -370,7 +376,7 @@ export function registerRecordingTools(server) {
370
376
  'this way lives wherever you put it (e.g. a document via rootr_write), not in the recording node.',
371
377
  annotations: { readOnlyHint: true },
372
378
  inputSchema: {
373
- recordingId: z.string().describe('RECORDING node id'),
379
+ recordingId: z.string().describe(`RECORDING node id${NODE_TARGET_HINT}`),
374
380
  filePath: z
375
381
  .string()
376
382
  .optional()
@@ -387,6 +393,7 @@ export function registerRecordingTools(server) {
387
393
  async ({ recordingId, filePath, urlOnly }) => {
388
394
  try {
389
395
  const client = makeClient();
396
+ recordingId = await resolveNodeId(client, recordingId);
390
397
  const rec = await client.getRecording(recordingId);
391
398
  if (!rec?.audio?.attachmentId) {
392
399
  throw new RootrApiError('이 녹음에는 아직 오디오가 없습니다. rootr_upload_recording_audio로 먼저 올리세요.');
@@ -1,6 +1,32 @@
1
1
  import { loadConfig, keyForWorkspace } from '../config.js';
2
2
  import { RootrClient, RootrApiError } from '../client.js';
3
3
  import { currentClientOverride } from '../context.js';
4
+ import { resolveTarget, requireResolved } from '../resolve.js';
5
+
6
+ /**
7
+ * Suffix for the `.describe()` of any node-id parameter: every typed-node tool
8
+ * takes a path too, exactly like rootr_read, so an agent that only knows where
9
+ * a node lives in the tree never has to hunt for its uuid.
10
+ */
11
+ export const NODE_TARGET_HINT = ', or its path starting with "/"';
12
+
13
+ /**
14
+ * Resolve a path-or-id argument to a node id. Paths go through the by-path
15
+ * endpoint; anything else is already an id and is passed through untouched.
16
+ */
17
+ export async function resolveNodeId(client, target) {
18
+ return requireResolved(await resolveTarget(client, target));
19
+ }
20
+
21
+ /**
22
+ * Same as resolveNodeId, but for optional/nullable arguments (parentId,
23
+ * targetDatabaseId, scopeNodeId …) — undefined/null pass through untouched so
24
+ * "omitted" and "explicitly cleared" keep their meaning.
25
+ */
26
+ export async function resolveOptionalNodeId(client, target) {
27
+ if (target === undefined || target === null || target === '') return target;
28
+ return resolveNodeId(client, target);
29
+ }
4
30
 
5
31
  export function textResult(text) {
6
32
  return { content: [{ type: 'text', text }] };