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,815 @@
1
+ import { z } from "zod";
2
+ import { withResolvedDatabase, buildGuidedResponse, safeJsonParse, parseOrThrow, } from "./shared.js";
3
+ import { coalesceText, createPreview, stringifyAncestor } from "./searchUtils.js";
4
+ // 针对个人库的默认优化(可根据需要调整/扩展)
5
+ const DEFAULT_KNOWN = {
6
+ // 常用表头 Tag(若存在则优先加入候选)
7
+ tagIds: [
8
+ "ExWWcna6cyLPRSy3W", // 待办
9
+ "oZbSs7aaFPNTjLPMD", // Todo
10
+ "J3yx9nbpeBW8S9q4v", // TODO
11
+ ],
12
+ // 针对“待办”表已知的列与选项(存在即用作补充)
13
+ statusForTasks: {
14
+ tagId: "ExWWcna6cyLPRSy3W",
15
+ statusAttrId: "aQb9u7XMjFL96GGYc",
16
+ unfinishedOptionId: "jTCTqykroBRsA2vYm",
17
+ finishedOptionId: "CotJ4eARGeLvtLRBa",
18
+ },
19
+ };
20
+ export const listTodosInputSchema = z.object({
21
+ dbPath: z.string().optional().describe("数据库文件路径(默认自动发现)"),
22
+ status: z
23
+ .enum(["unfinished", "finished", "all"]) // 轻量枚举
24
+ .optional()
25
+ .describe("任务完成状态筛选(默认 unfinished)"),
26
+ // 指定候选表头 Tag 的 ID(优先级高于 tagTitles)
27
+ tagIds: z.array(z.string()).optional().describe("优先指定表头 Tag 的 ID 列表"),
28
+ // 通过标题自动发现表头 Tag,默认内置常见别名
29
+ tagTitles: z
30
+ .array(z.string())
31
+ .optional()
32
+ .describe("按标题自动发现表头 Tag(默认内置常见别名)"),
33
+ // 优先仅使用 Todo/TODO 标签,若存在则忽略其他(用户未显式传 tagIds/tagTitles 时生效)
34
+ preferTodoOnly: z
35
+ .boolean()
36
+ .optional()
37
+ .describe("若存在 Todo/TODO 标签,则仅使用该表头(默认 false)"),
38
+ // 即使无状态列,也始终纳入这些“仅打标签”的任务标题(默认 Todo/TODO)
39
+ alwaysIncludeTagOnlyTitles: z
40
+ .array(z.string())
41
+ .optional()
42
+ .describe("始终纳入仅打标签的任务标题(默认 [Todo, TODO])"),
43
+ // 限定祖先范围(仅返回该节点子树内的任务)
44
+ ancestorId: z.string().optional().describe("限定祖先范围,仅返回该子树内任务"),
45
+ includeDescendants: z.boolean().optional().describe("是否包含子孙(默认 true)"),
46
+ // 截止日、状态等列的候选名称(用于自动解析)
47
+ statusAttrTitles: z.array(z.string()).optional().describe("状态列的候选名称"),
48
+ unfinishedOptionTitles: z.array(z.string()).optional().describe("未完成选项的候选名称"),
49
+ finishedOptionTitles: z.array(z.string()).optional().describe("已完成选项的候选名称"),
50
+ dueDateAttrTitles: z.array(z.string()).optional().describe("截止日列的候选名称"),
51
+ // 额外筛选:截止日期上/下界(ISO 字符串或毫秒/秒时间戳)
52
+ dueAfter: z
53
+ .union([z.string(), z.number()])
54
+ .optional()
55
+ .describe("截止日下界(ISO/毫秒/秒)"),
56
+ dueBefore: z
57
+ .union([z.string(), z.number()])
58
+ .optional()
59
+ .describe("截止日上界(ISO/毫秒/秒)"),
60
+ // 排序:若存在截止日列,默认 dueAsc,否则 updatedAtDesc
61
+ sort: z
62
+ .enum(["dueAsc", "dueDesc", "updatedAtAsc", "updatedAtDesc"]) // 轻量枚举
63
+ .optional()
64
+ .describe("排序方式(默认:有截止日则按 dueAsc,否则 updatedAtDesc)"),
65
+ // 当包含 Todo/TODO 标签结果时,优先将其排在前面(默认 true)
66
+ preferTodoFirst: z
67
+ .boolean()
68
+ .optional()
69
+ .describe("当包含 Todo/TODO 时优先置顶(默认 true)"),
70
+ limit: z
71
+ .number()
72
+ .int()
73
+ .min(1)
74
+ .max(200)
75
+ .optional()
76
+ .describe("返回条数上限(默认 50)"),
77
+ offset: z
78
+ .number()
79
+ .int()
80
+ .min(0)
81
+ .optional()
82
+ .describe("分页偏移量(默认 0)"),
83
+ snippetLength: z
84
+ .number()
85
+ .int()
86
+ .min(40)
87
+ .max(300)
88
+ .optional()
89
+ .describe("结果摘要长度(默认 160)"),
90
+ // 当 status=all 且某 Tag 没有 Status 列时,是否包含“仅打标签的行”(无法区分完成状态)
91
+ includeTagOnlyWhenNoStatus: z
92
+ .boolean()
93
+ .optional()
94
+ .describe("status=all 且无状态列时,是否包含仅打标签的行(默认 true)"),
95
+ });
96
+ export const listTodosSchema = listTodosInputSchema;
97
+ export async function executeListTodos(params) {
98
+ const parsed = parseOrThrow(listTodosInputSchema, params, { label: "list_todos" });
99
+ const status = parsed.status ?? "unfinished";
100
+ const limit = parsed.limit ?? 50;
101
+ const offset = parsed.offset ?? 0;
102
+ const snippetLength = parsed.snippetLength ?? 160;
103
+ const includeDescendants = parsed.includeDescendants ?? true;
104
+ const preferTodoOnly = parsed.preferTodoOnly ?? false;
105
+ const alwaysIncludeTagOnlyTitles = (parsed.alwaysIncludeTagOnlyTitles && parsed.alwaysIncludeTagOnlyTitles.length > 0)
106
+ ? parsed.alwaysIncludeTagOnlyTitles
107
+ : ["Todo", "TODO"];
108
+ const includeTagOnlyWhenNoStatus = parsed.includeTagOnlyWhenNoStatus ?? true;
109
+ // 默认内置常用标题/别名(按你的个人库习惯可自行拓展)
110
+ const tagTitles = (parsed.tagTitles && parsed.tagTitles.length > 0)
111
+ ? parsed.tagTitles
112
+ : ["待办", "Todo", "TODO", "Tasks", "Task", "Todos", "TODOs"];
113
+ const statusAttrTitles = (parsed.statusAttrTitles && parsed.statusAttrTitles.length > 0)
114
+ ? parsed.statusAttrTitles
115
+ : ["Status", "状态"];
116
+ const unfinishedOptionTitles = (parsed.unfinishedOptionTitles && parsed.unfinishedOptionTitles.length > 0)
117
+ ? parsed.unfinishedOptionTitles
118
+ : ["Unfinished", "未完成"];
119
+ const finishedOptionTitles = (parsed.finishedOptionTitles && parsed.finishedOptionTitles.length > 0)
120
+ ? parsed.finishedOptionTitles
121
+ : ["Finished", "已完成", "Done", "完成"];
122
+ const dueDateAttrTitles = (parsed.dueDateAttrTitles && parsed.dueDateAttrTitles.length > 0)
123
+ ? parsed.dueDateAttrTitles
124
+ : ["Due", "Due Date", "截止", "截止日期", "到期", "到期日"];
125
+ const sortPref = parsed.sort;
126
+ const dueAfter = parsed.dueAfter;
127
+ const dueBefore = parsed.dueBefore;
128
+ const preferTodoFirst = parsed.preferTodoFirst ?? false;
129
+ const { result, info } = await withResolvedDatabase(parsed.dbPath, async (db) => {
130
+ // 1) 收集候选 Tag
131
+ const tagIdSet = new Set();
132
+ if (Array.isArray(parsed.tagIds)) {
133
+ for (const id of parsed.tagIds)
134
+ tagIdSet.add(id);
135
+ }
136
+ // 注入已知常用 Tag(存在即加入)
137
+ for (const id of DEFAULT_KNOWN.tagIds) {
138
+ if (remExists(db, id))
139
+ tagIdSet.add(id);
140
+ }
141
+ const discovered = discoverTagsByTitle(db, tagTitles);
142
+ // 若用户未显式给 tagIds/tagTitles,且 preferTodoOnly=true,则仅保留 Todo/TODO
143
+ if (!parsed.tagIds && !parsed.tagTitles && preferTodoOnly) {
144
+ const todoOnly = discovered.filter((t) => /^(todo)$/i.test(t.name.trim()));
145
+ if (todoOnly.length > 0) {
146
+ for (const { id } of todoOnly)
147
+ tagIdSet.add(id);
148
+ }
149
+ else {
150
+ for (const { id } of discovered)
151
+ tagIdSet.add(id);
152
+ }
153
+ }
154
+ else {
155
+ for (const { id } of discovered)
156
+ tagIdSet.add(id);
157
+ }
158
+ const tagIds = Array.from(tagIdSet);
159
+ if (tagIds.length === 0) {
160
+ return {
161
+ schemas: [],
162
+ rows: [],
163
+ dueCandidate: undefined,
164
+ items: [],
165
+ totalCandidates: 0,
166
+ totalMatched: 0,
167
+ hasMore: false,
168
+ nextOffset: null,
169
+ };
170
+ }
171
+ // 2) 为每个 Tag 解析属性/选项(重复利用 readRemTable 的思路)
172
+ const schemas = [];
173
+ for (const tagId of tagIds) {
174
+ const tagDocRow = db.prepare("SELECT doc FROM quanta WHERE _id = ?").get(tagId);
175
+ if (!tagDocRow?.doc)
176
+ continue;
177
+ const tagDoc = safeJsonParse(tagDocRow.doc);
178
+ const tagName = summarizeTitle(tagDoc);
179
+ const ctx = loadProperties(db, tagId);
180
+ const schema = {
181
+ tagId,
182
+ tagName,
183
+ };
184
+ // 定位 Status 属性及 Unfinished/Finished 选项
185
+ const statusProps = ctx.properties.filter((p) => p.kind === "select" &&
186
+ (statusAttrTitles.includes(p.name) || p.options.some((o) => unfinishedOptionTitles.includes(o.name) || finishedOptionTitles.includes(o.name))));
187
+ const statusProp = statusProps[0];
188
+ if (statusProp) {
189
+ schema.statusAttrId = statusProp.id;
190
+ const unfinishedOpt = statusProp.options.find((o) => unfinishedOptionTitles.includes(o.name));
191
+ const finishedOpt = statusProp.options.find((o) => finishedOptionTitles.includes(o.name));
192
+ if (unfinishedOpt)
193
+ schema.unfinishedOptionId = unfinishedOpt.id;
194
+ if (finishedOpt)
195
+ schema.finishedOptionId = finishedOpt.id;
196
+ }
197
+ // 若该 Tag 是“待办”,且未解析出列或选项,使用已知默认补充
198
+ if (schema.tagId === DEFAULT_KNOWN.statusForTasks.tagId &&
199
+ (!schema.statusAttrId || !schema.unfinishedOptionId || !schema.finishedOptionId)) {
200
+ if (remExists(db, DEFAULT_KNOWN.statusForTasks.statusAttrId)) {
201
+ schema.statusAttrId = schema.statusAttrId ?? DEFAULT_KNOWN.statusForTasks.statusAttrId;
202
+ }
203
+ if (remExists(db, DEFAULT_KNOWN.statusForTasks.unfinishedOptionId)) {
204
+ schema.unfinishedOptionId =
205
+ schema.unfinishedOptionId ?? DEFAULT_KNOWN.statusForTasks.unfinishedOptionId;
206
+ }
207
+ if (remExists(db, DEFAULT_KNOWN.statusForTasks.finishedOptionId)) {
208
+ schema.finishedOptionId =
209
+ schema.finishedOptionId ?? DEFAULT_KNOWN.statusForTasks.finishedOptionId;
210
+ }
211
+ }
212
+ // 定位截止日列(date 类型优先;或按标题别名匹配)
213
+ const dueProps = ctx.properties.filter((p) => p.kind === "date" || dueDateAttrTitles.includes(p.name));
214
+ const dueProp = dueProps[0];
215
+ if (dueProp)
216
+ schema.dueDateAttrId = dueProp.id;
217
+ schemas.push(schema);
218
+ }
219
+ // 3) 收集候选行(根据 status 选择策略)
220
+ const candidate = [];
221
+ for (const schema of schemas) {
222
+ const ctx = loadProperties(db, schema.tagId);
223
+ const statusProp = schema.statusAttrId
224
+ ? ctx.properties.find((p) => p.id === schema.statusAttrId)
225
+ : undefined;
226
+ if (status === "unfinished" || status === "finished") {
227
+ // 仅使用选项映射(pd)快速定位
228
+ const optionId = status === "unfinished" ? schema.unfinishedOptionId : schema.finishedOptionId;
229
+ if (statusProp && optionId) {
230
+ const option = statusProp.options.find((o) => o.id === optionId);
231
+ let pushed = 0;
232
+ if (option && option.rowIds.size > 0) {
233
+ for (const rowId of option.rowIds) {
234
+ candidate.push({ id: rowId, tagId: schema.tagId, source: "table" });
235
+ pushed++;
236
+ }
237
+ }
238
+ if (pushed === 0) {
239
+ const set = queryRowsByAttributeOption(db, schema.tagId, schema.statusAttrId, optionId);
240
+ for (const rowId of set) {
241
+ candidate.push({ id: rowId, tagId: schema.tagId, source: "table" });
242
+ }
243
+ }
244
+ }
245
+ // 若无状态属性/选项则跳过该 Tag(保持高精度)
246
+ }
247
+ else {
248
+ // status = all
249
+ // 优先尽量包含有状态列的行(所有选项的行集合)
250
+ let added = 0;
251
+ if (statusProp) {
252
+ const seen = new Set();
253
+ for (const opt of statusProp.options) {
254
+ for (const rowId of opt.rowIds) {
255
+ if (!seen.has(rowId)) {
256
+ seen.add(rowId);
257
+ candidate.push({ id: rowId, tagId: schema.tagId, source: "table" });
258
+ added++;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ // 补充“仅打标签”的行(没有状态列),可选开启
264
+ if (!statusProp && includeTagOnlyWhenNoStatus) {
265
+ const rows = loadRows(db, schema.tagId, { limit: Math.min(limit * 4, 2000), offset: 0 });
266
+ for (const row of rows.rows) {
267
+ candidate.push({ id: row.id, tagId: schema.tagId, source: "tag-only" });
268
+ added++;
269
+ }
270
+ }
271
+ }
272
+ // 无论 status 如何,若该标签属于“仅打标签也纳入”的标题列表,则补充其行(tag-only)
273
+ if (alwaysIncludeTagOnlyTitles.some((t) => t.trim().toLowerCase() === schema.tagName.trim().toLowerCase())) {
274
+ const rows = loadRows(db, schema.tagId, { limit: Math.min(limit * 4, 2000), offset: 0 });
275
+ for (const row of rows.rows) {
276
+ candidate.push({ id: row.id, tagId: schema.tagId, source: "tag-only" });
277
+ }
278
+ }
279
+ }
280
+ // 去重
281
+ const merged = new Map();
282
+ for (const item of candidate) {
283
+ const prev = merged.get(item.id);
284
+ if (!prev) {
285
+ merged.set(item.id, item);
286
+ }
287
+ else {
288
+ // 优先保留 table 来源
289
+ if (prev.source === "tag-only" && item.source === "table") {
290
+ merged.set(item.id, item);
291
+ }
292
+ }
293
+ }
294
+ const allIds = Array.from(merged.keys());
295
+ // 4) 取元信息(文本、祖先、时间)
296
+ const meta = fetchMetadata(db, allIds);
297
+ // 祖先范围过滤
298
+ let filtered = allIds.filter((id) => {
299
+ if (!parsed.ancestorId)
300
+ return true;
301
+ const m = meta.get(id);
302
+ if (!m)
303
+ return false;
304
+ if (!includeDescendants)
305
+ return m.parentId === parsed.ancestorId;
306
+ return m.ancestorIds.includes(parsed.ancestorId) || m.parentId === parsed.ancestorId;
307
+ });
308
+ // 5) 截止日筛选 + 排序(截止日优先,否则更新时间)
309
+ // 若存在多个 Tag,优先选第一个解析到 due 的 schema
310
+ const dueAttrId = schemas.find((s) => s.dueDateAttrId)?.dueDateAttrId;
311
+ const dueValues = dueAttrId ? fetchAttributeSortValues(db, filtered, dueAttrId) : new Map();
312
+ if (dueAttrId && (dueAfter != null || dueBefore != null)) {
313
+ const lower = dueAfter != null ? normalizeDateInput(dueAfter) : null;
314
+ const upper = dueBefore != null ? normalizeDateInput(dueBefore) : null;
315
+ filtered = filtered.filter((id) => {
316
+ const v = dueValues.get(id);
317
+ let ts = null;
318
+ if (typeof v === "number")
319
+ ts = v;
320
+ else if (typeof v === "string") {
321
+ const parsed = Date.parse(v);
322
+ ts = Number.isFinite(parsed) ? parsed : null;
323
+ }
324
+ if (ts == null)
325
+ return false;
326
+ if (lower != null && ts < lower)
327
+ return false;
328
+ if (upper != null && ts > upper)
329
+ return false;
330
+ return true;
331
+ });
332
+ }
333
+ // 默认按更新时间倒序(最新在前)。如需到期优先,显式传入 sort=dueAsc/desc
334
+ const sortMode = sortPref ?? "updatedAtDesc";
335
+ // 形成 Todo 标签集合(名称为 Todo/TODO 或已知默认 Todo/TODO)
336
+ const todoTagIds = new Set(schemas
337
+ .filter((s) => s.tagName.trim().toLowerCase() === "todo")
338
+ .map((s) => s.tagId));
339
+ // 合并默认已知 Todo/TODO ID
340
+ for (const id of ["oZbSs7aaFPNTjLPMD", "J3yx9nbpeBW8S9q4v"])
341
+ todoTagIds.add(id);
342
+ filtered.sort((a, b) => {
343
+ const ma = meta.get(a);
344
+ const mb = meta.get(b);
345
+ const av = dueValues.get(a);
346
+ const bv = dueValues.get(b);
347
+ if (preferTodoFirst) {
348
+ const srcA = merged.get(a);
349
+ const srcB = merged.get(b);
350
+ const pa = srcA && todoTagIds.has(srcA.tagId) ? 0 : 1;
351
+ const pb = srcB && todoTagIds.has(srcB.tagId) ? 0 : 1;
352
+ if (pa !== pb)
353
+ return pa - pb;
354
+ }
355
+ switch (sortMode) {
356
+ case "dueAsc": {
357
+ // 先有值再无值,数值升序/字符串升序
358
+ const cmp = compareSortValues(av, bv);
359
+ if (cmp !== 0)
360
+ return cmp;
361
+ return (mb?.updatedAt ?? 0) - (ma?.updatedAt ?? 0);
362
+ }
363
+ case "dueDesc": {
364
+ const cmp = compareSortValues(bv, av);
365
+ if (cmp !== 0)
366
+ return cmp;
367
+ return (mb?.updatedAt ?? 0) - (ma?.updatedAt ?? 0);
368
+ }
369
+ case "updatedAtAsc":
370
+ return (ma?.updatedAt ?? 0) - (mb?.updatedAt ?? 0);
371
+ case "updatedAtDesc":
372
+ default:
373
+ return (mb?.updatedAt ?? 0) - (ma?.updatedAt ?? 0);
374
+ }
375
+ });
376
+ const totalMatched = filtered.length;
377
+ const paginated = filtered.slice(offset, offset + limit);
378
+ const hasMore = offset + limit < filtered.length;
379
+ const nextOffset = hasMore ? offset + limit : null;
380
+ // 6) 整形返回
381
+ const items = paginated.map((id) => {
382
+ const m = meta.get(id);
383
+ const src = merged.get(id);
384
+ const text = m?.text ?? "";
385
+ const { title, snippet, truncated } = createPreview(text, snippetLength);
386
+ return {
387
+ id,
388
+ title,
389
+ snippet,
390
+ truncated,
391
+ ancestor: m?.ancestorText ?? null,
392
+ ancestorIds: m?.ancestorIds ?? [],
393
+ parentId: m?.parentId ?? null,
394
+ updatedAt: m?.updatedAt ?? null,
395
+ createdAt: m?.createdAt ?? null,
396
+ source: src?.source ?? "tag-only",
397
+ tagId: src?.tagId ?? "",
398
+ };
399
+ });
400
+ return {
401
+ schemas,
402
+ rows: items.map((x) => ({ id: x.id, tagId: x.tagId, source: x.source })),
403
+ dueCandidate: dueAttrId,
404
+ items,
405
+ totalCandidates: allIds.length,
406
+ totalMatched,
407
+ hasMore,
408
+ nextOffset,
409
+ };
410
+ });
411
+ const suggestions = [];
412
+ if (result.items && result.items.length > 0) {
413
+ suggestions.push("查看全文:outline_rem_subtree 或 inspect_rem_doc");
414
+ if (result.hasMore && result.nextOffset != null) {
415
+ suggestions.push(`还有更多结果,可再次调用 list_todos 并设置 offset=${result.nextOffset}`);
416
+ }
417
+ }
418
+ else {
419
+ suggestions.push("未命中,可确认表头 Tag 或调整默认别名列表 tagTitles");
420
+ }
421
+ const schemaSumm = result.schemas
422
+ .map((s) => `${s.tagName}(${s.tagId})`)
423
+ .join(", ");
424
+ const guidance = result.totalMatched > 0
425
+ ? `共识别标签:${schemaSumm}。本次命中 ${result.totalMatched} 条,返回 ${result.items.length} 条(offset=${offset},limit=${limit})。`
426
+ : tagTitles.length > 0
427
+ ? `未在 ${tagTitles.join("/")} 下找到匹配任务。`
428
+ : `未找到匹配任务。`;
429
+ const payload = {
430
+ dbPath: info.dbPath,
431
+ resolution: info.source,
432
+ dirName: info.dirName,
433
+ guidance,
434
+ status: (parsed.status ?? "unfinished"),
435
+ limit,
436
+ offset,
437
+ hasMore: result.hasMore,
438
+ nextOffset: result.nextOffset,
439
+ totalCandidates: result.totalCandidates ?? 0,
440
+ totalMatched: result.totalMatched ?? 0,
441
+ usedSchemas: result.schemas,
442
+ items: result.items ?? [],
443
+ };
444
+ return buildGuidedResponse(payload, suggestions);
445
+ }
446
+ export function registerListTodos(server) {
447
+ server.tool("list_todos", `<usecase>高频待办查询:按个人常用标签与表格属性快速列出任务(默认仅未完成)。</usecase>
448
+ <instructions>
449
+ - 内置解析表头 Tag(如 “待办”/“Todo”/“TODO” 等),并定位 Status/Unfinished、Due/Due Date 等列。
450
+ - 当 status=unfinished/finished 时,直接使用选项行映射(pd)快速命中任务,避免全表扫描。
451
+ - 支持 ancestorId 限定子树范围,支持按截止日或更新时间排序,支持分页。
452
+ - 如需纳入仅“打标签”的任务(无状态列),可在 status=all 时设置 includeTagOnlyWhenNoStatus=true。
453
+ </instructions>`, listTodosInputSchema.shape, async (input) => executeListTodos(input));
454
+ }
455
+ // -------- 实现细节 & 复用自 read_table_rem/execute_search_query 的思路 --------
456
+ function discoverTagsByTitle(db, titles) {
457
+ if (!titles || titles.length === 0)
458
+ return [];
459
+ const placeholders = titles.map(() => "?").join(",");
460
+ const rows = db
461
+ .prepare(`SELECT _id AS id, doc
462
+ FROM quanta
463
+ WHERE json_extract(doc, '$.key[0]') IN (${placeholders})`)
464
+ .all(...titles);
465
+ return rows.map((r) => ({ id: r.id, name: summarizeTitle(safeJsonParse(r.doc)) }));
466
+ }
467
+ function remExists(db, id) {
468
+ try {
469
+ const row = db.prepare("SELECT 1 FROM quanta WHERE _id = ? LIMIT 1").get(id);
470
+ return !!row;
471
+ }
472
+ catch (_) {
473
+ return false;
474
+ }
475
+ }
476
+ function summarizeTitle(doc) {
477
+ const key = doc?.key;
478
+ if (!Array.isArray(key) || key.length === 0)
479
+ return "";
480
+ const first = key[0];
481
+ if (typeof first === "string")
482
+ return first;
483
+ if (first && typeof first === "object" && typeof first.text === "string") {
484
+ return first.text;
485
+ }
486
+ return "";
487
+ }
488
+ function loadProperties(db, tagId) {
489
+ const stmt = db.prepare(`SELECT _id AS id, doc, json_extract(doc, '$.rcrs') AS rawType
490
+ FROM quanta
491
+ WHERE json_extract(doc, '$.parent') = @tagId
492
+ AND json_extract(doc, '$.rcrs') IS NOT NULL
493
+ ORDER BY json_extract(doc, '$.f')`);
494
+ const optionStmt = db.prepare(`SELECT _id AS id, doc, json_extract(doc, '$.rcre') AS rawOptionType
495
+ FROM quanta
496
+ WHERE json_extract(doc, '$.parent') = @parent
497
+ AND json_extract(doc, '$.rcre') IS NOT NULL
498
+ ORDER BY json_extract(doc, '$.f')`);
499
+ const properties = [];
500
+ const optionNameById = new Map();
501
+ const propertyRows = stmt.all({ tagId });
502
+ for (const row of propertyRows) {
503
+ const doc = safeJsonParse(row.doc);
504
+ const rawType = typeof row.rawType === "string" ? row.rawType : null;
505
+ const typeCode = rawType ? rawType.split(".")[1] ?? null : null;
506
+ const kind = mapPropertyType(typeCode);
507
+ const name = summarizeTitle(doc);
508
+ const options = [];
509
+ const optionRows = optionStmt.all({ parent: row.id });
510
+ for (const optionRow of optionRows) {
511
+ const optionDoc = safeJsonParse(optionRow.doc);
512
+ const optionName = summarizeTitle(optionDoc);
513
+ const pdRaw = optionDoc?.pd;
514
+ const pdObject = typeof pdRaw === "string" ? safeJsonParse(pdRaw) : pdRaw;
515
+ const rowIds = new Set();
516
+ if (pdObject && typeof pdObject === "object") {
517
+ for (const key of Object.keys(pdObject)) {
518
+ if (key)
519
+ rowIds.add(key);
520
+ }
521
+ }
522
+ options.push({ id: optionRow.id, name: optionName, rowIds });
523
+ optionNameById.set(optionRow.id, optionName);
524
+ }
525
+ properties.push({ id: row.id, name, rawType, kind, options });
526
+ }
527
+ return { properties, optionNameById };
528
+ }
529
+ function mapPropertyType(code) {
530
+ if (!code)
531
+ return "unknown";
532
+ switch (code) {
533
+ case "s":
534
+ return "select";
535
+ case "m":
536
+ return "multi_select";
537
+ case "t":
538
+ return "text";
539
+ case "n":
540
+ return "number";
541
+ case "d":
542
+ return "date";
543
+ case "c":
544
+ return "checkbox";
545
+ default:
546
+ return `unknown(${code})`;
547
+ }
548
+ }
549
+ function loadRows(db, tagId, options) {
550
+ const countRow = db
551
+ .prepare(`SELECT COUNT(*) AS total
552
+ FROM (
553
+ SELECT q._id
554
+ FROM quanta q
555
+ JOIN json_each(q.doc, '$.tp') jt
556
+ ON 1 = 1
557
+ WHERE jt.key = @tagId
558
+ GROUP BY q._id
559
+ )`)
560
+ .get({ tagId });
561
+ const rows = db
562
+ .prepare(`SELECT q._id AS id, q.doc AS doc
563
+ FROM quanta q
564
+ JOIN json_each(q.doc, '$.tp') jt
565
+ ON 1 = 1
566
+ WHERE jt.key = @tagId
567
+ GROUP BY q._id
568
+ ORDER BY COALESCE(json_extract(q.doc, '$.u'), json_extract(q.doc, '$.createdAt'), 0) DESC
569
+ LIMIT @limit OFFSET @offset`)
570
+ .all({ tagId, limit: options.limit, offset: options.offset });
571
+ return { rows, total: countRow?.total ?? 0 };
572
+ }
573
+ function fetchMetadata(db, ids) {
574
+ if (ids.length === 0)
575
+ return new Map();
576
+ const placeholders = ids.map(() => "?").join(",");
577
+ const stmt = db.prepare(`SELECT aliasId, id, doc, ancestor_not_ref_text AS ancestorNotRefText, ancestor_ids AS ancestorIds,
578
+ freqCounter, freqTime
579
+ FROM remsSearchInfos
580
+ WHERE id IN (${placeholders})`);
581
+ const rows = stmt.all(...ids);
582
+ const quantaStmt = db.prepare(`SELECT _id, doc
583
+ FROM quanta
584
+ WHERE _id IN (${placeholders})`);
585
+ const quantaRows = quantaStmt.all(...ids);
586
+ const quantaMap = new Map();
587
+ for (const row of quantaRows) {
588
+ const doc = safeJsonParse(row.doc);
589
+ if (doc)
590
+ quantaMap.set(row._id, doc);
591
+ }
592
+ const result = new Map();
593
+ for (const row of rows) {
594
+ const doc = safeJsonParse(row.doc) ?? {};
595
+ const text = coalesceText(doc.kt, doc.ke);
596
+ const ancestor = stringifyAncestor(row.ancestorNotRefText, row.ancestorIds);
597
+ const remDoc = quantaMap.get(row.id);
598
+ const updatedAt = typeof remDoc?.u === "number" ? remDoc.u : null;
599
+ const createdAt = typeof remDoc?.createdAt === "number" ? remDoc.createdAt : null;
600
+ const parentId = typeof remDoc?.parent === "string" ? remDoc.parent : null;
601
+ result.set(row.id, {
602
+ id: row.id,
603
+ aliasId: row.aliasId,
604
+ text,
605
+ ancestorText: ancestor.text || null,
606
+ ancestorIds: ancestor.ids,
607
+ parentId,
608
+ updatedAt,
609
+ createdAt,
610
+ });
611
+ }
612
+ return result;
613
+ }
614
+ function fetchAttributeSortValues(db, ids, attributeId) {
615
+ if (ids.length === 0)
616
+ return new Map();
617
+ const placeholders = ids.map(() => "?").join(",");
618
+ const stmt = db.prepare(`SELECT json_extract(doc, '$.parent') AS parentId, doc
619
+ FROM quanta
620
+ WHERE json_extract(doc, '$.key[0]._id') = ?
621
+ AND json_extract(doc, '$.parent') IN (${placeholders})`);
622
+ const rows = stmt.all(attributeId, ...ids);
623
+ const result = new Map();
624
+ const dateCache = new Map();
625
+ for (const row of rows) {
626
+ if (!row.parentId)
627
+ continue;
628
+ const parsed = safeJsonParse(row.doc);
629
+ if (!parsed)
630
+ continue;
631
+ const value = extractSortValue(parsed, db, dateCache);
632
+ if (value != null)
633
+ result.set(row.parentId, value);
634
+ }
635
+ return result;
636
+ }
637
+ function extractSortValue(doc, db, cache) {
638
+ const rawValue = doc.value;
639
+ const tokens = Array.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : [];
640
+ const numbers = [];
641
+ const strings = [];
642
+ const refs = [];
643
+ for (const token of tokens) {
644
+ if (typeof token === "string") {
645
+ strings.push(token);
646
+ const num = Number(token);
647
+ if (Number.isFinite(num))
648
+ numbers.push(num);
649
+ continue;
650
+ }
651
+ if (token && typeof token === "object") {
652
+ const obj = token;
653
+ const t = obj.text;
654
+ if (typeof t === "string") {
655
+ strings.push(t);
656
+ const num = Number(t);
657
+ if (Number.isFinite(num))
658
+ numbers.push(num);
659
+ continue;
660
+ }
661
+ if (obj.i === "q" && typeof obj._id === "string") {
662
+ refs.push(String(obj._id));
663
+ continue;
664
+ }
665
+ }
666
+ }
667
+ if (numbers.length > 0)
668
+ return Math.min(...numbers);
669
+ const dateValues = [];
670
+ for (const ref of refs) {
671
+ const ts = resolveDateReference(db, ref, cache);
672
+ if (ts != null)
673
+ dateValues.push(ts);
674
+ }
675
+ if (dateValues.length > 0)
676
+ return Math.min(...dateValues);
677
+ if (strings.length > 0)
678
+ return strings[0];
679
+ return null;
680
+ }
681
+ function compareSortValues(a, b) {
682
+ const aU = a == null;
683
+ const bU = b == null;
684
+ if (aU && bU)
685
+ return 0;
686
+ if (aU)
687
+ return 1; // 无值靠后
688
+ if (bU)
689
+ return -1;
690
+ if (typeof a === "number" && typeof b === "number")
691
+ return a - b;
692
+ if (typeof a === "string" && typeof b === "string")
693
+ return a.localeCompare(b);
694
+ // 数值优先于字符串
695
+ if (typeof a === "number")
696
+ return -1;
697
+ if (typeof b === "number")
698
+ return 1;
699
+ return 0;
700
+ }
701
+ function normalizeDateInput(value) {
702
+ if (typeof value === "number")
703
+ return value > 10_000 ? value : value * 1000;
704
+ const trimmed = value.trim();
705
+ const lower = trimmed.toLowerCase();
706
+ if (lower === "today") {
707
+ const d = new Date();
708
+ d.setHours(23, 59, 59, 999);
709
+ return d.getTime();
710
+ }
711
+ if (lower === "yesterday") {
712
+ const d = new Date();
713
+ d.setDate(d.getDate() - 1);
714
+ d.setHours(23, 59, 59, 999);
715
+ return d.getTime();
716
+ }
717
+ if (lower === "tomorrow") {
718
+ const d = new Date();
719
+ d.setDate(d.getDate() + 1);
720
+ d.setHours(23, 59, 59, 999);
721
+ return d.getTime();
722
+ }
723
+ if (/^\d{10}$/.test(trimmed))
724
+ return Number(trimmed) * 1000;
725
+ if (/^\d{13}$/.test(trimmed))
726
+ return Number(trimmed);
727
+ const parsed = Date.parse(trimmed);
728
+ return Number.isFinite(parsed) ? parsed : null;
729
+ }
730
+ // 在缺少选项 pd 映射时,通过属性值 Rem 反查行:
731
+ // 条件:key[0]._id = statusAttrId 且 value 数组中包含 { i:'q', _id: optionId }
732
+ // 同时要求该行具备指定 tagId(通过行的 tp[tagId].t = 1)
733
+ function queryRowsByAttributeOption(db, tagId, statusAttrId, optionId) {
734
+ try {
735
+ const rows = db
736
+ .prepare(`WITH attr AS (
737
+ SELECT json_extract(doc,'$.parent') AS rowId
738
+ FROM quanta, json_each(quanta.doc, '$.value') je
739
+ WHERE json_extract(quanta.doc,'$.key[0]._id') = @statusAttrId
740
+ AND json_type(json_extract(quanta.doc,'$.value')) = 'array'
741
+ AND json_extract(je.value, '$.i') = 'q'
742
+ AND json_extract(je.value, '$._id') = @optionId
743
+ )
744
+ SELECT q._id AS rowId
745
+ FROM quanta q
746
+ JOIN attr a ON q._id = a.rowId
747
+ WHERE json_extract(q.doc, '$.tp."${tagId}".t') = 1`)
748
+ .all({ statusAttrId, optionId });
749
+ return new Set(rows.map((r) => r.rowId).filter(Boolean));
750
+ }
751
+ catch (_) {
752
+ const rows = db
753
+ .prepare(`SELECT json_extract(doc,'$.parent') AS rowId
754
+ FROM quanta
755
+ WHERE json_extract(doc,'$.key[0]._id') = @statusAttrId
756
+ AND doc LIKE @needle`)
757
+ .all({ statusAttrId, needle: `%"_id":"${optionId}"%` });
758
+ const ids = rows.map((r) => r.rowId).filter((x) => !!x);
759
+ if (ids.length === 0)
760
+ return new Set();
761
+ const placeholders = ids.map(() => "?").join(",");
762
+ const filtered = db
763
+ .prepare(`SELECT _id AS rowId
764
+ FROM quanta
765
+ WHERE _id IN (${placeholders})
766
+ AND json_extract(doc, '$.tp."${tagId}".t') = 1`)
767
+ .all(...ids);
768
+ return new Set(filtered.map((r) => r.rowId));
769
+ }
770
+ }
771
+ function resolveDateReference(db, remId, cache) {
772
+ if (cache.has(remId))
773
+ return cache.get(remId) ?? null;
774
+ const row = db.prepare("SELECT doc FROM quanta WHERE _id = ?").get(remId);
775
+ if (!row?.doc) {
776
+ cache.set(remId, null);
777
+ return null;
778
+ }
779
+ const data = safeJsonParse(row.doc);
780
+ let result = null;
781
+ const crt = data?.crt;
782
+ const d = crt?.d;
783
+ const seconds = (() => {
784
+ const s = d?.s;
785
+ if (typeof s === "number")
786
+ return s;
787
+ if (typeof s === "string") {
788
+ const n = Number(s);
789
+ return Number.isFinite(n) ? n : undefined;
790
+ }
791
+ return undefined;
792
+ })();
793
+ if (seconds && Number.isFinite(seconds)) {
794
+ result = seconds * 1000;
795
+ }
796
+ if (!result) {
797
+ const dArray = d?.d?.v;
798
+ if (Array.isArray(dArray) && dArray[0] != null) {
799
+ const iso = String(dArray[0]);
800
+ const parsed = Date.parse(iso);
801
+ if (Number.isFinite(parsed))
802
+ result = parsed;
803
+ }
804
+ }
805
+ if (!result && Array.isArray(data?.key)) {
806
+ const keyCandidate = data.key[0];
807
+ if (typeof keyCandidate === "string") {
808
+ const fromKey = Date.parse(keyCandidate);
809
+ if (Number.isFinite(fromKey))
810
+ result = fromKey;
811
+ }
812
+ }
813
+ cache.set(remId, result);
814
+ return result;
815
+ }