rootr-cli 0.14.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 CHANGED
@@ -275,6 +275,22 @@ 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
296
  `rootr mcp`는 stdio로 MCP(Model Context Protocol) 서버를 구동합니다. 제공 도구 152종:
@@ -527,6 +543,28 @@ rootr ask "왜 결제 실패가 늘었지?" --workspace ws_123
527
543
  | `rootr_save_as_template` | 지금 노드를(또는 즉석 정의를) 템플릿으로 저장 |
528
544
  | `rootr_update_template` | 템플릿 수정 |
529
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
+
530
568
  #### 제보 (인증 불필요)
531
569
 
532
570
  | 도구 | 설명 |
package/bin/rootr.js CHANGED
@@ -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
 
@@ -1756,6 +1847,8 @@ async function main() {
1756
1847
  return cmdFeedback(args);
1757
1848
  case 'scaffold':
1758
1849
  return cmdScaffold(args);
1850
+ case 'memory':
1851
+ return cmdMemory(args);
1759
1852
  case 'mcp':
1760
1853
  return cmdMcp(args);
1761
1854
  default:
package/lib/client.js CHANGED
@@ -1673,4 +1673,48 @@ export class RootrClient {
1673
1673
  }
1674
1674
  return data;
1675
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
+
1676
1720
  }
@@ -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.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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.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": {