remnote-bridge 0.1.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.
Files changed (135) hide show
  1. package/dist/cli/commands/connect.d.ts +12 -0
  2. package/dist/cli/commands/connect.js +124 -0
  3. package/dist/cli/commands/disconnect.d.ts +11 -0
  4. package/dist/cli/commands/disconnect.js +100 -0
  5. package/dist/cli/commands/edit-rem.d.ts +13 -0
  6. package/dist/cli/commands/edit-rem.js +83 -0
  7. package/dist/cli/commands/edit-tree.d.ts +14 -0
  8. package/dist/cli/commands/edit-tree.js +67 -0
  9. package/dist/cli/commands/health.d.ts +12 -0
  10. package/dist/cli/commands/health.js +100 -0
  11. package/dist/cli/commands/install-skill.d.ts +6 -0
  12. package/dist/cli/commands/install-skill.js +39 -0
  13. package/dist/cli/commands/read-context.d.ts +20 -0
  14. package/dist/cli/commands/read-context.js +77 -0
  15. package/dist/cli/commands/read-globe.d.ts +16 -0
  16. package/dist/cli/commands/read-globe.js +60 -0
  17. package/dist/cli/commands/read-rem.d.ts +16 -0
  18. package/dist/cli/commands/read-rem.js +80 -0
  19. package/dist/cli/commands/read-tree.d.ts +17 -0
  20. package/dist/cli/commands/read-tree.js +85 -0
  21. package/dist/cli/commands/search.d.ts +12 -0
  22. package/dist/cli/commands/search.js +65 -0
  23. package/dist/cli/config.d.ts +55 -0
  24. package/dist/cli/config.js +139 -0
  25. package/dist/cli/daemon/daemon.d.ts +11 -0
  26. package/dist/cli/daemon/daemon.js +186 -0
  27. package/dist/cli/daemon/dev-server.d.ts +26 -0
  28. package/dist/cli/daemon/dev-server.js +81 -0
  29. package/dist/cli/daemon/pid.d.ts +34 -0
  30. package/dist/cli/daemon/pid.js +67 -0
  31. package/dist/cli/daemon/send-request.d.ts +24 -0
  32. package/dist/cli/daemon/send-request.js +92 -0
  33. package/dist/cli/handlers/context-read-handler.d.ts +18 -0
  34. package/dist/cli/handlers/context-read-handler.js +24 -0
  35. package/dist/cli/handlers/edit-handler.d.ts +30 -0
  36. package/dist/cli/handlers/edit-handler.js +133 -0
  37. package/dist/cli/handlers/globe-read-handler.d.ts +17 -0
  38. package/dist/cli/handlers/globe-read-handler.js +23 -0
  39. package/dist/cli/handlers/read-handler.d.ts +16 -0
  40. package/dist/cli/handlers/read-handler.js +78 -0
  41. package/dist/cli/handlers/rem-cache.d.ts +19 -0
  42. package/dist/cli/handlers/rem-cache.js +63 -0
  43. package/dist/cli/handlers/tree-edit-handler.d.ts +30 -0
  44. package/dist/cli/handlers/tree-edit-handler.js +188 -0
  45. package/dist/cli/handlers/tree-parser.d.ts +95 -0
  46. package/dist/cli/handlers/tree-parser.js +506 -0
  47. package/dist/cli/handlers/tree-read-handler.d.ts +28 -0
  48. package/dist/cli/handlers/tree-read-handler.js +53 -0
  49. package/dist/cli/main.d.ts +7 -0
  50. package/dist/cli/main.js +300 -0
  51. package/dist/cli/protocol.d.ts +39 -0
  52. package/dist/cli/protocol.js +35 -0
  53. package/dist/cli/server/config-server.d.ts +26 -0
  54. package/dist/cli/server/config-server.js +363 -0
  55. package/dist/cli/server/ws-server.d.ts +68 -0
  56. package/dist/cli/server/ws-server.js +335 -0
  57. package/dist/cli/utils/output.d.ts +11 -0
  58. package/dist/cli/utils/output.js +13 -0
  59. package/dist/mcp/daemon-client.d.ts +31 -0
  60. package/dist/mcp/daemon-client.js +99 -0
  61. package/dist/mcp/index.d.ts +7 -0
  62. package/dist/mcp/index.js +68 -0
  63. package/dist/mcp/instructions.d.ts +1 -0
  64. package/dist/mcp/instructions.js +249 -0
  65. package/dist/mcp/resources/edit-tree-guide.d.ts +1 -0
  66. package/dist/mcp/resources/edit-tree-guide.js +197 -0
  67. package/dist/mcp/resources/error-reference.d.ts +1 -0
  68. package/dist/mcp/resources/error-reference.js +132 -0
  69. package/dist/mcp/resources/outline-format.d.ts +1 -0
  70. package/dist/mcp/resources/outline-format.js +104 -0
  71. package/dist/mcp/resources/rem-object-fields.d.ts +1 -0
  72. package/dist/mcp/resources/rem-object-fields.js +331 -0
  73. package/dist/mcp/resources/separator-flashcard.d.ts +1 -0
  74. package/dist/mcp/resources/separator-flashcard.js +120 -0
  75. package/dist/mcp/tools/edit-tools.d.ts +5 -0
  76. package/dist/mcp/tools/edit-tools.js +47 -0
  77. package/dist/mcp/tools/infra-tools.d.ts +5 -0
  78. package/dist/mcp/tools/infra-tools.js +43 -0
  79. package/dist/mcp/tools/read-tools.d.ts +5 -0
  80. package/dist/mcp/tools/read-tools.js +195 -0
  81. package/dist/mcp/types.d.ts +12 -0
  82. package/dist/mcp/types.js +4 -0
  83. package/docs/instruction/connect.md +158 -0
  84. package/docs/instruction/disconnect.md +146 -0
  85. package/docs/instruction/edit-rem.md +509 -0
  86. package/docs/instruction/edit-tree.md +419 -0
  87. package/docs/instruction/health.md +159 -0
  88. package/docs/instruction/overall.md +751 -0
  89. package/docs/instruction/read-context.md +353 -0
  90. package/docs/instruction/read-globe.md +206 -0
  91. package/docs/instruction/read-rem.md +476 -0
  92. package/docs/instruction/read-tree.md +428 -0
  93. package/docs/instruction/search.md +196 -0
  94. package/package.json +41 -0
  95. package/remnote-plugin/package.json +48 -0
  96. package/remnote-plugin/postcss.config.js +5 -0
  97. package/remnote-plugin/public/bridge-icon.svg +8 -0
  98. package/remnote-plugin/public/manifest.json +22 -0
  99. package/remnote-plugin/src/bridge/message-router.ts +57 -0
  100. package/remnote-plugin/src/bridge/websocket-client.ts +245 -0
  101. package/remnote-plugin/src/index.css +1 -0
  102. package/remnote-plugin/src/services/breadcrumb.ts +26 -0
  103. package/remnote-plugin/src/services/create-rem.ts +59 -0
  104. package/remnote-plugin/src/services/delete-rem.ts +29 -0
  105. package/remnote-plugin/src/services/index.ts +16 -0
  106. package/remnote-plugin/src/services/move-rem.ts +39 -0
  107. package/remnote-plugin/src/services/powerup-filter.ts +31 -0
  108. package/remnote-plugin/src/services/read-context.ts +368 -0
  109. package/remnote-plugin/src/services/read-globe.ts +197 -0
  110. package/remnote-plugin/src/services/read-rem.ts +284 -0
  111. package/remnote-plugin/src/services/read-tree.ts +222 -0
  112. package/remnote-plugin/src/services/rem-builder.ts +124 -0
  113. package/remnote-plugin/src/services/reorder-children.ts +61 -0
  114. package/remnote-plugin/src/services/search.ts +56 -0
  115. package/remnote-plugin/src/services/write-rem-fields.ts +254 -0
  116. package/remnote-plugin/src/settings.ts +12 -0
  117. package/remnote-plugin/src/style.css +45 -0
  118. package/remnote-plugin/src/test-scripts/AGENTS.md +46 -0
  119. package/remnote-plugin/src/test-scripts/test-actions.ts +230 -0
  120. package/remnote-plugin/src/test-scripts/test-powerup-rendering.ts +722 -0
  121. package/remnote-plugin/src/test-scripts/test-rem-type-mapping.ts +283 -0
  122. package/remnote-plugin/src/test-scripts/test-richtext-builder.ts +207 -0
  123. package/remnote-plugin/src/test-scripts/test-richtext-matrix.ts +332 -0
  124. package/remnote-plugin/src/test-scripts/test-richtext-remaining.ts +245 -0
  125. package/remnote-plugin/src/test-scripts/test-rw-fields.ts +399 -0
  126. package/remnote-plugin/src/types.ts +419 -0
  127. package/remnote-plugin/src/utils/elision.ts +45 -0
  128. package/remnote-plugin/src/utils/index.ts +10 -0
  129. package/remnote-plugin/src/utils/tree-serializer.ts +269 -0
  130. package/remnote-plugin/src/widgets/bridge_widget.tsx +170 -0
  131. package/remnote-plugin/src/widgets/index.tsx +82 -0
  132. package/remnote-plugin/tailwind.config.js +7 -0
  133. package/remnote-plugin/tsconfig.json +21 -0
  134. package/remnote-plugin/webpack.config.js +125 -0
  135. package/skill/SKILL.md +428 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * WebSocket Server
