rootr-cli 0.8.0 → 0.8.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
@@ -296,15 +296,20 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
296
296
 
297
297
  | 도구 | 설명 |
298
298
  |---|---|
299
- | `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** |
300
- | `rootr_list_rows` | 행만 조회(스키마 제외) |
301
- | `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로 지정) |
302
- | `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값 변경) |
299
+ | `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** (`includeBody: true` 면 행마다 상세 내용까지) |
300
+ | `rootr_list_rows` | 행만 조회(스키마 제외, `includeBody` 지원) |
301
+ | `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로 지정, `body`로 상세 내용까지) |
302
+ | `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값 변경, `body`로 상세 내용 교체) |
303
303
  | `rootr_delete_row` | 행 삭제 |
304
304
  | `rootr_add_column` | **열 추가** — 기존 열·행은 그대로 |
305
305
  | `rootr_update_column` | 열 이름 변경 / select 선택지 교체 |
306
306
  | `rootr_delete_column` | 열 삭제(파괴적) |
307
307
 
308
+ 행의 **상세 내용(본문 메모)** — 화면에서 칸반 카드를 열면 속성 아래에 적는 그 글 — 은
309
+ `body`로 오갑니다. 실체는 표 밑에 달리는 문서 한 장이고, 없으면 쓸 때 만들어집니다.
310
+ 명령줄에서도 됩니다: `rootr db row-add <표> --values '{"제목":"킥오프"}' --body "..."`,
311
+ `rootr db row-body <표> <rowId> --file memo.md`, `rootr db rows <표> --body`.
312
+
308
313
  열은 **한 번에 하나씩** 다룹니다. REST의 `PATCH /databases/:id`는 `properties`
309
314
  배열을 통째로 교체하기 때문에, 에이전트가 배열을 다시 만들다 빠뜨린 열이 그대로
310
315
  사라집니다. 위 세 도구는 서버가 병합하므로 나머지 열이 없어지지 않습니다.
