rootr-cli 0.4.0 → 0.4.1

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/README.md CHANGED
@@ -206,6 +206,13 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
206
206
 
207
207
  `rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 29종:
208
208
 
209
+ 노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
210
+ `recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
211
+ 받습니다 — CLI와 같은 규칙으로 `/`로 시작하면 경로로 해석합니다.
212
+ 즉 `rootr_add_row`에 `databaseId: "/제작 공정/칸반"`을 그대로 넘길 수 있어,
213
+ 경로만 아는 에이전트가 uuid를 찾아 헤맬 필요가 없습니다.
214
+ `rootr_list`·`rootr_search` 출력도 각 줄 끝에 `[노드 id]`를 함께 표시합니다.
215
+
209
216
  #### 문서 (workspace 키, docs:read/docs:write)
210
217
 
211
218
  | 도구 | 설명 |
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
 
3
3
  import { z } from 'zod';
4
4
 
5
- import { jsonResult, errorResult, makeClient, requireWorkspace } from './shared.js';
5
+ import { jsonResult, errorResult, makeClient, requireWorkspace, resolveOptionalNodeId, NODE_TARGET_HINT } from './shared.js';
6
6
  import { resolveTarget, requireResolved } from '../resolve.js';
7
7
  import { RootrApiError } from '../client.js';
8
8
 
@@ -24,7 +24,7 @@ const uploadInputSchema = {
24
24
  .string()
25
25
  .optional()
26
26
  .describe('Base64-encoded file bytes; use this when the caller has the file in memory instead of on disk'),
27
- nodeId: z.string().optional().describe('Document/node id to associate the attachment with (permission-checked)'),
27
+ nodeId: z.string().optional().describe(`Document/node id${NODE_TARGET_HINT} to associate the attachment with (permission-checked)`),
28
28
  workspace: z.string().optional().describe('Workspace id; defaults to ROOTR_WORKSPACE/config'),
29
29
  };
30
30
 
