remnote-bridge 0.1.12 → 0.1.13
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/dist/cli/commands/edit-rem.js +5 -5
- package/dist/cli/commands/read-rem.js +3 -1
- package/dist/cli/handlers/edit-handler.js +49 -140
- package/dist/cli/handlers/read-handler.js +5 -6
- package/dist/cli/handlers/rem-cache.js +10 -5
- package/dist/cli/main.js +18 -10
- package/dist/mcp/format.js +43 -0
- package/dist/mcp/index.js +0 -55
- package/dist/mcp/instructions.js +405 -289
- package/dist/mcp/resources/edit-rem-guide.js +37 -157
- package/dist/mcp/resources/edit-tree-guide.js +1 -1
- package/dist/mcp/resources/error-reference.js +9 -13
- package/dist/mcp/resources/rem-object-fields.js +3 -3
- package/dist/mcp/tools/edit-tools.js +69 -8
- package/dist/mcp/tools/infra-tools.js +14 -26
- package/dist/mcp/tools/read-tools.js +135 -26
- package/package.json +1 -1
- package/skills/remnote-bridge/SKILL.md +34 -34
- package/skills/remnote-bridge/instructions/edit-rem.md +105 -347
- package/skills/remnote-bridge/instructions/overall.md +23 -14
- package/skills/remnote-bridge/instructions/read-rem.md +5 -5
- package/skills/remnote-bridge/instructions/search.md +4 -4
- package/skills/remnote-bridge/instructions/setup.md +5 -6
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* edit-rem 命令
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 直接修改 Rem 的属性字段。
|
|
5
|
+
* 两道防线保证安全:缓存存在性、并发检测。
|
|
6
6
|
* - 退出码:0 成功 / 1 业务错误 / 2 守护进程不可达
|
|
7
7
|
*/
|
|
8
8
|
import { sendDaemonRequest } from '../daemon/send-request.js';
|
|
9
9
|
import { jsonOutput, handleCommandError } from '../utils/output.js';
|
|
10
10
|
export async function editRemCommand(remId, options) {
|
|
11
|
-
const { json,
|
|
11
|
+
const { json, changes } = options;
|
|
12
12
|
let result;
|
|
13
13
|
try {
|
|
14
|
-
result = await sendDaemonRequest('edit_rem', { remId,
|
|
14
|
+
result = await sendDaemonRequest('edit_rem', { remId, changes });
|
|
15
15
|
}
|
|
16
16
|
catch (err) {
|
|
17
17
|
handleCommandError(err, 'edit-rem', json);
|
|
@@ -32,7 +32,7 @@ export async function editRemCommand(remId, options) {
|
|
|
32
32
|
else {
|
|
33
33
|
if (editResult.ok) {
|
|
34
34
|
if (editResult.changes.length === 0) {
|
|
35
|
-
console.log('
|
|
35
|
+
console.log('无变更(未发现可写入的变更字段)');
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
38
|
console.log(`已更新字段: ${editResult.changes.join(', ')}`);
|
|
@@ -20,7 +20,9 @@ export async function readRemCommand(remId, options = {}) {
|
|
|
20
20
|
payload.full = true;
|
|
21
21
|
}
|
|
22
22
|
else if (fields) {
|
|
23
|
-
payload.fields =
|
|
23
|
+
payload.fields = Array.isArray(fields)
|
|
24
|
+
? fields
|
|
25
|
+
: fields.split(',').map(f => f.trim()).filter(Boolean);
|
|
24
26
|
}
|
|
25
27
|
let result;
|
|
26
28
|
try {
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EditHandler — edit-rem 命令的编排器
|
|
3
3
|
*
|
|
4
|
-
* 在 daemon
|
|
4
|
+
* 在 daemon 层实现两道防线 + 字段白名单校验 + 直接转发 changes。
|
|
5
5
|
* Plugin 只负责原子写入(write_rem_fields)。
|
|
6
6
|
*
|
|
7
7
|
* 防线 1:缓存存在性检查(必须先 read 再 edit)
|
|
8
8
|
* 防线 2:乐观并发检测(当前 JSON 与缓存 JSON 比较)
|
|
9
|
-
* 防线 3:str_replace 精确匹配(old_str 必须唯一匹配)
|
|
10
|
-
*
|
|
11
|
-
* Portal 专用路径:type === 'portal' 时,在简化 JSON(9 字段)上执行 str_replace,
|
|
12
|
-
* 推导变更后调用专用写入逻辑。
|
|
13
9
|
*/
|
|
14
|
-
import { PORTAL_FIELDS } from './read-handler.js';
|
|
15
10
|
/** 只读字段集合 — 变更这些字段只产生警告,不执行写入 */
|
|
16
11
|
const READ_ONLY_FIELDS = new Set([
|
|
17
12
|
'id',
|
|
@@ -30,10 +25,22 @@ const READ_ONLY_FIELDS = new Set([
|
|
|
30
25
|
'isPowerup', 'isPowerupEnum', 'isPowerupProperty',
|
|
31
26
|
'isPowerupPropertyListItem', 'isPowerupSlot',
|
|
32
27
|
]);
|
|
33
|
-
/**
|
|
34
|
-
const
|
|
35
|
-
'
|
|
28
|
+
/** 可写字段白名单 — 21 个可写字段 */
|
|
29
|
+
const WRITABLE_FIELDS = new Set([
|
|
30
|
+
'text', 'backText', 'type', 'isDocument', 'parent',
|
|
31
|
+
'fontSize', 'highlightColor',
|
|
32
|
+
'isTodo', 'todoStatus', 'isCode', 'isQuote', 'isListItem', 'isCardItem', 'isSlot', 'isProperty',
|
|
33
|
+
'enablePractice', 'practiceDirection',
|
|
34
|
+
'tags', 'sources', 'positionAmongstSiblings', 'portalDirectlyIncludedRem',
|
|
36
35
|
]);
|
|
36
|
+
/** 枚举字段的合法值 */
|
|
37
|
+
const ENUM_VALUES = {
|
|
38
|
+
type: new Set(['concept', 'descriptor', 'default']),
|
|
39
|
+
practiceDirection: new Set(['forward', 'backward', 'both', 'none']),
|
|
40
|
+
highlightColor: new Set(['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Purple', 'Gray', 'Brown', 'Pink', null]),
|
|
41
|
+
fontSize: new Set(['H1', 'H2', 'H3', null]),
|
|
42
|
+
todoStatus: new Set(['Finished', 'Unfinished', null]),
|
|
43
|
+
};
|
|
37
44
|
export class EditHandler {
|
|
38
45
|
cache;
|
|
39
46
|
forwardToPlugin;
|
|
@@ -42,145 +49,61 @@ export class EditHandler {
|
|
|
42
49
|
this.forwardToPlugin = forwardToPlugin;
|
|
43
50
|
}
|
|
44
51
|
async handleEditRem(payload) {
|
|
45
|
-
const { remId,
|
|
52
|
+
const { remId, changes } = payload;
|
|
53
|
+
if (!changes || typeof changes !== 'object' || Object.keys(changes).length === 0) {
|
|
54
|
+
return { ok: true, changes: [], warnings: [] };
|
|
55
|
+
}
|
|
46
56
|
// ── 防线 1: 缓存存在性检查 ──
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
57
|
+
const cachedObj = this.cache.get('rem:' + remId);
|
|
58
|
+
if (!cachedObj) {
|
|
49
59
|
throw new Error(`Rem ${remId} has not been read yet. Read it first before editing.`);
|
|
50
60
|
}
|
|
51
61
|
// ── 防线 2: 乐观并发检测 ──
|
|
52
62
|
const currentRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
53
63
|
const currentJson = JSON.stringify(currentRemObject, null, 2);
|
|
64
|
+
const cachedJson = JSON.stringify(cachedObj, null, 2);
|
|
54
65
|
if (currentJson !== cachedJson) {
|
|
55
66
|
// 不更新缓存 — 迫使 AI re-read
|
|
56
67
|
throw new Error(`Rem ${remId} has been modified since last read. Please read it again before editing.`);
|
|
57
68
|
}
|
|
58
|
-
// ──
|
|
59
|
-
const cachedObj = JSON.parse(cachedJson);
|
|
60
|
-
if (cachedObj.type === 'portal') {
|
|
61
|
-
return this.handlePortalEdit(remId, cachedJson, cachedObj, oldStr, newStr);
|
|
62
|
-
}
|
|
63
|
-
// ── 普通 Rem 路径 ──
|
|
64
|
-
return this.handleNormalEdit(remId, cachedJson, cachedObj, oldStr, newStr);
|
|
65
|
-
}
|
|
66
|
-
/** Portal 专用编辑路径:在简化 JSON 上执行 str_replace */
|
|
67
|
-
async handlePortalEdit(remId, cachedJson, fullObj, oldStr, newStr) {
|
|
68
|
-
// 从完整对象提取简化 JSON
|
|
69
|
-
const simplified = {};
|
|
70
|
-
for (const field of PORTAL_FIELDS) {
|
|
71
|
-
if (field in fullObj) {
|
|
72
|
-
simplified[field] = fullObj[field];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const simplifiedJson = JSON.stringify(simplified, null, 2);
|
|
76
|
-
// ── 防线 3: str_replace 在简化 JSON 上精确匹配 ──
|
|
77
|
-
const matchCount = countOccurrences(simplifiedJson, oldStr);
|
|
78
|
-
if (matchCount === 0) {
|
|
79
|
-
throw new Error(`old_str not found in the simplified Portal JSON of rem ${remId}`);
|
|
80
|
-
}
|
|
81
|
-
if (matchCount > 1) {
|
|
82
|
-
throw new Error(`old_str matches ${matchCount} locations in Portal rem ${remId}. ` +
|
|
83
|
-
`Make old_str more specific to match exactly once.`);
|
|
84
|
-
}
|
|
85
|
-
const modifiedSimplifiedJson = simplifiedJson.replace(oldStr, newStr);
|
|
86
|
-
// JSON 解析
|
|
87
|
-
let modified;
|
|
88
|
-
try {
|
|
89
|
-
modified = JSON.parse(modifiedSimplifiedJson);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
throw new Error('The replacement produced invalid JSON. Check your new_str for syntax errors.');
|
|
93
|
-
}
|
|
94
|
-
// 推导变更字段
|
|
95
|
-
const changes = {};
|
|
69
|
+
// ── 遍历 changes keys:分类过滤 ──
|
|
96
70
|
const warnings = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
71
|
+
const writableChanges = {};
|
|
72
|
+
for (const key of Object.keys(changes)) {
|
|
73
|
+
if (READ_ONLY_FIELDS.has(key)) {
|
|
74
|
+
warnings.push(`Field '${key}' is read-only and was ignored`);
|
|
75
|
+
}
|
|
76
|
+
else if (!WRITABLE_FIELDS.has(key)) {
|
|
77
|
+
warnings.push(`Field '${key}' is unknown and was ignored`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
writableChanges[key] = changes[key];
|
|
105
81
|
}
|
|
106
82
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
remId,
|
|
114
|
-
changes,
|
|
115
|
-
}));
|
|
116
|
-
if (writeResult.failed) {
|
|
117
|
-
return {
|
|
118
|
-
ok: false,
|
|
119
|
-
changes: [],
|
|
120
|
-
warnings,
|
|
121
|
-
error: `Failed to update field '${writeResult.failed.field}': ${writeResult.failed.error}`,
|
|
122
|
-
appliedChanges: writeResult.applied,
|
|
123
|
-
failedField: writeResult.failed.field,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
// ── 写入成功 → 从 Plugin 重新获取完整 Rem 并更新缓存(D5)──
|
|
127
|
-
const freshRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
128
|
-
const freshJson = JSON.stringify(freshRemObject, null, 2);
|
|
129
|
-
this.cache.set('rem:' + remId, freshJson);
|
|
130
|
-
return {
|
|
131
|
-
ok: true,
|
|
132
|
-
changes: Object.keys(changes),
|
|
133
|
-
warnings,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
/** 普通 Rem 编辑路径:在完整 JSON 上执行 str_replace */
|
|
137
|
-
async handleNormalEdit(remId, cachedJson, original, oldStr, newStr) {
|
|
138
|
-
// ── 防线 3: str_replace 精确匹配 ──
|
|
139
|
-
const matchCount = countOccurrences(cachedJson, oldStr);
|
|
140
|
-
if (matchCount === 0) {
|
|
141
|
-
throw new Error(`old_str not found in the serialized JSON of rem ${remId}`);
|
|
142
|
-
}
|
|
143
|
-
if (matchCount > 1) {
|
|
144
|
-
throw new Error(`old_str matches ${matchCount} locations in rem ${remId}. ` +
|
|
145
|
-
`Make old_str more specific to match exactly once.`);
|
|
146
|
-
}
|
|
147
|
-
const modifiedJson = cachedJson.replace(oldStr, newStr);
|
|
148
|
-
// 1. JSON 解析
|
|
149
|
-
let modified;
|
|
150
|
-
try {
|
|
151
|
-
modified = JSON.parse(modifiedJson);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
throw new Error('The replacement produced invalid JSON. Check your new_str for syntax errors.');
|
|
155
|
-
}
|
|
156
|
-
// 2. 推导变更字段
|
|
157
|
-
const changes = {};
|
|
158
|
-
const warnings = [];
|
|
159
|
-
for (const key of Object.keys(modified)) {
|
|
160
|
-
if (JSON.stringify(modified[key]) !== JSON.stringify(original[key])) {
|
|
161
|
-
if (READ_ONLY_FIELDS.has(key)) {
|
|
162
|
-
warnings.push(`Field '${key}' is read-only and was ignored`);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
changes[key] = modified[key];
|
|
83
|
+
// ── 枚举值范围校验 ──
|
|
84
|
+
for (const [field, allowedValues] of Object.entries(ENUM_VALUES)) {
|
|
85
|
+
if (field in writableChanges) {
|
|
86
|
+
const value = writableChanges[field];
|
|
87
|
+
if (!allowedValues.has(value)) {
|
|
88
|
+
throw new Error(`Invalid value for '${field}': ${JSON.stringify(value)}. Allowed: ${[...allowedValues].map(v => JSON.stringify(v)).join(', ')}`);
|
|
166
89
|
}
|
|
167
90
|
}
|
|
168
91
|
}
|
|
169
|
-
//
|
|
170
|
-
if ('todoStatus' in
|
|
171
|
-
const isTodo =
|
|
92
|
+
// ── 语义校验:todoStatus 非 null 但 isTodo 未启用 ──
|
|
93
|
+
if ('todoStatus' in writableChanges && writableChanges.todoStatus !== null) {
|
|
94
|
+
const isTodo = writableChanges.isTodo ?? cachedObj.isTodo;
|
|
172
95
|
if (!isTodo) {
|
|
173
96
|
warnings.push("Setting 'todoStatus' without 'isTodo: true' may have no effect");
|
|
174
97
|
}
|
|
175
98
|
}
|
|
176
|
-
//
|
|
177
|
-
if (Object.keys(
|
|
99
|
+
// ── 空变更检查 ──
|
|
100
|
+
if (Object.keys(writableChanges).length === 0) {
|
|
178
101
|
return { ok: true, changes: [], warnings };
|
|
179
102
|
}
|
|
180
103
|
// ── 发送变更到 Plugin ──
|
|
181
104
|
const writeResult = (await this.forwardToPlugin('write_rem_fields', {
|
|
182
105
|
remId,
|
|
183
|
-
changes,
|
|
106
|
+
changes: writableChanges,
|
|
184
107
|
}));
|
|
185
108
|
if (writeResult.failed) {
|
|
186
109
|
// 部分失败 — 不更新缓存(迫使 AI re-read)
|
|
@@ -193,27 +116,13 @@ export class EditHandler {
|
|
|
193
116
|
failedField: writeResult.failed.field,
|
|
194
117
|
};
|
|
195
118
|
}
|
|
196
|
-
// ── 写入成功 → 从 Plugin 重新获取完整 Rem
|
|
119
|
+
// ── 写入成功 → 从 Plugin 重新获取完整 Rem 并更新缓存 ──
|
|
197
120
|
const freshRemObject = await this.forwardToPlugin('read_rem', { remId });
|
|
198
|
-
|
|
199
|
-
this.cache.set('rem:' + remId, freshJson);
|
|
121
|
+
this.cache.set('rem:' + remId, freshRemObject);
|
|
200
122
|
return {
|
|
201
123
|
ok: true,
|
|
202
|
-
changes: Object.keys(
|
|
124
|
+
changes: Object.keys(writableChanges),
|
|
203
125
|
warnings,
|
|
204
126
|
};
|
|
205
127
|
}
|
|
206
128
|
}
|
|
207
|
-
/** 统计 needle 在 haystack 中出现的次数 */
|
|
208
|
-
function countOccurrences(haystack, needle) {
|
|
209
|
-
let count = 0;
|
|
210
|
-
let pos = 0;
|
|
211
|
-
while (true) {
|
|
212
|
-
pos = haystack.indexOf(needle, pos);
|
|
213
|
-
if (pos === -1)
|
|
214
|
-
break;
|
|
215
|
-
count++;
|
|
216
|
-
pos += needle.length; // 非重叠匹配,与 String.replace 行为一致
|
|
217
|
-
}
|
|
218
|
-
return count;
|
|
219
|
-
}
|
|
@@ -44,17 +44,16 @@ export class ReadHandler {
|
|
|
44
44
|
const includePowerup = payload.includePowerup ?? false;
|
|
45
45
|
// 转发到 Plugin
|
|
46
46
|
const remObject = await this.forwardToPlugin('read_rem', { remId, includePowerup });
|
|
47
|
-
// 缓存完整
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
this.onLog?.(`缓存 Rem ${remId.slice(0, 8)}... (${fullJson.length} bytes)`, 'info');
|
|
47
|
+
// 缓存完整 RemObject 对象
|
|
48
|
+
this.cache.set(cacheKey, remObject);
|
|
49
|
+
this.onLog?.(`缓存 Rem ${remId.slice(0, 8)}...`, 'info');
|
|
51
50
|
// 字段过滤
|
|
52
51
|
const fields = payload.fields;
|
|
53
52
|
const full = payload.full;
|
|
54
53
|
let result;
|
|
55
54
|
if (full) {
|
|
56
|
-
// --full → 返回完整对象(含 R-F
|
|
57
|
-
result = remObject;
|
|
55
|
+
// --full → 返回完整对象(含 R-F 字段)。浅拷贝避免污染缓存对象。
|
|
56
|
+
result = { ...remObject };
|
|
58
57
|
}
|
|
59
58
|
else if (fields) {
|
|
60
59
|
// --fields 过滤:只返回指定字段 + id
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RemCache — LRU 缓存,存储 Rem
|
|
2
|
+
* RemCache — LRU 缓存,存储 Rem 数据
|
|
3
3
|
*
|
|
4
4
|
* 缓存存储在 daemon 内存中,生命周期与 daemon 一致。
|
|
5
5
|
* disconnect 关闭 daemon → 缓存自然消失。
|
|
6
|
+
*
|
|
7
|
+
* 泛化值类型:不同 key 前缀存储不同类型的数据:
|
|
8
|
+
* - rem:{remId} → RemObject 对象
|
|
9
|
+
* - tree:{remId} → Markdown outline 字符串
|
|
10
|
+
* - tree-depth:{remId} 等 → 参数值字符串
|
|
6
11
|
*/
|
|
7
12
|
export class RemCache {
|
|
8
13
|
cache = new Map();
|
|
@@ -15,19 +20,19 @@ export class RemCache {
|
|
|
15
20
|
if (!entry)
|
|
16
21
|
return null;
|
|
17
22
|
entry.lastAccess = Date.now();
|
|
18
|
-
return entry.
|
|
23
|
+
return entry.data;
|
|
19
24
|
}
|
|
20
25
|
/** 获取缓存条目的创建时间(ISO 8601),不存在返回 null */
|
|
21
26
|
getCreatedAt(remId) {
|
|
22
27
|
const entry = this.cache.get(remId);
|
|
23
28
|
return entry ? entry.createdAt : null;
|
|
24
29
|
}
|
|
25
|
-
set(remId,
|
|
30
|
+
set(remId, data) {
|
|
26
31
|
const now = Date.now();
|
|
27
32
|
const createdAt = new Date(now).toISOString();
|
|
28
33
|
if (this.cache.has(remId)) {
|
|
29
34
|
const entry = this.cache.get(remId);
|
|
30
|
-
entry.
|
|
35
|
+
entry.data = data;
|
|
31
36
|
entry.lastAccess = now;
|
|
32
37
|
entry.createdAt = createdAt;
|
|
33
38
|
return;
|
|
@@ -46,7 +51,7 @@ export class RemCache {
|
|
|
46
51
|
this.cache.delete(oldestId);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
|
-
this.cache.set(remId, {
|
|
54
|
+
this.cache.set(remId, { data, lastAccess: now, createdAt });
|
|
50
55
|
}
|
|
51
56
|
has(remId) {
|
|
52
57
|
return this.cache.has(remId);
|
package/dist/cli/main.js
CHANGED
|
@@ -121,7 +121,7 @@ program
|
|
|
121
121
|
const input = parseJsonInput('read-rem', remIdOrJson);
|
|
122
122
|
if (!input)
|
|
123
123
|
return;
|
|
124
|
-
await readRemCommand(input.remId, { json, fields: input.fields
|
|
124
|
+
await readRemCommand(input.remId, { json, fields: input.fields, full: input.full, includePowerup: input.includePowerup });
|
|
125
125
|
}
|
|
126
126
|
else {
|
|
127
127
|
if (!remIdOrJson) {
|
|
@@ -282,7 +282,7 @@ program
|
|
|
282
282
|
process.exitCode = 1;
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
|
-
await searchCommand(input.query, { json, limit: input.numResults?.toString() });
|
|
285
|
+
await searchCommand(input.query, { json, limit: (input.limit ?? input.numResults)?.toString() });
|
|
286
286
|
}
|
|
287
287
|
else {
|
|
288
288
|
if (!queryOrJson) {
|
|
@@ -295,16 +295,15 @@ program
|
|
|
295
295
|
});
|
|
296
296
|
program
|
|
297
297
|
.command('edit-rem [remIdOrJson]')
|
|
298
|
-
.description('
|
|
299
|
-
.option('--
|
|
300
|
-
.option('--new-str <newStr>', '替换后的新文本片段')
|
|
298
|
+
.description('直接修改 Rem 的属性字段')
|
|
299
|
+
.option('--changes <changesJson>', '要修改的字段及新值(JSON 字符串)')
|
|
301
300
|
.action(async (remIdOrJson, cmdOpts) => {
|
|
302
301
|
const { json } = program.opts();
|
|
303
302
|
if (json) {
|
|
304
|
-
const input = parseJsonInput('edit-rem', remIdOrJson, ['
|
|
303
|
+
const input = parseJsonInput('edit-rem', remIdOrJson, ['changes']);
|
|
305
304
|
if (!input)
|
|
306
305
|
return;
|
|
307
|
-
await editRemCommand(input.remId, { json,
|
|
306
|
+
await editRemCommand(input.remId, { json, changes: input.changes });
|
|
308
307
|
}
|
|
309
308
|
else {
|
|
310
309
|
if (!remIdOrJson) {
|
|
@@ -312,12 +311,21 @@ program
|
|
|
312
311
|
process.exitCode = 1;
|
|
313
312
|
return;
|
|
314
313
|
}
|
|
315
|
-
if (!cmdOpts.
|
|
316
|
-
console.error('错误: --
|
|
314
|
+
if (!cmdOpts.changes) {
|
|
315
|
+
console.error('错误: --changes 是必需的');
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
let changes;
|
|
320
|
+
try {
|
|
321
|
+
changes = JSON.parse(cmdOpts.changes);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
console.error('错误: --changes 不是合法的 JSON');
|
|
317
325
|
process.exitCode = 1;
|
|
318
326
|
return;
|
|
319
327
|
}
|
|
320
|
-
await editRemCommand(remIdOrJson, { json,
|
|
328
|
+
await editRemCommand(remIdOrJson, { json, changes });
|
|
321
329
|
}
|
|
322
330
|
});
|
|
323
331
|
// mcp 子命令
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 返回值格式化工具
|
|
3
|
+
*
|
|
4
|
+
* 两种模式:
|
|
5
|
+
* - formatFrontmatter:YAML frontmatter + body(read 类工具)
|
|
6
|
+
* - formatDataJson:剥离 wrapper 的 JSON(action/infra 工具)
|
|
7
|
+
*/
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// 模式 A:Frontmatter + Body
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* 将元数据序列化为 YAML frontmatter,拼接 body 内容。
|
|
13
|
+
*
|
|
14
|
+
* - null/undefined 的值自动跳过
|
|
15
|
+
* - 数字/布尔:裸值(`nodeCount: 42`)
|
|
16
|
+
* - 字符串:JSON 引号(`mode: "focus"`)
|
|
17
|
+
* - 数组/对象:JSON 内联(`ancestors: [{"id":"x"}]`)
|
|
18
|
+
* - 无元数据时省略 `---` 分隔符,直接返回 body
|
|
19
|
+
*/
|
|
20
|
+
export function formatFrontmatter(meta, body) {
|
|
21
|
+
const entries = Object.entries(meta).filter(([, v]) => v !== undefined && v !== null);
|
|
22
|
+
if (entries.length === 0)
|
|
23
|
+
return body;
|
|
24
|
+
const lines = entries.map(([k, v]) => {
|
|
25
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
26
|
+
return `${k}: ${v}`;
|
|
27
|
+
return `${k}: ${JSON.stringify(v)}`;
|
|
28
|
+
});
|
|
29
|
+
return `---\n${lines.join('\n')}\n---\n\n${body}`;
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// 模式 B:Data JSON(剥离 ok/command/timestamp wrapper)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* 从 CLI 响应中剥离 ok/command/timestamp,返回剩余字段的 JSON。
|
|
36
|
+
*
|
|
37
|
+
* 用于 action/infra 工具——AI 不需要看到冗余的 wrapper 字段。
|
|
38
|
+
* callCli 已在 ok===false 时抛出 CliError,成功路径 ok 始终为 true。
|
|
39
|
+
*/
|
|
40
|
+
export function formatDataJson(response) {
|
|
41
|
+
const { ok: _ok, command: _cmd, timestamp: _ts, ...rest } = response;
|
|
42
|
+
return JSON.stringify(rest, null, 2);
|
|
43
|
+
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -9,12 +9,6 @@ import { SERVER_INSTRUCTIONS } from './instructions.js';
|
|
|
9
9
|
import { registerReadTools } from './tools/read-tools.js';
|
|
10
10
|
import { registerEditTools } from './tools/edit-tools.js';
|
|
11
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_REM_GUIDE_CONTENT } from './resources/edit-rem-guide.js';
|
|
15
|
-
import { EDIT_TREE_GUIDE_CONTENT } from './resources/edit-tree-guide.js';
|
|
16
|
-
import { ERROR_REFERENCE_CONTENT } from './resources/error-reference.js';
|
|
17
|
-
import { SEPARATOR_FLASHCARD_CONTENT } from './resources/separator-flashcard.js';
|
|
18
12
|
export async function startMcpServer() {
|
|
19
13
|
const server = new FastMCP({
|
|
20
14
|
name: 'remnote-bridge',
|
|
@@ -24,54 +18,5 @@ export async function startMcpServer() {
|
|
|
24
18
|
registerInfraTools(server);
|
|
25
19
|
registerReadTools(server);
|
|
26
20
|
registerEditTools(server);
|
|
27
|
-
// Resources
|
|
28
|
-
server.addResource({
|
|
29
|
-
uri: 'remnote://outline-format',
|
|
30
|
-
name: 'Markdown 大纲格式规范',
|
|
31
|
-
mimeType: 'text/markdown',
|
|
32
|
-
async load() {
|
|
33
|
-
return { text: OUTLINE_FORMAT_CONTENT };
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
server.addResource({
|
|
37
|
-
uri: 'remnote://rem-object-fields',
|
|
38
|
-
name: 'RemObject 字段完整参考',
|
|
39
|
-
mimeType: 'text/markdown',
|
|
40
|
-
async load() {
|
|
41
|
-
return { text: REM_OBJECT_FIELDS_CONTENT };
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
server.addResource({
|
|
45
|
-
uri: 'remnote://edit-rem-guide',
|
|
46
|
-
name: 'edit_rem 操作指南',
|
|
47
|
-
mimeType: 'text/markdown',
|
|
48
|
-
async load() {
|
|
49
|
-
return { text: EDIT_REM_GUIDE_CONTENT };
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
server.addResource({
|
|
53
|
-
uri: 'remnote://edit-tree-guide',
|
|
54
|
-
name: 'edit_tree 操作指南',
|
|
55
|
-
mimeType: 'text/markdown',
|
|
56
|
-
async load() {
|
|
57
|
-
return { text: EDIT_TREE_GUIDE_CONTENT };
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
server.addResource({
|
|
61
|
-
uri: 'remnote://error-reference',
|
|
62
|
-
name: '错误诊断与恢复参考',
|
|
63
|
-
mimeType: 'text/markdown',
|
|
64
|
-
async load() {
|
|
65
|
-
return { text: ERROR_REFERENCE_CONTENT };
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
server.addResource({
|
|
69
|
-
uri: 'remnote://separator-flashcard',
|
|
70
|
-
name: '分隔符与闪卡参考',
|
|
71
|
-
mimeType: 'text/markdown',
|
|
72
|
-
async load() {
|
|
73
|
-
return { text: SEPARATOR_FLASHCARD_CONTENT };
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
21
|
await server.start({ transportType: 'stdio' });
|
|
77
22
|
}
|