3
+ *
4
+ * 守护进程的核心基础设施:监听端口,管理 Plugin 和 CLI 命令的连接。
5
+ * - 接受 Plugin 的 hello 握手
6
+ * - 向 Plugin 发送 ping 心跳
7
+ * - 分发 CLI 请求到对应的 handler
8
+ * - 只允许一个 Plugin 连接
9
+ *
10
+ * 业务编排逻辑在 handlers/ 中,本文件只做连接管理和请求分发。
11
+ */
12
+ import { WebSocketServer, WebSocket } from 'ws';
13
+ import { isHelloMessage, isPongMessage, isBridgeRequest, isBridgeResponse } from '../protocol.js';
14
+ import { DEFAULT_DEFAULTS } from '../config.js';
15
+ import { RemCache } from '../handlers/rem-cache.js';
16
+ import { ReadHandler } from '../handlers/read-handler.js';
17
+ import { EditHandler } from '../handlers/edit-handler.js';
18
+ import { TreeReadHandler } from '../handlers/tree-read-handler.js';
19
+ import { TreeEditHandler } from '../handlers/tree-edit-handler.js';
20
+ import { GlobeReadHandler } from '../handlers/globe-read-handler.js';
21
+ import { ContextReadHandler } from '../handlers/context-read-handler.js';
22
+ import crypto from 'crypto';
23
+ const PLUGIN_REQUEST_TIMEOUT_MS = 15_000;
24
+ export class BridgeServer {
25
+ wss = null;
26
+ pluginSocket = null;
27
+ pluginVersion = null;
28
+ pluginSdkReady = false;
29
+ pingTimer = null;
30
+ pongTimer = null;
31
+ startTime = Date.now();
32
+ pendingPluginRequests = new Map();
33
+ // 业务编排器
34
+ readHandler;
35
+ editHandler;
36
+ treeReadHandler;
37
+ treeEditHandler;
38
+ globeReadHandler;
39
+ contextReadHandler;
40
+ defaults;
41
+ config;
42
+ /** 每当收到 CLI 命令请求时触发(用于刷新守护进程超时计时器) */
43
+ onCliRequest;
44
+ constructor(config) {
45
+ this.config = {
46
+ port: config.port,
47
+ host: config.host ?? '127.0.0.1',
48
+ pingIntervalMs: config.pingIntervalMs ?? 30_000,
49
+ pongTimeoutMs: config.pongTimeoutMs ?? 10_000,
50
+ onLog: config.onLog,
51
+ getTimeoutRemaining: config.getTimeoutRemaining,
52
+ };
53
+ this.defaults = config.defaults ?? DEFAULT_DEFAULTS;
54
+ const defaults = this.defaults;
55
+ const remCache = new RemCache(defaults.cacheMaxSize);
56
+ const forwardFn = (action, payload) => this.forwardToPlugin(action, payload);
57
+ this.readHandler = new ReadHandler(remCache, forwardFn, config.onLog);
58
+ this.editHandler = new EditHandler(remCache, forwardFn);
59
+ this.treeReadHandler = new TreeReadHandler(remCache, forwardFn, config.onLog, defaults);
60
+ this.treeEditHandler = new TreeEditHandler(remCache, forwardFn, defaults);
61
+ this.globeReadHandler = new GlobeReadHandler(forwardFn, defaults);
62
+ this.contextReadHandler = new ContextReadHandler(forwardFn, defaults);
63
+ }
64
+ log(message, level = 'info') {
65
+ this.config.onLog?.(message, level);
66
+ }
67
+ /**
68
+ * 启动 WS Server。返回 Promise,监听成功后 resolve。
69
+ */
70
+ start() {
71
+ return new Promise((resolve, reject) => {
72
+ this.startTime = Date.now();
73
+ this.wss = new WebSocketServer({
74
+ port: this.config.port,
75
+ host: this.config.host,
76
+ maxPayload: 1 * 1024 * 1024, // 1MB,足够所有 JSON 消息
77
+ });
78
+ this.wss.on('listening', () => {
79
+ this.log(`WS Server 监听 ${this.config.host}:${this.config.port}`);
80
+ this.startPingInterval();
81
+ resolve();
82
+ });
83
+ this.wss.on('error', (err) => {
84
+ this.log(`WS Server 错误: ${err.message}`, 'error');
85
+ reject(err);
86
+ });
87
+ this.wss.on('connection', (ws) => {
88
+ this.handleConnection(ws);
89
+ });
90
+ });
91
+ }
92
+ /**
93
+ * 关闭 WS Server。
94
+ */
95
+ stop() {
96
+ return new Promise((resolve) => {
97
+ this.stopPingInterval();
98
+ if (this.pluginSocket) {
99
+ this.pluginSocket.close(1000, 'Server shutdown');
100
+ this.pluginSocket = null;
101
+ }
102
+ if (this.wss) {
103
+ this.wss.close(() => {
104
+ this.wss = null;
105
+ resolve();
106
+ });
107
+ }
108
+ else {
109
+ resolve();
110
+ }
111
+ });
112
+ }
113
+ handleConnection(ws) {
114
+ // 等待第一条消息来区分 Plugin 和 CLI 命令
115
+ let identified = false;
116
+ ws.on('message', (data) => {
117
+ let message;
118
+ try {
119
+ message = JSON.parse(data.toString());
120
+ }
121
+ catch {
122
+ this.log('收到无法解析的消息', 'warn');
123
+ return;
124
+ }
125
+ // 第一条消息用来识别连接类型
126
+ if (!identified) {
127
+ if (isHelloMessage(message)) {
128
+ identified = true;
129
+ this.handlePluginHello(ws, message);
130
+ return;
131
+ }
132
+ if (isBridgeRequest(message)) {
133
+ identified = true;
134
+ this.handleCliRequest(ws, message);
135
+ return;
136
+ }
137
+ this.log('收到未知类型的首条消息', 'warn');
138
+ return;
139
+ }
140
+ // 已识别为 Plugin 的后续消息
141
+ if (ws === this.pluginSocket) {
142
+ if (isPongMessage(message)) {
143
+ this.handlePong();
144
+ return;
145
+ }
146
+ // Plugin 返回的 BridgeResponse(子请求的响应)
147
+ if (isBridgeResponse(message)) {
148
+ this.handlePluginResponse(message);
149
+ return;
150
+ }
151
+ return;
152
+ }
153
+ // 已识别为 CLI 的后续请求
154
+ if (isBridgeRequest(message)) {
155
+ this.handleCliRequest(ws, message);
156
+ }
157
+ });
158
+ ws.on('close', () => {
159
+ if (ws === this.pluginSocket) {
160
+ this.log('Plugin 已断开', 'warn');
161
+ this.pluginSocket = null;
162
+ this.pluginVersion = null;
163
+ this.pluginSdkReady = false;
164
+ // 清理可能残留的 pong 超时定时器
165
+ if (this.pongTimer) {
166
+ clearTimeout(this.pongTimer);
167
+ this.pongTimer = null;
168
+ }
169
+ // 拒绝所有等待中的 Plugin 子请求
170
+ for (const [id, pending] of this.pendingPluginRequests) {
171
+ clearTimeout(pending.timer);
172
+ pending.reject(new Error('Plugin 已断开'));
173
+ this.pendingPluginRequests.delete(id);
174
+ }
175
+ }
176
+ });
177
+ ws.on('error', (err) => {
178
+ this.log(`连接错误: ${err.message}`, 'error');
179
+ });
180
+ }
181
+ handlePluginHello(ws, hello) {
182
+ if (this.pluginSocket && this.pluginSocket.readyState === WebSocket.OPEN) {
183
+ // 已有 Plugin 连接,拒绝新连接
184
+ this.log(`拒绝 Plugin 连接(已有连接)`, 'warn');
185
+ ws.close(4000, 'Another plugin is already connected');
186
+ return;
187
+ }
188
+ this.pluginSocket = ws;
189
+ this.pluginVersion = hello.version;
190
+ this.pluginSdkReady = hello.sdkReady ?? false;
191
+ this.log(`Plugin 已连接 (v${hello.version}, SDK: ${this.pluginSdkReady ? '就绪' : '未就绪'})`);
192
+ }
193
+ async handleCliRequest(ws, request) {
194
+ // 通知守护进程刷新超时计时器
195
+ this.onCliRequest?.();
196
+ // get_status:内部处理(不需要 Plugin)
197
+ if (request.action === 'get_status') {
198
+ const result = this.getStatus();
199
+ const response = { id: request.id, result };
200
+ ws.send(JSON.stringify(response));
201
+ return;
202
+ }
203
+ // 以下 action 都需要 Plugin 连接
204
+ if (!this.pluginSocket || this.pluginSocket.readyState !== WebSocket.OPEN) {
205
+ const response = {
206
+ id: request.id,
207
+ error: 'Plugin 未连接,请确认 RemNote 已打开且插件已激活',
208
+ };
209
+ ws.send(JSON.stringify(response));
210
+ return;
211
+ }
212
+ // 分发到对应 handler
213
+ try {
214
+ let result;
215
+ if (request.action === 'read_rem') {
216
+ result = await this.readHandler.handleReadRem(request.payload);
217
+ }
218
+ else if (request.action === 'read_tree') {
219
+ result = await this.treeReadHandler.handleReadTree(request.payload);
220
+ }
221
+ else if (request.action === 'edit_tree') {
222
+ result = await this.treeEditHandler.handleEditTree(request.payload);
223
+ }
224
+ else if (request.action === 'read_globe') {
225
+ result = await this.globeReadHandler.handleReadGlobe(request.payload);
226
+ }
227
+ else if (request.action === 'read_context') {
228
+ result = await this.contextReadHandler.handleReadContext(request.payload);
229
+ }
230
+ else if (request.action === 'edit_rem') {
231
+ result = await this.editHandler.handleEditRem(request.payload);
232
+ }
233
+ else if (request.action === 'search') {
234
+ // search:注入默认 numResults 后转发给 Plugin
235
+ const searchPayload = { ...request.payload };
236
+ if (searchPayload.numResults == null) {
237
+ searchPayload.numResults = this.defaults.searchNumResults;
238
+ }
239
+ result = await this.forwardToPlugin('search', searchPayload);
240
+ }
241
+ else {
242
+ // 其他 action:直接转发给 Plugin
243
+ result = await this.forwardToPlugin(request.action, request.payload);
244
+ }
245
+ const response = { id: request.id, result };
246
+ ws.send(JSON.stringify(response));
247
+ }
248
+ catch (error) {
249
+ const response = {
250
+ id: request.id,
251
+ error: error instanceof Error ? error.message : String(error),
252
+ };
253
+ ws.send(JSON.stringify(response));
254
+ }
255
+ }
256
+ /**
257
+ * 向 Plugin 发送子请求并等待响应。
258
+ * 生成独立的请求 ID,通过 pendingPluginRequests 关联 Promise。
259
+ */
260
+ forwardToPlugin(action, payload) {
261
+ return new Promise((resolve, reject) => {
262
+ if (!this.pluginSocket || this.pluginSocket.readyState !== WebSocket.OPEN) {
263
+ reject(new Error('Plugin 未连接'));
264
+ return;
265
+ }
266
+ const requestId = crypto.randomUUID();
267
+ const timer = setTimeout(() => {
268
+ this.pendingPluginRequests.delete(requestId);
269
+ reject(new Error(`Plugin 请求超时 (${PLUGIN_REQUEST_TIMEOUT_MS / 1000}s): ${action}`));
270
+ }, PLUGIN_REQUEST_TIMEOUT_MS);
271
+ this.pendingPluginRequests.set(requestId, { resolve, reject, timer });
272
+ const subRequest = {
273
+ id: requestId,
274
+ action,
275
+ payload,
276
+ };
277
+ this.pluginSocket.send(JSON.stringify(subRequest));
278
+ this.log(`转发到 Plugin: ${action} (${requestId.slice(0, 8)}...)`);
279
+ });
280
+ }
281
+ handlePluginResponse(response) {
282
+ const pending = this.pendingPluginRequests.get(response.id);
283
+ if (!pending) {
284
+ this.log(`收到未知 ID 的 Plugin 响应: ${response.id}`, 'warn');
285
+ return;
286
+ }
287
+ clearTimeout(pending.timer);
288
+ this.pendingPluginRequests.delete(response.id);
289
+ if (response.error) {
290
+ pending.reject(new Error(response.error));
291
+ }
292
+ else {
293
+ pending.resolve(response.result);
294
+ }
295
+ }
296
+ handlePong() {
297
+ if (this.pongTimer) {
298
+ clearTimeout(this.pongTimer);
299
+ this.pongTimer = null;
300
+ }
301
+ }
302
+ startPingInterval() {
303
+ this.pingTimer = setInterval(() => {
304
+ if (this.pluginSocket?.readyState === WebSocket.OPEN) {
305
+ this.pluginSocket.send(JSON.stringify({ type: 'ping' }));
306
+ this.pongTimer = setTimeout(() => {
307
+ this.log('Plugin 心跳超时,断开连接', 'warn');
308
+ this.pluginSocket?.close(4001, 'Pong timeout');
309
+ this.pluginSocket = null;
310
+ this.pluginVersion = null;
311
+ this.pluginSdkReady = false;
312
+ }, this.config.pongTimeoutMs);
313
+ }
314
+ }, this.config.pingIntervalMs);
315
+ }
316
+ stopPingInterval() {
317
+ if (this.pingTimer) {
318
+ clearInterval(this.pingTimer);
319
+ this.pingTimer = null;
320
+ }
321
+ if (this.pongTimer) {
322
+ clearTimeout(this.pongTimer);
323
+ this.pongTimer = null;
324
+ }
325
+ }
326
+ /** 获取当前状态(timeoutRemaining 通过构造时注入的回调获取) */
327
+ getStatus() {
328
+ return {
329
+ pluginConnected: this.pluginSocket?.readyState === WebSocket.OPEN,
330
+ sdkReady: this.pluginSdkReady,
331
+ uptime: Math.floor((Date.now() - this.startTime) / 1000),
332
+ timeoutRemaining: this.config.getTimeoutRemaining?.() ?? 0,
333
+ };
334
+ }
335
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * output.ts — CLI 输出工具函数
3
+ *
4
+ * 统一 JSON 输出格式,自动注入 timestamp 字段。
5
+ */
6
+ /**
7
+ * 输出 JSON 到 stdout,自动追加 ISO 8601 时间戳。
8
+ *
9
+ * 所有 CLI 命令的 --json 模式应通过此函数输出。
10
+ */
11
+ export declare function jsonOutput(data: Record<string, unknown>): void;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * output.ts — CLI 输出工具函数
3
+ *
4
+ * 统一 JSON 输出格式,自动注入 timestamp 字段。
5
+ */
6
+ /**
7
+ * 输出 JSON 到 stdout,自动追加 ISO 8601 时间戳。
8
+ *
9
+ * 所有 CLI 命令的 --json 模式应通过此函数输出。
10
+ */
11
+ export function jsonOutput(data) {
12
+ console.log(JSON.stringify({ ...data, timestamp: new Date().toISOString() }));
13
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * daemon-client.ts
3
+ *
4
+ * 封装对 remnote-bridge CLI 的子进程调用(--json 模式)。
5
+ * MCP Server 通过此模块与 CLI 守护进程通信。
6
+ *
7
+ * 调用约定(来自 src/cli/main.ts):
8
+ * --json 是全局选项,位于命令名之前。
9
+ * 无 payload:remnote-bridge --json <command>
10
+ * 有 payload:remnote-bridge --json <command> '<JSON>'
11
+ */
12
+ import type { CliResponse } from './types.js';
13
+ export declare class CliError extends Error {
14
+ /** CLI 返回的 command 字段 */
15
+ readonly command: string;
16
+ /** CLI 返回的完整响应(如果成功解析了 JSON) */
17
+ readonly response?: CliResponse;
18
+ constructor(message: string, command: string, response?: CliResponse);
19
+ }
20
+ /**
21
+ * 通过子进程调用 remnote-bridge --json 模式。
22
+ *
23
+ * @param command CLI 命令名(如 'read-rem', 'edit-tree', 'health')
24
+ * @param payload JSON 参数对象(无参数的命令可省略)
25
+ * @param options 可选配置
26
+ * @returns 解析后的 CliResponse(ok === true)
27
+ * @throws {CliError} 当 CLI 返回 ok: false 或进程异常时
28
+ */
29
+ export declare function callCli(command: string, payload?: Record<string, unknown>, options?: {
30
+ timeoutMs?: number;
31
+ }): Promise<CliResponse>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * daemon-client.ts
3
+ *
4
+ * 封装对 remnote-bridge CLI 的子进程调用(--json 模式)。
5
+ * MCP Server 通过此模块与 CLI 守护进程通信。
6
+ *
7
+ * 调用约定(来自 src/cli/main.ts):
8
+ * --json 是全局选项,位于命令名之前。
9
+ * 无 payload:remnote-bridge --json <command>
10
+ * 有 payload:remnote-bridge --json <command> '<JSON>'
11
+ */
12
+ import { execFile } from 'node:child_process';
13
+ // ---------------------------------------------------------------------------
14
+ // 配置
15
+ // ---------------------------------------------------------------------------
16
+ /** CLI 可执行文件名(对应 package.json bin 字段) */
17
+ const CLI_BIN = 'remnote-bridge';
18
+ /** 子进程默认超时(毫秒) */
19
+ const DEFAULT_TIMEOUT_MS = 30_000;
20
+ // ---------------------------------------------------------------------------
21
+ // 错误类
22
+ // ---------------------------------------------------------------------------
23
+ export class CliError extends Error {
24
+ /** CLI 返回的 command 字段 */
25
+ command;
26
+ /** CLI 返回的完整响应(如果成功解析了 JSON) */
27
+ response;
28
+ constructor(message, command, response) {
29
+ super(message);
30
+ this.name = 'CliError';
31
+ this.command = command;
32
+ this.response = response;
33
+ }
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // 核心函数
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * 通过子进程调用 remnote-bridge --json 模式。
40
+ *
41
+ * @param command CLI 命令名(如 'read-rem', 'edit-tree', 'health')
42
+ * @param payload JSON 参数对象(无参数的命令可省略)
43
+ * @param options 可选配置
44
+ * @returns 解析后的 CliResponse(ok === true)
45
+ * @throws {CliError} 当 CLI 返回 ok: false 或进程异常时
46
+ */
47
+ export async function callCli(command, payload, options) {
48
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
49
+ // 构造参数列表:--json <command> [jsonStr]
50
+ const args = ['--json', command];
51
+ if (payload !== undefined) {
52
+ args.push(JSON.stringify(payload));
53
+ }
54
+ const stdout = await execCliProcess(args, timeoutMs);
55
+ // 解析 JSON 输出
56
+ let response;
57
+ try {
58
+ response = JSON.parse(stdout);
59
+ }
60
+ catch {
61
+ throw new CliError(`CLI 输出不是合法 JSON: ${stdout.slice(0, 200)}`, command);
62
+ }
63
+ // ok === false 时抛出错误
64
+ if (!response.ok) {
65
+ throw new CliError(response.error ?? `命令 ${command} 失败(无错误描述)`, command, response);
66
+ }
67
+ return response;
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // 内部辅助
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * 执行 CLI 子进程并返回 stdout。
74
+ */
75
+ function execCliProcess(args, timeoutMs) {
76
+ return new Promise((resolve, reject) => {
77
+ execFile(CLI_BIN, args, {
78
+ timeout: timeoutMs,
79
+ maxBuffer: 10 * 1024 * 1024, // 10 MB — read-globe/read-tree 可能较大
80
+ env: { ...process.env },
81
+ }, (error, stdout, stderr) => {
82
+ if (error) {
83
+ // 超时
84
+ if (error.killed) {
85
+ reject(new CliError(`CLI 子进程超时(${timeoutMs}ms): ${CLI_BIN} ${args.join(' ')}`, args[1] ?? 'unknown'));
86
+ return;
87
+ }
88
+ // 非零退出码但有 stdout(CLI 可能在 stdout 输出了 JSON 错误)
89
+ if (stdout.trim()) {
90
+ resolve(stdout.trim());
91
+ return;
92
+ }
93
+ reject(new CliError(`CLI 子进程异常: ${error.message}${stderr ? ` | stderr: ${stderr}` : ''}`, args[1] ?? 'unknown'));
94
+ return;
95
+ }
96
+ resolve(stdout.trim());
97
+ });
98
+ });
99
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * remnote-bridge MCP Server 入口
3
+ *
4
+ * 通过 FastMCP 将 RemNote 知识库操作暴露为 MCP 工具。
5
+ * 所有业务逻辑委托给 remnote-bridge CLI(通过 daemon-client 子进程调用)。
6
+ */
7
+ export declare function startMcpServer(): Promise<void>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * remnote-bridge MCP Server 入口
3
+ *
4
+ * 通过 FastMCP 将 RemNote 知识库操作暴露为 MCP 工具。
5
+ * 所有业务逻辑委托给 remnote-bridge CLI(通过 daemon-client 子进程调用)。
6
+ */
7
+ import { FastMCP } from 'fastmcp';
8
+ import { SERVER_INSTRUCTIONS } from './instructions.js';
9
+ import { registerReadTools } from './tools/read-tools.js';
10
+ import { registerEditTools } from './tools/edit-tools.js';
11
+ import { registerInfraTools } from './tools/infra-tools.js';
12
+ import { OUTLINE_FORMAT_CONTENT } from './resources/outline-format.js';
13
+ import { REM_OBJECT_FIELDS_CONTENT } from './resources/rem-object-fields.js';
14
+ import { EDIT_TREE_GUIDE_CONTENT } from './resources/edit-tree-guide.js';
15
+ import { ERROR_REFERENCE_CONTENT } from './resources/error-reference.js';
16
+ import { SEPARATOR_FLASHCARD_CONTENT } from './resources/separator-flashcard.js';
17
+ export async function startMcpServer() {
18
+ const server = new FastMCP({
19
+ name: 'remnote-bridge',
20
+ version: '0.1.0',
21
+ instructions: SERVER_INSTRUCTIONS,
22
+ });
23
+ registerInfraTools(server);
24
+ registerReadTools(server);
25
+ registerEditTools(server);
26
+ // Resources
27
+ server.addResource({
28
+ uri: 'remnote://outline-format',
29
+ name: 'Markdown 大纲格式规范',
30
+ mimeType: 'text/markdown',
31
+ async load() {
32
+ return { text: OUTLINE_FORMAT_CONTENT };
33
+ },
34
+ });
35
+ server.addResource({
36
+ uri: 'remnote://rem-object-fields',
37
+ name: 'RemObject 字段完整参考',
38
+ mimeType: 'text/markdown',
39
+ async load() {
40
+ return { text: REM_OBJECT_FIELDS_CONTENT };
41
+ },
42
+ });
43
+ server.addResource({
44
+ uri: 'remnote://edit-tree-guide',
45
+ name: 'edit_tree 操作指南',
46
+ mimeType: 'text/markdown',
47
+ async load() {
48
+ return { text: EDIT_TREE_GUIDE_CONTENT };
49
+ },
50
+ });
51
+ server.addResource({
52
+ uri: 'remnote://error-reference',
53
+ name: '错误诊断与恢复参考',
54
+ mimeType: 'text/markdown',
55
+ async load() {
56
+ return { text: ERROR_REFERENCE_CONTENT };
57
+ },
58
+ });
59
+ server.addResource({
60
+ uri: 'remnote://separator-flashcard',
61
+ name: '分隔符与闪卡参考',
62
+ mimeType: 'text/markdown',
63
+ async load() {
64
+ return { text: SEPARATOR_FLASHCARD_CONTENT };
65
+ },
66
+ });
67
+ await server.start({ transportType: 'stdio' });
68
+ }
@@ -0,0 +1 @@
1
+ export declare const SERVER_INSTRUCTIONS = "\n# RemNote Bridge MCP Server \u2014 Agent \u64CD\u4F5C\u624B\u518C\n\nRemNote \u77E5\u8BC6\u5E93\u64CD\u4F5C\u5DE5\u5177\u96C6\u3002\u4F60\u53EF\u4EE5\u901A\u8FC7\u8FD9\u4E9B\u5DE5\u5177\u8BFB\u53D6\u3001\u641C\u7D22\u3001\u7F16\u8F91\u7528\u6237\u7684 RemNote \u7B14\u8BB0\u548C\u77E5\u8BC6\u7ED3\u6784\u3002\u4E0D\u80FD\u64CD\u63A7\u95EA\u5361\uFF08Card/Flashcard\uFF09\u672C\u8EAB\u2014\u2014\u95EA\u5361\u7531 RemNote \u6839\u636E Rem \u5C5E\u6027\u81EA\u52A8\u751F\u6210\uFF1B\u4E5F\u4E0D\u80FD\u7BA1\u7406 Plugin \u6216\u7CFB\u7EDF\u914D\u7F6E\u3002\n\n---\n\n## 1. Core Concepts\n\n### Everything is Rem\n\n\u7528\u6237\u8BF4\u7684\"\u7B14\u8BB0\"\u3001\"\u6587\u6863\"\u3001\"\u6587\u4EF6\u5939\"\u3001\"\u95EA\u5361\"\uFF0C\u5E95\u5C42\u90FD\u662F **Rem**\u3002\u4E0D\u540C\u5F62\u6001\u901A\u8FC7\u5C5E\u6027\u533A\u5206\uFF1A\n\n| \u7528\u6237\u8BF4\u7684 | \u5B9E\u9645\u4E0A\u662F | \u533A\u5206\u65B9\u5F0F |\n|:---------|:---------|:---------|\n| \u7B14\u8BB0 / \u6761\u76EE | Rem | \u6700\u57FA\u672C\u7684\u6570\u636E\u5355\u5143 |\n| \u6587\u6863 / \u9875\u9762 | Rem\uFF08\\`isDocument=true\\`\uFF09 | \u53EF\u72EC\u7ACB\u6253\u5F00\u7684\u9875\u9762 |\n| \u6587\u4EF6\u5939 | Rem\uFF08Document + \u5B50\u8282\u70B9\u5168\u662F Document\uFF09 | UI \u6982\u5FF5\uFF0C\u65E0\u72EC\u7ACB\u6807\u8BB0 |\n| \u95EA\u5361 | Card\u2014\u2014\u7531 Rem \u5C5E\u6027\u81EA\u52A8\u751F\u6210 | **\u4E0D\u53EF\u76F4\u63A5\u64CD\u63A7** |\n\n### Type \u7CFB\u7EDF\n\nRem \u6709\u4E24\u4E2A\u72EC\u7ACB\u7EF4\u5EA6\uFF1A**type**\uFF08\u95EA\u5361\u8BED\u4E49\uFF09\u548C **isDocument**\uFF08\u9875\u9762\u8BED\u4E49\uFF09\uFF0C\u4E8C\u8005\u4E92\u4E0D\u5F71\u54CD\u3002\n\n| type | \u542B\u4E49 | UI \u8868\u73B0 |\n|:-----|:-----|:--------|\n| \\`concept\\` | \u6982\u5FF5\u5B9A\u4E49 | \u6587\u5B57**\u52A0\u7C97** |\n| \\`descriptor\\` | \u63CF\u8FF0/\u5C5E\u6027 | \u6587\u5B57*\u659C\u4F53* |\n| \\`default\\` | \u666E\u901A Rem | \u6B63\u5E38\u5B57\u91CD |\n| \\`portal\\` | \u5D4C\u5165\u5F15\u7528\u5BB9\u5668 | \u7D2B\u8272\u8FB9\u6846\uFF08**\u53EA\u8BFB**\uFF0C\u4E0D\u53EF\u8BBE\u7F6E\uFF09 |\n\n### Separator \u4E0E\u95EA\u5361\u521B\u5EFA\n\n\u5206\u9694\u7B26\u51B3\u5B9A Rem \u7684 type\u3001backText \u548C\u7EC3\u4E60\u65B9\u5411\u3002\u521B\u5EFA\u95EA\u5361\u7684\u672C\u8D28\u662F\u8BBE\u7F6E\u6B63\u786E\u7684\u5206\u9694\u7B26\uFF1A\n\n| \u5206\u9694\u7B26 | type | \u7528\u9014 |\n|:-------|:-----|:-----|\n| \\`::\\` | concept | \u6982\u5FF5\u5B9A\u4E49\uFF08\u53CC\u5411\uFF09 |\n| \\`;;\\` | descriptor | \u63CF\u8FF0\u5C5E\u6027\uFF08\u6B63\u5411\uFF09 |\n| \\`>>\\` | default | \u6B63\u5411\u95EE\u7B54 |\n| \\`<<\\` | default | \u53CD\u5411\u95EE\u7B54 |\n| \\`<>\\` | default | \u53CC\u5411\u95EE\u7B54 |\n| \\`>>>\\` | default | \u591A\u884C\u7B54\u6848\uFF08\u5B50 Rem \u4E3A\u7B54\u6848\uFF09 |\n| \\`{{}}\\` | default | \u5B8C\u5F62\u586B\u7A7A |\n\n\u5B8C\u6574\u5206\u9694\u7B26-\u95EA\u5361\u6620\u5C04\u8868\u89C1 \\`resource://separator-flashcard\\`\u3002\n\n### \u94FE\u63A5\u673A\u5236\n\n| \u673A\u5236 | \u7528\u6237\u64CD\u4F5C | \u8BF4\u660E |\n|:-----|:---------|:-----|\n| Reference \\`[[\\` | \u6587\u672C\u5185\u5F15\u7528 | \u53EA\u662F\u6307\u9488\uFF0C\u4E0D\u540C\u6B65\u7F16\u8F91 |\n| Tag \\`##\\` | \u9644\u52A0\u6807\u7B7E | \u5206\u7C7B\u6807\u8BB0 |\n| Portal \\`((\\` | \u5D4C\u5165\u5B9E\u65F6\u89C6\u56FE | \u7F16\u8F91\u540C\u6B65\uFF0C\u5927\u7EB2\u4E2D\u6807\u4E3A \\`type:portal\\` |\n\n### Powerup \u8FC7\u6EE4\n\nRemNote \u7684\u683C\u5F0F\u8BBE\u7F6E\uFF08\u6807\u9898\u3001\u9AD8\u4EAE\u3001\u4EE3\u7801\u7B49\uFF09\u4F1A\u6CE8\u5165\u9690\u85CF\u7684\u7CFB\u7EDF Tag \u548C\u5B50 Rem\u3002\u8FD9\u4E9B\u566A\u97F3\u6570\u636E**\u9ED8\u8BA4\u81EA\u52A8\u8FC7\u6EE4**\uFF0C\u4F60\u901A\u5E38\u65E0\u9700\u5173\u5FC3\u3002\n\n---\n\n## 2. Session Lifecycle\n\n\u6240\u6709\u64CD\u4F5C\u90FD\u4F9D\u8D56\u4E00\u4E2A\u6D3B\u8DC3\u7684\u4F1A\u8BDD\u3002\u4F1A\u8BDD = \u5B88\u62A4\u8FDB\u7A0B\u7684\u751F\u547D\u5468\u671F\u3002\n\n\\`\\`\\`\nconnect \u2192 \u542F\u52A8 daemon\uFF08\u5E42\u7B49\uFF0C\u91CD\u590D\u8C03\u7528\u5B89\u5168\uFF09\n \u2193\nhealth \u2192 \u786E\u8BA4\u4E09\u5C42\u5C31\u7EEA\uFF08daemon / Plugin / SDK\uFF09\n \u2193\n\u4E1A\u52A1\u64CD\u4F5C\uFF08read / search / edit\uFF09\n \u2193\ndisconnect \u2192 \u5173\u95ED daemon\uFF0C\u6E05\u7A7A\u6240\u6709\u7F13\u5B58\n\\`\\`\\`\n\n**\u5173\u952E\u8981\u70B9**\uFF1A\n- \\`connect\\` \u662F\u6240\u6709\u4E1A\u52A1\u64CD\u4F5C\u7684\u524D\u63D0\uFF0C\u672A connect \u65F6\u4EFB\u4F55\u547D\u4EE4\u90FD\u4F1A\u62A5\"\u5B88\u62A4\u8FDB\u7A0B\u672A\u8FD0\u884C\"\n- \\`health\\` \u68C0\u67E5\u4E09\u5C42\u72B6\u6001\uFF1Adaemon \u8FD0\u884C \u2192 Plugin \u5DF2\u8FDE\u63A5 \u2192 SDK \u5C31\u7EEA\uFF0C\u4E09\u8005\u5168\u90E8\u901A\u8FC7\u624D\u80FD\u6267\u884C\u4E1A\u52A1\u547D\u4EE4\n- \\`disconnect\\` \u4F1A\u9500\u6BC1\u6240\u6709\u7F13\u5B58\uFF0C\u4E4B\u524D\u7684 read \u7ED3\u679C\u5168\u90E8\u5931\u6548\n- daemon \u9ED8\u8BA4 30 \u5206\u949F\u65E0\u6D3B\u52A8\u81EA\u52A8\u5173\u95ED\n\n---\n\n## 3. Common Scenarios\n\n### \u573A\u666F A\uFF1A\u63A2\u7D22\u77E5\u8BC6\u5E93\n\n> \u7528\u6237\u8BF4\uFF1A\"\u5E2E\u6211\u770B\u770B\u77E5\u8BC6\u5E93\u91CC\u6709\u4EC0\u4E48\"\u3001\"\u6709\u54EA\u4E9B\u6587\u6863\"\n\n\u4F7F\u7528 \\`read_globe\\`\u3002\u5B83\u8FD4\u56DE\u77E5\u8BC6\u5E93\u4E2D\u6240\u6709 Document \u7684\u5C42\u7EA7\u7ED3\u6784\u9E1F\u77B0\u56FE\uFF0C\u975E Document \u5185\u5BB9\u4E0D\u5C55\u5F00\u3002\u8FD9\u662F\u4E86\u89E3\u77E5\u8BC6\u5E93\u6574\u4F53\u7EC4\u7EC7\u7684\u8D77\u70B9\u3002\u62FF\u5230\u611F\u5174\u8DA3\u7684 remId \u540E\uFF0C\u53EF\u4EE5\u7528 \\`read_tree\\` \u6DF1\u5165\u3002\n\n### \u573A\u666F B\uFF1A\u641C\u7D22\u5E76\u6DF1\u5165\n\n> \u7528\u6237\u8BF4\uFF1A\"\u641C\u4E00\u4E0B\u5173\u4E8E X \u7684\u7B14\u8BB0\"\u3001\"\u627E\u627E X \u5728\u54EA\u91CC\"\n\n\u5148\u7528 \\`search\\` \u641C\u7D22\u5173\u952E\u8BCD\uFF0C\u83B7\u5F97\u5339\u914D\u7684 Rem \u5217\u8868\uFF08\u542B remId\uFF09\u3002\u7136\u540E\u6839\u636E\u9700\u8981\uFF1A\n- \u7528 \\`read_rem\\` \u67E5\u770B\u67D0\u6761\u7ED3\u679C\u7684\u8BE6\u7EC6\u5C5E\u6027\n- \u7528 \\`read_tree\\` \u5C55\u5F00\u67D0\u6761\u7ED3\u679C\u7684\u5B50\u6811\u7ED3\u6784\n\n**\u6CE8\u610F**\uFF1A\u4E2D\u6587\u7B49\u65E0\u7A7A\u683C\u8BED\u8A00\u641C\u7D22\u6548\u679C\u8F83\u5DEE\u3002\u5982\u679C\u5B8C\u6574\u8BCD\u641C\u7D22\u65E0\u7ED3\u679C\uFF0C\u5C1D\u8BD5\u7528\u5355\u4E2A\u6700\u5177\u533A\u5206\u5EA6\u7684\u5B57\u641C\u7D22\uFF1B\u5982\u679C\u4ECD\u7136\u65E0\u679C\uFF0C\u6539\u7528 \\`read_globe\\` \u2192 \\`read_tree\\` \u624B\u52A8\u5B9A\u4F4D\u3002\n\n### \u573A\u666F C\uFF1A\u4E86\u89E3\u5F53\u524D\u4E0A\u4E0B\u6587\n\n> \u7528\u6237\u8BF4\uFF1A\"\u6211\u73B0\u5728\u5728\u770B\u4EC0\u4E48\"\u3001\"\u5F53\u524D\u9875\u9762\u662F\u4EC0\u4E48\"\n\n\u4F7F\u7528 \\`read_context\\`\uFF1A\n- **focus \u6A21\u5F0F**\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u4EE5\u7528\u6237\u5F53\u524D\u5149\u6807\u6240\u5728\u7684 Rem \u4E3A\u4E2D\u5FC3\uFF0C\u6784\u5EFA\u9C7C\u773C\u89C6\u56FE\u2014\u2014\u7126\u70B9\u5904\u5B8C\u5168\u5C55\u5F00\uFF0C\u5468\u56F4\u9012\u51CF\u3002\u7126\u70B9\u884C\u4EE5 \\`* \\` \u524D\u7F00\u6807\u8BB0\u3002\u9700\u8981\u7528\u6237\u5728 RemNote \u4E2D\u5DF2\u70B9\u51FB\u67D0\u4E2A Rem\u3002\n- **page \u6A21\u5F0F**\uFF1A\u4EE5\u5F53\u524D\u6253\u5F00\u7684\u9875\u9762\u4E3A\u6839\uFF0C\u5747\u5300\u5C55\u5F00\u5B50\u6811\u3002\n\n\u4E24\u79CD\u6A21\u5F0F\u90FD\u4F1A\u8FD4\u56DE\u9762\u5305\u5C51\u8DEF\u5F84\uFF0C\u5E2E\u52A9\u4F60\u7406\u89E3\u5F53\u524D\u4F4D\u7F6E\u5728\u77E5\u8BC6\u5E93\u4E2D\u7684\u5C42\u7EA7\u3002\n\n### \u573A\u666F D\uFF1A\u4FEE\u6539\u6587\u672C\u6216\u5C5E\u6027\n\n> \u7528\u6237\u8BF4\uFF1A\"\u628A\u8FD9\u4E2A\u6807\u9898\u6539\u6210...\"\u3001\"\u628A\u7C7B\u578B\u6539\u6210\u6982\u5FF5\"\u3001\"\u52A0\u4E2A\u9AD8\u4EAE\"\n\n\u5DE5\u4F5C\u6D41\u7A0B\uFF1A**\u5FC5\u987B\u5148 read \u518D edit**\u3002\n\n1. \\`read_rem\\` \u83B7\u53D6\u76EE\u6807 Rem \u7684 JSON \u5C5E\u6027\uFF08\u5EFA\u7ACB\u7F13\u5B58\uFF09\n2. \u5728\u8FD4\u56DE\u7684 JSON \u6587\u672C\u4E2D\u5B9A\u4F4D\u8981\u4FEE\u6539\u7684\u90E8\u5206\n3. \\`edit_rem\\` \u7528 str_replace \u66FF\u6362\uFF1AoldStr \u7CBE\u786E\u5339\u914D\u539F\u6587\uFF0CnewStr \u662F\u4FEE\u6539\u540E\u7684\u6587\u672C\n\nstr_replace \u64CD\u4F5C\u7684\u662F\u683C\u5F0F\u5316 JSON \u6587\u672C\uFF082 \u7A7A\u683C\u7F29\u8FDB\uFF09\u3002oldStr \u8981\u5305\u542B\u8DB3\u591F\u4E0A\u4E0B\u6587\uFF08\u5982\u5B57\u6BB5\u540D + \u503C\uFF09\uFF0C\u907F\u514D\u6A21\u7CCA\u5339\u914D\u3002\u66FF\u6362\u540E\u5FC5\u987B\u662F\u5408\u6CD5 JSON\u3002\n\n### \u573A\u666F E\uFF1A\u4FEE\u6539\u7ED3\u6784\uFF08\u65B0\u589E/\u5220\u9664/\u79FB\u52A8/\u91CD\u6392\uFF09\n\n> \u7528\u6237\u8BF4\uFF1A\"\u5728\u8FD9\u4E0B\u9762\u52A0\u51E0\u4E2A\u5B50\u9879\"\u3001\"\u5220\u6389\u8FD9\u4E2A\"\u3001\"\u628A\u8FD9\u4E2A\u79FB\u5230\u90A3\u4E0B\u9762\"\n\n\u5DE5\u4F5C\u6D41\u7A0B\uFF1A**\u5FC5\u987B\u5148 read_tree \u518D edit_tree**\u3002\n\n1. \\`read_tree\\` \u83B7\u53D6\u76EE\u6807\u533A\u57DF\u7684 Markdown \u5927\u7EB2\uFF08\u5EFA\u7ACB\u7F13\u5B58\uFF09\n2. \u5728\u5927\u7EB2\u4E2D\u7528 str_replace \u8FDB\u884C\u7ED3\u6784\u4FEE\u6539\uFF1A\n - **\u65B0\u589E**\uFF1A\u63D2\u5165\u65E0 remId \u6CE8\u91CA\u7684\u65B0\u884C\uFF08\u901A\u8FC7\u7F29\u8FDB\u786E\u5B9A\u7236\u5B50\u5173\u7CFB\uFF09\n - **\u5220\u9664**\uFF1A\u79FB\u9664\u5E26 remId \u7684\u884C\uFF08\u5FC5\u987B\u540C\u65F6\u5220\u9664\u6240\u6709\u5B50\u884C\uFF09\n - **\u79FB\u52A8**\uFF1A\u6539\u53D8\u884C\u7684\u7F29\u8FDB\u7EA7\u522B\u6216\u4F4D\u7F6E\n - **\u91CD\u6392**\uFF1A\u8C03\u6362\u540C\u7EA7\u884C\u7684\u987A\u5E8F\n\n**\u7EA2\u7EBF**\uFF1Aedit_tree **\u7981\u6B62\u4FEE\u6539\u5DF2\u6709\u884C\u7684\u6587\u5B57\u5185\u5BB9**\u2014\u2014\u6539\u5185\u5BB9\u5FC5\u987B\u7528 edit_rem\u3002edit_tree \u53EA\u505A\u7ED3\u6784\u64CD\u4F5C\u3002\n\n### \u573A\u666F F\uFF1A\u521B\u5EFA\u95EA\u5361\n\n> \u7528\u6237\u8BF4\uFF1A\"\u521B\u5EFA\u4E00\u4E2A\u6982\u5FF5\u5B9A\u4E49\"\u3001\"\u505A\u4E2A\u6B63\u5411\u95EE\u7B54\u5361\"\n\n\u521B\u5EFA\u95EA\u5361\u7684\u672C\u8D28\u662F\u521B\u5EFA\u5E26\u6B63\u786E\u5206\u9694\u7B26\u7684 Rem\u3002\u901A\u8FC7 \\`edit_tree\\` \u65B0\u589E\u884C\u65F6\u4F7F\u7528\u7BAD\u5934\u5206\u9694\u7B26\uFF1A\n\n- \u6982\u5FF5\u5B9A\u4E49\uFF1A\\`\u65B0\u6982\u5FF5 \u2192 \u5B9A\u4E49\u5185\u5BB9\\`\uFF08\u7136\u540E\u7528 edit_rem \u5C06 type \u6539\u4E3A concept\uFF09\n- \u6B63\u5411\u95EE\u7B54\uFF1A\\`\u95EE\u9898 \u2192 \u7B54\u6848\\`\n- \u591A\u884C\u7B54\u6848\uFF1A\\`\u95EE\u9898 \u2193\\`\uFF08\u5B50\u884C\u81EA\u52A8\u6210\u4E3A\u7B54\u6848\uFF09\n- \u53CC\u5411\u95EE\u7B54\uFF1A\\`\u95EE\u9898 \u2194 \u7B54\u6848\\`\n\n\u5982\u679C\u8981\u4FEE\u6539\u73B0\u6709 Rem \u7684\u95EA\u5361\u884C\u4E3A\uFF08\u6539\u5206\u9694\u7B26\u7C7B\u578B\uFF09\uFF0C\u4F7F\u7528 \\`read_rem\\` \u2192 \\`edit_rem\\` \u4FEE\u6539 type\u3001backText\u3001practiceDirection \u7B49\u5B57\u6BB5\u3002\n\n### \u573A\u666F G\uFF1A\u6392\u67E5\u8FDE\u63A5\u95EE\u9898\n\n> \u7528\u6237\u8BF4\uFF1A\"\u8FDE\u4E0D\u4E0A\"\u3001\"\u547D\u4EE4\u62A5\u9519\u4E86\"\n\n\u4F7F\u7528 \\`health\\` \u68C0\u67E5\u4E09\u5C42\u72B6\u6001\uFF0C\u7136\u540E\u5BF9\u75C7\u5904\u7406\uFF1A\n- daemon \u672A\u8FD0\u884C \u2192 \u6267\u884C \\`connect\\`\n- Plugin \u672A\u8FDE\u63A5 \u2192 \u63D0\u9192\u7528\u6237\u6253\u5F00 RemNote \u5E76\u786E\u8BA4\u63D2\u4EF6\u5DF2\u52A0\u8F7D\n- SDK \u672A\u5C31\u7EEA \u2192 \u7B49\u5F85\u51E0\u79D2\u540E\u91CD\u8BD5 health\n\n---\n\n## 4. Safety Rules\n\n### \u9EC4\u91D1\u6CD5\u5219\uFF1A\u5148 read \u518D edit\n\n- \\`edit_rem\\` \u524D\u5FC5\u987B\u5148 \\`read_rem\\` \u540C\u4E00\u4E2A remId\n- \\`edit_tree\\` \u524D\u5FC5\u987B\u5148 \\`read_tree\\` \u540C\u4E00\u4E2A remId\n\n\u8DF3\u8FC7 read \u76F4\u63A5 edit \u4F1A\u88AB\u62D2\u7EDD\u3002\u8FD9\u4E0D\u662F\u5EFA\u8BAE\uFF0C\u662F\u786C\u6027\u8981\u6C42\u3002\n\n### \u4E09\u9053\u9632\u7EBF\n\n\u6BCF\u6B21 edit \u64CD\u4F5C\u90FD\u7ECF\u8FC7\u4E09\u91CD\u5B89\u5168\u68C0\u67E5\uFF1A\n1. **\u7F13\u5B58\u5B58\u5728**\uFF1A\u5FC5\u987B\u6709\u5BF9\u5E94\u7684 read \u7F13\u5B58\n2. **\u5E76\u53D1\u68C0\u6D4B**\uFF1Aedit \u65F6\u91CD\u65B0\u8BFB\u53D6\u6700\u65B0\u6570\u636E\uFF0C\u4E0E\u7F13\u5B58\u6BD4\u8F83\u3002\u5982\u679C Rem \u5728 read \u4E4B\u540E\u88AB\u5916\u90E8\u4FEE\u6539\uFF0C\u62D2\u7EDD\u7F16\u8F91\u2014\u2014\u4F60\u5FC5\u987B\u91CD\u65B0 read\n3. **\u7CBE\u786E\u5339\u914D**\uFF1AoldStr \u5FC5\u987B\u5728\u76EE\u6807\u6587\u672C\u4E2D\u6070\u597D\u5339\u914D 1 \u6B21\n\n### edit_tree \u7981\u6B62\u4E8B\u9879\n\n- \u7981\u6B62\u4FEE\u6539\u5DF2\u6709\u884C\u7684\u6587\u5B57\u5185\u5BB9\uFF08\u6539\u5185\u5BB9\u7528 edit_rem\uFF09\n- \u7981\u6B62\u5220\u9664\u6839\u8282\u70B9\n- \u7981\u6B62\u5220\u9664\u6709\u9690\u85CF\u5B50\u8282\u70B9\u7684\u884C\uFF08\u5148\u7528\u66F4\u5927 depth \u91CD\u65B0 read_tree\uFF09\n- \u7981\u6B62\u5220\u9664\u7236\u884C\u4F46\u4FDD\u7559\u5B50\u884C\uFF08\u5FC5\u987B\u4E00\u8D77\u5220\uFF09\n- \u7981\u6B62\u4FEE\u6539\u6216\u5220\u9664\u7701\u7565\u5360\u4F4D\u7B26\uFF08\u5148\u7528\u66F4\u5927\u53C2\u6570\u91CD\u65B0 read_tree \u5C55\u5F00\uFF09\n\n\u8BE6\u7EC6\u89C4\u5219\u89C1\u5404\u5DE5\u5177\u7684 description\u3002\n\n---\n\n## 5. Output Format Quick Reference\n\n\\`read_tree\\`\u3001\\`read_globe\\`\u3001\\`read_context\\` \u8FD4\u56DE Markdown \u5927\u7EB2\u683C\u5F0F\u3002\n\n### \u884C\u7ED3\u6784\n\n\\`\\`\\`\n{\u7F29\u8FDB}{Markdown \u524D\u7F00}{\u5185\u5BB9}{\u7BAD\u5934}{backText} <!-- {remId} {\u6807\u8BB0} -->\n\\`\\`\\`\n\n- \u7F29\u8FDB\uFF1A\u6BCF\u7EA7 2 \u7A7A\u683C\n- Markdown \u524D\u7F00\uFF1A\\`# \\`(H1)\u3001\\`## \\`(H2)\u3001\\`### \\`(H3)\u3001\\`- [ ] \\`(\u5F85\u529E)\u3001\\`- [x] \\`(\u5DF2\u5B8C\u6210)\u3001\\`\\\\\\`\\`(\u4EE3\u7801)\n\n### \u5173\u952E\u5143\u6570\u636E\u6807\u8BB0\n\n| \u6807\u8BB0 | \u542B\u4E49 |\n|:-----|:-----|\n| \\`type:concept\\` / \\`type:descriptor\\` / \\`type:portal\\` | Rem \u7C7B\u578B\uFF08default \u4E0D\u6807\u8BB0\uFF09 |\n| \\`doc\\` | \u662F\u6587\u6863\u9875\u9762 |\n| \\`children:N\\` | \u6709 N \u4E2A\u672A\u5C55\u5F00\u7684\u5B50\u8282\u70B9 |\n| \\`tag:Name(id)\\` | \u5DF2\u9644\u52A0\u7684\u6807\u7B7E |\n| \\`role:card-item\\` | \u591A\u884C\u95EA\u5361\u7684\u7B54\u6848\u884C |\n| \\`top\\` | \u77E5\u8BC6\u5E93\u9876\u5C42 Rem |\n\n### \u7BAD\u5934\u542B\u4E49\n\n- **\u5355\u884C\u7BAD\u5934** \\`\u2192\\` \\`\u2190\\` \\`\u2194\\`\uFF1Atext \u4E0E backText \u4E4B\u95F4\u7684\u5206\u9694\uFF0C\u65B9\u5411\u4E3A practiceDirection\n- **\u591A\u884C\u7BAD\u5934** \\`\u2193\\` \\`\u2191\\` \\`\u2195\\`\uFF1A\u8868\u793A\u5B50\u8282\u70B9\u4E3A\u7B54\u6848\u7684\u591A\u884C\u95EA\u5361\n\n### \u7701\u7565\u5360\u4F4D\u7B26\n\n\\`\\`\\`\n<!--...elided 3 siblings (parent:remIdB range:2-4 total:5)-->\n<!--...elided >=10 nodes (parent:remIdB range:5-14 total:20)-->\n\\`\\`\\`\n\n\\`siblings\\` \u4E3A\u7CBE\u786E\u7701\u7565\uFF08\u8D85 maxSiblings\uFF09\uFF0C\\`>=N nodes\\` \u4E3A\u9884\u7B97\u8017\u5C3D\u7684\u975E\u7CBE\u786E\u7701\u7565\u3002\n\n\u8BE6\u7EC6\u683C\u5F0F\u89C4\u8303\u89C1 \\`resource://outline-format\\`\u3002\n\n---\n\n## 6. Error Quick Reference\n\n| \u9519\u8BEF\u4FE1\u606F | \u539F\u56E0 | \u6062\u590D\u64CD\u4F5C |\n|:---------|:-----|:---------|\n| \u5B88\u62A4\u8FDB\u7A0B\u672A\u8FD0\u884C | \u672A connect \u6216 daemon \u5DF2\u8D85\u65F6 | \u6267\u884C \\`connect\\` |\n| Plugin \u672A\u8FDE\u63A5 | RemNote \u672A\u6253\u5F00\u6216\u63D2\u4EF6\u672A\u52A0\u8F7D | \u63D0\u9192\u7528\u6237\u6253\u5F00 RemNote |\n| has not been read yet | \u672A\u5148\u6267\u884C read | \u5148 read \u540C\u4E00\u4E2A remId\uFF0C\u518D\u91CD\u8BD5 edit |\n| has been modified since last read | Rem \u5728 read \u540E\u88AB\u5916\u90E8\u4FEE\u6539 | \u91CD\u65B0 read \u83B7\u53D6\u6700\u65B0\u72B6\u6001\uFF0C\u518D\u91CD\u8BD5 |\n| old_str not found | oldStr \u5728\u6587\u672C\u4E2D\u4E0D\u5B58\u5728 | \u68C0\u67E5\u662F\u5426\u7CBE\u786E\u5339\u914D\uFF08\u542B\u5F15\u53F7\u3001\u7A7A\u683C\u3001\u6362\u884C\uFF09 |\n| old_str matches N locations | oldStr \u5339\u914D\u591A\u5904 | \u6269\u5927 oldStr \u8303\u56F4\uFF0C\u5305\u542B\u66F4\u591A\u4E0A\u4E0B\u6587\u4EE5\u552F\u4E00\u5B9A\u4F4D |\n| invalid JSON | edit_rem \u66FF\u6362\u540E\u4E0D\u662F\u5408\u6CD5 JSON | \u68C0\u67E5 newStr \u7684\u5F15\u53F7\u3001\u9017\u53F7\u3001\u62EC\u53F7 |\n| Content modification not allowed | edit_tree \u4E2D\u4FEE\u6539\u4E86\u884C\u5185\u5BB9 | \u6539\u7528 \\`edit_rem\\` \u4FEE\u6539\u5185\u5BB9 |\n| orphan_detected | \u5220\u4E86\u7236\u884C\u4F46\u4FDD\u7559\u4E86\u5B50\u884C | \u540C\u65F6\u5220\u9664\u6240\u6709\u5B50\u884C |\n| folded_delete | \u5220\u9664\u6709\u9690\u85CF\u5B50\u8282\u70B9\u7684\u884C | \u7528\u66F4\u5927 depth \u91CD\u65B0 read_tree |\n\n\u5B8C\u6574\u9519\u8BEF\u53C2\u8003\u89C1 \\`resource://error-reference\\`\u3002\n";