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
@@ -0,0 +1,430 @@
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 { spawn } from 'node:child_process';
6
+ import { AppConfig } from '../../services/AppConfig.js';
7
+ import { CliError, isCliError } from '../../services/Errors.js';
8
+ import { Payload } from '../../services/Payload.js';
9
+ import { RefResolver } from '../../services/RefResolver.js';
10
+ import { writeFailure, writeSuccess } from '../_shared.js';
11
+ import { enqueueOps, normalizeOp } from '../_enqueue.js';
12
+ function optionToUndefined(opt) {
13
+ return Option.isSome(opt) ? opt.value : undefined;
14
+ }
15
+ function clampInt(value, min, max) {
16
+ if (!Number.isFinite(value))
17
+ return min;
18
+ return Math.max(min, Math.min(max, value));
19
+ }
20
+ function normalizeUnicodeSpaces(text) {
21
+ return text
22
+ .replace(/\u00A0/g, ' ')
23
+ .replace(/\u2007/g, ' ')
24
+ .replace(/\u202F/g, ' ')
25
+ .replace(/\u3000/g, ' ')
26
+ .replace(/\u200B/g, '')
27
+ .replace(/\u200C/g, '')
28
+ .replace(/\u200D/g, '')
29
+ .replace(/\u2060/g, '');
30
+ }
31
+ function normalizeLine(line) {
32
+ const s = normalizeUnicodeSpaces(line);
33
+ return s.replace(/[ \t]+/g, ' ').trim();
34
+ }
35
+ const NOISE_LINES = new Set([
36
+ '长按识别二维码查看原文',
37
+ '长按识别二维码阅读全文',
38
+ '长按识别二维码查看',
39
+ '点击阅读原文',
40
+ '阅读原文',
41
+ ]);
42
+ const SECTION_PREFIXES = new Set(['🔥', '📖', '🛠', '📢', '🎨', '🤖', '🧰', '📰', '📚', '🎬']);
43
+ const NOTE_PREFIXES = new Set(['💡', '📌', '📝']);
44
+ function isUrl(s) {
45
+ return /^https?:\/\//.test(s);
46
+ }
47
+ function isShortHeading(s) {
48
+ if (!s)
49
+ return false;
50
+ if (s.endsWith(':') || s.endsWith(':')) {
51
+ const core = s.slice(0, -1).trim();
52
+ return core.length >= 1 && core.length <= 8;
53
+ }
54
+ return false;
55
+ }
56
+ function isSectionHeading(s) {
57
+ if (!s)
58
+ return false;
59
+ if (isUrl(s))
60
+ return false;
61
+ const first = s.slice(0, 2);
62
+ for (const p of SECTION_PREFIXES) {
63
+ if (first.startsWith(p))
64
+ return s.length <= 24;
65
+ }
66
+ const keywords = ['本周热点', '文章和视频', '代码和工具', '生态系统'];
67
+ return keywords.some((k) => s.includes(k)) && s.length <= 24;
68
+ }
69
+ function isMetaLine(s) {
70
+ return s.startsWith('本期看点:') || s.startsWith('编辑:');
71
+ }
72
+ function isAuthorLike(s) {
73
+ if (!s || isUrl(s))
74
+ return false;
75
+ const first = s.slice(0, 2);
76
+ for (const p of SECTION_PREFIXES) {
77
+ if (first.startsWith(p))
78
+ return false;
79
+ }
80
+ for (const p of NOTE_PREFIXES) {
81
+ if (first.startsWith(p))
82
+ return false;
83
+ }
84
+ if (isShortHeading(s) || isSectionHeading(s) || isMetaLine(s))
85
+ return false;
86
+ if (/\d/.test(s))
87
+ return false;
88
+ if (s.length > 40)
89
+ return false;
90
+ if (/[。!?———::]/.test(s))
91
+ return false;
92
+ return /[A-Za-z\u4E00-\u9FFF]/.test(s);
93
+ }
94
+ function toBlocks(lines) {
95
+ const blocks = [];
96
+ let buf = [];
97
+ const flush = () => {
98
+ if (buf.length > 0) {
99
+ blocks.push(buf.join(' ').trim());
100
+ buf = [];
101
+ }
102
+ };
103
+ for (const raw of lines) {
104
+ const s = normalizeLine(raw);
105
+ if (!s) {
106
+ flush();
107
+ continue;
108
+ }
109
+ if (NOISE_LINES.has(s))
110
+ continue;
111
+ buf.push(s);
112
+ }
113
+ flush();
114
+ return blocks;
115
+ }
116
+ function addChild(parent, text) {
117
+ const n = { text, children: [] };
118
+ parent.children.push(n);
119
+ return n;
120
+ }
121
+ function clampAdd(parent, text, parentDepth, maxDepth) {
122
+ if (parentDepth + 1 > maxDepth) {
123
+ parent.text = `${parent.text};${text}`;
124
+ return parent;
125
+ }
126
+ return addChild(parent, text);
127
+ }
128
+ function renderMarkdown(root, indent = ' ') {
129
+ const out = [];
130
+ const emit = (node, depth) => {
131
+ out.push(`${indent.repeat(depth)}- ${node.text}`);
132
+ for (const c of node.children)
133
+ emit(c, depth + 1);
134
+ };
135
+ emit(root, 0);
136
+ return out.join('\n').trimEnd() + '\n';
137
+ }
138
+ function outlineify(params) {
139
+ const blocks = toBlocks(params.content.split(/\r?\n/));
140
+ const maxDepth = Math.max(2, params.maxDepth);
141
+ const root = { text: params.title, children: [] };
142
+ if (params.url) {
143
+ clampAdd(root, `原文:${params.url}`, 1, maxDepth);
144
+ }
145
+ let currentSection = null;
146
+ let currentSectionDepth = 1;
147
+ let currentSub = null;
148
+ let currentSubDepth = 2;
149
+ let currentItem = null;
150
+ let currentItemDepth = 3;
151
+ let lastWasUrl = false;
152
+ const container = () => {
153
+ if (currentSub)
154
+ return { parent: currentSub, depth: currentSubDepth };
155
+ if (currentSection)
156
+ return { parent: currentSection, depth: currentSectionDepth };
157
+ return { parent: root, depth: 1 };
158
+ };
159
+ const attachToItemOrContainer = (text) => {
160
+ let parent = container().parent;
161
+ let depth = container().depth;
162
+ if (currentItem) {
163
+ parent = currentItem;
164
+ depth = currentItemDepth;
165
+ }
166
+ clampAdd(parent, text, depth, maxDepth);
167
+ };
168
+ for (const b of blocks) {
169
+ if (isMetaLine(b)) {
170
+ clampAdd(root, b, 1, maxDepth);
171
+ currentItem = null;
172
+ lastWasUrl = false;
173
+ continue;
174
+ }
175
+ if (isSectionHeading(b)) {
176
+ currentSection = clampAdd(root, b, 1, maxDepth);
177
+ currentSectionDepth = 2;
178
+ currentSub = null;
179
+ currentItem = null;
180
+ lastWasUrl = false;
181
+ continue;
182
+ }
183
+ if (isShortHeading(b)) {
184
+ const name = b.slice(0, -1).trim();
185
+ const parent = currentSection ?? root;
186
+ const parentDepth = currentSection ? currentSectionDepth : 1;
187
+ currentSub = clampAdd(parent, name, parentDepth, maxDepth);
188
+ currentSubDepth = Math.min(parentDepth + 1, maxDepth);
189
+ currentItem = null;
190
+ lastWasUrl = false;
191
+ continue;
192
+ }
193
+ if (isUrl(b)) {
194
+ attachToItemOrContainer(b);
195
+ lastWasUrl = true;
196
+ continue;
197
+ }
198
+ if (lastWasUrl && currentItem && isAuthorLike(b)) {
199
+ attachToItemOrContainer(`署名:${b}`);
200
+ lastWasUrl = false;
201
+ continue;
202
+ }
203
+ const first = b.slice(0, 2);
204
+ if (currentItem && Array.from(NOTE_PREFIXES).some((p) => first.startsWith(p))) {
205
+ clampAdd(currentItem, b, currentItemDepth, maxDepth);
206
+ lastWasUrl = false;
207
+ continue;
208
+ }
209
+ const { parent, depth } = container();
210
+ currentItem = clampAdd(parent, b, depth, maxDepth);
211
+ currentItemDepth = Math.min(depth + 1, maxDepth);
212
+ lastWasUrl = false;
213
+ }
214
+ return renderMarkdown(root);
215
+ }
216
+ function runProcess(command, args, timeoutMs) {
217
+ return Effect.async((resume) => {
218
+ const child = spawn(command, [...args], { stdio: 'pipe' });
219
+ let stdout = '';
220
+ let stderr = '';
221
+ let timedOut = false;
222
+ child.stdout.setEncoding('utf8');
223
+ child.stderr.setEncoding('utf8');
224
+ child.stdout.on('data', (d) => (stdout += d));
225
+ child.stderr.on('data', (d) => (stderr += d));
226
+ const timer = setTimeout(() => {
227
+ timedOut = true;
228
+ try {
229
+ child.kill('SIGKILL');
230
+ }
231
+ catch (_) { }
232
+ }, Math.max(0, timeoutMs));
233
+ child.on('error', (error) => {
234
+ clearTimeout(timer);
235
+ if (error?.code === 'ENOENT') {
236
+ resume(Effect.fail(new CliError({
237
+ code: 'DEPENDENCY_MISSING',
238
+ message: '找不到 agent-browser(需要先安装并确保在 PATH 内)',
239
+ exitCode: 1,
240
+ })));
241
+ return;
242
+ }
243
+ resume(Effect.fail(new CliError({
244
+ code: 'INTERNAL',
245
+ message: '启动子进程失败',
246
+ exitCode: 1,
247
+ details: { command, error: String(error?.message || error) },
248
+ })));
249
+ });
250
+ child.on('close', (code) => {
251
+ clearTimeout(timer);
252
+ if (timedOut) {
253
+ resume(Effect.fail(new CliError({
254
+ code: 'TIMEOUT',
255
+ message: `agent-browser 超时(${timeoutMs}ms)`,
256
+ exitCode: 1,
257
+ details: { command, args, stderr: stderr.trim() },
258
+ })));
259
+ return;
260
+ }
261
+ resume(Effect.succeed({ stdout, stderr, exitCode: typeof code === 'number' ? code : 1 }));
262
+ });
263
+ });
264
+ }
265
+ function agentBrowser(cdpPort, args, timeoutMs) {
266
+ return runProcess('agent-browser', ['--cdp', String(cdpPort), ...args], timeoutMs).pipe(Effect.flatMap((res) => {
267
+ if (res.exitCode === 0)
268
+ return Effect.void;
269
+ return Effect.fail(new CliError({
270
+ code: 'AGENT_BROWSER_FAILED',
271
+ message: 'agent-browser 执行失败',
272
+ exitCode: 1,
273
+ details: { args, stderr: res.stderr.trim() },
274
+ }));
275
+ }));
276
+ }
277
+ function agentBrowserJson(cdpPort, args, timeoutMs) {
278
+ return runProcess('agent-browser', ['--cdp', String(cdpPort), ...args, '--json'], timeoutMs).pipe(Effect.flatMap((res) => {
279
+ if (res.exitCode !== 0) {
280
+ return Effect.fail(new CliError({
281
+ code: 'AGENT_BROWSER_FAILED',
282
+ message: 'agent-browser 执行失败',
283
+ exitCode: 1,
284
+ details: { args, stderr: res.stderr.trim() },
285
+ }));
286
+ }
287
+ return Effect.try({
288
+ try: () => JSON.parse(res.stdout.trim()),
289
+ catch: (e) => new CliError({
290
+ code: 'INTERNAL',
291
+ message: 'agent-browser 输出不是合法 JSON',
292
+ exitCode: 1,
293
+ details: { args, error: String(e?.message || e) },
294
+ }),
295
+ });
296
+ }));
297
+ }
298
+ function mergeMeta(base, extra) {
299
+ if (!extra || typeof extra !== 'object' || Array.isArray(extra))
300
+ return base;
301
+ return { ...base, ...extra };
302
+ }
303
+ const parent = Options.text('parent').pipe(Options.optional, Options.map(optionToUndefined));
304
+ const ref = Options.text('ref').pipe(Options.optional, Options.map(optionToUndefined));
305
+ const cdpPort = Options.integer('cdp-port').pipe(Options.optional, Options.map(optionToUndefined));
306
+ const maxDepth = Options.integer('max-depth').pipe(Options.optional, Options.map(optionToUndefined));
307
+ const waitMs = Options.integer('wait-ms').pipe(Options.optional, Options.map(optionToUndefined));
308
+ const titleSuffix = Options.text('title-suffix').pipe(Options.optional, Options.map(optionToUndefined));
309
+ const clientId = Options.text('client-id').pipe(Options.optional, Options.map(optionToUndefined));
310
+ const idempotencyKey = Options.text('idempotency-key').pipe(Options.optional, Options.map(optionToUndefined));
311
+ const priority = Options.integer('priority').pipe(Options.optional, Options.map(optionToUndefined));
312
+ const metaSpec = Options.text('meta').pipe(Options.optional, Options.map(optionToUndefined));
313
+ export const wechatOutlineCommand = Command.make('outline', {
314
+ url: Options.text('url'),
315
+ parent,
316
+ ref,
317
+ cdpPort,
318
+ maxDepth,
319
+ waitMs,
320
+ titleSuffix,
321
+ notify: Options.boolean('notify'),
322
+ ensureWs: Options.boolean('ensure-ws'),
323
+ dryRun: Options.boolean('dry-run'),
324
+ priority,
325
+ clientId,
326
+ idempotencyKey,
327
+ meta: metaSpec,
328
+ }, ({ url, parent, ref, cdpPort, maxDepth, waitMs, titleSuffix, notify, ensureWs, dryRun, priority, clientId, idempotencyKey, meta, }) => Effect.gen(function* () {
329
+ const cfg = yield* AppConfig;
330
+ const payloadSvc = yield* Payload;
331
+ const refs = yield* RefResolver;
332
+ if (parent && ref) {
333
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--parent 与 --ref 只能二选一', exitCode: 2 }));
334
+ }
335
+ const resolvedParent = ref ? yield* refs.resolve(ref) : parent;
336
+ if (!resolvedParent) {
337
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '必须提供 --parent 或 --ref', exitCode: 2 }));
338
+ }
339
+ const port = clampInt(cdpPort ?? 9001, 1, 65535);
340
+ const depth = clampInt(maxDepth ?? 5, 2, 50);
341
+ const wait = clampInt(waitMs ?? 5000, 0, 120_000);
342
+ const suffix = titleSuffix !== undefined ? titleSuffix : '(结构化大纲)';
343
+ const list = yield* agentBrowserJson(port, ['tab', 'list'], 10_000);
344
+ const origActiveRaw = list?.data?.active;
345
+ const origActive = typeof origActiveRaw === 'number' ? origActiveRaw : Number.parseInt(String(origActiveRaw || '0'), 10) || 0;
346
+ const created = yield* agentBrowserJson(port, ['tab', 'new'], 10_000);
347
+ const newTabRaw = created?.data?.index;
348
+ const newTab = typeof newTabRaw === 'number' ? newTabRaw : Number.parseInt(String(newTabRaw || ''), 10);
349
+ if (!Number.isFinite(newTab)) {
350
+ return yield* Effect.fail(new CliError({
351
+ code: 'AGENT_BROWSER_FAILED',
352
+ message: '创建新 tab 失败(未返回 tab index)',
353
+ exitCode: 1,
354
+ }));
355
+ }
356
+ const cleanup = agentBrowser(port, ['tab', String(origActive)], 10_000)
357
+ .pipe(Effect.catchAll(() => Effect.void))
358
+ .pipe(Effect.zipRight(agentBrowser(port, ['tab', 'close', String(newTab)], 10_000).pipe(Effect.catchAll(() => Effect.void))));
359
+ const extracted = yield* Effect.gen(function* () {
360
+ yield* agentBrowser(port, ['tab', String(newTab)], 10_000);
361
+ yield* agentBrowser(port, ['open', url], 30_000);
362
+ if (wait > 0)
363
+ yield* agentBrowser(port, ['wait', String(wait)], wait + 5_000);
364
+ const js = "(() => { const title = (document.querySelector('#activity-name')?.innerText || document.querySelector('h1')?.innerText || document.title || '').trim(); const el = document.querySelector('#js_content'); const content = (el?.innerText || '').trim(); return { title, content }; })()";
365
+ const res = yield* agentBrowserJson(port, ['eval', js], 30_000);
366
+ return res;
367
+ }).pipe(Effect.ensuring(cleanup));
368
+ const rawTitle = String(extracted?.data?.result?.title || '').trim();
369
+ const rawContent = String(extracted?.data?.result?.content || '').trim();
370
+ if (!rawTitle || !rawContent) {
371
+ return yield* Effect.fail(new CliError({
372
+ code: 'EXTRACT_FAILED',
373
+ message: '无法从页面提取 title/content;请确认页面已加载且包含 #js_content',
374
+ exitCode: 1,
375
+ }));
376
+ }
377
+ const title = suffix ? `${rawTitle}${suffix}` : rawTitle;
378
+ const markdown = outlineify({ title, url, content: rawContent, maxDepth: depth });
379
+ const baseMeta = {
380
+ source: 'wechat',
381
+ url,
382
+ title,
383
+ maxDepth: depth,
384
+ bytes: Buffer.byteLength(rawContent, 'utf8'),
385
+ };
386
+ const extraMeta = meta ? yield* payloadSvc.readJson(meta) : undefined;
387
+ const metaValue = mergeMeta(baseMeta, extraMeta);
388
+ const op = yield* Effect.try({
389
+ try: () => normalizeOp({
390
+ type: 'create_tree_with_markdown',
391
+ payload: { parentId: resolvedParent, markdown, parseMode: 'smart' },
392
+ }, payloadSvc.normalizeKeys),
393
+ catch: (e) => isCliError(e)
394
+ ? e
395
+ : new CliError({
396
+ code: 'INVALID_PAYLOAD',
397
+ message: '生成 op 失败',
398
+ exitCode: 2,
399
+ details: { error: String(e?.message || e) },
400
+ }),
401
+ });
402
+ const resolvedClientId = clientId?.trim() || 'wechat-outline';
403
+ if (dryRun) {
404
+ yield* writeSuccess({
405
+ data: { dry_run: true, ops: [op], meta: payloadSvc.normalizeKeys(metaValue) },
406
+ md: [
407
+ `- dry_run: true`,
408
+ `- url: ${url}`,
409
+ `- title: ${title}`,
410
+ `- parent_id: ${resolvedParent}`,
411
+ `- max_depth: ${depth}`,
412
+ ].join('\n'),
413
+ });
414
+ return;
415
+ }
416
+ const data = yield* enqueueOps({
417
+ ops: [op],
418
+ priority,
419
+ clientId: resolvedClientId,
420
+ idempotencyKey,
421
+ meta: metaValue,
422
+ notify,
423
+ ensureWs,
424
+ });
425
+ yield* writeSuccess({
426
+ data,
427
+ ids: [data.txn_id, ...data.op_ids],
428
+ md: `- txn_id: ${data.txn_id}\n- op_ids: ${data.op_ids.length}\n- notified: ${data.notified}\n- sent: ${data.sent ?? ''}\n`,
429
+ });
430
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,76 @@
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 { RefResolver } from '../../services/RefResolver.js';
8
+ import { writeFailure, writeSuccess } from '../_shared.js';
9
+ import { enqueueOps, normalizeOp } from '../_enqueue.js';
10
+ function optionToUndefined(opt) {
11
+ return Option.isSome(opt) ? opt.value : undefined;
12
+ }
13
+ function readOptionalText(name) {
14
+ return Options.text(name).pipe(Options.optional, Options.map(optionToUndefined));
15
+ }
16
+ const parent = readOptionalText('parent');
17
+ const ref = readOptionalText('ref');
18
+ const clientId = readOptionalText('client-id');
19
+ const idempotencyKey = readOptionalText('idempotency-key');
20
+ const metaSpec = readOptionalText('meta');
21
+ const priority = Options.integer('priority').pipe(Options.optional, Options.map(optionToUndefined));
22
+ export const writeBulletCommand = Command.make('bullet', {
23
+ parent,
24
+ ref,
25
+ text: Options.text('text'),
26
+ notify: Options.boolean('notify'),
27
+ ensureWs: Options.boolean('ensure-ws'),
28
+ dryRun: Options.boolean('dry-run'),
29
+ priority,
30
+ clientId,
31
+ idempotencyKey,
32
+ meta: metaSpec,
33
+ }, ({ parent, ref, text, notify, ensureWs, dryRun, priority, clientId, idempotencyKey, meta }) => Effect.gen(function* () {
34
+ if (parent && ref) {
35
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--parent 与 --ref 只能二选一', exitCode: 2 }));
36
+ }
37
+ if (!parent && !ref) {
38
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '必须提供 --parent 或 --ref', exitCode: 2 }));
39
+ }
40
+ const refs = yield* RefResolver;
41
+ const payloadSvc = yield* Payload;
42
+ const parentId = ref ? yield* refs.resolve(ref) : parent;
43
+ const op = yield* Effect.try({
44
+ try: () => normalizeOp({ type: 'create_rem', payload: { parentId, text } }, payloadSvc.normalizeKeys),
45
+ catch: (e) => isCliError(e)
46
+ ? e
47
+ : new CliError({
48
+ code: 'INVALID_PAYLOAD',
49
+ message: '生成 op 失败',
50
+ exitCode: 2,
51
+ details: { error: String(e?.message || e) },
52
+ }),
53
+ });
54
+ const metaValue = meta ? yield* payloadSvc.readJson(meta) : undefined;
55
+ if (dryRun) {
56
+ yield* writeSuccess({
57
+ data: { dry_run: true, ops: [op], meta: metaValue ? payloadSvc.normalizeKeys(metaValue) : undefined },
58
+ md: `- dry_run: true\n- op: create_rem\n- parent_id: ${parentId}\n`,
59
+ });
60
+ return;
61
+ }
62
+ const data = yield* enqueueOps({
63
+ ops: [op],
64
+ priority,
65
+ clientId,
66
+ idempotencyKey,
67
+ meta: metaValue,
68
+ notify,
69
+ ensureWs,
70
+ });
71
+ yield* writeSuccess({
72
+ data,
73
+ ids: [data.txn_id, ...data.op_ids],
74
+ md: `- txn_id: ${data.txn_id}\n- op_ids: ${data.op_ids.length}\n- notified: ${data.notified}\n- sent: ${data.sent ?? ''}\n`,
75
+ });
76
+ }).pipe(Effect.catchAll(writeFailure)));
@@ -0,0 +1,4 @@
1
+ import { Command } from '@effect/cli';
2
+ import { writeBulletCommand } from './bullet.js';
3
+ import { writeMdCommand } from './md.js';
4
+ export const writeCommand = Command.make('write', {}).pipe(Command.withSubcommands([writeMdCommand, writeBulletCommand]));
@@ -0,0 +1,91 @@
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 { CliError, isCliError } from '../../services/Errors.js';
7
+ import { Payload } from '../../services/Payload.js';
8
+ import { RefResolver } from '../../services/RefResolver.js';
9
+ import { writeFailure, writeSuccess } from '../_shared.js';
10
+ import { enqueueOps, normalizeOp } from '../_enqueue.js';
11
+ function optionToUndefined(opt) {
12
+ return Option.isSome(opt) ? opt.value : undefined;
13
+ }
14
+ function readOptionalText(name) {
15
+ return Options.text(name).pipe(Options.optional, Options.map(optionToUndefined));
16
+ }
17
+ const parent = readOptionalText('parent');
18
+ const ref = readOptionalText('ref');
19
+ const clientId = readOptionalText('client-id');
20
+ const idempotencyKey = readOptionalText('idempotency-key');
21
+ const metaSpec = readOptionalText('meta');
22
+ const priority = Options.integer('priority').pipe(Options.optional, Options.map(optionToUndefined));
23
+ export const writeMdCommand = Command.make('md', {
24
+ parent,
25
+ ref,
26
+ file: Options.text('file'),
27
+ notify: Options.boolean('notify'),
28
+ ensureWs: Options.boolean('ensure-ws'),
29
+ dryRun: Options.boolean('dry-run'),
30
+ priority,
31
+ clientId,
32
+ idempotencyKey,
33
+ meta: metaSpec,
34
+ }, ({ parent, ref, file, notify, ensureWs, dryRun, priority, clientId, idempotencyKey, meta }) => Effect.gen(function* () {
35
+ if (parent && ref) {
36
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--parent 与 --ref 只能二选一', exitCode: 2 }));
37
+ }
38
+ if (!parent && !ref) {
39
+ return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '必须提供 --parent 或 --ref', exitCode: 2 }));
40
+ }
41
+ const refs = yield* RefResolver;
42
+ const payloadSvc = yield* Payload;
43
+ const parentId = ref ? yield* refs.resolve(ref) : parent;
44
+ const markdown = yield* Effect.tryPromise({
45
+ try: async () => await fs.readFile(file, 'utf8'),
46
+ catch: (e) => {
47
+ if (e?.code === 'ENOENT') {
48
+ return new CliError({ code: 'INVALID_ARGS', message: `文件不存在:${file}`, exitCode: 2 });
49
+ }
50
+ return new CliError({
51
+ code: 'INTERNAL',
52
+ message: '读取文件失败',
53
+ exitCode: 1,
54
+ details: { file, error: String(e?.message || e) },
55
+ });
56
+ },
57
+ });
58
+ const op = yield* Effect.try({
59
+ try: () => normalizeOp({ type: 'create_tree_with_markdown', payload: { parentId, markdown } }, payloadSvc.normalizeKeys),
60
+ catch: (e) => isCliError(e)
61
+ ? e
62
+ : new CliError({
63
+ code: 'INVALID_PAYLOAD',
64
+ message: '生成 op 失败',
65
+ exitCode: 2,
66
+ details: { error: String(e?.message || e) },
67
+ }),
68
+ });
69
+ const metaValue = meta ? yield* payloadSvc.readJson(meta) : undefined;
70
+ if (dryRun) {
71
+ yield* writeSuccess({
72
+ data: { dry_run: true, ops: [op], meta: metaValue ? payloadSvc.normalizeKeys(metaValue) : undefined },
73
+ md: `- dry_run: true\n- op: create_tree_with_markdown\n- parent_id: ${parentId}\n`,
74
+ });
75
+ return;
76
+ }
77
+ const data = yield* enqueueOps({
78
+ ops: [op],
79
+ priority,
80
+ clientId,
81
+ idempotencyKey,
82
+ meta: metaValue,
83
+ notify,
84
+ ensureWs,
85
+ });
86
+ yield* writeSuccess({
87
+ data,
88
+ ids: [data.txn_id, ...data.op_ids],
89
+ md: `- txn_id: ${data.txn_id}\n- op_ids: ${data.op_ids.length}\n- notified: ${data.notified}\n- sent: ${data.sent ?? ''}\n`,
90
+ });
91
+ }).pipe(Effect.catchAll(writeFailure)));