@@ -48,6 +48,7 @@ export function registerAttachmentTools(server) {
48
48
  try {
49
49
  const client = makeClient();
50
50
  const ws = requireWorkspace(workspace);
51
+ nodeId = await resolveOptionalNodeId(client, nodeId);
51
52
  const data = readAttachmentBytes({ filePath, base64Data });
52
53
  const { id } = await client.uploadAttachment(ws, { filename, mimeType, nodeId, data });
53
54
  const url = client.attachmentRawUrl(id);
@@ -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
  * Shared context repeated (shortened) across tool descriptions so an agent
@@ -61,7 +61,7 @@ export function registerCrmTools(server) {
61
61
  inputSchema: {
62
62
  name: z.string().optional().describe('CRM node name'),
63
63
  path: z.string().optional().describe('Full tree path to create at, e.g. "/sales/crm"'),
64
- parentId: z.string().optional().describe('Parent folder/node id to create the CRM under'),
64
+ parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
65
65
  icon: z.string().optional().describe('Emoji icon for the CRM node'),
66
66
  pipelines: z
67
67
  .array(pipelineShape)
@@ -81,6 +81,7 @@ export function registerCrmTools(server) {
81
81
  try {
82
82
  const ws = requireWorkspace(workspace);
83
83
  const client = makeClient(ws);
84
+ parentId = await resolveOptionalNodeId(client, parentId);
84
85
  const result = await client.createCrm(ws, {
85
86
  name,
86
87
  path,
@@ -111,7 +112,7 @@ export function registerCrmTools(server) {
111
112
  CRM_CONTEXT,
112
113
  annotations: { readOnlyHint: true, destructiveHint: false },
113
114
  inputSchema: {
114
- crmId: z.string().describe('CRM node id'),
115
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
115
116
  includeSummary: z
116
117
  .boolean()
117
118
  .optional()
@@ -121,6 +122,7 @@ export function registerCrmTools(server) {
121
122
  async ({ crmId, includeSummary }) => {
122
123
  try {
123
124
  const client = makeClient();
125
+ crmId = await resolveNodeId(client, crmId);
124
126
  const crm = await client.getCrm(crmId);
125
127
  if (includeSummary) {
126
128
  const summary = await client.getCrmSummary(crmId);
@@ -144,7 +146,7 @@ export function registerCrmTools(server) {
144
146
  CRM_CONTEXT,
145
147
  annotations: { readOnlyHint: false, destructiveHint: false },
146
148
  inputSchema: {
147
- crmId: z.string().describe('CRM node id'),
149
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
148
150
  pipelines: z
149
151
  .array(pipelineShape)
150
152
  .optional()
@@ -157,6 +159,7 @@ export function registerCrmTools(server) {
157
159
  async ({ crmId, pipelines, fields, config, icon }) => {
158
160
  try {
159
161
  const client = makeClient();
162
+ crmId = await resolveNodeId(client, crmId);
160
163
  const result = await client.updateCrm(crmId, {
161
164
  pipelines: coerceJson(pipelines),
162
165
  fields: coerceJson(fields),
@@ -182,7 +185,7 @@ export function registerCrmTools(server) {
182
185
  CRM_CONTEXT,
183
186
  annotations: { readOnlyHint: true, destructiveHint: false },
184
187
  inputSchema: {
185
- crmId: z.string().describe('CRM node id'),
188
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
186
189
  q: z.string().optional().describe('Free-text search (name/domain/etc.)'),
187
190
  ownerId: z.string().optional().describe('Filter by owning user id'),
188
191
  tag: z.string().optional().describe('Filter by tag'),
@@ -194,6 +197,7 @@ export function registerCrmTools(server) {
194
197
  async ({ crmId, q, ownerId, tag, sort, limit, offset }) => {
195
198
  try {
196
199
  const client = makeClient();
200
+ crmId = await resolveNodeId(client, crmId);
197
201
  const result = await client.listCrmCompanies(crmId, { q, ownerId, tag, sort, limit, offset });
198
202
  return jsonResult(result);
199
203
  } catch (err) {
@@ -213,7 +217,7 @@ export function registerCrmTools(server) {
213
217
  CRM_CONTEXT,
214
218
  annotations: { readOnlyHint: false, destructiveHint: false },
215
219
  inputSchema: {
216
- crmId: z.string().describe('CRM node id'),
220
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
217
221
  companyId: z.string().optional().describe('Existing company id to update/delete; omit to create a new company'),
218
222
  delete: z.boolean().optional().describe('If true (with companyId), delete the company instead of updating'),
219
223
  name: z.string().optional().describe('Company name (required when creating)'),
@@ -231,6 +235,7 @@ export function registerCrmTools(server) {
231
235
  async ({ crmId, companyId, delete: doDelete, custom, ...rest }) => {
232
236
  try {
233
237
  const client = makeClient();
238
+ crmId = await resolveNodeId(client, crmId);
234
239
  if (companyId && doDelete) {
235
240
  await client.deleteCrmCompany(companyId);
236
241
  return jsonResult({ deleted: true, companyId });
@@ -283,7 +288,7 @@ export function registerCrmTools(server) {
283
288
  CRM_CONTEXT,
284
289
  annotations: { readOnlyHint: true, destructiveHint: false },
285
290
  inputSchema: {
286
- crmId: z.string().describe('CRM node id'),
291
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
287
292
  q: z.string().optional().describe('Free-text search (name/email/etc.)'),
288
293
  companyId: z.string().optional().describe('Filter by company id'),
289
294
  lifecycleStage: z.string().optional().describe('lead | mql | sql | opportunity | customer | evangelist'),
@@ -298,6 +303,7 @@ export function registerCrmTools(server) {
298
303
  async ({ crmId, q, companyId, lifecycleStage, leadStatus, ownerId, tag, sort, limit, offset }) => {
299
304
  try {
300
305
  const client = makeClient();
306
+ crmId = await resolveNodeId(client, crmId);
301
307
  const result = await client.listCrmContacts(crmId, {
302
308
  q,
303
309
  companyId,
@@ -327,7 +333,7 @@ export function registerCrmTools(server) {
327
333
  CRM_CONTEXT,
328
334
  annotations: { readOnlyHint: false, destructiveHint: false },
329
335
  inputSchema: {
330
- crmId: z.string().describe('CRM node id'),
336
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
331
337
  contactId: z.string().optional().describe('Existing contact id to update/delete; omit to create a new contact'),
332
338
  delete: z.boolean().optional().describe('If true (with contactId), delete the contact instead of updating'),
333
339
  name: z.string().optional().describe('Contact name (required when creating)'),
@@ -353,6 +359,7 @@ export function registerCrmTools(server) {
353
359
  async ({ crmId, contactId, delete: doDelete, custom, ...rest }) => {
354
360
  try {
355
361
  const client = makeClient();
362
+ crmId = await resolveNodeId(client, crmId);
356
363
  if (contactId && doDelete) {
357
364
  await client.deleteCrmContact(contactId);
358
365
  return jsonResult({ deleted: true, contactId });
@@ -401,7 +408,7 @@ export function registerCrmTools(server) {
401
408
  CRM_CONTEXT,
402
409
  annotations: { readOnlyHint: true, destructiveHint: false },
403
410
  inputSchema: {
404
- crmId: z.string().describe('CRM node id'),
411
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
405
412
  q: z.string().optional().describe('Free-text search (deal name/etc.)'),
406
413
  pipelineId: z.string().optional().describe('Filter by pipeline id'),
407
414
  stageId: z.string().optional().describe('Filter by stage id'),
@@ -418,6 +425,7 @@ export function registerCrmTools(server) {
418
425
  async ({ crmId, q, pipelineId, stageId, status, ownerId, companyId, contactId, tag, sort, limit, offset }) => {
419
426
  try {
420
427
  const client = makeClient();
428
+ crmId = await resolveNodeId(client, crmId);
421
429
  const result = await client.listCrmDeals(crmId, {
422
430
  q,
423
431
  pipelineId,
@@ -450,7 +458,7 @@ export function registerCrmTools(server) {
450
458
  CRM_CONTEXT,
451
459
  annotations: { readOnlyHint: false, destructiveHint: false },
452
460
  inputSchema: {
453
- crmId: z.string().describe('CRM node id'),
461
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
454
462
  dealId: z.string().optional().describe('Existing deal id to update/delete; omit to create a new deal'),
455
463
  delete: z.boolean().optional().describe('If true (with dealId), delete the deal instead of updating'),
456
464
  name: z.string().optional().describe('Deal name (required when creating)'),
@@ -478,6 +486,7 @@ export function registerCrmTools(server) {
478
486
  async ({ crmId, dealId, delete: doDelete, contacts, custom, ...rest }) => {
479
487
  try {
480
488
  const client = makeClient();
489
+ crmId = await resolveNodeId(client, crmId);
481
490
  if (dealId && doDelete) {
482
491
  await client.deleteCrmDeal(dealId);
483
492
  return jsonResult({ deleted: true, dealId });
@@ -552,7 +561,7 @@ export function registerCrmTools(server) {
552
561
  CRM_CONTEXT,
553
562
  annotations: { readOnlyHint: false, destructiveHint: false },
554
563
  inputSchema: {
555
- crmId: z.string().describe('CRM node id'),
564
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
556
565
  type: z.string().describe('note | call | email | meeting'),
557
566
  subject: z.string().optional(),
558
567
  body: z.string().optional().describe('Activity body/notes (markdown/plain text)'),
@@ -565,6 +574,7 @@ export function registerCrmTools(server) {
565
574
  async ({ crmId, ...rest }) => {
566
575
  try {
567
576
  const client = makeClient();
577
+ crmId = await resolveNodeId(client, crmId);
568
578
  const result = await client.createCrmActivity(crmId, rest);
569
579
  return jsonResult(result);
570
580
  } catch (err) {
@@ -582,7 +592,7 @@ export function registerCrmTools(server) {
582
592
  CRM_CONTEXT,
583
593
  annotations: { readOnlyHint: true, destructiveHint: false },
584
594
  inputSchema: {
585
- crmId: z.string().describe('CRM node id'),
595
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
586
596
  contactId: z.string().optional(),
587
597
  companyId: z.string().optional(),
588
598
  dealId: z.string().optional(),
@@ -594,6 +604,7 @@ export function registerCrmTools(server) {
594
604
  async ({ crmId, contactId, companyId, dealId, type, limit, offset }) => {
595
605
  try {
596
606
  const client = makeClient();
607
+ crmId = await resolveNodeId(client, crmId);
597
608
  const result = await client.listCrmActivities(crmId, { contactId, companyId, dealId, type, limit, offset });
598
609
  return jsonResult(result);
599
610
  } catch (err) {
@@ -615,7 +626,7 @@ export function registerCrmTools(server) {
615
626
  CRM_CONTEXT,
616
627
  annotations: { readOnlyHint: false, destructiveHint: false },
617
628
  inputSchema: {
618
- crmId: z.string().describe('CRM node id (required when creating; ignored when updating)'),
629
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT} (required when creating; ignored when updating)`),
619
630
  taskId: z.string().optional().describe('Existing task id to update; omit to create a new task'),
620
631
  title: z.string().optional().describe('Task title (required when creating)'),
621
632
  type: z.string().optional().describe('todo | call | email | meeting'),
@@ -630,6 +641,7 @@ export function registerCrmTools(server) {
630
641
  async ({ crmId, taskId, ...rest }) => {
631
642
  try {
632
643
  const client = makeClient();
644
+ crmId = await resolveNodeId(client, crmId);
633
645
  const result = taskId ? await client.updateCrmTask(taskId, rest) : await client.createCrmTask(crmId, rest);
634
646
  return jsonResult(result);
635
647
  } catch (err) {
@@ -648,7 +660,7 @@ export function registerCrmTools(server) {
648
660
  CRM_CONTEXT,
649
661
  annotations: { readOnlyHint: true, destructiveHint: false },
650
662
  inputSchema: {
651
- crmId: z.string().describe('CRM node id'),
663
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
652
664
  status: z.string().optional().describe('open | done | all'),
653
665
  assigneeId: z.string().optional(),
654
666
  due: z.string().optional().describe('overdue | today | upcoming'),
@@ -662,6 +674,7 @@ export function registerCrmTools(server) {
662
674
  async ({ crmId, status, assigneeId, due, contactId, companyId, dealId, limit, offset }) => {
663
675
  try {
664
676
  const client = makeClient();
677
+ crmId = await resolveNodeId(client, crmId);
665
678
  const result = await client.listCrmTasks(crmId, {
666
679
  status,
667
680
  assigneeId,
@@ -693,7 +706,7 @@ export function registerCrmTools(server) {
693
706
  CRM_CONTEXT,
694
707
  annotations: { readOnlyHint: false, destructiveHint: false },
695
708
  inputSchema: {
696
- crmId: z.string().describe('CRM node id'),
709
+ crmId: z.string().describe(`CRM node id${NODE_TARGET_HINT}`),
697
710
  entity: z.string().describe('contact | company | deal'),
698
711
  rows: z.array(z.record(z.unknown())).describe('Array of row objects to import, matching the entity shape'),
699
712
  },
@@ -701,6 +714,7 @@ export function registerCrmTools(server) {
701
714
  async ({ crmId, entity, rows }) => {
702
715
  try {
703
716
  const client = makeClient();
717
+ crmId = await resolveNodeId(client, crmId);
704
718
  const result = await client.importCrm(crmId, { entity, rows: coerceJson(rows) });
705
719
  return jsonResult(result);
706
720
  } catch (err) {
@@ -1,6 +1,9 @@
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
+
5
+ /** Human text for the `databaseId` param — accepts a path just like the document tools. */
6
+ const DATABASE_TARGET_DESC = `DATABASE node id${NODE_TARGET_HINT} (e.g. "/제작 공정/칸반")`;
4
7
 
5
8
  /**
6
9
  * Resolve a `{propertyNameOrId: value}` map to `{propertyId: value}` using the
@@ -40,15 +43,16 @@ export function registerDatabaseTools(server) {
40
43
  'property ids/names and, for board/kanban views, which select column holds the status. Only works on DATABASE nodes.',
41
44
  annotations: { readOnlyHint: true, destructiveHint: false },
42
45
  inputSchema: {
43
- databaseId: z.string().describe('DATABASE node id'),
46
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
44
47
  },
45
48
  },
46
49
  async ({ databaseId }) => {
47
50
  try {
48
51
  const client = makeClient();
52
+ const id = await resolveNodeId(client, databaseId);
49
53
  const [database, rows] = await Promise.all([
50
- client.getDatabase(databaseId),
51
- client.listRows(databaseId),
54
+ client.getDatabase(id),
55
+ client.listRows(id),
52
56
  ]);
53
57
  return jsonResult({ database, rows });
54
58
  } catch (err) {
@@ -66,13 +70,13 @@ export function registerDatabaseTools(server) {
66
70
  'Use rootr_read_database instead when you also need the property/column definitions.',
67
71
  annotations: { readOnlyHint: true, destructiveHint: false },
68
72
  inputSchema: {
69
- databaseId: z.string().describe('DATABASE node id'),
73
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
70
74
  },
71
75
  },
72
76
  async ({ databaseId }) => {
73
77
  try {
74
78
  const client = makeClient();
75
- const rows = await client.listRows(databaseId);
79
+ const rows = await client.listRows(await resolveNodeId(client, databaseId));
76
80
  return jsonResult({ rows });
77
81
  } catch (err) {
78
82
  return errorResult(err);
@@ -91,7 +95,7 @@ export function registerDatabaseTools(server) {
91
95
  'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index.',
92
96
  annotations: { readOnlyHint: false, destructiveHint: false },
93
97
  inputSchema: {
94
- databaseId: z.string().describe('DATABASE node id'),
98
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
95
99
  values: z.record(z.unknown()).describe('{ propertyNameOrId: value } for the new row'),
96
100
  position: z.number().optional().describe('0-based insert index; appended at the end if omitted'),
97
101
  },
@@ -99,9 +103,10 @@ export function registerDatabaseTools(server) {
99
103
  async ({ databaseId, values, position }) => {
100
104
  try {
101
105
  const client = makeClient();
102
- const database = await client.getDatabase(databaseId);
106
+ const id = await resolveNodeId(client, databaseId);
107
+ const database = await client.getDatabase(id);
103
108
  const resolved = resolveValueKeys(database.properties || [], values);
104
- const row = await client.createRow(databaseId, { values: resolved, position });
109
+ const row = await client.createRow(id, { values: resolved, position });
105
110
  return jsonResult(row);
106
111
  } catch (err) {
107
112
  return errorResult(err);
@@ -120,7 +125,7 @@ export function registerDatabaseTools(server) {
120
125
  'Optionally set `position` to reorder the row.',
121
126
  annotations: { readOnlyHint: false, destructiveHint: false },
122
127
  inputSchema: {
123
- databaseId: z.string().describe('DATABASE node id'),
128
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
124
129
  rowId: z.string().describe('Row id (must belong to the database)'),
125
130
  values: z.record(z.unknown()).optional().describe('{ propertyNameOrId: newValue | null } partial patch'),
126
131
  position: z.number().optional().describe('New 0-based position for the row'),
@@ -129,12 +134,13 @@ export function registerDatabaseTools(server) {
129
134
  async ({ databaseId, rowId, values, position }) => {
130
135
  try {
131
136
  const client = makeClient();
137
+ const id = await resolveNodeId(client, databaseId);
132
138
  let resolved;
133
139
  if (values !== undefined) {
134
- const database = await client.getDatabase(databaseId);
140
+ const database = await client.getDatabase(id);
135
141
  resolved = resolveValueKeys(database.properties || [], values);
136
142
  }
137
- const row = await client.updateRow(databaseId, rowId, { values: resolved, position });
143
+ const row = await client.updateRow(id, rowId, { values: resolved, position });
138
144
  return jsonResult(row);
139
145
  } catch (err) {
140
146
  return errorResult(err);
@@ -151,14 +157,14 @@ export function registerDatabaseTools(server) {
151
157
  'Verify the row via rootr_read_database first — do not guess rowIds.',
152
158
  annotations: { readOnlyHint: false, destructiveHint: true },
153
159
  inputSchema: {
154
- databaseId: z.string().describe('DATABASE node id'),
160
+ databaseId: z.string().describe(DATABASE_TARGET_DESC),
155
161
  rowId: z.string().describe('Row id to delete (must belong to the database)'),
156
162
  },
157
163
  },
158
164
  async ({ databaseId, rowId }) => {
159
165
  try {
160
166
  const client = makeClient();
161
- await client.deleteRow(databaseId, rowId);
167
+ await client.deleteRow(await resolveNodeId(client, databaseId), rowId);
162
168
  return jsonResult({ deleted: true, rowId });
163
169
  } catch (err) {
164
170
  return errorResult(err);
@@ -31,7 +31,12 @@ export function registerDocumentTools(server) {
31
31
  const p = n.path || '';
32
32
  return p === path || p.startsWith(path.endsWith('/') ? path : path + '/');
33
33
  })
34
- .map((n) => `${n.type || '?'} ${n.path || n.name || n.id}`)
34
+ .map((n) => {
35
+ const label = n.path || n.name || n.id;
36
+ // Keep the id visible: tools that take a node id (databases, sheets,
37
+ // whiteboards, …) are otherwise unreachable from a listing.
38
+ return n.id && label !== n.id ? `${n.type || '?'} ${label} [${n.id}]` : `${n.type || '?'} ${label}`;
39
+ })
35
40
  .sort();
36
41
  return textResult(rows.length ? rows.join('\n') : '(no matching nodes)');
37
42
  } catch (err) {
@@ -189,7 +194,11 @@ export function registerDocumentTools(server) {
189
194
  try {
190
195
  const client = makeClient();
191
196
  const results = await client.search(query);
192
- const rows = results.map((r) => `${r.path || r.name || r.nodeId} — ${r.snippet || ''}`);
197
+ const rows = results.map((r) => {
198
+ const label = r.path || r.name || r.nodeId;
199
+ const id = r.nodeId && label !== r.nodeId ? ` [${r.nodeId}]` : '';
200
+ return `${label}${id} — ${r.snippet || ''}`;
201
+ });
193
202
  return textResult(rows.length ? rows.join('\n') : '(no results)');
194
203
  } catch (err) {
195
204
  return errorResult(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
  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) {
@@ -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 }] };
@@ -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 CELL_VALUE = z
6
6
  .union([z.string(), z.number(), z.boolean(), z.null()])
@@ -32,7 +32,7 @@ export function registerSpreadsheetTools(server) {
32
32
  annotations: { readOnlyHint: false, destructiveHint: false },
33
33
  inputSchema: {
34
34
  name: z.string().optional().describe('Spreadsheet name/title'),
35
- parentId: z.string().optional().describe('Parent folder/node id to create it under'),
35
+ parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
36
36
  icon: z.string().optional().describe('Emoji icon for the node'),
37
37
  data: z
38
38
  .object({ sheets: z.array(z.record(z.unknown())).optional() })
@@ -46,6 +46,7 @@ export function registerSpreadsheetTools(server) {
46
46
  try {
47
47
  const ws = requireWorkspace(workspace);
48
48
  const client = makeClient(ws);
49
+ parentId = await resolveOptionalNodeId(client, parentId);
49
50
  const result = await client.createSpreadsheet(ws, { name, parentId, icon, data, config });
50
51
  return jsonResult(result);
51
52
  } catch (err) {
@@ -64,12 +65,13 @@ export function registerSpreadsheetTools(server) {
64
65
  'and cell contents.',
65
66
  annotations: { readOnlyHint: true, destructiveHint: false },
66
67
  inputSchema: {
67
- spreadsheetId: z.string().describe('SPREADSHEET node id'),
68
+ spreadsheetId: z.string().describe(`SPREADSHEET node id${NODE_TARGET_HINT}`),
68
69
  },
69
70
  },
70
71
  async ({ spreadsheetId }) => {
71
72
  try {
72
73
  const client = makeClient();
74
+ spreadsheetId = await resolveNodeId(client, spreadsheetId);
73
75
  const result = await client.getSpreadsheet(spreadsheetId);
74
76
  return jsonResult(result);
75
77
  } catch (err) {
@@ -88,7 +90,7 @@ export function registerSpreadsheetTools(server) {
88
90
  'you want to keep, or prefer rootr_create_sheet / rootr_patch_spreadsheet_cells for incremental changes).',
89
91
  annotations: { readOnlyHint: false, destructiveHint: false },
90
92
  inputSchema: {
91
- spreadsheetId: z.string().describe('SPREADSHEET node id'),
93
+ spreadsheetId: z.string().describe(`SPREADSHEET node id${NODE_TARGET_HINT}`),
92
94
  sheets: z
93
95
  .array(z.record(z.unknown()))
94
96
  .optional()
@@ -100,6 +102,7 @@ export function registerSpreadsheetTools(server) {
100
102
  async ({ spreadsheetId, sheets, config, icon }) => {
101
103
  try {
102
104
  const client = makeClient();
105
+ spreadsheetId = await resolveNodeId(client, spreadsheetId);
103
106
  const result = await client.updateSpreadsheet(spreadsheetId, { sheets, config, icon });
104
107
  return jsonResult(result);
105
108
  } catch (err) {
@@ -115,7 +118,7 @@ export function registerSpreadsheetTools(server) {
115
118
  description: 'Add a new empty sheet (tab) to an existing Rootr (루터) SPREADSHEET, without touching other sheets.',
116
119
  annotations: { readOnlyHint: false, destructiveHint: false },
117
120
  inputSchema: {
118
- spreadsheetId: z.string().describe('SPREADSHEET node id'),
121
+ spreadsheetId: z.string().describe(`SPREADSHEET node id${NODE_TARGET_HINT}`),
119
122
  name: z.string().optional().describe('Sheet name, e.g. "Q1 Budget"'),
120
123
  rowCount: z.number().int().min(1).max(10_000).optional().describe('Row count (default 1000)'),
121
124
  colCount: z.number().int().min(1).max(500).optional().describe('Column count (default 52)'),
@@ -124,6 +127,7 @@ export function registerSpreadsheetTools(server) {
124
127
  async ({ spreadsheetId, name, rowCount, colCount }) => {
125
128
  try {
126
129
  const client = makeClient();
130
+ spreadsheetId = await resolveNodeId(client, spreadsheetId);
127
131
  const result = await client.createSheet(spreadsheetId, { name, rowCount, colCount });
128
132
  return jsonResult(result);
129
133
  } catch (err) {
@@ -141,7 +145,7 @@ export function registerSpreadsheetTools(server) {
141
145
  'Call rootr_read_spreadsheet first to get the exact sheetId.',
142
146
  annotations: { readOnlyHint: false, destructiveHint: false },
143
147
  inputSchema: {
144
- spreadsheetId: z.string().describe('SPREADSHEET node id'),
148
+ spreadsheetId: z.string().describe(`SPREADSHEET node id${NODE_TARGET_HINT}`),
145
149
  sheetId: z.string().describe('Sheet id to update'),
146
150
  name: z.string().optional().describe('New sheet name'),
147
151
  rowCount: z.number().int().min(1).max(10_000).optional(),
@@ -157,6 +161,7 @@ export function registerSpreadsheetTools(server) {
157
161
  async ({ spreadsheetId, sheetId, name, rowCount, colCount, rowSizes, colSizes, merges }) => {
158
162
  try {
159
163
  const client = makeClient();
164
+ spreadsheetId = await resolveNodeId(client, spreadsheetId);
160
165
  const result = await client.patchSheet(spreadsheetId, sheetId, {
161
166
  name,
162
167
  rowCount,
@@ -179,13 +184,14 @@ export function registerSpreadsheetTools(server) {
179
184
  description: 'Delete one sheet (tab) from a Rootr (루터) SPREADSHEET by sheetId. Not recoverable — read first to confirm.',
180
185
  annotations: { readOnlyHint: false, destructiveHint: true },
181
186
  inputSchema: {
182
- spreadsheetId: z.string().describe('SPREADSHEET node id'),
187
+ spreadsheetId: z.string().describe(`SPREADSHEET node id${NODE_TARGET_HINT}`),
183
188
  sheetId: z.string().describe('Sheet id to delete'),
184
189
  },
185
190
  },
186
191
  async ({ spreadsheetId, sheetId }) => {
187
192
  try {
188
193
  const client = makeClient();
194
+ spreadsheetId = await resolveNodeId(client, spreadsheetId);
189
195
  await client.deleteSheet(spreadsheetId, sheetId);
190
196
  return jsonResult({ deleted: sheetId });
191
197
  } catch (err) {
@@ -204,7 +210,7 @@ export function registerSpreadsheetTools(server) {
204
210
  CELLS_GUIDE,
205
211
  annotations: { readOnlyHint: false, destructiveHint: false },
206
212
  inputSchema: {
207
- spreadsheetId: z.string().describe('SPREADSHEET node id'),
213
+ spreadsheetId: z.string().describe(`SPREADSHEET node id${NODE_TARGET_HINT}`),
208
214
  sheetId: z.string().describe('Sheet id to patch'),
209
215
  cells: z
210
216
  .record(cellShape.nullable())
@@ -214,6 +220,7 @@ export function registerSpreadsheetTools(server) {
214
220
  async ({ spreadsheetId, sheetId, cells }) => {
215
221
  try {
216
222
  const client = makeClient();
223
+ spreadsheetId = await resolveNodeId(client, spreadsheetId);
217
224
  const result = await client.patchCells(spreadsheetId, sheetId, cells);
218
225
  return jsonResult(result);
219
226
  } 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, resolveOptionalNodeId, NODE_TARGET_HINT } from './shared.js';
4
4
 
5
5
  const VALID_EVENTS = [
6
6
  'document.created',
@@ -60,7 +60,7 @@ export function registerWebhookTools(server) {
60
60
  url: z.string().describe('HTTPS endpoint to receive webhook POSTs'),
61
61
  events: z.array(z.string()).describe(`Event names to subscribe to, from: ${VALID_EVENTS.join(', ')}`),
62
62
  name: z.string().optional().describe('Human-readable name for the webhook'),
63
- scopeNodeId: z.string().optional().describe('Restrict to this node and its descendants'),
63
+ scopeNodeId: z.string().optional().describe(`Node id${NODE_TARGET_HINT} — restrict to this node and its descendants`),
64
64
  filterTags: z.array(z.string()).optional().describe('Only fire for nodes carrying one of these tags'),
65
65
  debounceSeconds: z.number().optional().describe('Trailing debounce window in seconds (server-capped)'),
66
66
  workspace: z
@@ -73,6 +73,7 @@ export function registerWebhookTools(server) {
73
73
  try {
74
74
  const ws = requireWorkspace(workspace);
75
75
  const client = makeClient(ws);
76
+ scopeNodeId = await resolveOptionalNodeId(client, scopeNodeId);
76
77
  const result = await client.createWebhook(ws, {
77
78
  name,
78
79
  url,
@@ -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 shapeShape = z
6
6
  .object({
@@ -67,7 +67,7 @@ export function registerWhiteboardTools(server) {
67
67
  annotations: { readOnlyHint: false, destructiveHint: false },
68
68
  inputSchema: {
69
69
  name: z.string().optional().describe('Whiteboard name/title'),
70
- parentId: z.string().optional().describe('Parent folder/node id to create it under'),
70
+ parentId: z.string().optional().describe(`Parent folder/node id${NODE_TARGET_HINT}`),
71
71
  icon: z.string().optional().describe('Emoji icon for the node'),
72
72
  scene: z
73
73
  .union([sceneShape, excalidrawSceneShape])
@@ -81,6 +81,7 @@ export function registerWhiteboardTools(server) {
81
81
  try {
82
82
  const ws = requireWorkspace(workspace);
83
83
  const client = makeClient(ws);
84
+ parentId = await resolveOptionalNodeId(client, parentId);
84
85
  const result = await client.createWhiteboard(ws, { name, parentId, icon, scene, config });
85
86
  return jsonResult(result);
86
87
  } catch (err) {
@@ -101,13 +102,14 @@ export function registerWhiteboardTools(server) {
101
102
  'the current ids.',
102
103
  annotations: { readOnlyHint: true, destructiveHint: false },
103
104
  inputSchema: {
104
- whiteboardId: z.string().describe('WHITEBOARD node id'),
105
+ whiteboardId: z.string().describe(`WHITEBOARD node id${NODE_TARGET_HINT}`),
105
106
  mode: z.enum(['summary', 'full']).optional().describe('"summary" (default) or "full" (raw elements too)'),
106
107
  },
107
108
  },
108
109
  async ({ whiteboardId, mode }) => {
109
110
  try {
110
111
  const client = makeClient();
112
+ whiteboardId = await resolveNodeId(client, whiteboardId);
111
113
  const result = await client.getWhiteboard(whiteboardId);
112
114
  const { scene, ...rest } = result ?? {};
113
115
  if (mode !== 'full') return jsonResult(rest);
@@ -139,7 +141,7 @@ export function registerWhiteboardTools(server) {
139
141
  AUTHORING_GUIDE,
140
142
  annotations: { readOnlyHint: false, destructiveHint: false },
141
143
  inputSchema: {
142
- whiteboardId: z.string().describe('WHITEBOARD node id'),
144
+ whiteboardId: z.string().describe(`WHITEBOARD node id${NODE_TARGET_HINT}`),
143
145
  scene: z
144
146
  .union([sceneShape, excalidrawSceneShape])
145
147
  .optional()
@@ -155,6 +157,7 @@ export function registerWhiteboardTools(server) {
155
157
  async ({ whiteboardId, scene, merge, config, icon }) => {
156
158
  try {
157
159
  const client = makeClient();
160
+ whiteboardId = await resolveNodeId(client, whiteboardId);
158
161
  const result = await client.updateWhiteboard(whiteboardId, { scene, merge, config, icon });
159
162
  const { scene: _scene, ...rest } = result ?? {};
160
163
  return jsonResult(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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": {