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,178 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Option from 'effect/Option';
5
+ import { AppConfig } from '../../services/AppConfig.js';
6
+ import { RemDb } from '../../services/RemDb.js';
7
+ import { writeFailure, writeSuccess } from '../_shared.js';
8
+ function optionToUndefined(opt) {
9
+ return Option.isSome(opt) ? opt.value : undefined;
10
+ }
11
+ function pad2(n) {
12
+ return String(n).padStart(2, '0');
13
+ }
14
+ function msToLocalStr(ms) {
15
+ const d = new Date(ms);
16
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
17
+ }
18
+ function cleanTitle(input) {
19
+ if (typeof input !== 'string')
20
+ return '';
21
+ return input.split(/\s+/).join(' ').trim();
22
+ }
23
+ function asInt(value) {
24
+ const n = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : 0;
25
+ return Number.isFinite(n) ? n : 0;
26
+ }
27
+ function clampInt(value, min, max) {
28
+ if (!Number.isFinite(value))
29
+ return min;
30
+ return Math.max(min, Math.min(max, value));
31
+ }
32
+ function scalar(db, sql, params) {
33
+ const row = db.prepare(sql).get(...params);
34
+ return asInt(row?.c ?? row?.count ?? row?.['count(*)']);
35
+ }
36
+ const days = Options.integer('days').pipe(Options.optional, Options.map(optionToUndefined));
37
+ const maxParents = Options.integer('max-parents').pipe(Options.optional, Options.map(optionToUndefined));
38
+ const perParent = Options.integer('per-parent').pipe(Options.optional, Options.map(optionToUndefined));
39
+ export const dbRecentCommand = Command.make('recent', {
40
+ days,
41
+ maxParents,
42
+ perParent,
43
+ noParentGroup: Options.boolean('no-parent-group'),
44
+ }, ({ days, maxParents, perParent, noParentGroup }) => Effect.gen(function* () {
45
+ const d = clampInt(days ?? 15, 1, 3650);
46
+ const maxP = clampInt(maxParents ?? 20, 1, 500);
47
+ const perP = clampInt(perParent ?? 10, 1, 500);
48
+ const cutoffMs = Date.now() - d * 86400 * 1000;
49
+ const cfg = yield* AppConfig;
50
+ const remDb = yield* RemDb;
51
+ const result = yield* remDb.withDb(cfg.remnoteDb, (db) => {
52
+ const counts = {
53
+ quanta_created: scalar(db, "select count(*) as c from quanta where json_extract(doc,'$.createdAt') >= ?", [
54
+ cutoffMs,
55
+ ]),
56
+ quanta_touched: scalar(db, "select count(*) as c from quanta where json_extract(doc,'$.m') >= ?", [
57
+ cutoffMs,
58
+ ]),
59
+ rem_created: scalar(db, `
60
+ select count(*) as c
61
+ from quanta q
62
+ join remsSearchInfos r on r.id=q._id
63
+ where json_extract(q.doc,'$.createdAt') >= ?
64
+ `, [cutoffMs]),
65
+ rem_created_and_edited: scalar(db, `
66
+ select count(*) as c
67
+ from quanta q
68
+ join remsSearchInfos r on r.id=q._id
69
+ where json_extract(q.doc,'$.createdAt') >= ?
70
+ and json_extract(q.doc,'$.m') > json_extract(q.doc,'$.createdAt')
71
+ `, [cutoffMs]),
72
+ rem_modified_old: scalar(db, `
73
+ select count(*) as c
74
+ from quanta q
75
+ join remsSearchInfos r on r.id=q._id
76
+ where json_extract(q.doc,'$.m') >= ?
77
+ and json_extract(q.doc,'$.createdAt') < ?
78
+ `, [cutoffMs, cutoffMs]),
79
+ };
80
+ const rows = db
81
+ .prepare(`
82
+ select
83
+ q._id as id,
84
+ cast(json_extract(q.doc,'$.createdAt') as integer) as createdAt,
85
+ cast(json_extract(q.doc,'$.m') as integer) as updatedAt,
86
+ json_extract(r.doc,'$.r') as preview,
87
+ json_extract(q.doc,'$.parent') as parentId,
88
+ json_extract(pr.doc,'$.r') as parentPreview
89
+ from quanta q
90
+ join remsSearchInfos r on r.id=q._id
91
+ left join remsSearchInfos pr on pr.id = json_extract(q.doc,'$.parent')
92
+ where cast(json_extract(q.doc,'$.createdAt') as integer) >= ?
93
+ order by createdAt desc
94
+ `)
95
+ .all(cutoffMs);
96
+ const items = rows.map((row) => {
97
+ const createdAt = asInt(row.createdAt);
98
+ const updatedAt = asInt(row.updatedAt);
99
+ const preview = cleanTitle(row.preview) || '<no preview>';
100
+ const parentId = typeof row.parentId === 'string' ? row.parentId : row.parentId ? String(row.parentId) : '';
101
+ const parentPreview = cleanTitle(row.parentPreview) || '<unknown parent>';
102
+ return {
103
+ id: String(row.id),
104
+ created_at: createdAt,
105
+ updated_at: updatedAt,
106
+ preview,
107
+ parent_id: parentId,
108
+ parent_preview: parentPreview,
109
+ edited_after_create: createdAt > 0 && updatedAt > createdAt,
110
+ };
111
+ });
112
+ return { counts, items };
113
+ });
114
+ const dbPath = result.info.dbPath;
115
+ const cutoffText = msToLocalStr(cutoffMs);
116
+ const header = [
117
+ `# RemNote 近 ${d} 天新增/修改概览`,
118
+ ``,
119
+ `- db: \`${dbPath}\``,
120
+ `- cutoff: \`${cutoffText}\``,
121
+ `- rem(索引): 新增 \`${result.result.counts.rem_created}\`;新建后编辑 \`${result.result.counts.rem_created_and_edited}\`;仅修改旧内容 \`${result.result.counts.rem_modified_old}\``,
122
+ `- quanta(全量): createdAt>=cutoff \`${result.result.counts.quanta_created}\`;m>=cutoff \`${result.result.counts.quanta_touched}\``,
123
+ ``,
124
+ ].join('\n');
125
+ const ids = [];
126
+ const mdLines = [header.trimEnd()];
127
+ if (result.result.items.length > 0) {
128
+ if (noParentGroup) {
129
+ mdLines.push('## 新增 Rem(按时间倒序)', '');
130
+ const take = Math.min(result.result.items.length, maxP * perP);
131
+ for (const row of result.result.items.slice(0, take)) {
132
+ ids.push(row.id);
133
+ const marker = row.edited_after_create ? '(后续有编辑)' : '';
134
+ mdLines.push(`- \`${msToLocalStr(row.created_at)}\` ${row.preview} \`${row.id}\`${marker}`);
135
+ }
136
+ }
137
+ else {
138
+ const groups = new Map();
139
+ for (const row of result.result.items) {
140
+ const key = row.parent_id;
141
+ const existing = groups.get(key);
142
+ if (existing) {
143
+ existing.items.push(row);
144
+ }
145
+ else {
146
+ groups.set(key, { parent_id: key, parent_title: row.parent_preview, items: [row] });
147
+ }
148
+ }
149
+ const ordered = Array.from(groups.values())
150
+ .sort((a, b) => {
151
+ const aMax = Math.max(...a.items.map((r) => r.created_at));
152
+ const bMax = Math.max(...b.items.map((r) => r.created_at));
153
+ return bMax - aMax;
154
+ })
155
+ .slice(0, maxP);
156
+ mdLines.push(`## 新增 Rem(按 parent 分组,最多 ${maxP} 组 × 每组 ${perP} 条)`, '');
157
+ for (const group of ordered) {
158
+ const items = [...group.items].sort((a, b) => b.created_at - a.created_at);
159
+ mdLines.push(`### ${group.parent_title} (${group.items.length})`, '');
160
+ for (const row of items.slice(0, perP)) {
161
+ ids.push(row.id);
162
+ const marker = row.edited_after_create ? '(后续有编辑)' : '';
163
+ mdLines.push(`- \`${msToLocalStr(row.created_at)}\` ${row.preview} \`${row.id}\`${marker}`);
164
+ }
165
+ mdLines.push('');
166
+ }
167
+ }
168
+ }
169
+ const data = {
170
+ db_path: dbPath,
171
+ resolution: result.info.source,
172
+ days: d,
173
+ cutoff_ms: cutoffMs,
174
+ counts: result.result.counts,
175
+ items: result.result.items,
176
+ };
177
+ yield* writeSuccess({ data, ids, md: mdLines.join('\n').trimEnd() + '\n' });
178
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,124 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Effect from 'effect/Effect';
3
+ import { constants as FS_CONSTANTS, promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { AppConfig } from '../services/AppConfig.js';
6
+ import { DaemonFiles } from '../services/DaemonFiles.js';
7
+ import { Queue } from '../services/Queue.js';
8
+ import { RemDb } from '../services/RemDb.js';
9
+ import { WsClient } from '../services/WsClient.js';
10
+ import { writeFailure, writeSuccess } from './_shared.js';
11
+ import { WS_HEALTH_TIMEOUT_MS } from './ws/_shared.js';
12
+ async function canWritePath(filePath) {
13
+ try {
14
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
15
+ await fs.access(path.dirname(filePath), FS_CONSTANTS.W_OK);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ export const doctorCommand = Command.make('doctor', {}, () => Effect.gen(function* () {
23
+ const cfg = yield* AppConfig;
24
+ const queue = yield* Queue;
25
+ const remDb = yield* RemDb;
26
+ const ws = yield* WsClient;
27
+ const daemonFiles = yield* DaemonFiles;
28
+ const queueStats = yield* queue.stats({ dbPath: cfg.queueDb }).pipe(Effect.either);
29
+ const remnote = yield* remDb
30
+ .withDb(cfg.remnoteDb, (db) => {
31
+ db.prepare('SELECT 1 FROM quanta LIMIT 1').get();
32
+ const hasSearchInfos = !!db
33
+ .prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='remsSearchInfos' LIMIT 1`)
34
+ .get();
35
+ const hasContents = !!db
36
+ .prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name='remsContents' LIMIT 1`)
37
+ .get();
38
+ return { has_search_index: hasSearchInfos && hasContents };
39
+ })
40
+ .pipe(Effect.either);
41
+ const wsHealth = yield* ws.health({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS }).pipe(Effect.either);
42
+ const wsClients = yield* ws.queryClients({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS }).pipe(Effect.either);
43
+ const pidFilePath = daemonFiles.defaultPidFile();
44
+ const logFilePath = daemonFiles.defaultLogFile();
45
+ const pidWritable = yield* Effect.tryPromise({
46
+ try: async () => await canWritePath(pidFilePath),
47
+ catch: () => false,
48
+ });
49
+ const logWritable = yield* Effect.tryPromise({
50
+ try: async () => await canWritePath(logFilePath),
51
+ catch: () => false,
52
+ });
53
+ const data = {
54
+ queue: {
55
+ ok: queueStats._tag === 'Right',
56
+ db_path: cfg.queueDb,
57
+ stats: queueStats._tag === 'Right' ? queueStats.right : undefined,
58
+ error: queueStats._tag === 'Left' ? queueStats.left.message : undefined,
59
+ },
60
+ remnote_db: {
61
+ ok: remnote._tag === 'Right',
62
+ db_path: remnote._tag === 'Right' ? remnote.right.info.dbPath : cfg.remnoteDb,
63
+ resolution: remnote._tag === 'Right' ? remnote.right.info.source : undefined,
64
+ has_search_index: remnote._tag === 'Right' ? remnote.right.result.has_search_index : undefined,
65
+ error: remnote._tag === 'Left' ? remnote.left.message : undefined,
66
+ },
67
+ ws: {
68
+ ok: wsHealth._tag === 'Right',
69
+ url: cfg.wsUrl,
70
+ rtt_ms: wsHealth._tag === 'Right' ? wsHealth.right.rtt_ms : undefined,
71
+ error: wsHealth._tag === 'Left' ? wsHealth.left.message : undefined,
72
+ clients: wsClients._tag === 'Right' ? wsClients.right.clients : [],
73
+ },
74
+ daemon_files: {
75
+ pid_file: pidFilePath,
76
+ log_file: logFilePath,
77
+ pid_writable: pidWritable,
78
+ log_writable: logWritable,
79
+ },
80
+ };
81
+ const overallOk = data.queue.ok &&
82
+ data.remnote_db.ok &&
83
+ data.ws.ok &&
84
+ data.daemon_files.pid_writable &&
85
+ data.daemon_files.log_writable;
86
+ const hints = [];
87
+ if (!data.ws.ok)
88
+ hints.push('可尝试:remnote ws ensure / remnote ws status');
89
+ if (data.ws.ok && data.ws.clients.length === 0)
90
+ hints.push('WS 可达但暂无插件连接:请确认插件已启用并连接控制通道');
91
+ if (!data.queue.ok)
92
+ hints.push('队列 DB 不可用:检查 --queue-db 或 REMNOTE_QUEUE_DB 路径权限');
93
+ if (!data.remnote_db.ok)
94
+ hints.push('RemNote DB 不可用:检查 --remnote-db 或运行 remnote db backups 选择备份路径');
95
+ if (!data.remnote_db.has_search_index && data.remnote_db.ok) {
96
+ hints.push('RemNote DB 缺少搜索索引表:read search/query 可能不可用,请在 RemNote 客户端完成索引生成或使用较新备份');
97
+ }
98
+ if (!data.daemon_files.pid_writable || !data.daemon_files.log_writable) {
99
+ hints.push('daemon 文件不可写:检查 HOME 目录权限或自定义 ws start --pid-file/--log-file 路径');
100
+ }
101
+ const md = [
102
+ `# doctor`,
103
+ `- overall_ok: ${overallOk}`,
104
+ `- queue_ok: ${data.queue.ok}`,
105
+ `- queue_db: ${data.queue.db_path}`,
106
+ `- remnote_db_ok: ${data.remnote_db.ok}`,
107
+ `- remnote_db: ${data.remnote_db.db_path ?? ''}`,
108
+ `- remnote_db_resolution: ${data.remnote_db.resolution ?? ''}`,
109
+ `- remnote_search_index_ok: ${data.remnote_db.has_search_index ?? ''}`,
110
+ `- ws_ok: ${data.ws.ok}`,
111
+ `- ws_url: ${data.ws.url}`,
112
+ `- ws_rtt_ms: ${data.ws.rtt_ms ?? ''}`,
113
+ `- ws_clients: ${data.ws.clients.length}`,
114
+ `- pid_file_writable: ${data.daemon_files.pid_writable}`,
115
+ `- pid_file: ${data.daemon_files.pid_file}`,
116
+ `- log_file_writable: ${data.daemon_files.log_writable}`,
117
+ `- log_file: ${data.daemon_files.log_file}`,
118
+ hints.length > 0 ? `\n## Hint` : '',
119
+ ...hints.map((h) => `- ${h}`),
120
+ ]
121
+ .filter(Boolean)
122
+ .join('\n');
123
+ yield* writeSuccess({ data: { overall_ok: overallOk, ...data, hints }, md });
124
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,73 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Layer from 'effect/Layer';
4
+ import * as Option from 'effect/Option';
5
+ import { applyCommand } from './apply.js';
6
+ import { configCommand } from './config/index.js';
7
+ import { doctorCommand } from './doctor.js';
8
+ import { dailyCommand } from './daily/index.js';
9
+ import { dbCommand } from './db/index.js';
10
+ import { opsCommand } from './ops/index.js';
11
+ import { queueCommand } from './queue/index.js';
12
+ import { readCommand } from './read/index.js';
13
+ import { todosCommand } from './todos/index.js';
14
+ import { topicCommand } from './topic/index.js';
15
+ import { writeCommand } from './write/index.js';
16
+ import { wsCommand } from './ws/index.js';
17
+ import { wechatCommand } from './wechat/index.js';
18
+ import { AppConfig } from '../services/AppConfig.js';
19
+ import { ConfigLive, resolveConfig } from '../services/Config.js';
20
+ import { DaemonFilesLive } from '../services/DaemonFiles.js';
21
+ import { OutputLive } from '../services/Output.js';
22
+ import { PayloadLive } from '../services/Payload.js';
23
+ import { ProcessLive } from '../services/Process.js';
24
+ import { QueueLive } from '../services/Queue.js';
25
+ import { RefResolverLive } from '../services/RefResolver.js';
26
+ import { RemDbLive } from '../services/RemDb.js';
27
+ import { WsClientLive } from '../services/WsClient.js';
28
+ function optionToUndefined(opt) {
29
+ return Option.isSome(opt) ? opt.value : undefined;
30
+ }
31
+ const remnoteDb = Options.text('remnote-db').pipe(Options.optional, Options.map(optionToUndefined));
32
+ const queueDb = Options.text('queue-db').pipe(Options.optional, Options.map(optionToUndefined));
33
+ const wsUrl = Options.text('ws-url').pipe(Options.optional, Options.map(optionToUndefined));
34
+ const consumerId = Options.text('consumer-id').pipe(Options.optional, Options.map(optionToUndefined));
35
+ const repo = Options.text('repo').pipe(Options.optional, Options.map(optionToUndefined));
36
+ const servicesLive = Layer.mergeAll(OutputLive, ConfigLive, PayloadLive, DaemonFilesLive, ProcessLive, WsClientLive, QueueLive, RefResolverLive, RemDbLive);
37
+ export const rootCommand = Command.make('remnote', {
38
+ json: Options.boolean('json'),
39
+ md: Options.boolean('md'),
40
+ ids: Options.boolean('ids'),
41
+ quiet: Options.boolean('quiet'),
42
+ debug: Options.boolean('debug'),
43
+ remnoteDb,
44
+ queueDb,
45
+ wsUrl,
46
+ consumerId,
47
+ repo,
48
+ }).pipe(Command.withSubcommands([
49
+ wsCommand,
50
+ queueCommand,
51
+ applyCommand,
52
+ readCommand,
53
+ dailyCommand,
54
+ wechatCommand,
55
+ topicCommand,
56
+ todosCommand,
57
+ dbCommand,
58
+ configCommand,
59
+ doctorCommand,
60
+ opsCommand,
61
+ writeCommand,
62
+ ]), Command.provideEffect(AppConfig, (args) => resolveConfig({
63
+ json: args.json,
64
+ md: args.md,
65
+ ids: args.ids,
66
+ quiet: args.quiet,
67
+ debug: args.debug,
68
+ remnoteDb: args.remnoteDb,
69
+ queueDb: args.queueDb,
70
+ wsUrl: args.wsUrl,
71
+ consumerId: args.consumerId,
72
+ repo: args.repo,
73
+ })), Command.provide(servicesLive));
@@ -0,0 +1,4 @@
1
+ import { Command } from '@effect/cli';
2
+ import { opsListCommand } from './list.js';
3
+ import { opsSchemaCommand } from './schema.js';
4
+ export const opsCommand = Command.make('ops', {}).pipe(Command.withSubcommands([opsListCommand, opsSchemaCommand]));
@@ -0,0 +1,12 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Effect from 'effect/Effect';
3
+ import { TYPES } from '../../adapters/mcp.js';
4
+ import { writeFailure, writeSuccess } from '../_shared.js';
5
+ export const opsListCommand = Command.make('list', {}, () => Effect.gen(function* () {
6
+ const types = Object.keys(TYPES).sort();
7
+ yield* writeSuccess({
8
+ data: { types },
9
+ ids: types,
10
+ md: types.map((t) => `- ${t}`).join('\n'),
11
+ });
12
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,77 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Effect from 'effect/Effect';
4
+ import { TYPES } from '../../adapters/mcp.js';
5
+ import { CliError } from '../../services/Errors.js';
6
+ import { writeFailure, writeSuccess } from '../_shared.js';
7
+ function typeHint(field) {
8
+ const f = field.toLowerCase();
9
+ if (f.includes('markdown'))
10
+ return 'string(markdown)';
11
+ if (f.endsWith('id') || f.endsWith('ids') || f.includes('parentid'))
12
+ return 'string(remId)';
13
+ if (f.includes('tags') || f.endsWith('ids'))
14
+ return 'string[]';
15
+ if (f.startsWith('is') || f.startsWith('include') || f.startsWith('exclude') || f.includes('create'))
16
+ return 'boolean';
17
+ if (f.includes('count') || f.includes('size') || f.includes('position') || f.includes('max') || f.includes('ms'))
18
+ return 'number';
19
+ return 'unknown';
20
+ }
21
+ function exampleValue(field) {
22
+ const f = field.toLowerCase();
23
+ if (f.includes('markdown'))
24
+ return '# Markdown...';
25
+ if (f.endsWith('ids') || f === 'tags')
26
+ return [];
27
+ if (f.endsWith('id') || f.includes('parentid'))
28
+ return '<remId>';
29
+ if (f.startsWith('is') || f.startsWith('include') || f.startsWith('exclude') || f.includes('create'))
30
+ return true;
31
+ if (f.includes('count') || f.includes('size') || f.includes('position') || f.includes('max') || f.includes('ms'))
32
+ return 0;
33
+ if (f.includes('url'))
34
+ return 'https://example.com';
35
+ return '<value>';
36
+ }
37
+ export const opsSchemaCommand = Command.make('schema', { type: Options.text('type') }, ({ type }) => Effect.gen(function* () {
38
+ const spec = TYPES[type];
39
+ if (!spec) {
40
+ return yield* Effect.fail(new CliError({
41
+ code: 'INVALID_ARGS',
42
+ message: `未知 op 类型:${type}`,
43
+ exitCode: 2,
44
+ hint: ['remnote ops list'],
45
+ }));
46
+ }
47
+ const required = Array.isArray(spec.required) ? spec.required : [];
48
+ const optional = Array.isArray(spec.optional) ? spec.optional : [];
49
+ const examplePayload = {};
50
+ for (const f of required)
51
+ examplePayload[f] = exampleValue(f);
52
+ const fields = [
53
+ ...required.map((name) => ({ name, required: true, type: typeHint(name) })),
54
+ ...optional.map((name) => ({ name, required: false, type: typeHint(name) })),
55
+ ];
56
+ const data = {
57
+ type,
58
+ description: spec.description ?? '',
59
+ fields,
60
+ example: { type, payload: examplePayload },
61
+ };
62
+ const md = [
63
+ `# ${type}`,
64
+ spec.description ? `- description: ${spec.description}` : '',
65
+ required.length > 0 ? `\n## required` : '',
66
+ ...required.map((f) => `- ${f} (${typeHint(f)})`),
67
+ optional.length > 0 ? `\n## optional` : '',
68
+ ...optional.map((f) => `- ${f} (${typeHint(f)})`),
69
+ `\n## example`,
70
+ '```json',
71
+ JSON.stringify(data.example, null, 2),
72
+ '```',
73
+ ]
74
+ .filter(Boolean)
75
+ .join('\n');
76
+ yield* writeSuccess({ data, md });
77
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,73 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Option from 'effect/Option';
5
+ import { CliError, isCliError } from '../../services/Errors.js';
6
+ import { Payload } from '../../services/Payload.js';
7
+ import { writeFailure, writeSuccess } from '../_shared.js';
8
+ import { enqueueOps, normalizeOps, parseEnqueuePayload } from '../_enqueue.js';
9
+ function optionToUndefined(opt) {
10
+ return Option.isSome(opt) ? opt.value : undefined;
11
+ }
12
+ function readOptionalText(name) {
13
+ return Options.text(name).pipe(Options.optional, Options.map(optionToUndefined));
14
+ }
15
+ const payloadSpec = Options.text('payload');
16
+ const metaSpec = readOptionalText('meta');
17
+ const clientId = readOptionalText('client-id');
18
+ const idempotencyKey = readOptionalText('idempotency-key');
19
+ const priority = Options.integer('priority').pipe(Options.optional, Options.map(optionToUndefined));
20
+ export const queueEnqueueCommand = Command.make('enqueue', {
21
+ payload: payloadSpec,
22
+ notify: Options.boolean('notify'),
23
+ ensureWs: Options.boolean('ensure-ws'),
24
+ priority,
25
+ clientId,
26
+ idempotencyKey,
27
+ meta: metaSpec,
28
+ }, ({ payload, notify, ensureWs, priority, clientId, idempotencyKey, meta }) => Effect.gen(function* () {
29
+ const payloadSvc = yield* Payload;
30
+ const raw = yield* payloadSvc.readJson(payload);
31
+ const parsed = yield* Effect.try({
32
+ try: () => parseEnqueuePayload(raw),
33
+ catch: (e) => isCliError(e)
34
+ ? e
35
+ : new CliError({
36
+ code: 'INVALID_PAYLOAD',
37
+ message: 'payload 形状不合法:必须是 ops 数组,或 { ops: [...] }',
38
+ exitCode: 2,
39
+ }),
40
+ });
41
+ const rawOps = parsed.ops;
42
+ if (rawOps.length === 0) {
43
+ return yield* Effect.fail(new CliError({ code: 'INVALID_PAYLOAD', message: 'ops 不能为空', exitCode: 2 }));
44
+ }
45
+ if (rawOps.length > 500) {
46
+ return yield* Effect.fail(new CliError({
47
+ code: 'PAYLOAD_TOO_LARGE',
48
+ message: `ops 数量过多(${rawOps.length}),请拆分后重试`,
49
+ exitCode: 2,
50
+ details: { ops: rawOps.length, max_ops: 500 },
51
+ }));
52
+ }
53
+ const ops = yield* normalizeOps(rawOps);
54
+ const metaFromFlag = meta ? yield* payloadSvc.readJson(meta) : undefined;
55
+ const metaValue = metaFromFlag ?? parsed.meta;
56
+ const resolvedPriority = priority ?? parsed.priority;
57
+ const resolvedClientId = clientId ?? parsed.clientId;
58
+ const resolvedIdempotencyKey = idempotencyKey ?? parsed.idempotencyKey;
59
+ const data = yield* enqueueOps({
60
+ ops,
61
+ priority: resolvedPriority,
62
+ clientId: resolvedClientId,
63
+ idempotencyKey: resolvedIdempotencyKey,
64
+ meta: metaValue,
65
+ notify,
66
+ ensureWs,
67
+ });
68
+ yield* writeSuccess({
69
+ data,
70
+ ids: [data.txn_id, ...data.op_ids],
71
+ md: `- txn_id: ${data.txn_id}\n- op_ids: ${data.op_ids.length}\n- notified: ${data.notified}\n- sent: ${data.sent ?? ''}\n`,
72
+ });
73
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,5 @@
1
+ import { Command } from '@effect/cli';
2
+ import { queueEnqueueCommand } from './enqueue.js';
3
+ import { queueInspectCommand } from './inspect.js';
4
+ import { queueStatsCommand } from './stats.js';
5
+ export const queueCommand = Command.make('queue', {}).pipe(Command.withSubcommands([queueStatsCommand, queueInspectCommand, queueEnqueueCommand]));
@@ -0,0 +1,26 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Option from 'effect/Option';
5
+ import { AppConfig } from '../../services/AppConfig.js';
6
+ import { CliError } from '../../services/Errors.js';
7
+ import { Queue } from '../../services/Queue.js';
8
+ import { writeFailure, writeSuccess } from '../_shared.js';
9
+ function optionToUndefined(opt) {
10
+ return Option.isSome(opt) ? opt.value : undefined;
11
+ }
12
+ const txn = Options.text('txn').pipe(Options.optional, Options.map(optionToUndefined));
13
+ const op = Options.text('op').pipe(Options.optional, Options.map(optionToUndefined));
14
+ export const queueInspectCommand = Command.make('inspect', { txn, op }, ({ txn, op }) => Effect.gen(function* () {
15
+ const cfg = yield* AppConfig;
16
+ const queue = yield* Queue;
17
+ if (txn && op) {
18
+ return yield* Effect.fail(new CliError({
19
+ code: 'INVALID_ARGS',
20
+ message: '--txn 与 --op 只能二选一',
21
+ exitCode: 2,
22
+ }));
23
+ }
24
+ const result = yield* queue.inspect({ dbPath: cfg.queueDb, txnId: txn, opId: op });
25
+ yield* writeSuccess({ data: result, md: `- txn_id: ${String(result?.txn?.txn_id ?? '')}\n- ops: ${result?.ops?.length ?? 0}\n` });
26
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,14 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Effect from 'effect/Effect';
3
+ import { AppConfig } from '../../services/AppConfig.js';
4
+ import { Queue } from '../../services/Queue.js';
5
+ import { writeFailure, writeSuccess } from '../_shared.js';
6
+ export const queueStatsCommand = Command.make('stats', {}, () => Effect.gen(function* () {
7
+ const cfg = yield* AppConfig;
8
+ const queue = yield* Queue;
9
+ const result = yield* queue.stats({ dbPath: cfg.queueDb });
10
+ yield* writeSuccess({
11
+ data: result,
12
+ md: `- pending: ${result.pending ?? ''}\n- in_progress: ${result.in_progress ?? ''}\n- dead: ${result.dead ?? ''}\n`,
13
+ });
14
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,35 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Effect from 'effect/Effect';
4
+ import * as Option from 'effect/Option';
5
+ import { executeFindRemsByReference } from '../../adapters/mcp.js';
6
+ import { AppConfig } from '../../services/AppConfig.js';
7
+ import { CliError } from '../../services/Errors.js';
8
+ import { writeFailure, writeSuccess } from '../_shared.js';
9
+ import { cliErrorFromUnknown } from '../_tool.js';
10
+ function optionToUndefined(opt) {
11
+ return Option.isSome(opt) ? opt.value : undefined;
12
+ }
13
+ const reference = Options.text('reference').pipe(Options.repeated);
14
+ const timeRange = Options.text('time').pipe(Options.optional, Options.map(optionToUndefined));
15
+ const maxDepth = Options.integer('max-depth').pipe(Options.optional, Options.map(optionToUndefined));
16
+ const limit = Options.integer('limit').pipe(Options.optional, Options.map(optionToUndefined));
17
+ const offset = Options.integer('offset').pipe(Options.optional, Options.map(optionToUndefined));
18
+ export const readByReferenceCommand = Command.make('by-reference', { reference, timeRange, maxDepth, limit, offset }, ({ reference, timeRange, maxDepth, limit, offset }) => Effect.gen(function* () {
19
+ if (!reference || reference.length === 0) {
20
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--reference 至少提供 1 个 Rem ID', exitCode: 2 }));
21
+ }
22
+ const cfg = yield* AppConfig;
23
+ const result = yield* Effect.tryPromise({
24
+ try: async () => await executeFindRemsByReference({
25
+ targetIds: reference,
26
+ dbPath: cfg.remnoteDb,
27
+ timeRange: timeRange,
28
+ maxDepth: maxDepth,
29
+ limit: limit,
30
+ offset: offset,
31
+ }),
32
+ catch: (e) => cliErrorFromUnknown(e, { code: 'DB_UNAVAILABLE' }),
33
+ });
34
+ yield* writeSuccess({ data: result, md: result.markdown ?? '' });
35
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,15 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Options from '@effect/cli/Options';
3
+ import * as Effect from 'effect/Effect';
4
+ import { executeGetRemConnections } from '../../adapters/mcp.js';
5
+ import { AppConfig } from '../../services/AppConfig.js';
6
+ import { writeFailure, writeSuccess } from '../_shared.js';
7
+ import { cliErrorFromUnknown, unwrapStructuredContent } from '../_tool.js';
8
+ export const readConnectionsCommand = Command.make('connections', { id: Options.text('id') }, ({ id }) => Effect.gen(function* () {
9
+ const cfg = yield* AppConfig;
10
+ const payload = yield* Effect.tryPromise({
11
+ try: async () => unwrapStructuredContent(await executeGetRemConnections({ id, dbPath: cfg.remnoteDb })),
12
+ catch: (e) => cliErrorFromUnknown(e, { code: 'DB_UNAVAILABLE' }),
13
+ });
14
+ yield* writeSuccess({ data: payload, md: payload.markdown ?? '' });
15
+ }).pipe(Effect.catchAll(writeFailure)));