rootr-cli 0.13.0 → 0.14.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.
- package/README.md +5 -2
- package/bin/rootr.js +9 -3
- package/lib/client.js +48 -2
- package/lib/mcp-tools/databases.js +9 -1
- package/lib/mcp-tools/issues.js +3 -2
- package/lib/mcp-tools/misc.js +59 -0
- package/lib/mcp-tools/workspaces.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -277,7 +277,7 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
277
277
|
|
|
278
278
|
## MCP 서버로 사용하기
|
|
279
279
|
|
|
280
|
-
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구
|
|
280
|
+
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 152종:
|
|
281
281
|
|
|
282
282
|
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
283
283
|
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
@@ -324,6 +324,7 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
324
324
|
| `rootr_scaffold_review` | 워크스페이스 불필요 | 짓기 2단계 — **만들기 전에** 설계를 검사받는다(점수·고칠 곳). 컴파일러처럼 고쳐 가며 짓는다 |
|
|
325
325
|
| `rootr_create_workspace` | **계정 키(PAT) + workspaces:create** | 짓기 3단계 — 계획된 트리로 새 워크스페이스 생성(검사 결과도 같이 돌려준다) |
|
|
326
326
|
| `rootr_scaffold_apply` | docs:write | 짓기 3단계 — 기존 워크스페이스에 트리 적용(검사 결과도 같이 돌려준다) |
|
|
327
|
+
| `rootr_delete_workspace` | **계정 키(PAT) + OWNER** | 짓다 실패해 남은 **빈 워크스페이스 치우기**. 이름을 그대로 다시 적어야 지워진다(되돌릴 수 없음) |
|
|
327
328
|
|
|
328
329
|
스캐폴딩은 **plan → 물어보기 → 설계 → review → 고치기 → apply** 한 바퀴입니다.
|
|
329
330
|
`review` 를 건너뛰면 "얇은 마크다운 파일 묶음"이 됩니다. CLI 로도 같은 세 단계를 쓸 수 있습니다:
|
|
@@ -369,7 +370,7 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
369
370
|
| 도구 | 설명 |
|
|
370
371
|
|---|---|
|
|
371
372
|
| `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** (`includeBody: true` 면 행마다 상세 내용까지) |
|
|
372
|
-
| `rootr_list_rows` | 행만 조회(스키마 제외, `includeBody`
|
|
373
|
+
| `rootr_list_rows` | 행만 조회(스키마 제외, `includeBody` 지원, `backlinks: true`면 "나를 가리키는 다른 표의 행"도 함께) |
|
|
373
374
|
| `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로 지정, `body`로 상세 내용까지) |
|
|
374
375
|
| `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값 변경, `body`로 상세 내용 교체) |
|
|
375
376
|
| `rootr_bulk_create_rows` | 행 여러 개를 한 번에 추가 |
|
|
@@ -579,6 +580,8 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
579
580
|
| 도구 | 설명 |
|
|
580
581
|
|---|---|
|
|
581
582
|
| `rootr_document_versions` | 문서 버전 히스토리 조회 |
|
|
583
|
+
| `rootr_read_document_version` | 그 시점 본문을 그대로 읽기(되돌리기 전에 확인용) |
|
|
584
|
+
| `rootr_restore_document_version` | 문서를 예전 버전으로 되돌리기 |
|
|
582
585
|
| `rootr_update_node` | 아무 노드나 이름·아이콘 변경 / 옮기기 |
|
|
583
586
|
| `rootr_delete_node` | 노드를 휴지통으로 이동(복구 가능) |
|
|
584
587
|
|
package/bin/rootr.js
CHANGED
|
@@ -28,7 +28,7 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
28
28
|
rootr use <id|이름|주소> 활성 워크스페이스 전환 (그 워크스페이스용 키도 기억)
|
|
29
29
|
rootr ask <question> [--workspace <w>] GraphRAG 질의응답 (RCA), 인용 포함 출력
|
|
30
30
|
rootr db create [--file <spec.json>] [--parent <path|id>] 표(DATABASE) 생성 — 컬럼 순서 그대로, 행까지 한 번에
|
|
31
|
-
rootr db rows <path|id> [--body] [--json] 표의 행 목록 (--body: 상세
|
|
31
|
+
rootr db rows <path|id> [--body] [--backlinks] [--json] 표의 행 목록 (--body: 상세 내용까지, --backlinks: 나를 가리키는 다른 표의 행까지)
|
|
32
32
|
rootr db lock <path|id> [--schema managers] [--rows own] [--locked "승인,점수"] 표 자물쇠 보기·걸기
|
|
33
33
|
rootr db notify <path|id> [--file rules.json] [--clear] 표 알림 규칙 보기·저장
|
|
34
34
|
rootr db row-add <path|id> --values '<json>' [--body <md>|--body-file <f>] 행 추가(상세 내용 포함)
|
|
@@ -1301,10 +1301,10 @@ async function cmdDbNotify(args) {
|
|
|
1301
1301
|
|
|
1302
1302
|
async function cmdDbRows(args) {
|
|
1303
1303
|
const target = args._[1];
|
|
1304
|
-
if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--json]');
|
|
1304
|
+
if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--backlinks] [--json]');
|
|
1305
1305
|
const client = makeClient();
|
|
1306
1306
|
const id = await resolveDatabaseId(client, target);
|
|
1307
|
-
const rows = await client.listRows(id, { withBody: !!args.body });
|
|
1307
|
+
const rows = await client.listRows(id, { withBody: !!args.body, backlinks: !!args.backlinks });
|
|
1308
1308
|
if (args.json) {
|
|
1309
1309
|
console.log(JSON.stringify(rows, null, 2));
|
|
1310
1310
|
return;
|
|
@@ -1317,6 +1317,12 @@ async function cmdDbRows(args) {
|
|
|
1317
1317
|
if (args.body && row.body) {
|
|
1318
1318
|
for (const line of String(row.body).trimEnd().split('\n')) console.log(` ${line}`);
|
|
1319
1319
|
}
|
|
1320
|
+
// BACKLINK — "나를 가리키는 다른 표의 행" 목록(있을 때만 표시).
|
|
1321
|
+
if (args.backlinks && row.backlinks && Object.keys(row.backlinks).length) {
|
|
1322
|
+
for (const [refDbId, group] of Object.entries(row.backlinks)) {
|
|
1323
|
+
console.log(` ← ${refDbId} (${group.propName}): ${group.rowIds.join(', ')}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1320
1326
|
}
|
|
1321
1327
|
}
|
|
1322
1328
|
|
package/lib/client.js
CHANGED
|
@@ -322,6 +322,29 @@ export class RootrClient {
|
|
|
322
322
|
return this.request(`/documents/${encodeURIComponent(id)}/versions${qs}`);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/** 특정 버전의 본문을 그대로 읽는다(되돌리기 전에 무엇이 들었는지 확인용). */
|
|
326
|
+
async getDocumentVersion(id, versionId) {
|
|
327
|
+
return this.request(
|
|
328
|
+
`/documents/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* DOC-RESTORE — **문서를 예전 버전으로 되돌린다.**
|
|
334
|
+
*
|
|
335
|
+
* 예전에는 목록 조회(getDocumentVersions)만 있고 되돌리는 문이 없었다. 그래서
|
|
336
|
+
* AI가 잘못 덮어써 내용이 통째로 날아가도 "되돌려 달라"는 부탁을 아무도 수행할
|
|
337
|
+
* 수 없었다 — 고객이 실제로 겪었다(2026-08-31 에이랩스 피드백: 일간 업무 일지
|
|
338
|
+
* 취합 문서의 대부분이 사라졌는데 AI가 복구하지 못함). 버전은 서버에 이미 다
|
|
339
|
+
* 쌓여 있었고, 부를 방법만 없었다.
|
|
340
|
+
*/
|
|
341
|
+
async restoreDocumentVersion(id, versionId) {
|
|
342
|
+
return this.request(
|
|
343
|
+
`/documents/${encodeURIComponent(id)}/versions/${encodeURIComponent(versionId)}/restore`,
|
|
344
|
+
{ method: 'POST' },
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
325
348
|
/** Move a node to the trash (recoverable). */
|
|
326
349
|
async deleteNode(id) {
|
|
327
350
|
return this.request(`/nodes/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
@@ -480,6 +503,23 @@ export class RootrClient {
|
|
|
480
503
|
});
|
|
481
504
|
}
|
|
482
505
|
|
|
506
|
+
/**
|
|
507
|
+
* WS-CLEANUP — **자기가 만든 잔해를 자기가 치울 수 있게.**
|
|
508
|
+
*
|
|
509
|
+
* 짓다가 실패하면 빈 워크스페이스가 남는데, 지우는 문이 없어서 에이전트가
|
|
510
|
+
* 치우지 못했다. 무료 한도가 5개라 몇 번 실패하면 사용자가 바로 막힌다
|
|
511
|
+
* (2026-08-30 실측: 치과 3개·아파트 4개가 잔해로 남음).
|
|
512
|
+
*
|
|
513
|
+
* 이름을 그대로 다시 적게 하는 것은 서버가 요구하는 안전장치다 — 지울 것을
|
|
514
|
+
* 정말 보고 있는지 확인한다.
|
|
515
|
+
*/
|
|
516
|
+
async deleteWorkspace({ workspaceId, confirmName }) {
|
|
517
|
+
return this.request(`/workspaces/${workspaceId}`, {
|
|
518
|
+
method: 'DELETE',
|
|
519
|
+
body: { confirmName },
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
483
523
|
/**
|
|
484
524
|
* Check a designed tree BEFORE building it — the compiler pass of scaffolding.
|
|
485
525
|
* Needs no workspace, creates nothing. Returns { score, counts, findings, summary }.
|
|
@@ -779,9 +819,15 @@ export class RootrClient {
|
|
|
779
819
|
/**
|
|
780
820
|
* All rows of a database, in position order. Returns the raw rows array.
|
|
781
821
|
* DB-BODY — `withBody` 를 주면 행마다 상세 내용(본문 메모)까지 함께 온다.
|
|
822
|
+
* BACKLINK — `backlinks` 를 주면 각 행에 "나를 가리키는 다른 표의 행" 정보
|
|
823
|
+
* (`{ [referrerDatabaseId]: { propName, rowIds } }`)가 함께 붙는다. 기본은 꺼져
|
|
824
|
+
* 있다(필드 자체가 없다).
|
|
782
825
|
*/
|
|
783
|
-
async listRows(databaseId, { withBody = false } = {}) {
|
|
784
|
-
const
|
|
826
|
+
async listRows(databaseId, { withBody = false, backlinks = false } = {}) {
|
|
827
|
+
const params = [];
|
|
828
|
+
if (withBody) params.push('body=1');
|
|
829
|
+
if (backlinks) params.push('backlinks=1');
|
|
830
|
+
const query = params.length ? `?${params.join('&')}` : '';
|
|
785
831
|
const data = await this.request(`/databases/${encodeURIComponent(databaseId)}/rows${query}`);
|
|
786
832
|
if (Array.isArray(data)) return data;
|
|
787
833
|
if (data && Array.isArray(data.rows)) return data.rows;
|
|
@@ -430,13 +430,21 @@ export function registerDatabaseTools(server) {
|
|
|
430
430
|
.boolean()
|
|
431
431
|
.optional()
|
|
432
432
|
.describe("true → also return each row's detail body (the note under a card). Off by default."),
|
|
433
|
+
backlinks: z
|
|
434
|
+
.boolean()
|
|
435
|
+
.optional()
|
|
436
|
+
.describe(
|
|
437
|
+
'true → also return each row\'s backlinks: other databases whose relation column points at ' +
|
|
438
|
+
'this row, as `{ [referrerDatabaseId]: { propName, rowIds } }`. Off by default (no field at all).',
|
|
439
|
+
),
|
|
433
440
|
},
|
|
434
441
|
},
|
|
435
|
-
async ({ databaseId, includeBody }) => {
|
|
442
|
+
async ({ databaseId, includeBody, backlinks }) => {
|
|
436
443
|
try {
|
|
437
444
|
const client = makeClient();
|
|
438
445
|
const rows = await client.listRows(await resolveNodeId(client, databaseId), {
|
|
439
446
|
withBody: includeBody === true,
|
|
447
|
+
backlinks: backlinks === true,
|
|
440
448
|
});
|
|
441
449
|
return jsonResult({ rows });
|
|
442
450
|
} catch (err) {
|
package/lib/mcp-tools/issues.js
CHANGED
|
@@ -148,8 +148,9 @@ export function registerIssueTools(server) {
|
|
|
148
148
|
{
|
|
149
149
|
title: 'Comment on an issue',
|
|
150
150
|
description:
|
|
151
|
-
'Add a comment to a Rootr (루터) issue within its tracker. Comments carry a user identity
|
|
152
|
-
'
|
|
151
|
+
'Add a comment to a Rootr (루터) issue within its tracker. Comments carry a user identity: ' +
|
|
152
|
+
'the comment is posted as the person the ROOTR_API_KEY belongs to. Both an account key (PAT) and a ' +
|
|
153
|
+
'workspace key work, since a workspace key is bound to the member who minted it.',
|
|
153
154
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
154
155
|
inputSchema: {
|
|
155
156
|
trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
|
package/lib/mcp-tools/misc.js
CHANGED
|
@@ -31,6 +31,65 @@ export function registerMiscTools(server) {
|
|
|
31
31
|
},
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* DOC-RESTORE — **되돌리기.** 목록만 있고 되돌리는 문이 없어서, AI가 잘못
|
|
36
|
+
* 덮어써 내용이 날아가도 아무도 복구할 수 없었다(2026-08-31 에이랩스 피드백:
|
|
37
|
+
* "AI에게 이전 버전으로 돌려달라고 했지만 불가능"). 버전은 서버에 이미 쌓여
|
|
38
|
+
* 있었고, 부를 방법만 없었다.
|
|
39
|
+
*/
|
|
40
|
+
server.registerTool(
|
|
41
|
+
'rootr_read_document_version',
|
|
42
|
+
{
|
|
43
|
+
title: 'Read one saved version of a document',
|
|
44
|
+
description:
|
|
45
|
+
'Read the full saved body of ONE past version of a Rootr (루터) document. Use this before ' +
|
|
46
|
+
'rootr_restore_document_version to confirm the version actually holds the content the user wants back — ' +
|
|
47
|
+
'restoring is a write, so look first. Get version ids from rootr_document_versions.',
|
|
48
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
49
|
+
inputSchema: {
|
|
50
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
51
|
+
versionId: z.string().describe('Version id from rootr_document_versions'),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
async ({ target, versionId }) => {
|
|
55
|
+
try {
|
|
56
|
+
const client = makeClient();
|
|
57
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
58
|
+
return jsonResult(await client.getDocumentVersion(id, versionId));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return errorResult(err);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
server.registerTool(
|
|
66
|
+
'rootr_restore_document_version',
|
|
67
|
+
{
|
|
68
|
+
title: 'Restore a document to an earlier version',
|
|
69
|
+
description:
|
|
70
|
+
'Roll a Rootr (루터) document back to one of its saved versions — the recovery path when an edit ' +
|
|
71
|
+
'(yours or anyone else\'s) destroyed content. WORKFLOW: rootr_document_versions to list, ' +
|
|
72
|
+
'rootr_read_document_version to confirm the right one, then restore. The current body is itself saved ' +
|
|
73
|
+
'as a new version first, so restoring is reversible — but it DOES overwrite what is there now, so ' +
|
|
74
|
+
'never guess at the version id. If the user says "undo what you just did" or "bring back the old ' +
|
|
75
|
+
'content", this is the tool.',
|
|
76
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
77
|
+
inputSchema: {
|
|
78
|
+
target: z.string().describe('Document path (starting with "/") or node id'),
|
|
79
|
+
versionId: z.string().describe('Version id to restore, from rootr_document_versions'),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
async ({ target, versionId }) => {
|
|
83
|
+
try {
|
|
84
|
+
const client = makeClient();
|
|
85
|
+
const id = requireResolved(await resolveTarget(client, target));
|
|
86
|
+
return jsonResult(await client.restoreDocumentVersion(id, versionId));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return errorResult(err);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
34
93
|
server.registerTool(
|
|
35
94
|
'rootr_update_node',
|
|
36
95
|
{
|
|
@@ -223,6 +223,46 @@ export function registerWorkspaceTools(server) {
|
|
|
223
223
|
},
|
|
224
224
|
);
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* WS-CLEANUP — **짓다 실패해서 남은 빈 워크스페이스를 에이전트가 치울 수 있게.**
|
|
228
|
+
*
|
|
229
|
+
* 스캐폴딩이 중간에 실패하면 껍데기만 남는데 지우는 문이 없었다. 무료 한도가
|
|
230
|
+
* 5개라 몇 번 실패하면 사용자가 바로 막히고, 그 잔해를 사람이 화면에서 하나씩
|
|
231
|
+
* 지워야 했다(2026-08-30 실측: 치과 3개·아파트 4개).
|
|
232
|
+
*
|
|
233
|
+
* 이름을 그대로 다시 적게 한다 — 서버가 요구하는 안전장치이자, 모델이 엉뚱한
|
|
234
|
+
* 워크스페이스를 지목했을 때 걸리는 그물이다.
|
|
235
|
+
*/
|
|
236
|
+
server.registerTool(
|
|
237
|
+
'rootr_delete_workspace',
|
|
238
|
+
{
|
|
239
|
+
title: 'Delete a workspace you created (cleanup after a failed scaffold)',
|
|
240
|
+
description:
|
|
241
|
+
'Permanently delete a Rootr (루터) workspace. Intended for cleaning up EMPTY leftovers from a scaffold ' +
|
|
242
|
+
'run that failed halfway — the free plan allows only a handful of workspaces, so abandoned shells block ' +
|
|
243
|
+
'the user from trying again. Requires OWNER on that workspace, and you must pass `confirmName` exactly ' +
|
|
244
|
+
'matching the workspace name (call rootr_workspaces first to read it). THIS CANNOT BE UNDONE: everything ' +
|
|
245
|
+
'inside is removed. Never call this on a workspace that holds the user\'s real work — if you did not ' +
|
|
246
|
+
'create it in this session, ask the user first.',
|
|
247
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
248
|
+
inputSchema: {
|
|
249
|
+
workspaceId: z.string().describe('Id of the workspace to delete'),
|
|
250
|
+
confirmName: z
|
|
251
|
+
.string()
|
|
252
|
+
.describe('The workspace name, typed again exactly — the server rejects the call if it does not match'),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
async ({ workspaceId, confirmName }) => {
|
|
256
|
+
try {
|
|
257
|
+
const client = makeClient();
|
|
258
|
+
await client.deleteWorkspace({ workspaceId, confirmName });
|
|
259
|
+
return jsonResult({ deleted: true, workspaceId });
|
|
260
|
+
} catch (err) {
|
|
261
|
+
return errorResult(err);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
226
266
|
server.registerTool(
|
|
227
267
|
'rootr_scaffold_review',
|
|
228
268
|
{
|