package/bin/rootr.js CHANGED
@@ -27,6 +27,9 @@ const USAGE = `Rootr CLI v${pkg.version}
27
27
  rootr use <id|이름|주소> 활성 워크스페이스 전환 (그 워크스페이스용 키도 기억)
28
28
  rootr ask <question> [--workspace <w>] GraphRAG 질의응답 (RCA), 인용 포함 출력
29
29
  rootr db create [--file <spec.json>] [--parent <path|id>] 표(DATABASE) 생성 — 컬럼 순서 그대로, 행까지 한 번에
30
+ rootr db rows <path|id> [--body] [--json] 표의 행 목록 (--body: 상세 내용까지)
31
+ rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
32
+ rootr db row-body <path|id> <rowId> [--file <f>] 행의 상세 내용(본문 메모) 쓰기 — 없으면 표준입력
30
33
  rootr config [--api-key <k>] [--workspace <w>] [--base-url <u>] 설정 조회/저장
31
34
  rootr config --add-key <k> [--label <name>] [--workspace <w>] 워크스페이스별 키 추가 등록
32
35
  rootr mcp MCP stdio 서버 구동 (도구 90종+ — README 참고)
@@ -444,8 +447,13 @@ async function cmdMcp() {
444
447
  */
445
448
  async function cmdDb(args) {
446
449
  const sub = args._[0];
450
+ if (sub === 'rows') return cmdDbRows(args);
451
+ if (sub === 'row-add') return cmdDbRowAdd(args);
452
+ if (sub === 'row-body') return cmdDbRowBody(args);
447
453
  if (sub !== 'create') {
448
- throw new Error('사용법: rootr db create [--file <spec.json>] [--parent <path|id>] [--ws <id>]');
454
+ throw new Error(
455
+ '사용법: rootr db create|rows|row-add|row-body … (자세한 건 rootr --help)',
456
+ );
449
457
  }
450
458
  const raw = typeof args.file === 'string' ? readFileSync(args.file, 'utf8') : await readStdin();
451
459
  let spec;
@@ -472,6 +480,84 @@ async function cmdDb(args) {
472
480
  if (cols) console.log(`컬럼 순서: ${cols}`);
473
481
  if (typeof db.rowCount === 'number') console.log(`행: ${db.rowCount}개`);
474
482
  }
483
+
484
+ /** 표의 행 목록. --body 를 주면 행마다 상세 내용(본문 메모)까지 가져온다. */
485
+ async function cmdDbRows(args) {
486
+ const target = args._[1];
487
+ if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--json]');
488
+ const client = makeClient();
489
+ const id = await resolveDatabaseId(client, target);
490
+ const rows = await client.listRows(id, { withBody: !!args.body });
491
+ if (args.json) {
492
+ console.log(JSON.stringify(rows, null, 2));
493
+ return;
494
+ }
495
+ for (const row of rows) {
496
+ const values = Object.values(row.values || {})
497
+ .map((v) => (v && typeof v === 'object' ? JSON.stringify(v) : String(v)))
498
+ .join(' | ');
499
+ console.log(`${row.id}\t${values}`);
500
+ if (args.body && row.body) {
501
+ for (const line of String(row.body).trimEnd().split('\n')) console.log(` ${line}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ /** 행 추가. 값은 JSON으로, 상세 내용(본문 메모)은 --body / --body-file 로. */
507
+ async function cmdDbRowAdd(args) {
508
+ const target = args._[1];
509
+ if (!target || typeof args.values !== 'string') {
510
+ throw new Error("사용법: rootr db row-add <path|id> --values '{\"제목\":\"...\"}' [--body <md>|--body-file <f>]");
511
+ }
512
+ let values;
513
+ try {
514
+ values = JSON.parse(args.values);
515
+ } catch {
516
+ throw new Error('--values 는 JSON이어야 합니다. 예: --values \'{"제목":"킥오프"}\'');
517
+ }
518
+ const body =
519
+ typeof args['body-file'] === 'string'
520
+ ? readFileSync(args['body-file'], 'utf8')
521
+ : typeof args.body === 'string'
522
+ ? args.body
523
+ : undefined;
524
+ const client = makeClient();
525
+ const id = await resolveDatabaseId(client, target);
526
+ const row = await client.createRow(id, { values, body });
527
+ console.log(`행 추가: ${row.id}`);
528
+ if (row.pageNodeId) console.log(`상세 내용 문서: ${row.pageNodeId}`);
529
+ }
530
+
531
+ /** 행의 상세 내용(본문 메모)을 통째로 쓴다. 내용은 인자·--file·표준입력 순으로 받는다. */
532
+ async function cmdDbRowBody(args) {
533
+ const target = args._[1];
534
+ const rowId = args._[2];
535
+ if (!target || !rowId) {
536
+ throw new Error('사용법: rootr db row-body <path|id> <rowId> [<본문>] [--file <f>]');
537
+ }
538
+ const body =
539
+ typeof args.file === 'string'
540
+ ? readFileSync(args.file, 'utf8')
541
+ : args._[3] !== undefined
542
+ ? String(args._[3])
543
+ : await readStdin();
544
+ if (!body) throw new Error('쓸 내용이 없습니다 (본문 인자, --file, 또는 표준입력 필요).');
545
+ const client = makeClient();
546
+ const id = await resolveDatabaseId(client, target);
547
+ const row = await client.updateRow(id, rowId, { body });
548
+ console.log(`상세 내용 저장: 행 ${rowId}${row.pageNodeId ? ` → 문서 ${row.pageNodeId}` : ''}`);
549
+ }
550
+
551
+ /** 경로로 줘도 되게 — 표 인자도 문서 도구들과 같은 규칙을 따른다. */
552
+ async function resolveDatabaseId(client, target) {
553
+ if (typeof target === 'string' && target.startsWith('/')) {
554
+ const resolved = await resolveTarget(client, target);
555
+ if (!resolved.existed) throw new Error(`표를 찾을 수 없습니다: ${target}`);
556
+ return resolved.id;
557
+ }
558
+ return target;
559
+ }
560
+
475
561
  async function main() {
476
562
  const argv = process.argv.slice(2);
477
563
  const args = parseArgs(argv);
package/lib/client.js CHANGED
@@ -618,25 +618,29 @@ export class RootrClient {
618
618
  return this.request(`/databases/${encodeURIComponent(databaseId)}`);
619
619
  }
620
620
 
621
- /** All rows of a database, in position order. Returns the raw rows array. */
622
- async listRows(databaseId) {
623
- const data = await this.request(`/databases/${encodeURIComponent(databaseId)}/rows`);
621
+ /**
622
+ * All rows of a database, in position order. Returns the raw rows array.
623
+ * DB-BODY — `withBody` 를 주면 행마다 상세 내용(본문 메모)까지 함께 온다.
624
+ */
625
+ async listRows(databaseId, { withBody = false } = {}) {
626
+ const query = withBody ? '?body=1' : '';
627
+ const data = await this.request(`/databases/${encodeURIComponent(databaseId)}/rows${query}`);
624
628
  if (Array.isArray(data)) return data;
625
629
  if (data && Array.isArray(data.rows)) return data.rows;
626
630
  return [];
627
631
  }
628
632
 
629
- async createRow(databaseId, { values, position } = {}) {
633
+ async createRow(databaseId, { values, position, body } = {}) {
630
634
  return this.request(`/databases/${encodeURIComponent(databaseId)}/rows`, {
631
635
  method: 'POST',
632
- body: { values, position },
636
+ body: { values, position, body },
633
637
  });
634
638
  }
635
639
 
636
- async updateRow(databaseId, rowId, { values, position } = {}) {
640
+ async updateRow(databaseId, rowId, { values, position, body } = {}) {
637
641
  return this.request(
638
642
  `/databases/${encodeURIComponent(databaseId)}/rows/${encodeURIComponent(rowId)}`,
639
- { method: 'PATCH', body: { values, position } },
643
+ { method: 'PATCH', body: { values, position, body } },
640
644
  );
641
645
  }
642
646
 
@@ -13,6 +13,15 @@ import {
13
13
  /** Human text for the `databaseId` param — accepts a path just like the document tools. */
14
14
  const DATABASE_TARGET_DESC = `DATABASE node id${NODE_TARGET_HINT} (e.g. "/제작 공정/칸반")`;
15
15
 
16
+ /**
17
+ * DB-BODY — 행의 상세 내용(본문 메모). 화면에서 칸반 카드를 열면 속성 아래에 붙는 그 글이며,
18
+ * 실체는 표 밑에 달리는 문서 하나다. 예전에는 REST 2단계로만 닿을 수 있어서 CLI·MCP에서는
19
+ * 아예 손댈 수 없었다.
20
+ */
21
+ const BODY_DESC =
22
+ "The row's detail body — the note shown under a kanban card's properties, in markdown " +
23
+ '(행의 상세 내용/본문 메모). Creates the row\'s page on demand; replaces the whole body.';
24
+
16
25
  /**
17
26
  * Resolve a `{propertyNameOrId: value}` map to `{propertyId: value}` using the
18
27
  * database schema, so agents can address columns by their human name OR their
@@ -164,15 +173,19 @@ export function registerDatabaseTools(server) {
164
173
  annotations: { readOnlyHint: true, destructiveHint: false },
165
174
  inputSchema: {
166
175
  databaseId: z.string().describe(DATABASE_TARGET_DESC),
176
+ includeBody: z
177
+ .boolean()
178
+ .optional()
179
+ .describe("true → also return each row's detail body (the note under a card). Off by default."),
167
180
  },
168
181
  },
169
- async ({ databaseId }) => {
182
+ async ({ databaseId, includeBody }) => {
170
183
  try {
171
184
  const client = makeClient();
172
185
  const id = await resolveNodeId(client, databaseId);
173
186
  const [database, rows] = await Promise.all([
174
187
  client.getDatabase(id),
175
- client.listRows(id),
188
+ client.listRows(id, { withBody: includeBody === true }),
176
189
  ]);
177
190
  return jsonResult({ database, rows });
178
191
  } catch (err) {
@@ -191,12 +204,18 @@ export function registerDatabaseTools(server) {
191
204
  annotations: { readOnlyHint: true, destructiveHint: false },
192
205
  inputSchema: {
193
206
  databaseId: z.string().describe(DATABASE_TARGET_DESC),
207
+ includeBody: z
208
+ .boolean()
209
+ .optional()
210
+ .describe("true → also return each row's detail body (the note under a card). Off by default."),
194
211
  },
195
212
  },
196
- async ({ databaseId }) => {
213
+ async ({ databaseId, includeBody }) => {
197
214
  try {
198
215
  const client = makeClient();
199
- const rows = await client.listRows(await resolveNodeId(client, databaseId));
216
+ const rows = await client.listRows(await resolveNodeId(client, databaseId), {
217
+ withBody: includeBody === true,
218
+ });
200
219
  return jsonResult({ rows });
201
220
  } catch (err) {
202
221
  return errorResult(err);
@@ -212,21 +231,24 @@ export function registerDatabaseTools(server) {
212
231
  'Append a row to a Rootr (루터) DATABASE. `values` maps property NAME (or id) → value. ' +
213
232
  'Value shapes by type: title/text/url/select/person → string; number → number; multi_select → string[]; ' +
214
233
  'checkbox → boolean; date → { start: "YYYY-MM-DD", end? }. A select value is the OPTION NAME. ' +
215
- 'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index.',
234
+ 'Call rootr_read_database first to learn the columns. Optionally set `position` to insert at an index. ' +
235
+ 'Pass `body` to also write the row\'s DETAIL PAGE (the note shown under a kanban card\'s properties) — ' +
236
+ 'that page is created on demand, so a row with `body` is the one-call equivalent of "메모 추가".',
216
237
  annotations: { readOnlyHint: false, destructiveHint: false },
217
238
  inputSchema: {
218
239
  databaseId: z.string().describe(DATABASE_TARGET_DESC),
219
240
  values: z.record(z.unknown()).describe('{ propertyNameOrId: value } for the new row'),
220
241
  position: z.number().optional().describe('0-based insert index; appended at the end if omitted'),
242
+ body: z.string().optional().describe(BODY_DESC),
221
243
  },
222
244
  },
223
- async ({ databaseId, values, position }) => {
245
+ async ({ databaseId, values, position, body }) => {
224
246
  try {
225
247
  const client = makeClient();
226
248
  const id = await resolveNodeId(client, databaseId);
227
249
  const database = await client.getDatabase(id);
228
250
  const resolved = resolveValueKeys(database.properties || [], values);
229
- const row = await client.createRow(id, { values: resolved, position });
251
+ const row = await client.createRow(id, { values: resolved, position, body });
230
252
  return jsonResult(row);
231
253
  } catch (err) {
232
254
  return errorResult(err);
@@ -249,9 +271,10 @@ export function registerDatabaseTools(server) {
249
271
  rowId: z.string().describe('Row id (must belong to the database)'),
250
272
  values: z.record(z.unknown()).optional().describe('{ propertyNameOrId: newValue | null } partial patch'),
251
273
  position: z.number().optional().describe('New 0-based position for the row'),
274
+ body: z.string().optional().describe(BODY_DESC),
252
275
  },
253
276
  },
254
- async ({ databaseId, rowId, values, position }) => {
277
+ async ({ databaseId, rowId, values, position, body }) => {
255
278
  try {
256
279
  const client = makeClient();
257
280
  const id = await resolveNodeId(client, databaseId);
@@ -260,7 +283,7 @@ export function registerDatabaseTools(server) {
260
283
  const database = await client.getDatabase(id);
261
284
  resolved = resolveValueKeys(database.properties || [], values);
262
285
  }
263
- const row = await client.updateRow(id, rowId, { values: resolved, position });
286
+ const row = await client.updateRow(id, rowId, { values: resolved, position, body });
264
287
  return jsonResult(row);
265
288
  } catch (err) {
266
289
  return errorResult(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.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": {