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,79 @@
1
+ import { Command } from '@effect/cli';
2
+ import { NodeContext, NodeRuntime } from '@effect/platform-node';
3
+ import * as ValidationError from '@effect/cli/ValidationError';
4
+ import * as Cause from 'effect/Cause';
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Exit from 'effect/Exit';
7
+ import * as Option from 'effect/Option';
8
+ import { rootCommand } from './commands/index.js';
9
+ import { cliErrorFromValidationError, exitCodeFromExit, fail, isCliError, toJsonError, } from './services/Errors.js';
10
+ const cli = Command.run(rootCommand, {
11
+ name: 'remnote',
12
+ version: '0.0.1',
13
+ });
14
+ const jsonRequested = process.argv.includes('--json');
15
+ const debugRequested = process.argv.includes('--debug');
16
+ cli(process.argv)
17
+ .pipe(Effect.provide(NodeContext.layer), Effect.exit, Effect.flatMap((exit) => Effect.sync(() => {
18
+ process.exitCode = exitCodeFromExit(exit);
19
+ if (Exit.isSuccess(exit))
20
+ return;
21
+ const failure = Cause.failureOption(exit.cause);
22
+ if (Option.isNone(failure)) {
23
+ if (jsonRequested) {
24
+ process.stdout.write(`${JSON.stringify(fail({
25
+ code: 'INTERNAL',
26
+ message: '未知运行时错误(defect)',
27
+ details: debugRequested ? { cause: Cause.pretty(exit.cause) } : undefined,
28
+ }))}\n`);
29
+ }
30
+ else if (!globalThis.__REMNOTE_CLI_ERROR_REPORTED__) {
31
+ ;
32
+ globalThis.__REMNOTE_CLI_ERROR_REPORTED__ = true;
33
+ process.stderr.write('未知运行时错误(defect)\n');
34
+ if (debugRequested)
35
+ process.stderr.write(Cause.pretty(exit.cause) + '\n');
36
+ }
37
+ return;
38
+ }
39
+ const error = failure.value;
40
+ if (ValidationError.isValidationError(error)) {
41
+ if (!jsonRequested)
42
+ return;
43
+ const cliError = cliErrorFromValidationError(error);
44
+ process.stdout.write(`${JSON.stringify(fail(toJsonError(cliError), cliError.hint))}\n`);
45
+ return;
46
+ }
47
+ if (isCliError(error)) {
48
+ if (jsonRequested) {
49
+ process.stdout.write(`${JSON.stringify(fail(toJsonError(error), error.hint))}\n`);
50
+ return;
51
+ }
52
+ if (!globalThis.__REMNOTE_CLI_ERROR_REPORTED__) {
53
+ ;
54
+ globalThis.__REMNOTE_CLI_ERROR_REPORTED__ = true;
55
+ process.stderr.write(`${error.message}\n`);
56
+ if (debugRequested && error.details !== undefined) {
57
+ process.stderr.write(`${JSON.stringify(error.details, null, 2)}\n`);
58
+ }
59
+ if (error.hint && error.hint.length > 0) {
60
+ process.stderr.write('Hint:\n');
61
+ for (const h of error.hint)
62
+ process.stderr.write(`- ${h}\n`);
63
+ }
64
+ }
65
+ return;
66
+ }
67
+ if (jsonRequested) {
68
+ process.stdout.write(`${JSON.stringify(fail({
69
+ code: 'INTERNAL',
70
+ message: String(error?.message || error || '未知错误'),
71
+ }))}\n`);
72
+ }
73
+ else if (!globalThis.__REMNOTE_CLI_ERROR_REPORTED__) {
74
+ ;
75
+ globalThis.__REMNOTE_CLI_ERROR_REPORTED__ = true;
76
+ process.stderr.write(String(error?.message || error || '未知错误') + '\n');
77
+ }
78
+ })))
79
+ .pipe(NodeRuntime.runMain);
@@ -0,0 +1,3 @@
1
+ import * as Context from 'effect/Context';
2
+ export class AppConfig extends Context.Tag('AppConfig')() {
3
+ }
@@ -0,0 +1,91 @@
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Layer from 'effect/Layer';
4
+ import { CliError, isCliError } from './Errors.js';
5
+ export class Config extends Context.Tag('Config')() {
6
+ }
7
+ function expandHome(targetPath) {
8
+ if (!targetPath.startsWith('~'))
9
+ return targetPath;
10
+ const home = process.env.HOME || process.env.USERPROFILE || '.';
11
+ if (targetPath === '~')
12
+ return home;
13
+ if (targetPath.startsWith('~/'))
14
+ return `${home}/${targetPath.slice(2)}`;
15
+ return targetPath.replace(/^~(?=$|\/)/, home);
16
+ }
17
+ function defaultQueueDbPath() {
18
+ const env = process.env.REMNOTE_QUEUE_DB || process.env.QUEUE_DB;
19
+ if (typeof env === 'string' && env.trim())
20
+ return expandHome(env.trim());
21
+ const home = process.env.HOME || process.env.USERPROFILE || '.';
22
+ return `${home}/.remnote-mcp/queue.sqlite`;
23
+ }
24
+ function defaultWsUrl() {
25
+ const env = process.env.WS_URL;
26
+ if (typeof env === 'string' && env.trim())
27
+ return env.trim();
28
+ return 'ws://localhost:3010/ws';
29
+ }
30
+ function defaultConsumerId() {
31
+ const env = process.env.REMNOTE_CONSUMER_ID;
32
+ if (typeof env === 'string' && env.trim())
33
+ return env.trim();
34
+ return 'remnote-plugin';
35
+ }
36
+ function defaultRepo() {
37
+ const env = process.env.REMNOTE_MCP_REPO;
38
+ return typeof env === 'string' && env.trim() ? expandHome(env.trim()) : undefined;
39
+ }
40
+ function pickFormat(flags) {
41
+ const json = flags.json === true;
42
+ const md = flags.md === true;
43
+ const ids = flags.ids === true;
44
+ const count = [json, md, ids].filter(Boolean).length;
45
+ if (count > 1) {
46
+ throw new CliError({
47
+ code: 'INVALID_ARGS',
48
+ message: '输出模式冲突:--json/--md/--ids 只能选择一个',
49
+ exitCode: 2,
50
+ details: { json, md, ids },
51
+ });
52
+ }
53
+ if (json)
54
+ return 'json';
55
+ if (ids)
56
+ return 'ids';
57
+ // 默认输出:md
58
+ return 'md';
59
+ }
60
+ export function resolveConfig(flags) {
61
+ return Effect.try({
62
+ try: () => {
63
+ const remnoteDbRaw = (flags.remnoteDb ?? process.env.REMNOTE_DB)?.trim();
64
+ const queueDbRaw = flags.queueDb?.trim();
65
+ const wsUrlRaw = flags.wsUrl?.trim();
66
+ const consumerIdRaw = flags.consumerId?.trim();
67
+ const repoRaw = flags.repo?.trim();
68
+ return {
69
+ format: pickFormat(flags),
70
+ quiet: flags.quiet === true,
71
+ debug: flags.debug === true,
72
+ remnoteDb: remnoteDbRaw ? expandHome(remnoteDbRaw) : undefined,
73
+ queueDb: expandHome(queueDbRaw || defaultQueueDbPath()),
74
+ wsUrl: wsUrlRaw || defaultWsUrl(),
75
+ consumerId: consumerIdRaw || defaultConsumerId(),
76
+ repo: repoRaw ? expandHome(repoRaw) : defaultRepo(),
77
+ };
78
+ },
79
+ catch: (error) => {
80
+ if (isCliError(error))
81
+ return error;
82
+ return new CliError({
83
+ code: 'INTERNAL',
84
+ message: '解析配置失败',
85
+ exitCode: 1,
86
+ details: { error: String(error?.message || error) },
87
+ });
88
+ },
89
+ });
90
+ }
91
+ export const ConfigLive = Layer.succeed(Config, { resolve: resolveConfig });
@@ -0,0 +1,91 @@
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Layer from 'effect/Layer';
4
+ import { promises as fs } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { CliError, isCliError } from './Errors.js';
7
+ export class DaemonFiles extends Context.Tag('DaemonFiles')() {
8
+ }
9
+ function homeDir() {
10
+ return process.env.HOME || process.env.USERPROFILE || '.';
11
+ }
12
+ function ensureDir(p) {
13
+ return fs.mkdir(path.dirname(p), { recursive: true }).then(() => undefined);
14
+ }
15
+ function defaultPidFile() {
16
+ return path.join(homeDir(), '.remnote-mcp', 'ws.pid');
17
+ }
18
+ function defaultLogFile() {
19
+ return path.join(homeDir(), '.remnote-mcp', 'ws.log');
20
+ }
21
+ async function writeJsonAtomic(filePath, json) {
22
+ await ensureDir(filePath);
23
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
24
+ await fs.writeFile(tmp, JSON.stringify(json), 'utf8');
25
+ await fs.rename(tmp, filePath);
26
+ }
27
+ export const DaemonFilesLive = Layer.succeed(DaemonFiles, {
28
+ defaultPidFile: () => defaultPidFile(),
29
+ defaultLogFile: () => defaultLogFile(),
30
+ readPidFile: (pidFilePath) => Effect.tryPromise({
31
+ try: async () => {
32
+ try {
33
+ const raw = await fs.readFile(pidFilePath, 'utf8');
34
+ const parsed = JSON.parse(raw);
35
+ return parsed;
36
+ }
37
+ catch (error) {
38
+ if (error?.code === 'ENOENT')
39
+ return undefined;
40
+ throw error;
41
+ }
42
+ },
43
+ catch: (error) => {
44
+ if (isCliError(error))
45
+ return error;
46
+ return new CliError({
47
+ code: 'INTERNAL',
48
+ message: '读取 pidfile 失败',
49
+ exitCode: 1,
50
+ details: { pid_file: pidFilePath, error: String(error?.message || error) },
51
+ });
52
+ },
53
+ }),
54
+ writePidFile: (pidFilePath, value) => Effect.tryPromise({
55
+ try: async () => {
56
+ await writeJsonAtomic(pidFilePath, value);
57
+ },
58
+ catch: (error) => {
59
+ if (isCliError(error))
60
+ return error;
61
+ return new CliError({
62
+ code: 'INTERNAL',
63
+ message: '写入 pidfile 失败',
64
+ exitCode: 1,
65
+ details: { pid_file: pidFilePath, error: String(error?.message || error) },
66
+ });
67
+ },
68
+ }),
69
+ deletePidFile: (pidFilePath) => Effect.tryPromise({
70
+ try: async () => {
71
+ try {
72
+ await fs.unlink(pidFilePath);
73
+ }
74
+ catch (error) {
75
+ if (error?.code === 'ENOENT')
76
+ return;
77
+ throw error;
78
+ }
79
+ },
80
+ catch: (error) => {
81
+ if (isCliError(error))
82
+ return error;
83
+ return new CliError({
84
+ code: 'INTERNAL',
85
+ message: '删除 pidfile 失败',
86
+ exitCode: 1,
87
+ details: { pid_file: pidFilePath, error: String(error?.message || error) },
88
+ });
89
+ },
90
+ }),
91
+ });
@@ -0,0 +1,49 @@
1
+ import * as Data from 'effect/Data';
2
+ import * as Exit from 'effect/Exit';
3
+ import * as Cause from 'effect/Cause';
4
+ import * as Option from 'effect/Option';
5
+ import * as ValidationError from '@effect/cli/ValidationError';
6
+ import * as HelpDoc from '@effect/cli/HelpDoc';
7
+ export class CliError extends Data.TaggedError('CliError') {
8
+ }
9
+ export function isCliError(u) {
10
+ return typeof u === 'object' && u !== null && u._tag === 'CliError';
11
+ }
12
+ export function ok(data) {
13
+ return { ok: true, data };
14
+ }
15
+ export function fail(error, hint) {
16
+ return hint && hint.length > 0 ? { ok: false, error, hint } : { ok: false, error };
17
+ }
18
+ export function toJsonError(error) {
19
+ return { code: error.code, message: error.message, details: error.details };
20
+ }
21
+ export function stripAnsi(input) {
22
+ return input.replace(/\u001b\[[0-9;]*m/g, '');
23
+ }
24
+ export function renderValidationErrorMessage(err) {
25
+ return stripAnsi(HelpDoc.toAnsiText(err.error)).trim();
26
+ }
27
+ export function cliErrorFromValidationError(err) {
28
+ return new CliError({
29
+ code: 'INVALID_ARGS',
30
+ message: renderValidationErrorMessage(err) || 'Invalid arguments',
31
+ details: { tag: err._tag },
32
+ exitCode: 2,
33
+ });
34
+ }
35
+ export function exitCodeFromExit(exit) {
36
+ if (Exit.isSuccess(exit))
37
+ return 0;
38
+ if (Cause.isInterruptedOnly(exit.cause))
39
+ return 0;
40
+ const failure = Cause.failureOption(exit.cause);
41
+ if (Option.isSome(failure)) {
42
+ const error = failure.value;
43
+ if (isCliError(error))
44
+ return error.exitCode;
45
+ if (ValidationError.isValidationError(error))
46
+ return 2;
47
+ }
48
+ return 1;
49
+ }
@@ -0,0 +1,16 @@
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Layer from 'effect/Layer';
4
+ export class Output extends Context.Tag('Output')() {
5
+ }
6
+ export const OutputLive = Layer.succeed(Output, {
7
+ stdout: (text) => Effect.sync(() => {
8
+ process.stdout.write(text);
9
+ }),
10
+ stderr: (text) => Effect.sync(() => {
11
+ process.stderr.write(text);
12
+ }),
13
+ json: (value) => Effect.sync(() => {
14
+ process.stdout.write(`${JSON.stringify(value)}\n`);
15
+ }),
16
+ });
@@ -0,0 +1,90 @@
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Layer from 'effect/Layer';
4
+ import { promises as fs } from 'node:fs';
5
+ import { CliError } from './Errors.js';
6
+ export class Payload extends Context.Tag('Payload')() {
7
+ }
8
+ const MAX_PAYLOAD_BYTES = 5 * 1024 * 1024;
9
+ function decamelize(s) {
10
+ return s
11
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
12
+ .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1_$2')
13
+ .toLowerCase();
14
+ }
15
+ function normalizeKeys(value) {
16
+ if (value == null || typeof value !== 'object')
17
+ return value;
18
+ if (Array.isArray(value))
19
+ return value.map((v) => normalizeKeys(v));
20
+ const out = {};
21
+ for (const [k, v] of Object.entries(value)) {
22
+ out[decamelize(k)] = normalizeKeys(v);
23
+ }
24
+ return out;
25
+ }
26
+ async function readAllStdin() {
27
+ if (process.stdin.isTTY) {
28
+ throw new CliError({
29
+ code: 'INVALID_ARGS',
30
+ message: '使用 --payload - 时需要从 stdin 管道输入 JSON',
31
+ exitCode: 2,
32
+ });
33
+ }
34
+ const chunks = [];
35
+ for await (const chunk of process.stdin) {
36
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
37
+ }
38
+ return Buffer.concat(chunks).toString('utf8');
39
+ }
40
+ async function readPayloadSpec(spec) {
41
+ if (spec === '-') {
42
+ return await readAllStdin();
43
+ }
44
+ if (spec.startsWith('@')) {
45
+ const filePath = spec.slice(1).trim();
46
+ if (!filePath) {
47
+ throw new CliError({ code: 'INVALID_ARGS', message: 'payload 文件路径不能为空', exitCode: 2 });
48
+ }
49
+ return await fs.readFile(filePath, 'utf8');
50
+ }
51
+ return spec;
52
+ }
53
+ export const PayloadLive = Layer.succeed(Payload, {
54
+ readJson: (spec) => Effect.tryPromise({
55
+ try: async () => {
56
+ const raw = await readPayloadSpec(spec);
57
+ const bytes = Buffer.byteLength(raw, 'utf8');
58
+ if (bytes > MAX_PAYLOAD_BYTES) {
59
+ throw new CliError({
60
+ code: 'PAYLOAD_TOO_LARGE',
61
+ message: `payload 过大(${bytes} bytes),请拆分后重试`,
62
+ exitCode: 2,
63
+ details: { bytes, max_bytes: MAX_PAYLOAD_BYTES },
64
+ });
65
+ }
66
+ try {
67
+ return JSON.parse(raw);
68
+ }
69
+ catch (error) {
70
+ throw new CliError({
71
+ code: 'INVALID_PAYLOAD',
72
+ message: 'payload 不是合法 JSON',
73
+ exitCode: 2,
74
+ details: { error: String(error?.message || error) },
75
+ });
76
+ }
77
+ },
78
+ catch: (error) => {
79
+ if (error && typeof error === 'object' && error._tag === 'CliError')
80
+ return error;
81
+ return new CliError({
82
+ code: 'INTERNAL',
83
+ message: '读取 payload 失败',
84
+ exitCode: 1,
85
+ details: { error: String(error?.message || error) },
86
+ });
87
+ },
88
+ }),
89
+ normalizeKeys,
90
+ });
@@ -0,0 +1,94 @@
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Layer from 'effect/Layer';
4
+ import { spawn } from 'node:child_process';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { CliError, isCliError } from './Errors.js';
8
+ export class Process extends Context.Tag('Process')() {
9
+ }
10
+ function ensureDirSync(filePath) {
11
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
12
+ }
13
+ export const ProcessLive = Layer.succeed(Process, {
14
+ isPidRunning: (pid) => Effect.sync(() => {
15
+ if (!Number.isFinite(pid) || pid <= 0)
16
+ return false;
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ if (error?.code === 'EPERM')
23
+ return true;
24
+ return false;
25
+ }
26
+ }),
27
+ spawnDetached: (params) => Effect.try({
28
+ try: () => {
29
+ ensureDirSync(params.logFile);
30
+ const fd = fs.openSync(params.logFile, 'a');
31
+ const child = spawn(params.command, [...params.args], {
32
+ cwd: params.cwd,
33
+ env: params.env,
34
+ detached: true,
35
+ stdio: ['ignore', fd, fd],
36
+ });
37
+ child.unref();
38
+ return child.pid;
39
+ },
40
+ catch: (error) => {
41
+ if (isCliError(error))
42
+ return error;
43
+ return new CliError({
44
+ code: 'INTERNAL',
45
+ message: '启动后台进程失败',
46
+ exitCode: 1,
47
+ details: { error: String(error?.message || error) },
48
+ });
49
+ },
50
+ }).pipe(Effect.flatMap((pid) => {
51
+ if (typeof pid !== 'number' || !Number.isFinite(pid)) {
52
+ return Effect.fail(new CliError({
53
+ code: 'INTERNAL',
54
+ message: '启动后台进程失败(pid 不可用)',
55
+ exitCode: 1,
56
+ }));
57
+ }
58
+ return Effect.succeed(pid);
59
+ })),
60
+ kill: (pid, signal) => Effect.try({
61
+ try: () => {
62
+ process.kill(pid, signal);
63
+ },
64
+ catch: (error) => new CliError({
65
+ code: 'INTERNAL',
66
+ message: `发送信号失败(${signal})`,
67
+ exitCode: 1,
68
+ details: { pid, signal, error: String(error?.message || error) },
69
+ }),
70
+ }),
71
+ waitForExit: ({ pid, timeoutMs }) => Effect.async((resume) => {
72
+ const deadline = Date.now() + Math.max(0, timeoutMs);
73
+ const tick = () => {
74
+ let alive = false;
75
+ try {
76
+ process.kill(pid, 0);
77
+ alive = true;
78
+ }
79
+ catch (error) {
80
+ alive = error?.code === 'EPERM';
81
+ }
82
+ if (!alive) {
83
+ resume(Effect.succeed(true));
84
+ return;
85
+ }
86
+ if (Date.now() >= deadline) {
87
+ resume(Effect.succeed(false));
88
+ return;
89
+ }
90
+ setTimeout(tick, 100);
91
+ };
92
+ tick();
93
+ }),
94
+ });
@@ -0,0 +1,120 @@
1
+ import * as Context from 'effect/Context';
2
+ import * as Effect from 'effect/Effect';
3
+ import * as Layer from 'effect/Layer';
4
+ import { CliError, isCliError } from './Errors.js';
5
+ import { enqueueTxn, getTxnIdByOpId, openQueueDb, queueStats as getQueueStats } from '../adapters/mcp.js';
6
+ export class Queue extends Context.Tag('Queue')() {
7
+ }
8
+ function safeParseJson(s) {
9
+ try {
10
+ return JSON.parse(s);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export const QueueLive = Layer.succeed(Queue, {
17
+ enqueue: ({ dbPath, ops, options }) => Effect.try({
18
+ try: () => {
19
+ const db = openQueueDb(dbPath);
20
+ try {
21
+ const txn_id = enqueueTxn(db, ops, options);
22
+ const opRows = db
23
+ .prepare(`SELECT op_id FROM ops WHERE txn_id=? ORDER BY op_seq ASC`)
24
+ .all(txn_id);
25
+ const op_ids = opRows.map((r) => String(r.op_id));
26
+ return { txn_id, op_ids };
27
+ }
28
+ finally {
29
+ db.close();
30
+ }
31
+ },
32
+ catch: (error) => {
33
+ if (isCliError(error))
34
+ return error;
35
+ return new CliError({
36
+ code: 'QUEUE_UNAVAILABLE',
37
+ message: '队列数据库不可用',
38
+ exitCode: 1,
39
+ details: { db_path: dbPath, error: String(error?.message || error) },
40
+ });
41
+ },
42
+ }),
43
+ stats: ({ dbPath }) => Effect.try({
44
+ try: () => {
45
+ const db = openQueueDb(dbPath);
46
+ try {
47
+ return getQueueStats(db);
48
+ }
49
+ finally {
50
+ db.close();
51
+ }
52
+ },
53
+ catch: (error) => new CliError({
54
+ code: 'QUEUE_UNAVAILABLE',
55
+ message: '队列数据库不可用',
56
+ exitCode: 1,
57
+ details: { db_path: dbPath, error: String(error?.message || error) },
58
+ }),
59
+ }),
60
+ inspect: ({ dbPath, txnId, opId }) => Effect.try({
61
+ try: () => {
62
+ const db = openQueueDb(dbPath);
63
+ try {
64
+ let resolvedTxnId = txnId;
65
+ if (!resolvedTxnId && opId) {
66
+ resolvedTxnId = getTxnIdByOpId(db, opId);
67
+ }
68
+ if (!resolvedTxnId) {
69
+ throw new CliError({
70
+ code: 'INVALID_ARGS',
71
+ message: '必须提供 --txn 或 --op',
72
+ exitCode: 2,
73
+ });
74
+ }
75
+ const txn = db.prepare(`SELECT * FROM txns WHERE txn_id=?`).get(resolvedTxnId);
76
+ if (!txn) {
77
+ throw new CliError({
78
+ code: 'INVALID_ARGS',
79
+ message: `未找到事务 ${resolvedTxnId}`,
80
+ exitCode: 2,
81
+ });
82
+ }
83
+ const ops = db.prepare(`SELECT * FROM ops WHERE txn_id=? ORDER BY op_seq ASC`).all(resolvedTxnId);
84
+ const results = ops.length === 0
85
+ ? []
86
+ : db
87
+ .prepare(`SELECT * FROM op_results WHERE op_id IN (${ops.map(() => '?').join(',')})`)
88
+ .all(...ops.map((o) => o.op_id));
89
+ const resMap = new Map();
90
+ for (const r of results)
91
+ resMap.set(String(r.op_id), r);
92
+ const idMap = db.prepare(`SELECT * FROM id_map WHERE source_txn=?`).all(resolvedTxnId);
93
+ const detail = ops.map((o) => ({
94
+ op_id: String(o.op_id),
95
+ seq: o.op_seq,
96
+ type: o.type,
97
+ status: o.status,
98
+ attempts: o.attempt_count,
99
+ next_attempt_at: o.next_attempt_at,
100
+ payload: safeParseJson(o.payload_json),
101
+ result: resMap.get(String(o.op_id)) || null,
102
+ }));
103
+ return { txn, ops: detail, id_map: idMap };
104
+ }
105
+ finally {
106
+ db.close();
107
+ }
108
+ },
109
+ catch: (error) => {
110
+ if (isCliError(error))
111
+ return error;
112
+ return new CliError({
113
+ code: 'QUEUE_UNAVAILABLE',
114
+ message: '队列数据库不可用',
115
+ exitCode: 1,
116
+ details: { db_path: dbPath, error: String(error?.message || error) },
117
+ });
118
+ },
119
+ }),
120
+ });