agent-remnote 0.0.1 → 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 (102) hide show
  1. package/cli.js +2 -0
  2. package/dist/apps/cli/src/adapters/mcp.js +1 -0
  3. package/dist/apps/cli/src/commands/_enqueue.js +138 -0
  4. package/dist/apps/cli/src/commands/_shared.js +57 -0
  5. package/dist/apps/cli/src/commands/_tool.js +28 -0
  6. package/dist/apps/cli/src/commands/apply.js +81 -0
  7. package/dist/apps/cli/src/commands/config/index.js +3 -0
  8. package/dist/apps/cli/src/commands/config/print.js +28 -0
  9. package/dist/apps/cli/src/commands/daily/index.js +4 -0
  10. package/dist/apps/cli/src/commands/daily/summary.js +25 -0
  11. package/dist/apps/cli/src/commands/daily/write.js +145 -0
  12. package/dist/apps/cli/src/commands/db/backups.js +23 -0
  13. package/dist/apps/cli/src/commands/db/index.js +4 -0
  14. package/dist/apps/cli/src/commands/db/recent.js +178 -0
  15. package/dist/apps/cli/src/commands/doctor.js +124 -0
  16. package/dist/apps/cli/src/commands/index.js +73 -0
  17. package/dist/apps/cli/src/commands/ops/index.js +4 -0
  18. package/dist/apps/cli/src/commands/ops/list.js +12 -0
  19. package/dist/apps/cli/src/commands/ops/schema.js +77 -0
  20. package/dist/apps/cli/src/commands/queue/enqueue.js +73 -0
  21. package/dist/apps/cli/src/commands/queue/index.js +5 -0
  22. package/dist/apps/cli/src/commands/queue/inspect.js +26 -0
  23. package/dist/apps/cli/src/commands/queue/stats.js +14 -0
  24. package/dist/apps/cli/src/commands/read/by-reference.js +35 -0
  25. package/dist/apps/cli/src/commands/read/connections.js +15 -0
  26. package/dist/apps/cli/src/commands/read/index.js +21 -0
  27. package/dist/apps/cli/src/commands/read/inspect.js +34 -0
  28. package/dist/apps/cli/src/commands/read/outline.js +59 -0
  29. package/dist/apps/cli/src/commands/read/query.js +95 -0
  30. package/dist/apps/cli/src/commands/read/references.js +41 -0
  31. package/dist/apps/cli/src/commands/read/resolve-ref.js +32 -0
  32. package/dist/apps/cli/src/commands/read/search.js +40 -0
  33. package/dist/apps/cli/src/commands/read/table.js +32 -0
  34. package/dist/apps/cli/src/commands/todos/index.js +3 -0
  35. package/dist/apps/cli/src/commands/todos/list.js +33 -0
  36. package/dist/apps/cli/src/commands/topic/index.js +3 -0
  37. package/dist/apps/cli/src/commands/topic/summary.js +44 -0
  38. package/dist/apps/cli/src/commands/wechat/index.js +3 -0
  39. package/dist/apps/cli/src/commands/wechat/outline.js +430 -0
  40. package/dist/apps/cli/src/commands/write/bullet.js +76 -0
  41. package/dist/apps/cli/src/commands/write/index.js +4 -0
  42. package/dist/apps/cli/src/commands/write/md.js +91 -0
  43. package/dist/apps/cli/src/commands/ws/_shared.js +129 -0
  44. package/dist/apps/cli/src/commands/ws/ensure.js +22 -0
  45. package/dist/apps/cli/src/commands/ws/health.js +15 -0
  46. package/dist/apps/cli/src/commands/ws/index.js +21 -0
  47. package/dist/apps/cli/src/commands/ws/logs.js +95 -0
  48. package/dist/apps/cli/src/commands/ws/restart.js +73 -0
  49. package/dist/apps/cli/src/commands/ws/serve.js +52 -0
  50. package/dist/apps/cli/src/commands/ws/start.js +70 -0
  51. package/dist/apps/cli/src/commands/ws/status.js +60 -0
  52. package/dist/apps/cli/src/commands/ws/stop.js +59 -0
  53. package/dist/apps/cli/src/commands/ws/trigger.js +20 -0
  54. package/dist/apps/cli/src/main.js +79 -0
  55. package/dist/apps/cli/src/services/AppConfig.js +3 -0
  56. package/dist/apps/cli/src/services/Config.js +91 -0
  57. package/dist/apps/cli/src/services/DaemonFiles.js +91 -0
  58. package/dist/apps/cli/src/services/Errors.js +49 -0
  59. package/dist/apps/cli/src/services/Output.js +16 -0
  60. package/dist/apps/cli/src/services/Payload.js +90 -0
  61. package/dist/apps/cli/src/services/Process.js +94 -0
  62. package/dist/apps/cli/src/services/Queue.js +120 -0
  63. package/dist/apps/cli/src/services/RefResolver.js +111 -0
  64. package/dist/apps/cli/src/services/RemDb.js +35 -0
  65. package/dist/apps/cli/src/services/WsClient.js +170 -0
  66. package/dist/apps/cli/tests/apply.contract.test.js +31 -0
  67. package/dist/apps/cli/tests/db-recent.contract.test.js +22 -0
  68. package/dist/apps/cli/tests/help.contract.test.js +30 -0
  69. package/dist/apps/cli/tests/helpers/runCli.js +45 -0
  70. package/dist/apps/cli/tests/ids-output.contract.test.js +30 -0
  71. package/dist/apps/cli/tests/payload-stdin.contract.test.js +15 -0
  72. package/dist/apps/cli/tests/read-search.contract.test.js +22 -0
  73. package/dist/apps/cli/tests/ws-health.contract.test.js +36 -0
  74. package/dist/apps/cli/vitest.config.js +7 -0
  75. package/dist/main.js +101037 -0
  76. package/dist/packages/mcp/src/public.js +18 -0
  77. package/dist/packages/mcp/src/queue/dao.js +165 -0
  78. package/dist/packages/mcp/src/queue/db.js +26 -0
  79. package/dist/packages/mcp/src/tools/executeSearchQuery.js +914 -0
  80. package/dist/packages/mcp/src/tools/findRemsByReference.js +447 -0
  81. package/dist/packages/mcp/src/tools/getRemConnections.js +566 -0
  82. package/dist/packages/mcp/src/tools/inspectRemDoc.js +60 -0
  83. package/dist/packages/mcp/src/tools/listRemBackups.js +35 -0
  84. package/dist/packages/mcp/src/tools/listRemReferences.js +421 -0
  85. package/dist/packages/mcp/src/tools/listSupportedOps.js +41 -0
  86. package/dist/packages/mcp/src/tools/listTodos.js +815 -0
  87. package/dist/packages/mcp/src/tools/outlineRemSubtree.js +203 -0
  88. package/dist/packages/mcp/src/tools/readRemTable.js +252 -0
  89. package/dist/packages/mcp/src/tools/resolveRemReference.js +174 -0
  90. package/dist/packages/mcp/src/tools/searchQueryTypes.js +127 -0
  91. package/dist/packages/mcp/src/tools/searchRemOverview.js +422 -0
  92. package/dist/packages/mcp/src/tools/searchUtils.js +32 -0
  93. package/dist/packages/mcp/src/tools/shared.js +393 -0
  94. package/dist/packages/mcp/src/tools/summarizeDailyNotes.js +221 -0
  95. package/dist/packages/mcp/src/tools/summarizeTopicActivity.js +605 -0
  96. package/dist/packages/mcp/src/tools/timeFilters.js +130 -0
  97. package/dist/packages/mcp/src/ws/bridge.js +377 -0
  98. package/package.json +40 -8
  99. package/README.md +0 -3
  100. package/dist/index.d.ts +0 -2
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/index.js +0 -5
@@ -0,0 +1,566 @@
1
+ import { z } from "zod";
2
+ import { buildGuidedResponse, parseOrThrow, withResolvedDatabase, safeJsonParse } from "./shared.js";
3
+ import { executeListRemReferences, listRemReferencesSchema, } from "./listRemReferences.js";
4
+ import { executeFindRemsByReference } from "./findRemsByReference.js";
5
+ import { TIME_RANGE_PATTERN, timeValueSchema } from "./timeFilters.js";
6
+ const inputShape = {
7
+ id: listRemReferencesSchema.shape.id.describe("起始 Rem ID"),
8
+ dbPath: z.string().optional().describe("数据库文件路径(默认自动发现)"),
9
+ includeDescendants: z
10
+ .boolean()
11
+ .optional()
12
+ .describe("是否将起始 Rem 的子树也纳入锚点(默认 false)"),
13
+ maxDepth: z
14
+ .number()
15
+ .int()
16
+ .min(0)
17
+ .max(10)
18
+ .optional()
19
+ .describe("展开起始 Rem 子树的最大深度(默认 0)"),
20
+ includeOccurrences: z.boolean().optional().describe("是否包含逐次出现的明细(默认 false)"),
21
+ resolveText: z.boolean().optional().describe("是否解析文本摘要(默认 true)"),
22
+ inboundMaxDepth: z
23
+ .number()
24
+ .int()
25
+ .min(1)
26
+ .max(3)
27
+ .optional()
28
+ .describe("入站多跳的最大深度(默认 1)"),
29
+ outboundMaxDepth: z
30
+ .number()
31
+ .int()
32
+ .min(1)
33
+ .max(3)
34
+ .optional()
35
+ .describe("出站多跳的最大深度(默认 1)"),
36
+ inboundMaxCandidates: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(1000)
41
+ .optional()
42
+ .describe("入站候选上限(默认 200)"),
43
+ // 可选分页与图输出控制
44
+ outboundLimit: z
45
+ .number()
46
+ .int()
47
+ .min(1)
48
+ .max(2000)
49
+ .optional()
50
+ .describe("出站结果上限(默认 2000)"),
51
+ outboundOffset: z
52
+ .number()
53
+ .int()
54
+ .min(0)
55
+ .optional()
56
+ .describe("出站结果偏移量"),
57
+ inboundGraph: z
58
+ .boolean()
59
+ .optional()
60
+ .describe("是否输出入站多跳图(默认 false)"),
61
+ inboundGraphMode: z
62
+ .enum(["auto", "sql", "scan"]).optional().describe("入站图计算模式(默认 auto)"),
63
+ inboundLimit: z
64
+ .number()
65
+ .int()
66
+ .min(1)
67
+ .max(2000)
68
+ .optional()
69
+ .describe("入站图结果上限(默认 2000)"),
70
+ inboundOffset: z
71
+ .number()
72
+ .int()
73
+ .min(0)
74
+ .optional()
75
+ .describe("入站图结果偏移量"),
76
+ inboundScanLimit: z
77
+ .number()
78
+ .int()
79
+ .min(1000)
80
+ .max(200000)
81
+ .optional()
82
+ .describe("入站扫描模式的扫描上限(默认 50k-200k 区间)"),
83
+ // 时间过滤(仅在入站图中生效;SQL/scan 两路径均尽力筛选)
84
+ timeRange: z
85
+ .union([
86
+ z.literal("all"),
87
+ z.literal("*"),
88
+ z.string().regex(TIME_RANGE_PATTERN, "timeRange 需形如 '30d'、'2w'、'12h'")
89
+ ])
90
+ .optional()
91
+ .describe("时间范围(如 30d/2w/12h 或 all/*)"),
92
+ createdAfter: timeValueSchema.optional().describe("创建时间下界(ISO/毫秒/秒)"),
93
+ createdBefore: timeValueSchema.optional().describe("创建时间上界(ISO/毫秒/秒)"),
94
+ updatedAfter: timeValueSchema.optional().describe("更新时间下界(ISO/毫秒/秒)"),
95
+ updatedBefore: timeValueSchema.optional().describe("更新时间上界(ISO/毫秒/秒)"),
96
+ };
97
+ export const getRemConnectionsSchema = z.object(inputShape);
98
+ export async function executeGetRemConnections(params) {
99
+ const parsed = parseOrThrow(getRemConnectionsSchema, params, { label: "get_rem_connections" });
100
+ const { payload: referencePayload, suggestions: baseSuggestions } = await executeListRemReferences({
101
+ id: parsed.id,
102
+ dbPath: parsed.dbPath,
103
+ includeDescendants: parsed.includeDescendants,
104
+ maxDepth: parsed.maxDepth,
105
+ includeOccurrences: parsed.includeOccurrences,
106
+ resolveText: parsed.resolveText,
107
+ includeInbound: true,
108
+ inboundMaxDepth: parsed.inboundMaxDepth,
109
+ inboundMaxCandidates: parsed.inboundMaxCandidates,
110
+ });
111
+ const outbound = referencePayload.references ?? [];
112
+ const inbound = referencePayload.inbound ?? [];
113
+ // 可选:扩展“出站多跳”(基于引用边,最多 3 层)。
114
+ let outboundExpanded;
115
+ const outboundMaxDepth = params.outboundMaxDepth ?? 1;
116
+ if (outboundMaxDepth > 1) {
117
+ try {
118
+ const res = await computeOutboundBfs({
119
+ dbPath: parsed.dbPath,
120
+ startIds: [parsed.id],
121
+ outboundMaxDepth,
122
+ includeDescendants: parsed.includeDescendants ?? false,
123
+ sourceTreeDepth: parsed.maxDepth ?? 0,
124
+ limit: params.outboundLimit,
125
+ offset: params.outboundOffset,
126
+ });
127
+ outboundExpanded = res;
128
+ }
129
+ catch (_) { }
130
+ }
131
+ // 可选:入站多跳图形输出(SQL/SCAN 双实现;auto 优先 SQL,必要时回退扫描)。
132
+ let inboundGraph;
133
+ const enableInboundGraph = params.inboundGraph === true;
134
+ if (enableInboundGraph) {
135
+ try {
136
+ const mode = params.inboundGraphMode || 'auto';
137
+ if (mode === 'sql' || mode === 'auto') {
138
+ const sqlRes = await computeInboundGraphSql({
139
+ dbPath: parsed.dbPath,
140
+ startIds: [parsed.id],
141
+ inboundMaxDepth: parsed.inboundMaxDepth ?? 1,
142
+ includeDescendants: parsed.includeDescendants ?? false,
143
+ sourceTreeDepth: parsed.maxDepth ?? 0,
144
+ limit: params.inboundLimit,
145
+ offset: params.inboundOffset,
146
+ timeRange: params.timeRange,
147
+ createdAfter: params.createdAfter,
148
+ createdBefore: params.createdBefore,
149
+ updatedAfter: params.updatedAfter,
150
+ updatedBefore: params.updatedBefore,
151
+ });
152
+ inboundGraph = sqlRes;
153
+ if (mode === 'auto') {
154
+ const directInboundCount = inbound.length;
155
+ const tooFew = (inboundGraph?.count ?? 0) === 0 && directInboundCount > 0;
156
+ if (tooFew) {
157
+ const scanRes = await computeInboundGraphScan({
158
+ dbPath: parsed.dbPath,
159
+ startIds: [parsed.id],
160
+ inboundMaxDepth: parsed.inboundMaxDepth ?? 1,
161
+ includeDescendants: parsed.includeDescendants ?? false,
162
+ sourceTreeDepth: parsed.maxDepth ?? 0,
163
+ limit: params.inboundLimit,
164
+ offset: params.inboundOffset,
165
+ scanLimit: params.inboundScanLimit,
166
+ timeRange: params.timeRange,
167
+ createdAfter: params.createdAfter,
168
+ createdBefore: params.createdBefore,
169
+ updatedAfter: params.updatedAfter,
170
+ updatedBefore: params.updatedBefore,
171
+ });
172
+ inboundGraph = scanRes;
173
+ }
174
+ }
175
+ }
176
+ else {
177
+ const scanRes = await computeInboundGraphScan({
178
+ dbPath: parsed.dbPath,
179
+ startIds: [parsed.id],
180
+ inboundMaxDepth: parsed.inboundMaxDepth ?? 1,
181
+ includeDescendants: parsed.includeDescendants ?? false,
182
+ sourceTreeDepth: parsed.maxDepth ?? 0,
183
+ limit: params.inboundLimit,
184
+ offset: params.inboundOffset,
185
+ scanLimit: params.inboundScanLimit,
186
+ timeRange: params.timeRange,
187
+ createdAfter: params.createdAfter,
188
+ createdBefore: params.createdBefore,
189
+ updatedAfter: params.updatedAfter,
190
+ updatedBefore: params.updatedBefore,
191
+ });
192
+ inboundGraph = scanRes;
193
+ }
194
+ }
195
+ catch (_) { }
196
+ }
197
+ const guidance = outbound.length + inbound.length > 0
198
+ ? `已整理 Rem ${referencePayload.remId} 的连接情况:出站 ${outbound.length} 项,入站 ${inbound.length} 项。`
199
+ : `未找到 Rem ${referencePayload.remId} 的出站或入站引用。`;
200
+ const payload = {
201
+ ...referencePayload,
202
+ guidance,
203
+ outbound,
204
+ inbound,
205
+ outboundCount: outbound.length,
206
+ inboundCount: inbound.length,
207
+ outboundExpandedDepth: outboundExpanded?.depthApplied ?? 1,
208
+ outboundExpandedCount: outboundExpanded?.count ?? 0,
209
+ outboundExpandedNodes: outboundExpanded?.nodes ?? [],
210
+ outboundExpandedEdges: outboundExpanded?.edges ?? [],
211
+ inboundGraphDepth: inboundGraph?.depthApplied ?? undefined,
212
+ inboundGraphCount: inboundGraph?.count ?? 0,
213
+ inboundGraphNodes: inboundGraph?.nodes ?? [],
214
+ inboundGraphEdges: inboundGraph?.edges ?? [],
215
+ };
216
+ const suggestions = [...baseSuggestions];
217
+ if (outbound.length > 0) {
218
+ pushSuggestion(suggestions, "查看出站引用详情:可继续使用 outline_rem_subtree id=<refId> 或 inspect_rem_doc");
219
+ }
220
+ if (payload.outboundExpandedCount > 0) {
221
+ pushSuggestion(suggestions, `已启用出站多跳(深度 ${payload.outboundExpandedDepth})。需要更广范围可将 outboundMaxDepth 调至 ${payload.outboundExpandedDepth < 3 ? payload.outboundExpandedDepth + 1 : 3}`);
222
+ }
223
+ else if (outboundMaxDepth > 1) {
224
+ pushSuggestion(suggestions, "未发现多跳出站引用,可尝试提高 outboundMaxDepth 或扩大锚点(includeDescendants=true)");
225
+ }
226
+ if (enableInboundGraph) {
227
+ if (payload.inboundGraphCount > 0) {
228
+ pushSuggestion(suggestions, `已启用入站多跳图(深度 ${payload.inboundGraphDepth})。可调整 inboundLimit/offset 分页查看更多`);
229
+ }
230
+ else {
231
+ pushSuggestion(suggestions, "未发现入站多跳结果,可尝试提高 inboundMaxDepth 或扩大锚点(includeDescendants=true)");
232
+ }
233
+ }
234
+ if (inbound.length > 0) {
235
+ pushSuggestion(suggestions, "查看入站引用上下文:可对 inbound.remId 调用 outline_rem_subtree includeEmpty=true");
236
+ }
237
+ return buildGuidedResponse(payload, suggestions);
238
+ }
239
+ // --- Internal helpers (outbound BFS) ---
240
+ async function computeOutboundBfs(args) {
241
+ const { result } = await withResolvedDatabase(args.dbPath, (db) => {
242
+ const start = new Set(args.startIds);
243
+ if (args.includeDescendants && args.sourceTreeDepth > 0) {
244
+ for (const id of expandDescendants(db, args.startIds, args.sourceTreeDepth))
245
+ start.add(id);
246
+ }
247
+ const visited = new Set(start);
248
+ const nodes = new Map();
249
+ const edges = [];
250
+ let depth = 1;
251
+ let frontier = new Set(start);
252
+ const limit = Math.max(1, Math.min(args.limit ?? 2000, 2000));
253
+ while (frontier.size > 0 && depth <= args.outboundMaxDepth && edges.length < limit) {
254
+ const next = new Set();
255
+ for (const fromId of frontier) {
256
+ const targets = getDirectOutboundRefs(db, fromId);
257
+ for (const toId of targets) {
258
+ edges.push({ from: fromId, to: toId, depth });
259
+ if (!visited.has(toId)) {
260
+ visited.add(toId);
261
+ next.add(toId);
262
+ if (!nodes.has(toId))
263
+ nodes.set(toId, { id: toId, depth, via: fromId });
264
+ }
265
+ if (edges.length >= limit)
266
+ break;
267
+ }
268
+ if (edges.length >= limit)
269
+ break;
270
+ }
271
+ if (edges.length >= limit)
272
+ break;
273
+ frontier = next;
274
+ depth += 1;
275
+ }
276
+ const all = Array.from(nodes.values());
277
+ const offset = Math.max(0, args.offset ?? 0);
278
+ const sliced = all.slice(offset, Math.min(offset + limit, all.length));
279
+ const details = enrichNodeDetails(db, sliced);
280
+ return {
281
+ nodes: details,
282
+ edges,
283
+ depthApplied: Math.min(depth - 1, args.outboundMaxDepth),
284
+ total: all.length,
285
+ };
286
+ });
287
+ return {
288
+ depthApplied: result.depthApplied,
289
+ nodes: result.nodes,
290
+ count: result.nodes.length,
291
+ edges: result.edges,
292
+ };
293
+ }
294
+ function expandDescendants(db, roots, maxDepth) {
295
+ if (!roots || roots.length === 0 || maxDepth <= 0)
296
+ return [];
297
+ const placeholders = roots.map(() => "?").join(",");
298
+ const sql = `WITH RECURSIVE tree(id, depth) AS (
299
+ SELECT _id, 0 FROM quanta WHERE _id IN (${placeholders})
300
+ UNION ALL
301
+ SELECT child._id, tree.depth + 1
302
+ FROM quanta child
303
+ JOIN tree ON json_extract(child.doc, '$.parent') = tree.id
304
+ WHERE tree.depth + 1 <= ?
305
+ )
306
+ SELECT id FROM tree WHERE depth > 0`;
307
+ const rows = db.prepare(sql).all(...roots, maxDepth);
308
+ return rows.map((r) => r.id);
309
+ }
310
+ function getDirectOutboundRefs(db, id) {
311
+ const row = db.prepare("SELECT doc FROM quanta WHERE _id = ?").get(id);
312
+ if (!row)
313
+ return new Set();
314
+ const doc = safeJsonParse(row.doc);
315
+ const refs = new Set();
316
+ if (!doc)
317
+ return refs;
318
+ collectReferences(doc.key, refs);
319
+ if (doc.value !== undefined)
320
+ collectReferences(doc.value, refs);
321
+ return refs;
322
+ }
323
+ function collectReferences(value, into) {
324
+ if (Array.isArray(value)) {
325
+ value.forEach((item) => collectReferences(item, into));
326
+ return;
327
+ }
328
+ if (value && typeof value === "object") {
329
+ const maybe = value;
330
+ if ((maybe.i === "q" || maybe.i === "p") && typeof maybe._id === "string") {
331
+ into.add(maybe._id);
332
+ return;
333
+ }
334
+ for (const child of Object.values(maybe))
335
+ collectReferences(child, into);
336
+ }
337
+ }
338
+ function enrichNodeDetails(db, nodes) {
339
+ if (nodes.length === 0)
340
+ return [];
341
+ const stmt = db.prepare(`SELECT
342
+ id,
343
+ json_extract(doc, '$.kt') AS plainText,
344
+ ancestor_not_ref_text AS ancestorText
345
+ FROM remsSearchInfos
346
+ WHERE id = ?`);
347
+ const fallback = db.prepare("SELECT doc FROM quanta WHERE _id = ?");
348
+ const out = [];
349
+ for (const n of nodes) {
350
+ let title = null;
351
+ let snippet = null;
352
+ let ancestor = null;
353
+ const info = stmt.get(n.id);
354
+ if (info) {
355
+ title = (info.plainText ?? '').trim() || null;
356
+ snippet = title;
357
+ ancestor = info.ancestorText ? info.ancestorText.trim() : null;
358
+ }
359
+ else {
360
+ const row = fallback.get(n.id);
361
+ if (row) {
362
+ const parsed = safeJsonParse(row.doc);
363
+ const rawKey = parsed?.key;
364
+ if (Array.isArray(rawKey)) {
365
+ const text = rawKey
366
+ .map((x) => (typeof x === 'string' ? x : ''))
367
+ .join('')
368
+ .replace(/\s+/g, ' ')
369
+ .trim();
370
+ if (text) {
371
+ title = text;
372
+ snippet = text;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ out.push({ ...n, title, snippet, ancestor });
378
+ }
379
+ return out;
380
+ }
381
+ // --- Internal helpers (inbound graph via direct JSON token scan) ---
382
+ async function computeInboundGraphScan(args) {
383
+ const { result } = await withResolvedDatabase(args.dbPath, async (db) => {
384
+ const start = new Set(args.startIds);
385
+ if (args.includeDescendants && args.sourceTreeDepth > 0) {
386
+ for (const id of expandDescendants(db, args.startIds, args.sourceTreeDepth))
387
+ start.add(id);
388
+ }
389
+ const visited = new Set();
390
+ const edges = [];
391
+ const nodeMap = new Map();
392
+ let depth = 1;
393
+ let frontier = new Set(Array.from(start));
394
+ const limit = Math.max(1, Math.min(args.limit ?? 2000, 2000));
395
+ const scanLimit = Math.max(1000, Math.min(args.scanLimit ?? 50000, 200000));
396
+ while (frontier.size > 0 && depth <= args.inboundMaxDepth && edges.length < limit) {
397
+ const next = new Set();
398
+ for (const target of frontier) {
399
+ const sources = getDirectInboundRefs(db, target, scanLimit, {
400
+ timeRange: args.timeRange,
401
+ createdAfter: args.createdAfter,
402
+ createdBefore: args.createdBefore,
403
+ updatedAfter: args.updatedAfter,
404
+ updatedBefore: args.updatedBefore,
405
+ });
406
+ for (const src of sources) {
407
+ edges.push({ from: src, to: target, depth });
408
+ if (!visited.has(src)) {
409
+ visited.add(src);
410
+ next.add(src);
411
+ if (!nodeMap.has(src))
412
+ nodeMap.set(src, { id: src, depth, via: target });
413
+ }
414
+ if (edges.length >= limit)
415
+ break;
416
+ }
417
+ if (edges.length >= limit)
418
+ break;
419
+ }
420
+ if (edges.length >= limit)
421
+ break;
422
+ frontier = next;
423
+ depth += 1;
424
+ }
425
+ const all = Array.from(nodeMap.values());
426
+ const offset = Math.max(0, args.offset ?? 0);
427
+ const sliced = all.slice(offset, Math.min(offset + limit, all.length));
428
+ const details = enrichNodeDetails(db, sliced);
429
+ return {
430
+ nodes: details,
431
+ edges,
432
+ depthApplied: Math.min(depth - 1, args.inboundMaxDepth),
433
+ total: all.length,
434
+ };
435
+ });
436
+ return {
437
+ depthApplied: result.depthApplied,
438
+ nodes: result.nodes,
439
+ count: result.nodes.length,
440
+ edges: result.edges,
441
+ };
442
+ }
443
+ function getDirectInboundRefs(db, targetId, scanLimit, filters) {
444
+ const out = new Set();
445
+ const needle = `"_id":"${targetId}"`;
446
+ try {
447
+ const stmt = db.prepare('SELECT _id, doc FROM quanta WHERE doc LIKE ? LIMIT ?');
448
+ for (const row of stmt.iterate(`%${needle}%`, scanLimit)) {
449
+ const doc = safeJsonParse(row.doc);
450
+ if (!doc)
451
+ continue;
452
+ if (!matchTimeFilters(doc, filters))
453
+ continue;
454
+ const refs = new Set();
455
+ collectReferences(doc.key, refs);
456
+ if (doc.value !== undefined)
457
+ collectReferences(doc.value, refs);
458
+ if (refs.has(targetId))
459
+ out.add(row._id);
460
+ }
461
+ }
462
+ catch (_) { }
463
+ return out;
464
+ }
465
+ function matchTimeFilters(doc, filters) {
466
+ if (!filters)
467
+ return true;
468
+ const created = pickTs(doc, ['createdAt', 'c', 'ct']);
469
+ const updated = pickTs(doc, ['m', 'lm', 'createdAt']);
470
+ const ca = normalizeMaybeTs(filters.createdAfter);
471
+ const cb = normalizeMaybeTs(filters.createdBefore);
472
+ const ua = normalizeMaybeTs(filters.updatedAfter);
473
+ const ub = normalizeMaybeTs(filters.updatedBefore);
474
+ if (ca != null && created != null && created < ca)
475
+ return false;
476
+ if (cb != null && created != null && created > cb)
477
+ return false;
478
+ if (ua != null && updated != null && updated < ua)
479
+ return false;
480
+ if (ub != null && updated != null && updated > ub)
481
+ return false;
482
+ return true;
483
+ }
484
+ function pickTs(obj, keys) {
485
+ for (const k of keys) {
486
+ const v = obj[k];
487
+ const n = typeof v === 'number' ? v : (typeof v === 'string' ? Number(v) : NaN);
488
+ if (Number.isFinite(n))
489
+ return n;
490
+ }
491
+ return null;
492
+ }
493
+ function normalizeMaybeTs(v) {
494
+ if (v == null)
495
+ return null;
496
+ if (typeof v === 'number')
497
+ return v;
498
+ const n = Number(v);
499
+ return Number.isFinite(n) ? n : null;
500
+ }
501
+ // SQL 版入站图(性能好,准确性依赖索引/SQL 条件覆盖度)
502
+ async function computeInboundGraphSql(args) {
503
+ const { result } = await withResolvedDatabase(args.dbPath, async (db) => {
504
+ const start = new Set(args.startIds);
505
+ if (args.includeDescendants && args.sourceTreeDepth > 0) {
506
+ for (const id of expandDescendants(db, args.startIds, args.sourceTreeDepth))
507
+ start.add(id);
508
+ }
509
+ const inbound = await executeFindRemsByReference({
510
+ targetIds: Array.from(start),
511
+ maxDepth: args.inboundMaxDepth,
512
+ limit: Math.max(1, Math.min(args.limit ?? 2000, 2000)),
513
+ offset: Math.max(0, args.offset ?? 0),
514
+ dbPath: args.dbPath,
515
+ detail: true,
516
+ timeRange: args.timeRange,
517
+ createdAfter: args.createdAfter,
518
+ createdBefore: args.createdBefore,
519
+ updatedAfter: args.updatedAfter,
520
+ updatedBefore: args.updatedBefore,
521
+ });
522
+ const nodes = inbound.matches.map((m) => ({
523
+ id: m.id,
524
+ depth: m.depth ?? 0,
525
+ via: Array.isArray(m.matchedTargets) && m.matchedTargets.length > 0 ? m.matchedTargets[0] : null,
526
+ title: m.title ?? null,
527
+ snippet: m.snippet ?? null,
528
+ ancestor: m.ancestor ?? null,
529
+ }));
530
+ const edges = [];
531
+ for (const m of inbound.matches) {
532
+ const tos = Array.isArray(m.matchedTargets) ? m.matchedTargets : [];
533
+ for (const t of tos)
534
+ edges.push({ from: m.id, to: t, depth: m.depth ?? 0 });
535
+ }
536
+ return {
537
+ nodes,
538
+ edges,
539
+ depthApplied: inbound.depthApplied,
540
+ total: inbound.totalCount,
541
+ };
542
+ });
543
+ return {
544
+ depthApplied: result.depthApplied,
545
+ nodes: result.nodes,
546
+ count: result.nodes.length,
547
+ edges: result.edges,
548
+ };
549
+ }
550
+ export function registerGetRemConnections(server) {
551
+ server.tool("get_rem_connections", `<usecase>同时查看某个 Rem 的出站引用和入站引用。</usecase>
552
+ <instructions>
553
+ - 默认只分析当前 Rem,可设 includeDescendants=true 扫描整棵子树。
554
+ - inboundMaxDepth 控制反向引用的搜索层级(默认 1 层)。
555
+ - outboundMaxDepth 可启用“出站多跳”(默认 1=仅直接出站,最大 3 层)。
556
+ - 可选分页:outboundLimit/outboundOffset;入站图:inboundGraph=true 时返回 nodes/edges(可配 inboundLimit/offset)。
557
+ - 结果将分别展示 outbound / inbound 列表,并在启用时补充出站/入站多跳图,便于顺藤摸瓜式追踪。
558
+ - 入站图模式:inboundGraphMode=sql|scan|auto(默认 auto 优先 SQL,不足则回退扫描);扫描上限 inboundScanLimit(默认 50000)。
559
+ - 时间过滤(仅作用于入站图):timeRange/createdAfter/createdBefore/updatedAfter/updatedBefore。
560
+ </instructions>`, inputShape, async (input) => executeGetRemConnections(input));
561
+ }
562
+ function pushSuggestion(list, text) {
563
+ if (!list.includes(text)) {
564
+ list.push(text);
565
+ }
566
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { buildGuidedResponse, summarizeKey, safeJsonParse, withResolvedDatabase, parseOrThrow, } from "./shared.js";
3
+ const inputShape = {
4
+ id: z.string().min(1, "id 必填").describe("目标 Rem ID"),
5
+ dbPath: z.string().optional().describe("数据库文件路径(默认自动发现)"),
6
+ expandReferences: z
7
+ .boolean()
8
+ .optional()
9
+ .describe("是否展开引用文本,仅用于摘要(默认 false)"),
10
+ maxReferenceDepth: z
11
+ .number()
12
+ .int()
13
+ .min(0)
14
+ .max(5)
15
+ .optional()
16
+ .describe("引用展开最大深度(默认 1)"),
17
+ };
18
+ export const inspectRemDocSchema = z.object(inputShape);
19
+ export async function executeInspectRemDoc(params) {
20
+ const parsed = parseOrThrow(inspectRemDocSchema, params, { label: "inspect_rem_doc" });
21
+ const expand = parsed.expandReferences ?? false;
22
+ const maxDepth = parsed.maxReferenceDepth ?? 1;
23
+ const { result, info } = await withResolvedDatabase(parsed.dbPath, async (db) => {
24
+ const row = db
25
+ .prepare("SELECT doc FROM quanta WHERE _id = ?")
26
+ .get(parsed.id);
27
+ if (!row) {
28
+ throw new Error(`未找到该 Rem(id=${parsed.id}),请确认 ID 是否存在于当前数据库`);
29
+ }
30
+ const doc = safeJsonParse(row.doc);
31
+ const keySummary = summarizeKey(doc?.key, db, { expand, maxDepth });
32
+ return {
33
+ id: parsed.id,
34
+ doc,
35
+ summary: {
36
+ text: keySummary.text,
37
+ references: keySummary.references,
38
+ },
39
+ };
40
+ });
41
+ return {
42
+ dbPath: info.dbPath,
43
+ resolution: info.source,
44
+ dirName: info.dirName,
45
+ ...result,
46
+ };
47
+ }
48
+ export function registerInspectRemDoc(server) {
49
+ server.tool("inspect_rem_doc", `<usecase>调试或深度分析 Rem 原始数据时使用。</usecase>
50
+ <instructions>
51
+ - 返回完整的 quanta.doc JSON,仅在确实需要底层字段时调用。
52
+ - 如需可读大纲,请优先使用 outline_rem_subtree。
53
+ </instructions>`, inputShape, async (input) => {
54
+ const parsed = parseOrThrow(inspectRemDocSchema, input, { label: "inspect_rem_doc" });
55
+ const result = await executeInspectRemDoc(parsed);
56
+ const guidance = `已获取 Rem ${result.id} 的原始 doc JSON。`;
57
+ const suggestions = ["如需可读内容,请改用 outline_rem_subtree 或 summarize_daily_notes"];
58
+ return buildGuidedResponse({ guidance, ...result }, suggestions);
59
+ });
60
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import os from "os";
3
+ import { discoverBackups, expandHome, makeToolResponse, parseOrThrow, REMNOTE_RELATIVE_DIR, } from "./shared.js";
4
+ const inputShape = {
5
+ basePath: z
6
+ .string()
7
+ .optional()
8
+ .describe("RemNote 基础目录(默认 ~/remnote)"),
9
+ limit: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(200)
14
+ .optional()
15
+ .describe("返回的备份条数上限(默认全部)"),
16
+ };
17
+ export const listRemBackupsSchema = z.object(inputShape);
18
+ export async function executeListRemBackups(params) {
19
+ const parsed = parseOrThrow(listRemBackupsSchema, params, { label: "list_rem_backups" });
20
+ const basePath = expandHome(parsed.basePath ?? `${os.homedir()}/${REMNOTE_RELATIVE_DIR}`);
21
+ const backups = await discoverBackups(basePath);
22
+ const limit = parsed.limit ?? backups.length;
23
+ return {
24
+ basePath,
25
+ total: backups.length,
26
+ items: backups.slice(0, limit),
27
+ };
28
+ }
29
+ export function registerListRemBackups(server) {
30
+ server.tool("list_rem_backups", "List available remnote backups under ~/remnote/**/backups (newest first).", inputShape, async (input) => {
31
+ const parsed = parseOrThrow(listRemBackupsSchema, input, { label: "list_rem_backups" });
32
+ const result = await executeListRemBackups(parsed);
33
+ return makeToolResponse(result);
34
+ });
35
+ }