agent-remnote 0.0.1 → 0.0.2

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 +100985 -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
package/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('./dist/main.js');
@@ -0,0 +1 @@
1
+ export { TYPES, discoverBackups, enqueueTxn, executeFindRemsByReference, executeGetRemConnections, executeInspectRemDoc, executeListRemBackups, executeListRemReferences, executeListTodos, executeOutlineRemSubtree, executeReadRemTable, executeResolveRemReference, executeSearchQuery, executeSearchRemOverview, executeSummarizeDailyNotes, executeSummarizeTopicActivity, formatDateWithPattern, getDateFormatting, getTxnIdByOpId, openQueueDb, queueStats, startWebSocketBridge, withResolvedDatabase, } from '../../../../packages/mcp/src/public.js';
@@ -0,0 +1,138 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import { AppConfig } from '../services/AppConfig.js';
3
+ import { CliError, isCliError } from '../services/Errors.js';
4
+ import { Payload } from '../services/Payload.js';
5
+ import { Queue } from '../services/Queue.js';
6
+ import { WsClient } from '../services/WsClient.js';
7
+ import { WS_HEALTH_TIMEOUT_MS, WS_START_WAIT_DEFAULT_MS, ensureWsDaemon } from './ws/_shared.js';
8
+ export function normalizeOp(raw, normalizer) {
9
+ if (!raw || typeof raw !== 'object') {
10
+ throw new CliError({ code: 'INVALID_PAYLOAD', message: 'op 必须是对象', exitCode: 2 });
11
+ }
12
+ const type = typeof raw.type === 'string' ? raw.type.trim() : '';
13
+ if (!type) {
14
+ throw new CliError({ code: 'INVALID_PAYLOAD', message: 'op.type 必填且必须是字符串', exitCode: 2 });
15
+ }
16
+ const payload = normalizer(raw.payload ?? {});
17
+ const idempotencyKey = typeof raw.idempotencyKey === 'string'
18
+ ? raw.idempotencyKey
19
+ : typeof raw.idempotency_key === 'string'
20
+ ? raw.idempotency_key
21
+ : undefined;
22
+ const maxAttempts = typeof raw.maxAttempts === 'number'
23
+ ? raw.maxAttempts
24
+ : typeof raw.max_attempts === 'number'
25
+ ? raw.max_attempts
26
+ : undefined;
27
+ const deliverAfterMs = typeof raw.deliverAfterMs === 'number'
28
+ ? raw.deliverAfterMs
29
+ : typeof raw.deliver_after_ms === 'number'
30
+ ? raw.deliver_after_ms
31
+ : undefined;
32
+ return {
33
+ type,
34
+ payload,
35
+ idempotencyKey,
36
+ maxAttempts,
37
+ deliverAfterMs,
38
+ };
39
+ }
40
+ function readOptionalString(obj, keys) {
41
+ for (const key of keys) {
42
+ const value = obj?.[key];
43
+ if (typeof value === 'string' && value.trim())
44
+ return value;
45
+ }
46
+ return undefined;
47
+ }
48
+ function readOptionalNumber(obj, keys) {
49
+ for (const key of keys) {
50
+ const value = obj?.[key];
51
+ if (typeof value === 'number' && Number.isFinite(value))
52
+ return value;
53
+ }
54
+ return undefined;
55
+ }
56
+ export function enqueueOps(params) {
57
+ return Effect.gen(function* () {
58
+ const cfg = yield* AppConfig;
59
+ const ws = yield* WsClient;
60
+ const queue = yield* Queue;
61
+ const payloadSvc = yield* Payload;
62
+ const enqueue = yield* queue.enqueue({
63
+ dbPath: cfg.queueDb,
64
+ ops: params.ops,
65
+ options: {
66
+ priority: params.priority,
67
+ clientId: params.clientId?.trim() || undefined,
68
+ idempotencyKey: params.idempotencyKey?.trim() || undefined,
69
+ meta: params.meta ? payloadSvc.normalizeKeys(params.meta) : undefined,
70
+ },
71
+ });
72
+ let notified = false;
73
+ let sent;
74
+ const warnings = [];
75
+ if (params.notify) {
76
+ if (params.ensureWs) {
77
+ const ensured = yield* ensureWsDaemon({ waitMs: WS_START_WAIT_DEFAULT_MS }).pipe(Effect.either);
78
+ if (ensured._tag === 'Left') {
79
+ warnings.push(`WS ensure 失败,跳过通知:${ensured.left.message}`);
80
+ }
81
+ }
82
+ const triggered = yield* ws
83
+ .triggerStartSync({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS, consumerId: cfg.consumerId })
84
+ .pipe(Effect.either);
85
+ if (triggered._tag === 'Right') {
86
+ notified = true;
87
+ sent = triggered.right.sent;
88
+ if (sent === 0) {
89
+ warnings.push('已入队,但未发现活跃 consumer/控制通道(sent=0)');
90
+ warnings.push('可尝试:remnote ws status / remnote ws trigger / 确认插件已连接控制通道');
91
+ }
92
+ }
93
+ else {
94
+ warnings.push(`已入队,但触发同步失败:${triggered.left.message}`);
95
+ warnings.push('可尝试:remnote ws ensure / remnote ws status');
96
+ }
97
+ }
98
+ return { ...enqueue, notified, sent, warnings: warnings.length > 0 ? warnings : undefined };
99
+ });
100
+ }
101
+ export function parseEnqueuePayload(raw) {
102
+ if (Array.isArray(raw))
103
+ return { ops: raw };
104
+ if (raw && typeof raw === 'object' && Array.isArray(raw.ops)) {
105
+ const obj = raw;
106
+ return {
107
+ ops: obj.ops,
108
+ priority: readOptionalNumber(obj, ['priority']),
109
+ clientId: readOptionalString(obj, ['clientId', 'client_id']),
110
+ idempotencyKey: readOptionalString(obj, ['idempotencyKey', 'idempotency_key']),
111
+ meta: obj.meta,
112
+ };
113
+ }
114
+ throw new CliError({
115
+ code: 'INVALID_PAYLOAD',
116
+ message: 'payload 形状不合法:必须是 ops 数组,或 { ops: [...] }',
117
+ exitCode: 2,
118
+ });
119
+ }
120
+ export function parseOpsPayload(raw) {
121
+ return parseEnqueuePayload(raw).ops;
122
+ }
123
+ export function normalizeOps(rawOps) {
124
+ return Effect.gen(function* () {
125
+ const payloadSvc = yield* Payload;
126
+ return yield* Effect.try({
127
+ try: () => rawOps.map((o) => normalizeOp(o, payloadSvc.normalizeKeys)),
128
+ catch: (e) => isCliError(e)
129
+ ? e
130
+ : new CliError({
131
+ code: 'INVALID_PAYLOAD',
132
+ message: 'payload 解析失败',
133
+ exitCode: 2,
134
+ details: { error: String(e?.message || e) },
135
+ }),
136
+ });
137
+ });
138
+ }
@@ -0,0 +1,57 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import { AppConfig } from '../services/AppConfig.js';
3
+ import { CliError, ok } from '../services/Errors.js';
4
+ import { Output } from '../services/Output.js';
5
+ function ensureTrailingNewline(text) {
6
+ return text.endsWith('\n') ? text : `${text}\n`;
7
+ }
8
+ export function writeSuccess(params) {
9
+ return Effect.gen(function* () {
10
+ const config = yield* AppConfig;
11
+ const out = yield* Output;
12
+ if (config.format === 'json') {
13
+ yield* out.json(ok(params.data));
14
+ return;
15
+ }
16
+ if (config.quiet)
17
+ return;
18
+ if (config.format === 'ids') {
19
+ if (!params.ids || params.ids.length === 0) {
20
+ return yield* Effect.fail(new CliError({
21
+ code: 'INVALID_ARGS',
22
+ message: '该命令不支持 --ids 输出',
23
+ exitCode: 2,
24
+ }));
25
+ }
26
+ yield* out.stdout(`${params.ids.join('\n')}\n`);
27
+ return;
28
+ }
29
+ const md = params.md ?? '';
30
+ if (md.trim().length > 0) {
31
+ yield* out.stdout(ensureTrailingNewline(md));
32
+ }
33
+ });
34
+ }
35
+ export function writeFailure(error) {
36
+ return Effect.gen(function* () {
37
+ const config = yield* AppConfig;
38
+ const out = yield* Output;
39
+ if (config.format === 'json') {
40
+ // `--json` 的失败 envelope 统一在 main.ts 处理,避免重复输出。
41
+ return yield* Effect.fail(error);
42
+ }
43
+ ;
44
+ globalThis.__REMNOTE_CLI_ERROR_REPORTED__ = true;
45
+ yield* out.stderr(ensureTrailingNewline(error.message));
46
+ if (config.debug && error.details !== undefined) {
47
+ yield* out.stderr(`${JSON.stringify(error.details, null, 2)}\n`);
48
+ }
49
+ if (error.hint && error.hint.length > 0) {
50
+ yield* out.stderr(`Hint:\n`);
51
+ for (const h of error.hint) {
52
+ yield* out.stderr(`- ${h}\n`);
53
+ }
54
+ }
55
+ return yield* Effect.fail(error);
56
+ });
57
+ }
@@ -0,0 +1,28 @@
1
+ import { CliError, isCliError } from '../services/Errors.js';
2
+ export function unwrapStructuredContent(value) {
3
+ if (!value || typeof value !== 'object')
4
+ return value;
5
+ if ('structuredContent' in value) {
6
+ return value.structuredContent;
7
+ }
8
+ return value;
9
+ }
10
+ export function cliErrorFromUnknown(error, params) {
11
+ if (isCliError(error))
12
+ return error;
13
+ const message = String(error?.message || error || '未知错误');
14
+ if (message.startsWith('参数错误')) {
15
+ return new CliError({
16
+ code: 'INVALID_ARGS',
17
+ message,
18
+ exitCode: 2,
19
+ details: params?.details ?? { error: message },
20
+ });
21
+ }
22
+ return new CliError({
23
+ code: params?.code ?? 'INTERNAL',
24
+ message,
25
+ exitCode: params?.exitCode ?? 1,
26
+ details: params?.details ?? { error: message },
27
+ });
28
+ }
@@ -0,0 +1,81 @@
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 applyCommand = Command.make('apply', {
21
+ payload: payloadSpec,
22
+ notify: Options.boolean('notify'),
23
+ ensureWs: Options.boolean('ensure-ws'),
24
+ dryRun: Options.boolean('dry-run'),
25
+ priority,
26
+ clientId,
27
+ idempotencyKey,
28
+ meta: metaSpec,
29
+ }, ({ payload, notify, ensureWs, dryRun, priority, clientId, idempotencyKey, meta }) => Effect.gen(function* () {
30
+ const payloadSvc = yield* Payload;
31
+ const raw = yield* payloadSvc.readJson(payload);
32
+ const parsed = yield* Effect.try({
33
+ try: () => parseEnqueuePayload(raw),
34
+ catch: (e) => isCliError(e)
35
+ ? e
36
+ : new CliError({
37
+ code: 'INVALID_PAYLOAD',
38
+ message: 'payload 形状不合法:必须是 ops 数组,或 { ops: [...] }',
39
+ exitCode: 2,
40
+ }),
41
+ });
42
+ const rawOps = parsed.ops;
43
+ if (rawOps.length === 0) {
44
+ return yield* Effect.fail(new CliError({ code: 'INVALID_PAYLOAD', message: 'ops 不能为空', exitCode: 2 }));
45
+ }
46
+ if (rawOps.length > 500) {
47
+ return yield* Effect.fail(new CliError({
48
+ code: 'PAYLOAD_TOO_LARGE',
49
+ message: `ops 数量过多(${rawOps.length}),请拆分后重试`,
50
+ exitCode: 2,
51
+ details: { ops: rawOps.length, max_ops: 500 },
52
+ }));
53
+ }
54
+ const ops = yield* normalizeOps(rawOps);
55
+ const metaFromFlag = meta ? yield* payloadSvc.readJson(meta) : undefined;
56
+ const metaValue = metaFromFlag ?? parsed.meta;
57
+ const resolvedPriority = priority ?? parsed.priority;
58
+ const resolvedClientId = clientId ?? parsed.clientId;
59
+ const resolvedIdempotencyKey = idempotencyKey ?? parsed.idempotencyKey;
60
+ if (dryRun) {
61
+ yield* writeSuccess({
62
+ data: { dry_run: true, ops, meta: metaValue ? payloadSvc.normalizeKeys(metaValue) : undefined },
63
+ md: `- dry_run: true\n- ops: ${ops.length}\n`,
64
+ });
65
+ return;
66
+ }
67
+ const data = yield* enqueueOps({
68
+ ops,
69
+ priority: resolvedPriority,
70
+ clientId: resolvedClientId,
71
+ idempotencyKey: resolvedIdempotencyKey,
72
+ meta: metaValue,
73
+ notify,
74
+ ensureWs,
75
+ });
76
+ yield* writeSuccess({
77
+ data,
78
+ ids: [data.txn_id, ...data.op_ids],
79
+ md: `- txn_id: ${data.txn_id}\n- op_ids: ${data.op_ids.length}\n- notified: ${data.notified}\n- sent: ${data.sent ?? ''}\n`,
80
+ });
81
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,3 @@
1
+ import { Command } from '@effect/cli';
2
+ import { configPrintCommand } from './print.js';
3
+ export const configCommand = Command.make('config', {}).pipe(Command.withSubcommands([configPrintCommand]));
@@ -0,0 +1,28 @@
1
+ import { Command } from '@effect/cli';
2
+ import * as Effect from 'effect/Effect';
3
+ import { AppConfig } from '../../services/AppConfig.js';
4
+ import { writeFailure, writeSuccess } from '../_shared.js';
5
+ export const configPrintCommand = Command.make('print', {}, () => Effect.gen(function* () {
6
+ const cfg = yield* AppConfig;
7
+ const data = {
8
+ format: cfg.format,
9
+ quiet: cfg.quiet,
10
+ debug: cfg.debug,
11
+ remnote_db: cfg.remnoteDb,
12
+ queue_db: cfg.queueDb,
13
+ ws_url: cfg.wsUrl,
14
+ consumer_id: cfg.consumerId,
15
+ repo: cfg.repo,
16
+ };
17
+ const md = [
18
+ `- format: ${data.format}`,
19
+ `- quiet: ${data.quiet}`,
20
+ `- debug: ${data.debug}`,
21
+ `- remnote_db: ${data.remnote_db ?? ''}`,
22
+ `- queue_db: ${data.queue_db}`,
23
+ `- ws_url: ${data.ws_url}`,
24
+ `- consumer_id: ${data.consumer_id}`,
25
+ `- repo: ${data.repo ?? ''}`,
26
+ ].join('\n');
27
+ yield* writeSuccess({ data, md });
28
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,4 @@
1
+ import { Command } from '@effect/cli';
2
+ import { dailySummaryCommand } from './summary.js';
3
+ import { dailyWriteCommand } from './write.js';
4
+ export const dailyCommand = Command.make('daily', {}).pipe(Command.withSubcommands([dailySummaryCommand, dailyWriteCommand]));
@@ -0,0 +1,25 @@
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 { executeSummarizeDailyNotes } from '../../adapters/mcp.js';
6
+ import { AppConfig } from '../../services/AppConfig.js';
7
+ import { writeFailure, writeSuccess } from '../_shared.js';
8
+ import { cliErrorFromUnknown } from '../_tool.js';
9
+ function optionToUndefined(opt) {
10
+ return Option.isSome(opt) ? opt.value : undefined;
11
+ }
12
+ const days = Options.integer('days').pipe(Options.optional, Options.map(optionToUndefined));
13
+ const maxLines = Options.integer('max-lines').pipe(Options.optional, Options.map(optionToUndefined));
14
+ export const dailySummaryCommand = Command.make('summary', { days, maxLines }, ({ days, maxLines }) => Effect.gen(function* () {
15
+ const cfg = yield* AppConfig;
16
+ const result = yield* Effect.tryPromise({
17
+ try: async () => await executeSummarizeDailyNotes({
18
+ dbPath: cfg.remnoteDb,
19
+ days: days,
20
+ maxLines: maxLines,
21
+ }),
22
+ catch: (e) => cliErrorFromUnknown(e, { code: 'DB_UNAVAILABLE' }),
23
+ });
24
+ yield* writeSuccess({ data: result, md: result.markdown ?? '' });
25
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,145 @@
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 { promises as fs } from 'node:fs';
6
+ import { formatDateWithPattern, getDateFormatting } from '../../adapters/mcp.js';
7
+ import { AppConfig } from '../../services/AppConfig.js';
8
+ import { CliError, isCliError } from '../../services/Errors.js';
9
+ import { Payload } from '../../services/Payload.js';
10
+ import { RemDb } from '../../services/RemDb.js';
11
+ import { writeFailure, writeSuccess } from '../_shared.js';
12
+ import { enqueueOps, normalizeOp } from '../_enqueue.js';
13
+ function optionToUndefined(opt) {
14
+ return Option.isSome(opt) ? opt.value : undefined;
15
+ }
16
+ function readOptionalText(name) {
17
+ return Options.text(name).pipe(Options.optional, Options.map(optionToUndefined));
18
+ }
19
+ const text = readOptionalText('text');
20
+ const mdFile = readOptionalText('md-file');
21
+ const date = readOptionalText('date');
22
+ const offsetDays = Options.integer('offset-days').pipe(Options.optional, Options.map(optionToUndefined));
23
+ const createIfMissing = Options.boolean('create-if-missing');
24
+ const noCreateIfMissing = Options.boolean('no-create-if-missing');
25
+ const clientId = readOptionalText('client-id');
26
+ const idempotencyKey = readOptionalText('idempotency-key');
27
+ const metaSpec = readOptionalText('meta');
28
+ const priority = Options.integer('priority').pipe(Options.optional, Options.map(optionToUndefined));
29
+ function parseDateInput(raw) {
30
+ const d = new Date(raw);
31
+ if (isNaN(d.getTime())) {
32
+ throw new CliError({ code: 'INVALID_ARGS', message: `日期不合法:${raw}`, exitCode: 2 });
33
+ }
34
+ return d;
35
+ }
36
+ function todayAtMidnight() {
37
+ const now = new Date();
38
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
39
+ }
40
+ export const dailyWriteCommand = Command.make('write', {
41
+ text,
42
+ mdFile,
43
+ date,
44
+ offsetDays,
45
+ prepend: Options.boolean('prepend'),
46
+ createIfMissing,
47
+ noCreateIfMissing,
48
+ notify: Options.boolean('notify'),
49
+ ensureWs: Options.boolean('ensure-ws'),
50
+ dryRun: Options.boolean('dry-run'),
51
+ priority,
52
+ clientId,
53
+ idempotencyKey,
54
+ meta: metaSpec,
55
+ }, ({ text, mdFile, date, offsetDays, prepend, createIfMissing, noCreateIfMissing, notify, ensureWs, dryRun, priority, clientId, idempotencyKey, meta, }) => Effect.gen(function* () {
56
+ if (text && mdFile) {
57
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--text 与 --md-file 只能二选一', exitCode: 2 }));
58
+ }
59
+ if (!text && !mdFile) {
60
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '必须提供 --text 或 --md-file', exitCode: 2 }));
61
+ }
62
+ if (date && offsetDays !== undefined) {
63
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--date 与 --offset-days 只能二选一', exitCode: 2 }));
64
+ }
65
+ if (createIfMissing && noCreateIfMissing) {
66
+ return yield* Effect.fail(new CliError({
67
+ code: 'INVALID_ARGS',
68
+ message: '--create-if-missing 与 --no-create-if-missing 只能二选一',
69
+ exitCode: 2,
70
+ }));
71
+ }
72
+ const cfg = yield* AppConfig;
73
+ const payloadSvc = yield* Payload;
74
+ const remDb = yield* RemDb;
75
+ const markdown = mdFile
76
+ ? yield* Effect.tryPromise({
77
+ try: async () => await fs.readFile(mdFile, 'utf8'),
78
+ catch: (e) => {
79
+ if (e?.code === 'ENOENT') {
80
+ return new CliError({ code: 'INVALID_ARGS', message: `文件不存在:${mdFile}`, exitCode: 2 });
81
+ }
82
+ return new CliError({
83
+ code: 'INTERNAL',
84
+ message: '读取文件失败',
85
+ exitCode: 1,
86
+ details: { file: mdFile, error: String(e?.message || e) },
87
+ });
88
+ },
89
+ })
90
+ : undefined;
91
+ const target = date
92
+ ? yield* Effect.try({
93
+ try: () => parseDateInput(date),
94
+ catch: (e) => (isCliError(e) ? e : new CliError({ code: 'INVALID_ARGS', message: '日期不合法', exitCode: 2 })),
95
+ })
96
+ : new Date(todayAtMidnight().getTime() + (offsetDays ?? 0) * 24 * 3600 * 1000);
97
+ const dateString = yield* remDb
98
+ .withDb(cfg.remnoteDb, async (db) => {
99
+ const fmt = (await getDateFormatting(db)) ?? 'yyyy/MM/dd';
100
+ return formatDateWithPattern(target, fmt);
101
+ })
102
+ .pipe(Effect.map((r) => r.result), Effect.catchAll(() => Effect.succeed(undefined)));
103
+ const createIfMissingBool = noCreateIfMissing ? false : createIfMissing ? true : undefined;
104
+ const payload = {
105
+ ...(markdown && markdown.trim() ? { markdown } : {}),
106
+ ...(text ? { text } : {}),
107
+ ...(date ? { date: target.toISOString() } : { offsetDays: offsetDays ?? 0 }),
108
+ ...(dateString ? { dateString } : {}),
109
+ ...(prepend ? { prepend: true } : {}),
110
+ ...(createIfMissingBool !== undefined ? { createIfMissing: createIfMissingBool } : {}),
111
+ };
112
+ const op = yield* Effect.try({
113
+ try: () => normalizeOp({ type: 'daily_note_write', payload }, payloadSvc.normalizeKeys),
114
+ catch: (e) => isCliError(e)
115
+ ? e
116
+ : new CliError({
117
+ code: 'INVALID_PAYLOAD',
118
+ message: '生成 op 失败',
119
+ exitCode: 2,
120
+ details: { error: String(e?.message || e) },
121
+ }),
122
+ });
123
+ const metaValue = meta ? yield* payloadSvc.readJson(meta) : undefined;
124
+ if (dryRun) {
125
+ yield* writeSuccess({
126
+ data: { dry_run: true, ops: [op], meta: metaValue ? payloadSvc.normalizeKeys(metaValue) : undefined },
127
+ md: `- dry_run: true\n- op: daily_note_write\n- date_string: ${dateString ?? ''}\n`,
128
+ });
129
+ return;
130
+ }
131
+ const data = yield* enqueueOps({
132
+ ops: [op],
133
+ priority,
134
+ clientId,
135
+ idempotencyKey,
136
+ meta: metaValue,
137
+ notify,
138
+ ensureWs,
139
+ });
140
+ yield* writeSuccess({
141
+ data,
142
+ ids: [data.txn_id, ...data.op_ids],
143
+ md: `- txn_id: ${data.txn_id}\n- op_ids: ${data.op_ids.length}\n- notified: ${data.notified}\n- sent: ${data.sent ?? ''}\n`,
144
+ });
145
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,23 @@
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 { executeListRemBackups } from '../../adapters/mcp.js';
6
+ import { writeFailure, writeSuccess } from '../_shared.js';
7
+ import { cliErrorFromUnknown } from '../_tool.js';
8
+ function optionToUndefined(opt) {
9
+ return Option.isSome(opt) ? opt.value : undefined;
10
+ }
11
+ const basePath = Options.text('base-path').pipe(Options.optional, Options.map(optionToUndefined));
12
+ const limit = Options.integer('limit').pipe(Options.optional, Options.map(optionToUndefined));
13
+ export const dbBackupsCommand = Command.make('backups', { basePath, limit }, ({ basePath, limit }) => Effect.tryPromise({
14
+ try: async () => await executeListRemBackups({ basePath, limit: limit }),
15
+ catch: (e) => cliErrorFromUnknown(e, { code: 'DB_UNAVAILABLE' }),
16
+ }).pipe(Effect.flatMap((result) => {
17
+ const md = [
18
+ `- base_path: ${result.basePath}`,
19
+ `- total: ${result.total}`,
20
+ ...(Array.isArray(result.items) ? result.items.map((it) => `- ${it.path}`) : []),
21
+ ].join('\n');
22
+ return writeSuccess({ data: result, md });
23
+ }), Effect.catchAll(writeFailure)));
@@ -0,0 +1,4 @@
1
+ import { Command } from '@effect/cli';
2
+ import { dbBackupsCommand } from './backups.js';
3
+ import { dbRecentCommand } from './recent.js';
4
+ export const dbCommand = Command.make('db', {}).pipe(Command.withSubcommands([dbBackupsCommand, dbRecentCommand]));