rootr-cli 0.13.0 → 0.14.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 +43 -2
- package/bin/rootr.js +102 -3
- package/lib/client.js +92 -2
- package/lib/mcp-tools/databases.js +9 -1
- package/lib/mcp-tools/issues.js +3 -2
- package/lib/mcp-tools/memory.js +185 -0
- package/lib/mcp-tools/misc.js +59 -0
- package/lib/mcp-tools/workspaces.js +40 -0
- package/lib/mcp.js +88 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,9 +275,25 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
275
275
|
워크스페이스의 지식그래프(GraphRAG)에 질문하고 답변 본문 + 인용(문서 경로: 인용문)을 출력합니다.
|
|
276
276
|
`ask` 스코프가 필요합니다. `--workspace`를 생략하면 설정된 기본 워크스페이스를 사용합니다.
|
|
277
277
|
|
|
278
|
+
### 팀의 기억 (memory)
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
rootr memory # 전체 회상 (상태·id·종류·출처)
|
|
282
|
+
rootr memory --query 세미나 --kind 지시 # 걸러서 보기
|
|
283
|
+
rootr memory add "보고 시각은 KST로만 표기한다" --kind 지시 --source /규칙.md
|
|
284
|
+
rootr memory edit <id> --content "고친 문장" # 그 기억만 고치기(준 칸만 바뀜)
|
|
285
|
+
rootr memory edit <id> --kind 참고 # 종류만 고치기
|
|
286
|
+
rootr memory forget <id> # 폐기(--hard면 행 삭제)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
세션이 끝나도 남는, 워크스페이스 전체가 공유하는 한 줄 사실입니다. MCP의
|
|
290
|
+
`rootr_remember`/`rootr_recall`/`rootr_forget`과 같은 길을 터미널에서 쓰는
|
|
291
|
+
명령이고, 화면의 "기억" 메뉴와도 같은 데이터를 봅니다. **긴 건 문서로, 한 줄은
|
|
292
|
+
기억으로** — 자세한 규칙은 MCP 도구 설명을 참고하세요.
|
|
293
|
+
|
|
278
294
|
## MCP 서버로 사용하기
|
|
279
295
|
|
|
280
|
-
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구
|
|
296
|
+
`rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 152종:
|
|
281
297
|
|
|
282
298
|
노드를 가리키는 인자(`target`, `databaseId`, `spreadsheetId`, `formId`, `crmId`, `trackerId`,
|
|
283
299
|
`recordingId`, `presentationId`, `whiteboardId`, `logId`, `parentId` 등)는 **노드 id와 경로를 모두**
|
|
@@ -324,6 +340,7 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
324
340
|
| `rootr_scaffold_review` | 워크스페이스 불필요 | 짓기 2단계 — **만들기 전에** 설계를 검사받는다(점수·고칠 곳). 컴파일러처럼 고쳐 가며 짓는다 |
|
|
325
341
|
| `rootr_create_workspace` | **계정 키(PAT) + workspaces:create** | 짓기 3단계 — 계획된 트리로 새 워크스페이스 생성(검사 결과도 같이 돌려준다) |
|
|
326
342
|
| `rootr_scaffold_apply` | docs:write | 짓기 3단계 — 기존 워크스페이스에 트리 적용(검사 결과도 같이 돌려준다) |
|
|
343
|
+
| `rootr_delete_workspace` | **계정 키(PAT) + OWNER** | 짓다 실패해 남은 **빈 워크스페이스 치우기**. 이름을 그대로 다시 적어야 지워진다(되돌릴 수 없음) |
|
|
327
344
|
|
|
328
345
|
스캐폴딩은 **plan → 물어보기 → 설계 → review → 고치기 → apply** 한 바퀴입니다.
|
|
329
346
|
`review` 를 건너뛰면 "얇은 마크다운 파일 묶음"이 됩니다. CLI 로도 같은 세 단계를 쓸 수 있습니다:
|
|
@@ -369,7 +386,7 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
369
386
|
| 도구 | 설명 |
|
|
370
387
|
|---|---|
|
|
371
388
|
| `rootr_read_database` | 스키마(열 정의)+행 전체 읽기 — **다른 표 도구를 쓰기 전에 먼저** (`includeBody: true` 면 행마다 상세 내용까지) |
|
|
372
|
-
| `rootr_list_rows` | 행만 조회(스키마 제외, `includeBody`
|
|
389
|
+
| `rootr_list_rows` | 행만 조회(스키마 제외, `includeBody` 지원, `backlinks: true`면 "나를 가리키는 다른 표의 행"도 함께) |
|
|
373
390
|
| `rootr_add_row` | 행 추가 (`values`는 열 이름 또는 id로 지정, `body`로 상세 내용까지) |
|
|
374
391
|
| `rootr_update_row` | 행 수정 (칸반 카드 이동 = select 열 값 변경, `body`로 상세 내용 교체) |
|
|
375
392
|
| `rootr_bulk_create_rows` | 행 여러 개를 한 번에 추가 |
|
|
@@ -526,6 +543,28 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
526
543
|
| `rootr_save_as_template` | 지금 노드를(또는 즉석 정의를) 템플릿으로 저장 |
|
|
527
544
|
| `rootr_update_template` | 템플릿 수정 |
|
|
528
545
|
|
|
546
|
+
#### 기억 (docs:read / docs:write)
|
|
547
|
+
|
|
548
|
+
| 도구 | 설명 |
|
|
549
|
+
|---|---|
|
|
550
|
+
| `rootr_remember` | 한 줄 사실을 기억시키기(같은 내용이면 갈아끼움). `memoryId`를 함께 주면 그 기억을 그 자리에서 고치기 |
|
|
551
|
+
| `rootr_recall` | 이 워크스페이스가 아는 것 꺼내 오기 |
|
|
552
|
+
| `rootr_forget` | 더 이상 맞지 않는 기억 폐기 |
|
|
553
|
+
|
|
554
|
+
**에이전트에게**: 이건 세션이 끝나도 남는, **워크스페이스 전체가 공유하는** 기억입니다
|
|
555
|
+
(개인 메모는 여기 두지 말고 자기 메모리에 두세요). **긴 건 문서로, 한 줄은
|
|
556
|
+
기억으로, 그리고 기억은 반드시 문서를 가리키게** 하세요 — 이야기를 기억에 밀어
|
|
557
|
+
넣으면 기억이 "짧은 문서 더미"가 되면서 둘 다 망합니다. 규칙·결정한 이유·한 번
|
|
558
|
+
겪은 함정을 남기고, 그 근거가 된 문서 경로를 `source`로 같이 주세요(그래야 그
|
|
559
|
+
문서가 바뀔 때 루터가 "확인 필요" 표시를 답니다).
|
|
560
|
+
|
|
561
|
+
기억은 워크스페이스를 처음 만질 때 **도구 응답에 자동으로 딸려옵니다** — 굳이
|
|
562
|
+
`rootr_recall`을 먼저 부르지 않아도 눈앞에 놓입니다. 이 자동 회상은 무료 조회
|
|
563
|
+
한도를 쓰지 않습니다.
|
|
564
|
+
|
|
565
|
+
사람이 직접 보고 고치려면 워크스페이스 왼쪽 메뉴의 **기억**에서 볼 수 있고,
|
|
566
|
+
"표로 열기"를 누르면 평범한 표로 열려 권한·기록이 그대로 따라옵니다.
|
|
567
|
+
|
|
529
568
|
#### 제보 (인증 불필요)
|
|
530
569
|
|
|
531
570
|
| 도구 | 설명 |
|
|
@@ -579,6 +618,8 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
|
|
|
579
618
|
| 도구 | 설명 |
|
|
580
619
|
|---|---|
|
|
581
620
|
| `rootr_document_versions` | 문서 버전 히스토리 조회 |
|
|
621
|
+
| `rootr_read_document_version` | 그 시점 본문을 그대로 읽기(되돌리기 전에 확인용) |
|
|
622
|
+
| `rootr_restore_document_version` | 문서를 예전 버전으로 되돌리기 |
|
|
582
623
|
| `rootr_update_node` | 아무 노드나 이름·아이콘 변경 / 옮기기 |
|
|
583
624
|
| `rootr_delete_node` | 노드를 휴지통으로 이동(복구 가능) |
|
|
584
625
|
|
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>] 행 추가(상세 내용 포함)
|
|
@@ -82,6 +82,10 @@ const USAGE = `Rootr CLI v${pkg.version}
|
|
|
82
82
|
rootr scaffold apply --file tree.json [--root /경로] 짓기 3단계 — 실제로 만들기(검사 결과도 같이 나옴)
|
|
83
83
|
rootr scaffold built [--root /경로] [--json] 짓고 나서 — 실제로 만들어진 것의 **모양**을 검사
|
|
84
84
|
rootr scaffold test [--root /경로] [--json] 짓고 나서 — 실제로 **돌려 본다**(관계·롤업·수식·폼·시트). 실패 시 exit 1
|
|
85
|
+
rootr memory [--query <q>] [--kind <k>] [--json] 팀의 기억 회상 — AI 에이전트가 읽는 같은 목록
|
|
86
|
+
rootr memory add "<내용>" [--kind 지시|사람|프로젝트|참고] [--source <경로|id>] [--related a,b] 기억 저장
|
|
87
|
+
rootr memory edit <id> [--content "<내용>"] [--kind …] [--source …] [--related a,b] 기억 고치기(준 칸만)
|
|
88
|
+
rootr memory forget <id> [--hard] 기억 폐기(--hard면 행 삭제)
|
|
85
89
|
rootr mcp MCP stdio 서버 구동 (도구 151종 — README 참고)
|
|
86
90
|
rootr --help 도움말
|
|
87
91
|
rootr --version 버전 출력
|
|
@@ -567,6 +571,93 @@ async function cmdMcp() {
|
|
|
567
571
|
await startMcpServer({ name: 'rootr', version: pkg.version });
|
|
568
572
|
}
|
|
569
573
|
|
|
574
|
+
// ---- memory (MEM-001) ---------------------------------------------------
|
|
575
|
+
//
|
|
576
|
+
// 팀의 기억 — 저장·회상·고치기·잊기. MCP의 rootr_remember/recall/forget과 같은 길을
|
|
577
|
+
// 터미널에서도 쓸 수 있게. 고치기(edit)는 2026-09-01에 붙었다.
|
|
578
|
+
|
|
579
|
+
const MEMORY_KINDS = ['지시', '사람', '프로젝트', '참고'];
|
|
580
|
+
|
|
581
|
+
function parseRelated(val) {
|
|
582
|
+
if (!val) return undefined;
|
|
583
|
+
if (Array.isArray(val)) return val;
|
|
584
|
+
return String(val)
|
|
585
|
+
.split(',')
|
|
586
|
+
.map((s) => s.trim())
|
|
587
|
+
.filter(Boolean);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function cmdMemory(args) {
|
|
591
|
+
const sub = args._.shift();
|
|
592
|
+
const client = makeClient();
|
|
593
|
+
|
|
594
|
+
if (!sub || sub === 'ls' || sub === 'list') {
|
|
595
|
+
const result = await client.recall({ query: args.query, kind: args.kind, limit: args.limit });
|
|
596
|
+
if (args.json) {
|
|
597
|
+
console.log(JSON.stringify(result, null, 2));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (!result.memories.length) {
|
|
601
|
+
console.log('아직 기억이 없습니다. rootr memory add "한 줄 사실" 으로 남겨 보세요.');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
for (const m of result.memories) {
|
|
605
|
+
const when = String(m.updatedAt ?? '').slice(0, 10);
|
|
606
|
+
console.log(`- [${m.status}] ${m.content}`);
|
|
607
|
+
console.log(` ${m.id} · ${m.kind}${m.source ? ` · ${m.source}` : ''} · ${when}`);
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (sub === 'add') {
|
|
613
|
+
const content = args._.join(' ');
|
|
614
|
+
if (!content) throw new Error('사용법: rootr memory add "<내용>" [--kind 지시|사람|프로젝트|참고] [--source <경로|id>] [--related a,b]');
|
|
615
|
+
if (args.kind && !MEMORY_KINDS.includes(args.kind)) {
|
|
616
|
+
throw new Error(`종류는 ${MEMORY_KINDS.join('/')} 중 하나여야 합니다: ${args.kind}`);
|
|
617
|
+
}
|
|
618
|
+
const result = await client.remember({
|
|
619
|
+
content,
|
|
620
|
+
kind: args.kind,
|
|
621
|
+
source: args.source,
|
|
622
|
+
related: parseRelated(args.related),
|
|
623
|
+
});
|
|
624
|
+
console.log(`${result.replaced ? '기억을 갈아끼웠습니다' : '기억했습니다'}: ${result.memory.content}`);
|
|
625
|
+
console.log(`id: ${result.memory.id} · 상태: ${result.memory.status}`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (sub === 'edit') {
|
|
630
|
+
const id = args._[0];
|
|
631
|
+
if (!id) throw new Error('사용법: rootr memory edit <id> [--content "<내용>"] [--kind …] [--source …] [--related a,b]');
|
|
632
|
+
const content = args._.slice(1).join(' ') || args.content;
|
|
633
|
+
if (!content && !args.kind && !args.source && !args.related) {
|
|
634
|
+
throw new Error('고칠 칸이 하나도 없습니다 — --content/--kind/--source/--related 중 하나는 주어야 합니다.');
|
|
635
|
+
}
|
|
636
|
+
if (args.kind && !MEMORY_KINDS.includes(args.kind)) {
|
|
637
|
+
throw new Error(`종류는 ${MEMORY_KINDS.join('/')} 중 하나여야 합니다: ${args.kind}`);
|
|
638
|
+
}
|
|
639
|
+
const result = await client.revise(id, {
|
|
640
|
+
...(content ? { content } : {}),
|
|
641
|
+
...(args.kind ? { kind: args.kind } : {}),
|
|
642
|
+
...(args.source !== undefined ? { source: args.source } : {}),
|
|
643
|
+
...(args.related !== undefined ? { related: parseRelated(args.related) } : {}),
|
|
644
|
+
});
|
|
645
|
+
console.log(`고쳤습니다: ${result.content}`);
|
|
646
|
+
console.log(`상태: ${result.status}${result.mergedIds?.length ? ` · 같은 사실 ${result.mergedIds.length}건을 폐기로 내렸습니다` : ''}`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (sub === 'forget') {
|
|
651
|
+
const id = args._[0];
|
|
652
|
+
if (!id) throw new Error('사용법: rootr memory forget <id> [--hard]');
|
|
653
|
+
await client.forget(id, { hard: args.hard === true });
|
|
654
|
+
console.log(args.hard ? '기억을 지웠습니다.' : '기억을 폐기 상태로 내렸습니다.');
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
throw new Error(`알 수 없는 하위명령: ${sub}\n사용법: rootr memory [ls|add|edit|forget] …`);
|
|
659
|
+
}
|
|
660
|
+
|
|
570
661
|
// ---- entry ------------------------------------------------------------
|
|
571
662
|
|
|
572
663
|
|
|
@@ -1301,10 +1392,10 @@ async function cmdDbNotify(args) {
|
|
|
1301
1392
|
|
|
1302
1393
|
async function cmdDbRows(args) {
|
|
1303
1394
|
const target = args._[1];
|
|
1304
|
-
if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--json]');
|
|
1395
|
+
if (!target) throw new Error('사용법: rootr db rows <path|id> [--body] [--backlinks] [--json]');
|
|
1305
1396
|
const client = makeClient();
|
|
1306
1397
|
const id = await resolveDatabaseId(client, target);
|
|
1307
|
-
const rows = await client.listRows(id, { withBody: !!args.body });
|
|
1398
|
+
const rows = await client.listRows(id, { withBody: !!args.body, backlinks: !!args.backlinks });
|
|
1308
1399
|
if (args.json) {
|
|
1309
1400
|
console.log(JSON.stringify(rows, null, 2));
|
|
1310
1401
|
return;
|
|
@@ -1317,6 +1408,12 @@ async function cmdDbRows(args) {
|
|
|
1317
1408
|
if (args.body && row.body) {
|
|
1318
1409
|
for (const line of String(row.body).trimEnd().split('\n')) console.log(` ${line}`);
|
|
1319
1410
|
}
|
|
1411
|
+
// BACKLINK — "나를 가리키는 다른 표의 행" 목록(있을 때만 표시).
|
|
1412
|
+
if (args.backlinks && row.backlinks && Object.keys(row.backlinks).length) {
|
|
1413
|
+
for (const [refDbId, group] of Object.entries(row.backlinks)) {
|
|
1414
|
+
console.log(` ← ${refDbId} (${group.propName}): ${group.rowIds.join(', ')}`);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1320
1417
|
}
|
|
1321
1418
|
}
|
|
1322
1419
|
|
|
@@ -1750,6 +1847,8 @@ async function main() {
|
|
|
1750
1847
|
return cmdFeedback(args);
|
|
1751
1848
|
case 'scaffold':
|
|
1752
1849
|
return cmdScaffold(args);
|
|
1850
|
+
case 'memory':
|
|
1851
|
+
return cmdMemory(args);
|
|
1753
1852
|
case 'mcp':
|
|
1754
1853
|
return cmdMcp(args);
|
|
1755
1854
|
default:
|
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;
|
|
@@ -1627,4 +1673,48 @@ export class RootrClient {
|
|
|
1627
1673
|
}
|
|
1628
1674
|
return data;
|
|
1629
1675
|
}
|
|
1676
|
+
|
|
1677
|
+
// ── MEM-001 에이전트 메모리 ────────────────────────────────────────────────
|
|
1678
|
+
//
|
|
1679
|
+
// 판단은 부르는 쪽 클로드가 한다. 저장·회상·고치기·잊기 네 문을 연다.
|
|
1680
|
+
|
|
1681
|
+
/** 기억한다(워크스페이스 공유). 같은 기억이 있으면 서버가 갈아끼운다(replaced=true). */
|
|
1682
|
+
async remember(body) {
|
|
1683
|
+
return this.request(`/workspaces/${encodeURIComponent(this.workspace)}/memories`, {
|
|
1684
|
+
method: 'POST',
|
|
1685
|
+
body,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/** 꺼내 온다. 인자가 없으면 이 워크스페이스의 기억 전부. */
|
|
1690
|
+
async recall({ query, kind, limit } = {}) {
|
|
1691
|
+
const qs = new URLSearchParams();
|
|
1692
|
+
if (query) qs.set('query', query);
|
|
1693
|
+
if (kind) qs.set('kind', kind);
|
|
1694
|
+
if (limit) qs.set('limit', String(limit));
|
|
1695
|
+
const tail = qs.toString() ? `?${qs}` : '';
|
|
1696
|
+
return this.request(`/workspaces/${encodeURIComponent(this.workspace)}/memories${tail}`);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* 고친다(2026-09-01) — id로 짚은 기억의 내용·종류·출처·관련을 바꾼다.
|
|
1701
|
+
* 생략한 칸은 안 바뀌고, 내용이 바뀌면 상태가 '유효'로 되돌아온다.
|
|
1702
|
+
* 화면이 같은 길을 쓴다.
|
|
1703
|
+
*/
|
|
1704
|
+
async revise(memoryId, body) {
|
|
1705
|
+
return this.request(
|
|
1706
|
+
`/workspaces/${encodeURIComponent(this.workspace)}/memories/${encodeURIComponent(memoryId)}`,
|
|
1707
|
+
{ method: 'PATCH', body },
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** 잊는다. 기본은 `폐기`로 내리기 — `hard`면 행을 실제로 지운다. */
|
|
1712
|
+
async forget(memoryId, { hard } = {}) {
|
|
1713
|
+
const tail = hard ? '?hard=true' : '';
|
|
1714
|
+
return this.request(
|
|
1715
|
+
`/workspaces/${encodeURIComponent(this.workspace)}/memories/${encodeURIComponent(memoryId)}${tail}`,
|
|
1716
|
+
{ method: 'DELETE' },
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1630
1720
|
}
|
|
@@ -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}`),
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { jsonResult, textResult, errorResult, makeClient } from './shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MEM-001 — 에이전트 메모리.
|
|
7
|
+
*
|
|
8
|
+
* 기술적으로는 `rootr_add_row`/`rootr_list_rows`로도 되는 일이다. 그런데도 도구를
|
|
9
|
+
* 따로 파는 이유는 하나다 — **이름이 "기억하기"여야 클로드가 자기 메모리로 인식하고
|
|
10
|
+
* 쓴다.** "표에 행 추가"로는 아무도 안 부른다(계획서 §3.2).
|
|
11
|
+
*
|
|
12
|
+
* 경계 규칙을 도구 설명에 그대로 박아 둔다. 안 그러면 메모리가 "짧은 문서 더미"가
|
|
13
|
+
* 되면서 문서와 메모리 둘 다 망한다:
|
|
14
|
+
*
|
|
15
|
+
* 긴 건 문서로, 한 줄은 메모리로, 그리고 메모리는 반드시 문서를 가리킬 것.
|
|
16
|
+
*/
|
|
17
|
+
export function registerMemoryTools(server) {
|
|
18
|
+
server.registerTool(
|
|
19
|
+
'rootr_remember',
|
|
20
|
+
{
|
|
21
|
+
title: 'Remember a durable fact for this workspace',
|
|
22
|
+
description:
|
|
23
|
+
'Save ONE short, present-tense fact that should change how you act next time in this Rootr (루터) ' +
|
|
24
|
+
'workspace — a team convention, a decision and its reason, a gotcha you just hit, a person\'s ' +
|
|
25
|
+
'preference. Memories are SHARED WITH THE WHOLE WORKSPACE, so everyone\'s assistant learns it too. ' +
|
|
26
|
+
'Keep personal notes in your own memory, not here.\n\n' +
|
|
27
|
+
'THE RULE: long things go in documents, one-liners go in memory, and a memory should point at a ' +
|
|
28
|
+
'document. Save the lesson ("captures must run in the parent window with data URIs"), not the ' +
|
|
29
|
+
'story — write the story as a document and pass its path as `source`.\n\n' +
|
|
30
|
+
'Always pass `source` when the fact came from a document: that is what lets Rootr flag the memory ' +
|
|
31
|
+
'as "needs checking" when that document changes. A memory with no source can never notice it went ' +
|
|
32
|
+
'stale.\n\n' +
|
|
33
|
+
'Saving the same fact twice is safe — Rootr replaces it instead of piling up duplicates.\n\n' +
|
|
34
|
+
'EDITING: pass `memoryId` (the `id` from rootr_recall) to fix that exact memory in place — ' +
|
|
35
|
+
'omit it to save a new one. Editing marks the memory 유효 again (a person or their assistant ' +
|
|
36
|
+
'just re-confirmed it).',
|
|
37
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
38
|
+
inputSchema: {
|
|
39
|
+
memoryId: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe(
|
|
43
|
+
'EDIT an existing memory instead of saving a new one — the `id` from rootr_recall. ' +
|
|
44
|
+
'Only the fields you pass change; content edits set the status back to 유효.',
|
|
45
|
+
),
|
|
46
|
+
content: z
|
|
47
|
+
.string()
|
|
48
|
+
.max(500)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(
|
|
51
|
+
'The fact, as one present-tense line. Not a summary of a conversation — something that changes ' +
|
|
52
|
+
'what you do next time. Required for a new memory; may be omitted with `memoryId` to edit ' +
|
|
53
|
+
'only kind/source/related.',
|
|
54
|
+
),
|
|
55
|
+
kind: z
|
|
56
|
+
.enum(['지시', '사람', '프로젝트', '참고'])
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(
|
|
59
|
+
'What kind of fact: 지시 = how the team wants work done, 사람 = someone\'s role/preference, ' +
|
|
60
|
+
'프로젝트 = ongoing work or constraints, 참고 = a pointer to an external resource',
|
|
61
|
+
),
|
|
62
|
+
source: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe(
|
|
66
|
+
'Where this came from: a document path starting with "/", or a node id. Strongly recommended — ' +
|
|
67
|
+
'this is what makes the memory notice when it goes stale.',
|
|
68
|
+
),
|
|
69
|
+
related: z
|
|
70
|
+
.array(z.string())
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('What this memory is about — people, systems, customers. Used to group memories later.'),
|
|
73
|
+
workspace: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
async ({ memoryId, content, kind, source, related, workspace }) => {
|
|
80
|
+
try {
|
|
81
|
+
const client = makeClient(workspace);
|
|
82
|
+
// memoryId가 왔으면 고치기, 없으면 새로 저장 — 같은 도구의 두 문.
|
|
83
|
+
if (memoryId) {
|
|
84
|
+
const result = await client.revise(memoryId, { content, kind, source, related });
|
|
85
|
+
const merged = result.mergedIds?.length
|
|
86
|
+
? `\n같은 사실이 다른 기억(${result.mergedIds.length}건)에도 있어 폐기로 내렸습니다 — 목록은 그 행 하나뿐입니다.`
|
|
87
|
+
: '';
|
|
88
|
+
return textResult(
|
|
89
|
+
`고쳤습니다: ${result.content}\n상태: ${result.status}` +
|
|
90
|
+
(result.source ? ` · 출처: ${result.source}` : '') +
|
|
91
|
+
merged,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const result = await client.remember({ content, kind, source, related });
|
|
95
|
+
return textResult(
|
|
96
|
+
`${result.replaced ? '기억을 갈아끼웠습니다' : '기억했습니다'}: ${result.memory.content}\n` +
|
|
97
|
+
`상태: ${result.memory.status}` +
|
|
98
|
+
(result.memory.source ? ` · 출처: ${result.memory.source}` : '') +
|
|
99
|
+
(result.memory.sourceId
|
|
100
|
+
? ''
|
|
101
|
+
: '\n(출처가 연결되지 않았습니다 — 이 기억은 나중에 낡아도 스스로 알아채지 못합니다.)'),
|
|
102
|
+
);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return errorResult(err);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
server.registerTool(
|
|
110
|
+
'rootr_recall',
|
|
111
|
+
{
|
|
112
|
+
title: 'Recall what this workspace already knows',
|
|
113
|
+
description:
|
|
114
|
+
'Read the durable facts this Rootr (루터) workspace has learned — team conventions, decisions and ' +
|
|
115
|
+
'their reasons, gotchas, people\'s preferences. Call this before you start real work in a ' +
|
|
116
|
+
'workspace, and whenever you are about to make an assumption someone may have already settled.\n\n' +
|
|
117
|
+
'Memories marked 확인필요 mean their source document changed — treat them as "was true, verify ' +
|
|
118
|
+
'before relying on it".',
|
|
119
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
120
|
+
inputSchema: {
|
|
121
|
+
query: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Optional keyword filter. Omit to get everything — the list is meant to be read whole.'),
|
|
125
|
+
kind: z.enum(['지시', '사람', '프로젝트', '참고']).optional().describe('Only this kind'),
|
|
126
|
+
limit: z.number().optional().describe('Max memories to return'),
|
|
127
|
+
workspace: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
async ({ query, kind, limit, workspace }) => {
|
|
134
|
+
try {
|
|
135
|
+
const client = makeClient(workspace);
|
|
136
|
+
const result = await client.recall({ query, kind, limit });
|
|
137
|
+
if (!result.memories?.length) {
|
|
138
|
+
return textResult(
|
|
139
|
+
'아직 기억이 없습니다.\n' +
|
|
140
|
+
'rootr_remember로 팀 규칙·결정·함정을 한 줄씩 남겨 두면, 다음 세션의 클로드가 그걸 읽고 시작합니다.',
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return jsonResult(result);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return errorResult(err);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
server.registerTool(
|
|
151
|
+
'rootr_forget',
|
|
152
|
+
{
|
|
153
|
+
title: 'Retire a memory that is no longer true',
|
|
154
|
+
description:
|
|
155
|
+
'Mark a memory as 폐기 (retired) — use this when a fact stopped being true, not when you merely ' +
|
|
156
|
+
'disagree with it. Memory is meant to be pruned: a workspace with 500 stale memories is worse than ' +
|
|
157
|
+
'one with 50 true ones.\n\n' +
|
|
158
|
+
'Retiring keeps the row so the team can still see what used to be true and why it changed. Pass ' +
|
|
159
|
+
'`hard` only when the memory should never have been saved (wrong workspace, personal data that ' +
|
|
160
|
+
'does not belong in a shared workspace).\n\n' +
|
|
161
|
+
'If a fact merely CHANGED, call rootr_remember with the new wording instead — that replaces it.',
|
|
162
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
163
|
+
inputSchema: {
|
|
164
|
+
memoryId: z.string().describe('Memory id (the `id` from rootr_recall)'),
|
|
165
|
+
hard: z
|
|
166
|
+
.boolean()
|
|
167
|
+
.optional()
|
|
168
|
+
.describe('Delete the row outright instead of retiring it. Rarely right.'),
|
|
169
|
+
workspace: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe('Workspace id; defaults to ROOTR_WORKSPACE/config if omitted'),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
async ({ memoryId, hard, workspace }) => {
|
|
176
|
+
try {
|
|
177
|
+
const client = makeClient(workspace);
|
|
178
|
+
const result = await client.forget(memoryId, { hard });
|
|
179
|
+
return textResult(result.removed ? '기억을 지웠습니다.' : '기억을 폐기 상태로 내렸습니다.');
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return errorResult(err);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
}
|
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
|
{
|
package/lib/mcp.js
CHANGED
|
@@ -20,7 +20,9 @@ import { registerRecordingTools } from './mcp-tools/recordings.js';
|
|
|
20
20
|
import { registerAttachmentTools } from './mcp-tools/attachments.js';
|
|
21
21
|
import { registerSignedDocTools } from './mcp-tools/signed.js';
|
|
22
22
|
import { registerFeedbackTools } from './mcp-tools/feedback.js';
|
|
23
|
+
import { registerMemoryTools } from './mcp-tools/memory.js';
|
|
23
24
|
import { withRateLimitSink } from './context.js';
|
|
25
|
+
import { makeClient } from './mcp-tools/shared.js';
|
|
24
26
|
|
|
25
27
|
/** Warn once the workspace has burned this share of its quota. */
|
|
26
28
|
const QUOTA_WARN_AT = 0.8;
|
|
@@ -63,6 +65,88 @@ function withQuotaWarnings(server) {
|
|
|
63
65
|
return server;
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* MEM-001 — **기억을 눈앞에 놓는다. 이 기능의 성패가 여기 달렸다.**
|
|
71
|
+
*
|
|
72
|
+
* 도구만 만들어 두면 클로드는 안 부른다. 이미 두 번 겪었다 — 고객센터에서
|
|
73
|
+
* 프롬프트로 "어려우면 넘겨라"라고 시켰는데 말만 하고 도구를 안 불러 티켓이 0건이던
|
|
74
|
+
* 일, 어시스턴트 답변 언어를 결국 코드로 강제해야 했던 일. **프롬프트로 시킨 건
|
|
75
|
+
* 안 지켜진다.**
|
|
76
|
+
*
|
|
77
|
+
* 그래서 회상을 클로드의 선의에 맡기지 않는다. 이 워크스페이스에서 **첫 도구를
|
|
78
|
+
* 부르는 순간** 응답 꼬리에 기억 목록을 통째로 실어 보낸다(무료 한도 안내를 얹는
|
|
79
|
+
* 바로 그 자리 — 클로드가 반드시 읽는 곳이다).
|
|
80
|
+
*
|
|
81
|
+
* 한 번만 얹는다. 매 응답마다 붙이면 대화가 길어질수록 같은 목록이 수십 번 쌓여
|
|
82
|
+
* 컨텍스트를 잡아먹고, 그러면 오히려 안 읽는다.
|
|
83
|
+
*
|
|
84
|
+
* ★ 이 자동 회상은 무료 하루 한도에 안 든다 — 서버 쪽 회상 엔드포인트에
|
|
85
|
+
* `@NotRateLimited()`가 걸려 있다(memory.controller.ts). 사용자가 시키지도 않은
|
|
86
|
+
* 호출로 한도가 닳으면 쓰지도 않았는데 막히는 꼴이 되기 때문이다.
|
|
87
|
+
*/
|
|
88
|
+
const memoryPrimed = new Set();
|
|
89
|
+
|
|
90
|
+
/** 기억 목록을 클로드가 읽을 한 덩어리 글로 만든다. */
|
|
91
|
+
function memoryBriefing(memories) {
|
|
92
|
+
if (!memories?.length) return null;
|
|
93
|
+
const live = memories.filter((m) => m.status !== '폐기');
|
|
94
|
+
if (!live.length) return null;
|
|
95
|
+
const line = (m) => {
|
|
96
|
+
const flags = [];
|
|
97
|
+
if (m.status === '확인필요') flags.push('확인필요');
|
|
98
|
+
const tail = flags.length ? ` [${flags.join('·')}]` : '';
|
|
99
|
+
const src = m.source ? ` (출처: ${m.source})` : '';
|
|
100
|
+
return `- ${m.content}${tail}${src}`;
|
|
101
|
+
};
|
|
102
|
+
return (
|
|
103
|
+
`\n\n---\n🧠 이 워크스페이스가 이미 아는 것 (${live.length}건) — 일을 시작하기 전에 읽으세요.\n` +
|
|
104
|
+
live.map(line).join('\n') +
|
|
105
|
+
`\n"확인필요"는 출처 문서가 바뀌었다는 뜻입니다 — 맞는지 확인하고 쓰세요.\n` +
|
|
106
|
+
`새로 알게 된 규칙·결정·함정이 있으면 rootr_remember로 한 줄 남겨 두세요.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function withMemoryRecall(server) {
|
|
111
|
+
const original = server.registerTool.bind(server);
|
|
112
|
+
server.registerTool = (name, spec, handler) =>
|
|
113
|
+
original(name, spec, async (...args) => {
|
|
114
|
+
const result = await handler(...args);
|
|
115
|
+
// 기억 도구 자신에게 기억을 얹으면 같은 목록이 두 번 나온다.
|
|
116
|
+
if (name === 'rootr_recall' || name === 'rootr_remember' || name === 'rootr_forget') return result;
|
|
117
|
+
if (!result || result.isError || !Array.isArray(result.content)) return result;
|
|
118
|
+
|
|
119
|
+
let client;
|
|
120
|
+
try {
|
|
121
|
+
client = makeClient(args?.[0]?.workspace);
|
|
122
|
+
} catch {
|
|
123
|
+
return result; // 워크스페이스가 안 정해진 도구(설정·워크스페이스 목록 등)
|
|
124
|
+
}
|
|
125
|
+
const ws = client.workspace;
|
|
126
|
+
if (!ws || memoryPrimed.has(ws)) return result;
|
|
127
|
+
memoryPrimed.add(ws);
|
|
128
|
+
|
|
129
|
+
let briefing = null;
|
|
130
|
+
try {
|
|
131
|
+
// 한도 제외는 서버가 건다(@NotRateLimited). 여기서는 그냥 부르면 된다.
|
|
132
|
+
const recalled = await client.recall({ limit: 200 });
|
|
133
|
+
briefing = memoryBriefing(recalled?.memories);
|
|
134
|
+
} catch {
|
|
135
|
+
// 기억을 못 읽는다고 원래 하던 일이 실패하면 안 된다. 조용히 넘어간다.
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
if (!briefing) return result;
|
|
139
|
+
|
|
140
|
+
const last = [...result.content].reverse().find((c) => c && c.type === 'text');
|
|
141
|
+
if (!last) return result;
|
|
142
|
+
return {
|
|
143
|
+
...result,
|
|
144
|
+
content: result.content.map((c) => (c === last ? { ...c, text: `${c.text}${briefing}` } : c)),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
return server;
|
|
148
|
+
}
|
|
149
|
+
|
|
66
150
|
/**
|
|
67
151
|
* Builds a fully-configured McpServer (all tool groups registered) without
|
|
68
152
|
* connecting a transport. Shared by the local stdio CLI server below and by
|
|
@@ -160,6 +244,9 @@ export function buildMcpServer({ name, version }) {
|
|
|
160
244
|
},
|
|
161
245
|
);
|
|
162
246
|
|
|
247
|
+
// 순서가 중요하다. 나중에 감싼 쪽이 바깥이므로, 기억 브리핑을 먼저 붙이고
|
|
248
|
+
// 한도 경고가 그 뒤에 붙는다 — 경고는 언제나 맨 끝에 있어야 눈에 띈다.
|
|
249
|
+
withMemoryRecall(server);
|
|
163
250
|
withQuotaWarnings(server);
|
|
164
251
|
|
|
165
252
|
registerDocumentTools(server);
|
|
@@ -181,6 +268,7 @@ export function buildMcpServer({ name, version }) {
|
|
|
181
268
|
registerAttachmentTools(server);
|
|
182
269
|
registerSignedDocTools(server);
|
|
183
270
|
registerFeedbackTools(server);
|
|
271
|
+
registerMemoryTools(server);
|
|
184
272
|
|
|
185
273
|
return server;
|
|
186
274
|
}
|