agent-remnote 0.1.0 → 0.2.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.
- package/CHANGELOG.md +11 -0
- package/README.md +28 -0
- package/dist/main.js +3563 -850
- package/package.json +19 -4
- package/dist/apps/cli/src/adapters/mcp.js +0 -1
- package/dist/apps/cli/src/commands/_enqueue.js +0 -138
- package/dist/apps/cli/src/commands/_shared.js +0 -57
- package/dist/apps/cli/src/commands/_tool.js +0 -28
- package/dist/apps/cli/src/commands/apply.js +0 -81
- package/dist/apps/cli/src/commands/config/index.js +0 -3
- package/dist/apps/cli/src/commands/config/print.js +0 -28
- package/dist/apps/cli/src/commands/daily/index.js +0 -4
- package/dist/apps/cli/src/commands/daily/summary.js +0 -25
- package/dist/apps/cli/src/commands/daily/write.js +0 -145
- package/dist/apps/cli/src/commands/db/backups.js +0 -23
- package/dist/apps/cli/src/commands/db/index.js +0 -4
- package/dist/apps/cli/src/commands/db/recent.js +0 -178
- package/dist/apps/cli/src/commands/doctor.js +0 -124
- package/dist/apps/cli/src/commands/index.js +0 -73
- package/dist/apps/cli/src/commands/ops/index.js +0 -4
- package/dist/apps/cli/src/commands/ops/list.js +0 -12
- package/dist/apps/cli/src/commands/ops/schema.js +0 -77
- package/dist/apps/cli/src/commands/queue/enqueue.js +0 -73
- package/dist/apps/cli/src/commands/queue/index.js +0 -5
- package/dist/apps/cli/src/commands/queue/inspect.js +0 -26
- package/dist/apps/cli/src/commands/queue/stats.js +0 -14
- package/dist/apps/cli/src/commands/read/by-reference.js +0 -35
- package/dist/apps/cli/src/commands/read/connections.js +0 -15
- package/dist/apps/cli/src/commands/read/index.js +0 -21
- package/dist/apps/cli/src/commands/read/inspect.js +0 -34
- package/dist/apps/cli/src/commands/read/outline.js +0 -59
- package/dist/apps/cli/src/commands/read/query.js +0 -95
- package/dist/apps/cli/src/commands/read/references.js +0 -41
- package/dist/apps/cli/src/commands/read/resolve-ref.js +0 -32
- package/dist/apps/cli/src/commands/read/search.js +0 -40
- package/dist/apps/cli/src/commands/read/table.js +0 -32
- package/dist/apps/cli/src/commands/todos/index.js +0 -3
- package/dist/apps/cli/src/commands/todos/list.js +0 -33
- package/dist/apps/cli/src/commands/topic/index.js +0 -3
- package/dist/apps/cli/src/commands/topic/summary.js +0 -44
- package/dist/apps/cli/src/commands/wechat/index.js +0 -3
- package/dist/apps/cli/src/commands/wechat/outline.js +0 -430
- package/dist/apps/cli/src/commands/write/bullet.js +0 -76
- package/dist/apps/cli/src/commands/write/index.js +0 -4
- package/dist/apps/cli/src/commands/write/md.js +0 -91
- package/dist/apps/cli/src/commands/ws/_shared.js +0 -129
- package/dist/apps/cli/src/commands/ws/ensure.js +0 -22
- package/dist/apps/cli/src/commands/ws/health.js +0 -15
- package/dist/apps/cli/src/commands/ws/index.js +0 -21
- package/dist/apps/cli/src/commands/ws/logs.js +0 -95
- package/dist/apps/cli/src/commands/ws/restart.js +0 -73
- package/dist/apps/cli/src/commands/ws/serve.js +0 -52
- package/dist/apps/cli/src/commands/ws/start.js +0 -70
- package/dist/apps/cli/src/commands/ws/status.js +0 -60
- package/dist/apps/cli/src/commands/ws/stop.js +0 -59
- package/dist/apps/cli/src/commands/ws/trigger.js +0 -20
- package/dist/apps/cli/src/main.js +0 -79
- package/dist/apps/cli/src/services/AppConfig.js +0 -3
- package/dist/apps/cli/src/services/Config.js +0 -91
- package/dist/apps/cli/src/services/DaemonFiles.js +0 -91
- package/dist/apps/cli/src/services/Errors.js +0 -49
- package/dist/apps/cli/src/services/Output.js +0 -16
- package/dist/apps/cli/src/services/Payload.js +0 -90
- package/dist/apps/cli/src/services/Process.js +0 -94
- package/dist/apps/cli/src/services/Queue.js +0 -120
- package/dist/apps/cli/src/services/RefResolver.js +0 -111
- package/dist/apps/cli/src/services/RemDb.js +0 -35
- package/dist/apps/cli/src/services/WsClient.js +0 -170
- package/dist/apps/cli/tests/apply.contract.test.js +0 -31
- package/dist/apps/cli/tests/db-recent.contract.test.js +0 -22
- package/dist/apps/cli/tests/help.contract.test.js +0 -30
- package/dist/apps/cli/tests/helpers/runCli.js +0 -45
- package/dist/apps/cli/tests/ids-output.contract.test.js +0 -30
- package/dist/apps/cli/tests/payload-stdin.contract.test.js +0 -15
- package/dist/apps/cli/tests/read-search.contract.test.js +0 -22
- package/dist/apps/cli/tests/ws-health.contract.test.js +0 -36
- package/dist/apps/cli/vitest.config.js +0 -7
- package/dist/packages/mcp/src/public.js +0 -18
- package/dist/packages/mcp/src/queue/dao.js +0 -165
- package/dist/packages/mcp/src/queue/db.js +0 -26
- package/dist/packages/mcp/src/tools/executeSearchQuery.js +0 -914
- package/dist/packages/mcp/src/tools/findRemsByReference.js +0 -447
- package/dist/packages/mcp/src/tools/getRemConnections.js +0 -566
- package/dist/packages/mcp/src/tools/inspectRemDoc.js +0 -60
- package/dist/packages/mcp/src/tools/listRemBackups.js +0 -35
- package/dist/packages/mcp/src/tools/listRemReferences.js +0 -421
- package/dist/packages/mcp/src/tools/listSupportedOps.js +0 -41
- package/dist/packages/mcp/src/tools/listTodos.js +0 -815
- package/dist/packages/mcp/src/tools/outlineRemSubtree.js +0 -203
- package/dist/packages/mcp/src/tools/readRemTable.js +0 -252
- package/dist/packages/mcp/src/tools/resolveRemReference.js +0 -174
- package/dist/packages/mcp/src/tools/searchQueryTypes.js +0 -127
- package/dist/packages/mcp/src/tools/searchRemOverview.js +0 -422
- package/dist/packages/mcp/src/tools/searchUtils.js +0 -32
- package/dist/packages/mcp/src/tools/shared.js +0 -393
- package/dist/packages/mcp/src/tools/summarizeDailyNotes.js +0 -221
- package/dist/packages/mcp/src/tools/summarizeTopicActivity.js +0 -605
- package/dist/packages/mcp/src/tools/timeFilters.js +0 -130
- package/dist/packages/mcp/src/ws/bridge.js +0 -377
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import * as Duration from 'effect/Duration';
|
|
2
|
-
import * as Effect from 'effect/Effect';
|
|
3
|
-
import * as Either from 'effect/Either';
|
|
4
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
5
|
-
import { DaemonFiles } from '../../services/DaemonFiles.js';
|
|
6
|
-
import { CliError, isCliError } from '../../services/Errors.js';
|
|
7
|
-
import { Process } from '../../services/Process.js';
|
|
8
|
-
import { WsClient } from '../../services/WsClient.js';
|
|
9
|
-
export const WS_HEALTH_TIMEOUT_MS = 2000;
|
|
10
|
-
export const WS_START_WAIT_DEFAULT_MS = 15_000;
|
|
11
|
-
export const WS_STOP_WAIT_DEFAULT_MS = 5_000;
|
|
12
|
-
function childCommandLine(params) {
|
|
13
|
-
const command = process.argv[0];
|
|
14
|
-
const script = process.argv[1];
|
|
15
|
-
if (!command || !script) {
|
|
16
|
-
throw new CliError({
|
|
17
|
-
code: 'INTERNAL',
|
|
18
|
-
message: '无法推导当前可执行入口(process.argv 不完整)',
|
|
19
|
-
exitCode: 1,
|
|
20
|
-
details: { argv: process.argv },
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
command,
|
|
25
|
-
args: [script, '--ws-url', params.wsUrl, '--queue-db', params.queueDb, 'ws', 'serve'],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function toPidFileValue(params) {
|
|
29
|
-
return {
|
|
30
|
-
pid: params.pid,
|
|
31
|
-
started_at: params.startedAt,
|
|
32
|
-
ws_url: params.wsUrl,
|
|
33
|
-
log_file: params.logFile,
|
|
34
|
-
cmd: params.cmd,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function waitForHealth(url, waitMs) {
|
|
38
|
-
return Effect.gen(function* () {
|
|
39
|
-
if (!Number.isFinite(waitMs) || waitMs < 0) {
|
|
40
|
-
return yield* Effect.fail(new CliError({
|
|
41
|
-
code: 'INVALID_ARGS',
|
|
42
|
-
message: '--wait 必须是非负整数(ms)',
|
|
43
|
-
exitCode: 2,
|
|
44
|
-
details: { wait_ms: waitMs },
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
47
|
-
if (waitMs === 0)
|
|
48
|
-
return;
|
|
49
|
-
const ws = yield* WsClient;
|
|
50
|
-
const deadline = Date.now() + waitMs;
|
|
51
|
-
while (Date.now() < deadline) {
|
|
52
|
-
const remaining = Math.max(0, deadline - Date.now());
|
|
53
|
-
const res = yield* ws
|
|
54
|
-
.health({ url, timeoutMs: Math.min(WS_HEALTH_TIMEOUT_MS, Math.max(1, remaining)) })
|
|
55
|
-
.pipe(Effect.either);
|
|
56
|
-
if (Either.isRight(res))
|
|
57
|
-
return;
|
|
58
|
-
yield* Effect.sleep(Duration.millis(300));
|
|
59
|
-
}
|
|
60
|
-
return yield* Effect.fail(new CliError({
|
|
61
|
-
code: 'WS_TIMEOUT',
|
|
62
|
-
message: `等待 WS 可用超时(${waitMs}ms)`,
|
|
63
|
-
exitCode: 1,
|
|
64
|
-
details: { url, wait_ms: waitMs },
|
|
65
|
-
hint: ['remnote ws status', 'remnote ws logs', 'remnote ws health --json'],
|
|
66
|
-
}));
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
export function startWsDaemon(params) {
|
|
70
|
-
return Effect.gen(function* () {
|
|
71
|
-
const cfg = yield* AppConfig;
|
|
72
|
-
const daemonFiles = yield* DaemonFiles;
|
|
73
|
-
const proc = yield* Process;
|
|
74
|
-
const ws = yield* WsClient;
|
|
75
|
-
const pidFilePath = params.pidFile ?? daemonFiles.defaultPidFile();
|
|
76
|
-
const logFilePath = params.logFile ?? daemonFiles.defaultLogFile();
|
|
77
|
-
const existingPidFile = yield* daemonFiles.readPidFile(pidFilePath);
|
|
78
|
-
if (existingPidFile) {
|
|
79
|
-
const alive = yield* proc.isPidRunning(existingPidFile.pid);
|
|
80
|
-
if (!alive) {
|
|
81
|
-
yield* daemonFiles.deletePidFile(pidFilePath);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
return { started: false, pid: existingPidFile.pid, pid_file: pidFilePath, log_file: existingPidFile.log_file };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const pre = yield* ws.health({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS }).pipe(Effect.either);
|
|
88
|
-
if (Either.isRight(pre)) {
|
|
89
|
-
return { started: false, pid_file: pidFilePath, log_file: logFilePath };
|
|
90
|
-
}
|
|
91
|
-
const cmd = yield* Effect.try({
|
|
92
|
-
try: () => childCommandLine({ wsUrl: cfg.wsUrl, queueDb: cfg.queueDb }),
|
|
93
|
-
catch: (e) => isCliError(e)
|
|
94
|
-
? e
|
|
95
|
-
: new CliError({
|
|
96
|
-
code: 'INTERNAL',
|
|
97
|
-
message: '启动 WS daemon 失败',
|
|
98
|
-
exitCode: 1,
|
|
99
|
-
details: { error: String(e?.message || e) },
|
|
100
|
-
}),
|
|
101
|
-
});
|
|
102
|
-
const pid = yield* proc.spawnDetached({ command: cmd.command, args: cmd.args, logFile: logFilePath });
|
|
103
|
-
yield* daemonFiles.writePidFile(pidFilePath, toPidFileValue({
|
|
104
|
-
pid,
|
|
105
|
-
startedAt: Date.now(),
|
|
106
|
-
wsUrl: cfg.wsUrl,
|
|
107
|
-
logFile: logFilePath,
|
|
108
|
-
cmd: [cmd.command, ...cmd.args],
|
|
109
|
-
}));
|
|
110
|
-
yield* waitForHealth(cfg.wsUrl, params.waitMs);
|
|
111
|
-
return { started: true, pid, pid_file: pidFilePath, log_file: logFilePath };
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
export function ensureWsDaemon(params) {
|
|
115
|
-
return Effect.gen(function* () {
|
|
116
|
-
const cfg = yield* AppConfig;
|
|
117
|
-
const ws = yield* WsClient;
|
|
118
|
-
const pre = yield* ws.health({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS }).pipe(Effect.either);
|
|
119
|
-
if (Either.isRight(pre)) {
|
|
120
|
-
const daemonFiles = yield* DaemonFiles;
|
|
121
|
-
return {
|
|
122
|
-
started: false,
|
|
123
|
-
pid_file: params.pidFile ?? daemonFiles.defaultPidFile(),
|
|
124
|
-
log_file: params.logFile ?? daemonFiles.defaultLogFile(),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return yield* startWsDaemon(params);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
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 { writeFailure, writeSuccess } from '../_shared.js';
|
|
6
|
-
import { WS_START_WAIT_DEFAULT_MS, ensureWsDaemon } from './_shared.js';
|
|
7
|
-
function optionToUndefined(opt) {
|
|
8
|
-
return Option.isSome(opt) ? opt.value : undefined;
|
|
9
|
-
}
|
|
10
|
-
const pidFile = Options.text('pid-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
11
|
-
const logFile = Options.text('log-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
12
|
-
export const wsEnsureCommand = Command.make('ensure', {
|
|
13
|
-
wait: Options.integer('wait').pipe(Options.withDefault(WS_START_WAIT_DEFAULT_MS)),
|
|
14
|
-
pidFile,
|
|
15
|
-
logFile,
|
|
16
|
-
}, ({ wait, pidFile, logFile }) => Effect.gen(function* () {
|
|
17
|
-
const result = yield* ensureWsDaemon({ waitMs: wait, pidFile, logFile });
|
|
18
|
-
yield* writeSuccess({
|
|
19
|
-
data: result,
|
|
20
|
-
md: `- started: ${result.started}\n- pid: ${result.pid ?? ''}\n- pid_file: ${result.pid_file}\n- log_file: ${result.log_file}\n`,
|
|
21
|
-
});
|
|
22
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Command } from '@effect/cli';
|
|
2
|
-
import * as Effect from 'effect/Effect';
|
|
3
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
4
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
5
|
-
import { WsClient } from '../../services/WsClient.js';
|
|
6
|
-
import { WS_HEALTH_TIMEOUT_MS } from './_shared.js';
|
|
7
|
-
export const wsHealthCommand = Command.make('health', {}, () => Effect.gen(function* () {
|
|
8
|
-
const cfg = yield* AppConfig;
|
|
9
|
-
const ws = yield* WsClient;
|
|
10
|
-
const result = yield* ws.health({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS });
|
|
11
|
-
yield* writeSuccess({
|
|
12
|
-
data: result,
|
|
13
|
-
md: `- url: ${result.url}\n- rtt_ms: ${result.rtt_ms}\n`,
|
|
14
|
-
});
|
|
15
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Command } from '@effect/cli';
|
|
2
|
-
import { wsEnsureCommand } from './ensure.js';
|
|
3
|
-
import { wsHealthCommand } from './health.js';
|
|
4
|
-
import { wsLogsCommand } from './logs.js';
|
|
5
|
-
import { wsRestartCommand } from './restart.js';
|
|
6
|
-
import { wsServeCommand } from './serve.js';
|
|
7
|
-
import { wsStartCommand } from './start.js';
|
|
8
|
-
import { wsStatusCommand } from './status.js';
|
|
9
|
-
import { wsStopCommand } from './stop.js';
|
|
10
|
-
import { wsTriggerCommand } from './trigger.js';
|
|
11
|
-
export const wsCommand = Command.make('ws', {}).pipe(Command.withSubcommands([
|
|
12
|
-
wsHealthCommand,
|
|
13
|
-
wsServeCommand,
|
|
14
|
-
wsStartCommand,
|
|
15
|
-
wsStopCommand,
|
|
16
|
-
wsRestartCommand,
|
|
17
|
-
wsEnsureCommand,
|
|
18
|
-
wsTriggerCommand,
|
|
19
|
-
wsStatusCommand,
|
|
20
|
-
wsLogsCommand,
|
|
21
|
-
]));
|
|
@@ -1,95 +0,0 @@
|
|
|
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 { promises as fs } from 'node:fs';
|
|
7
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
8
|
-
import { DaemonFiles } from '../../services/DaemonFiles.js';
|
|
9
|
-
import { CliError } from '../../services/Errors.js';
|
|
10
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
11
|
-
function optionToUndefined(opt) {
|
|
12
|
-
return Option.isSome(opt) ? opt.value : undefined;
|
|
13
|
-
}
|
|
14
|
-
const pidFile = Options.text('pid-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
15
|
-
const file = Options.text('file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
16
|
-
export const wsLogsCommand = Command.make('logs', {
|
|
17
|
-
pidFile,
|
|
18
|
-
file,
|
|
19
|
-
lines: Options.integer('lines').pipe(Options.withDefault(200)),
|
|
20
|
-
follow: Options.boolean('follow'),
|
|
21
|
-
}, ({ pidFile, file, lines, follow }) => Effect.gen(function* () {
|
|
22
|
-
const cfg = yield* AppConfig;
|
|
23
|
-
const daemonFiles = yield* DaemonFiles;
|
|
24
|
-
if (!Number.isFinite(lines) || lines <= 0) {
|
|
25
|
-
return yield* Effect.fail(new CliError({
|
|
26
|
-
code: 'INVALID_ARGS',
|
|
27
|
-
message: '--lines 必须是正整数',
|
|
28
|
-
exitCode: 2,
|
|
29
|
-
details: { lines },
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
if (cfg.format === 'ids') {
|
|
33
|
-
return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '该命令不支持 --ids 输出', exitCode: 2 }));
|
|
34
|
-
}
|
|
35
|
-
const pidFilePath = pidFile ?? daemonFiles.defaultPidFile();
|
|
36
|
-
const pidInfo = yield* daemonFiles.readPidFile(pidFilePath);
|
|
37
|
-
const logFilePath = file ?? pidInfo?.log_file ?? daemonFiles.defaultLogFile();
|
|
38
|
-
if (follow) {
|
|
39
|
-
if (cfg.format === 'json') {
|
|
40
|
-
return yield* Effect.fail(new CliError({ code: 'INVALID_ARGS', message: '--follow 与 --json 不兼容', exitCode: 2 }));
|
|
41
|
-
}
|
|
42
|
-
yield* Effect.tryPromise({
|
|
43
|
-
try: async () => {
|
|
44
|
-
await fs.stat(logFilePath);
|
|
45
|
-
},
|
|
46
|
-
catch: (e) => {
|
|
47
|
-
if (e?.code === 'ENOENT') {
|
|
48
|
-
return new CliError({
|
|
49
|
-
code: 'INTERNAL',
|
|
50
|
-
message: `日志文件不存在:${logFilePath}`,
|
|
51
|
-
exitCode: 1,
|
|
52
|
-
hint: ['remnote ws start', 'remnote ws status'],
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return new CliError({
|
|
56
|
-
code: 'INTERNAL',
|
|
57
|
-
message: '读取日志文件失败',
|
|
58
|
-
exitCode: 1,
|
|
59
|
-
details: { file: logFilePath, error: String(e?.message || e) },
|
|
60
|
-
});
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
yield* Effect.sync(() => {
|
|
64
|
-
const child = spawn('tail', ['-n', String(lines), '-f', logFilePath], { stdio: 'inherit' });
|
|
65
|
-
child.on('error', () => { });
|
|
66
|
-
});
|
|
67
|
-
yield* Effect.never;
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const content = yield* Effect.tryPromise({
|
|
71
|
-
try: async () => await fs.readFile(logFilePath, 'utf8'),
|
|
72
|
-
catch: (e) => {
|
|
73
|
-
if (e?.code === 'ENOENT') {
|
|
74
|
-
return new CliError({
|
|
75
|
-
code: 'INTERNAL',
|
|
76
|
-
message: `日志文件不存在:${logFilePath}`,
|
|
77
|
-
exitCode: 1,
|
|
78
|
-
hint: ['remnote ws start', 'remnote ws status'],
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return new CliError({
|
|
82
|
-
code: 'INTERNAL',
|
|
83
|
-
message: '读取日志文件失败',
|
|
84
|
-
exitCode: 1,
|
|
85
|
-
details: { file: logFilePath, error: String(e?.message || e) },
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
const all = content.split(/\r?\n/);
|
|
90
|
-
const tailLines = all.slice(Math.max(0, all.length - lines)).join('\n').trimEnd();
|
|
91
|
-
yield* writeSuccess({
|
|
92
|
-
data: { file: logFilePath, lines, content: tailLines },
|
|
93
|
-
md: tailLines.length > 0 ? `${tailLines}\n` : '',
|
|
94
|
-
});
|
|
95
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,73 +0,0 @@
|
|
|
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 { DaemonFiles } from '../../services/DaemonFiles.js';
|
|
6
|
-
import { CliError } from '../../services/Errors.js';
|
|
7
|
-
import { Process } from '../../services/Process.js';
|
|
8
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
9
|
-
import { WS_START_WAIT_DEFAULT_MS, WS_STOP_WAIT_DEFAULT_MS, startWsDaemon } from './_shared.js';
|
|
10
|
-
function optionToUndefined(opt) {
|
|
11
|
-
return Option.isSome(opt) ? opt.value : undefined;
|
|
12
|
-
}
|
|
13
|
-
const pidFile = Options.text('pid-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
14
|
-
const logFile = Options.text('log-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
15
|
-
export const wsRestartCommand = Command.make('restart', {
|
|
16
|
-
force: Options.boolean('force'),
|
|
17
|
-
wait: Options.integer('wait').pipe(Options.withDefault(WS_START_WAIT_DEFAULT_MS)),
|
|
18
|
-
pidFile,
|
|
19
|
-
logFile,
|
|
20
|
-
}, ({ force, wait, pidFile, logFile }) => Effect.gen(function* () {
|
|
21
|
-
const daemonFiles = yield* DaemonFiles;
|
|
22
|
-
const proc = yield* Process;
|
|
23
|
-
const pidFilePath = pidFile ?? daemonFiles.defaultPidFile();
|
|
24
|
-
const existing = yield* daemonFiles.readPidFile(pidFilePath);
|
|
25
|
-
let stopResult = { stopped: true, pid_file: pidFilePath };
|
|
26
|
-
if (existing) {
|
|
27
|
-
const alive = yield* proc.isPidRunning(existing.pid);
|
|
28
|
-
if (!alive) {
|
|
29
|
-
yield* daemonFiles.deletePidFile(pidFilePath);
|
|
30
|
-
stopResult = { stopped: true, stale: true, pid: existing.pid, pid_file: pidFilePath };
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
yield* proc.kill(existing.pid, 'SIGTERM');
|
|
34
|
-
const exited = yield* proc.waitForExit({ pid: existing.pid, timeoutMs: WS_STOP_WAIT_DEFAULT_MS });
|
|
35
|
-
if (!exited) {
|
|
36
|
-
if (!force) {
|
|
37
|
-
return yield* Effect.fail(new CliError({
|
|
38
|
-
code: 'INTERNAL',
|
|
39
|
-
message: `WS 服务未在 ${WS_STOP_WAIT_DEFAULT_MS}ms 内退出,请使用 --force`,
|
|
40
|
-
exitCode: 1,
|
|
41
|
-
details: { pid: existing.pid, pid_file: pidFilePath },
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
yield* proc.kill(existing.pid, 'SIGKILL');
|
|
45
|
-
const killed = yield* proc.waitForExit({ pid: existing.pid, timeoutMs: WS_STOP_WAIT_DEFAULT_MS });
|
|
46
|
-
if (!killed) {
|
|
47
|
-
return yield* Effect.fail(new CliError({
|
|
48
|
-
code: 'INTERNAL',
|
|
49
|
-
message: '强制停止失败(进程仍存活)',
|
|
50
|
-
exitCode: 1,
|
|
51
|
-
details: { pid: existing.pid, pid_file: pidFilePath },
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
yield* daemonFiles.deletePidFile(pidFilePath);
|
|
56
|
-
stopResult = { stopped: true, pid: existing.pid, pid_file: pidFilePath };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const startResult = yield* startWsDaemon({ waitMs: wait, pidFile, logFile });
|
|
60
|
-
const data = { stop: stopResult, start: startResult };
|
|
61
|
-
const md = [
|
|
62
|
-
`# ws restart`,
|
|
63
|
-
`- stop_stopped: ${stopResult.stopped}`,
|
|
64
|
-
`- stop_pid: ${stopResult.pid ?? ''}`,
|
|
65
|
-
`- stop_stale: ${stopResult.stale ?? ''}`,
|
|
66
|
-
`- stop_pid_file: ${stopResult.pid_file}`,
|
|
67
|
-
`- start_started: ${startResult.started}`,
|
|
68
|
-
`- start_pid: ${startResult.pid ?? ''}`,
|
|
69
|
-
`- start_pid_file: ${startResult.pid_file}`,
|
|
70
|
-
`- start_log_file: ${startResult.log_file}`,
|
|
71
|
-
].join('\n');
|
|
72
|
-
yield* writeSuccess({ data, md });
|
|
73
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Command } from '@effect/cli';
|
|
2
|
-
import * as Effect from 'effect/Effect';
|
|
3
|
-
import { startWebSocketBridge } from '../../adapters/mcp.js';
|
|
4
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
5
|
-
import { CliError, isCliError } from '../../services/Errors.js';
|
|
6
|
-
import { writeFailure } from '../_shared.js';
|
|
7
|
-
function parseWsUrl(url) {
|
|
8
|
-
let u;
|
|
9
|
-
try {
|
|
10
|
-
u = new URL(url);
|
|
11
|
-
}
|
|
12
|
-
catch (_) {
|
|
13
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl 不合法:${url}`, exitCode: 2 });
|
|
14
|
-
}
|
|
15
|
-
if (u.protocol !== 'ws:' && u.protocol !== 'wss:') {
|
|
16
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl 协议必须是 ws/wss:${url}`, exitCode: 2 });
|
|
17
|
-
}
|
|
18
|
-
const port = u.port ? Number(u.port) : u.protocol === 'wss:' ? 443 : 80;
|
|
19
|
-
const path = u.pathname && u.pathname.length > 0 ? u.pathname : '/ws';
|
|
20
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
21
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl 端口不合法:${url}`, exitCode: 2 });
|
|
22
|
-
}
|
|
23
|
-
if (!path.startsWith('/')) {
|
|
24
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl path 不合法:${url}`, exitCode: 2 });
|
|
25
|
-
}
|
|
26
|
-
return { port, path };
|
|
27
|
-
}
|
|
28
|
-
export const wsServeCommand = Command.make('serve', {}, () => Effect.gen(function* () {
|
|
29
|
-
const cfg = yield* AppConfig;
|
|
30
|
-
const { port, path } = yield* Effect.try({
|
|
31
|
-
try: () => parseWsUrl(cfg.wsUrl),
|
|
32
|
-
catch: (e) => isCliError(e)
|
|
33
|
-
? e
|
|
34
|
-
: new CliError({
|
|
35
|
-
code: 'INVALID_ARGS',
|
|
36
|
-
message: 'wsUrl 不合法',
|
|
37
|
-
exitCode: 2,
|
|
38
|
-
details: { ws_url: cfg.wsUrl, error: String(e?.message || e) },
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
41
|
-
process.env.REMNOTE_QUEUE_DB = cfg.queueDb;
|
|
42
|
-
const started = startWebSocketBridge({ enable: true, port, path });
|
|
43
|
-
if (!started) {
|
|
44
|
-
return yield* Effect.fail(new CliError({
|
|
45
|
-
code: 'WS_UNAVAILABLE',
|
|
46
|
-
message: `启动 WS bridge 失败(${cfg.wsUrl})`,
|
|
47
|
-
exitCode: 1,
|
|
48
|
-
details: { ws_url: cfg.wsUrl, port, path },
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
yield* Effect.never;
|
|
52
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,70 +0,0 @@
|
|
|
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 { startWebSocketBridge } from '../../adapters/mcp.js';
|
|
6
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
7
|
-
import { CliError, isCliError } from '../../services/Errors.js';
|
|
8
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
9
|
-
import { WS_START_WAIT_DEFAULT_MS, startWsDaemon } from './_shared.js';
|
|
10
|
-
function optionToUndefined(opt) {
|
|
11
|
-
return Option.isSome(opt) ? opt.value : undefined;
|
|
12
|
-
}
|
|
13
|
-
function parseWsUrl(url) {
|
|
14
|
-
let u;
|
|
15
|
-
try {
|
|
16
|
-
u = new URL(url);
|
|
17
|
-
}
|
|
18
|
-
catch (_) {
|
|
19
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl 不合法:${url}`, exitCode: 2 });
|
|
20
|
-
}
|
|
21
|
-
const port = u.port ? Number(u.port) : u.protocol === 'wss:' ? 443 : 80;
|
|
22
|
-
const path = u.pathname && u.pathname.length > 0 ? u.pathname : '/ws';
|
|
23
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
24
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl 端口不合法:${url}`, exitCode: 2 });
|
|
25
|
-
}
|
|
26
|
-
if (!path.startsWith('/')) {
|
|
27
|
-
throw new CliError({ code: 'INVALID_ARGS', message: `wsUrl path 不合法:${url}`, exitCode: 2 });
|
|
28
|
-
}
|
|
29
|
-
return { port, path };
|
|
30
|
-
}
|
|
31
|
-
const pidFile = Options.text('pid-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
32
|
-
const logFile = Options.text('log-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
33
|
-
export const wsStartCommand = Command.make('start', {
|
|
34
|
-
foreground: Options.boolean('foreground'),
|
|
35
|
-
wait: Options.integer('wait').pipe(Options.withDefault(WS_START_WAIT_DEFAULT_MS)),
|
|
36
|
-
pidFile,
|
|
37
|
-
logFile,
|
|
38
|
-
}, ({ foreground, wait, pidFile, logFile }) => Effect.gen(function* () {
|
|
39
|
-
const cfg = yield* AppConfig;
|
|
40
|
-
if (foreground) {
|
|
41
|
-
const { port, path } = yield* Effect.try({
|
|
42
|
-
try: () => parseWsUrl(cfg.wsUrl),
|
|
43
|
-
catch: (e) => isCliError(e)
|
|
44
|
-
? e
|
|
45
|
-
: new CliError({
|
|
46
|
-
code: 'INVALID_ARGS',
|
|
47
|
-
message: 'wsUrl 不合法',
|
|
48
|
-
exitCode: 2,
|
|
49
|
-
details: { ws_url: cfg.wsUrl, error: String(e?.message || e) },
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
process.env.REMNOTE_QUEUE_DB = cfg.queueDb;
|
|
53
|
-
const started = startWebSocketBridge({ enable: true, port, path });
|
|
54
|
-
if (!started) {
|
|
55
|
-
return yield* Effect.fail(new CliError({
|
|
56
|
-
code: 'WS_UNAVAILABLE',
|
|
57
|
-
message: `启动 WS bridge 失败(${cfg.wsUrl})`,
|
|
58
|
-
exitCode: 1,
|
|
59
|
-
details: { ws_url: cfg.wsUrl, port, path },
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
yield* Effect.never;
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const result = yield* startWsDaemon({ waitMs: wait, pidFile, logFile });
|
|
66
|
-
yield* writeSuccess({
|
|
67
|
-
data: result,
|
|
68
|
-
md: `- started: ${result.started}\n- pid: ${result.pid ?? ''}\n- pid_file: ${result.pid_file}\n- log_file: ${result.log_file}\n`,
|
|
69
|
-
});
|
|
70
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { Command } from '@effect/cli';
|
|
2
|
-
import * as Options from '@effect/cli/Options';
|
|
3
|
-
import * as Effect from 'effect/Effect';
|
|
4
|
-
import * as Either from 'effect/Either';
|
|
5
|
-
import * as Option from 'effect/Option';
|
|
6
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
7
|
-
import { DaemonFiles } from '../../services/DaemonFiles.js';
|
|
8
|
-
import { Process } from '../../services/Process.js';
|
|
9
|
-
import { WsClient } from '../../services/WsClient.js';
|
|
10
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
11
|
-
import { WS_HEALTH_TIMEOUT_MS } from './_shared.js';
|
|
12
|
-
function optionToUndefined(opt) {
|
|
13
|
-
return Option.isSome(opt) ? opt.value : undefined;
|
|
14
|
-
}
|
|
15
|
-
const pidFile = Options.text('pid-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
16
|
-
export const wsStatusCommand = Command.make('status', { pidFile }, ({ pidFile }) => Effect.gen(function* () {
|
|
17
|
-
const cfg = yield* AppConfig;
|
|
18
|
-
const daemonFiles = yield* DaemonFiles;
|
|
19
|
-
const proc = yield* Process;
|
|
20
|
-
const ws = yield* WsClient;
|
|
21
|
-
const pidFilePath = pidFile ?? daemonFiles.defaultPidFile();
|
|
22
|
-
let pidInfo = yield* daemonFiles.readPidFile(pidFilePath);
|
|
23
|
-
if (pidInfo) {
|
|
24
|
-
const alive = yield* proc.isPidRunning(pidInfo.pid);
|
|
25
|
-
if (!alive) {
|
|
26
|
-
yield* daemonFiles.deletePidFile(pidFilePath);
|
|
27
|
-
pidInfo = undefined;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const health = yield* ws.health({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS }).pipe(Effect.either);
|
|
31
|
-
const clients = yield* ws.queryClients({ url: cfg.wsUrl, timeoutMs: WS_HEALTH_TIMEOUT_MS }).pipe(Effect.either);
|
|
32
|
-
const data = {
|
|
33
|
-
service: {
|
|
34
|
-
running: !!pidInfo,
|
|
35
|
-
pid: pidInfo?.pid,
|
|
36
|
-
pid_file: pidFilePath,
|
|
37
|
-
started_at: pidInfo?.started_at,
|
|
38
|
-
log_file: pidInfo?.log_file,
|
|
39
|
-
},
|
|
40
|
-
ws: {
|
|
41
|
-
url: cfg.wsUrl,
|
|
42
|
-
healthy: Either.isRight(health),
|
|
43
|
-
rtt_ms: Either.isRight(health) ? health.right.rtt_ms : undefined,
|
|
44
|
-
error: Either.isLeft(health) ? health.left.message : undefined,
|
|
45
|
-
},
|
|
46
|
-
clients: Either.isRight(clients) ? clients.right.clients : [],
|
|
47
|
-
};
|
|
48
|
-
const md = [
|
|
49
|
-
`- service_running: ${data.service.running}`,
|
|
50
|
-
`- pid: ${data.service.pid ?? ''}`,
|
|
51
|
-
`- pid_file: ${data.service.pid_file}`,
|
|
52
|
-
`- log_file: ${data.service.log_file ?? ''}`,
|
|
53
|
-
`- started_at: ${data.service.started_at ?? ''}`,
|
|
54
|
-
`- ws_url: ${data.ws.url}`,
|
|
55
|
-
`- ws_healthy: ${data.ws.healthy}`,
|
|
56
|
-
`- ws_rtt_ms: ${data.ws.rtt_ms ?? ''}`,
|
|
57
|
-
`- clients: ${data.clients.length}`,
|
|
58
|
-
].join('\n');
|
|
59
|
-
yield* writeSuccess({ data, md });
|
|
60
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,59 +0,0 @@
|
|
|
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 { DaemonFiles } from '../../services/DaemonFiles.js';
|
|
6
|
-
import { CliError } from '../../services/Errors.js';
|
|
7
|
-
import { Process } from '../../services/Process.js';
|
|
8
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
9
|
-
import { WS_STOP_WAIT_DEFAULT_MS } from './_shared.js';
|
|
10
|
-
function optionToUndefined(opt) {
|
|
11
|
-
return Option.isSome(opt) ? opt.value : undefined;
|
|
12
|
-
}
|
|
13
|
-
const pidFile = Options.text('pid-file').pipe(Options.optional, Options.map(optionToUndefined));
|
|
14
|
-
export const wsStopCommand = Command.make('stop', { force: Options.boolean('force'), pidFile }, ({ force, pidFile }) => Effect.gen(function* () {
|
|
15
|
-
const daemonFiles = yield* DaemonFiles;
|
|
16
|
-
const proc = yield* Process;
|
|
17
|
-
const pidFilePath = pidFile ?? daemonFiles.defaultPidFile();
|
|
18
|
-
const existing = yield* daemonFiles.readPidFile(pidFilePath);
|
|
19
|
-
if (!existing) {
|
|
20
|
-
yield* writeSuccess({ data: { stopped: true, pid_file: pidFilePath }, md: `- stopped: true\n- pid_file: ${pidFilePath}\n` });
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const alive = yield* proc.isPidRunning(existing.pid);
|
|
24
|
-
if (!alive) {
|
|
25
|
-
yield* daemonFiles.deletePidFile(pidFilePath);
|
|
26
|
-
yield* writeSuccess({
|
|
27
|
-
data: { stopped: true, stale: true, pid: existing.pid, pid_file: pidFilePath },
|
|
28
|
-
md: `- stopped: true\n- stale: true\n- pid: ${existing.pid}\n- pid_file: ${pidFilePath}\n`,
|
|
29
|
-
});
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
yield* proc.kill(existing.pid, 'SIGTERM');
|
|
33
|
-
const exited = yield* proc.waitForExit({ pid: existing.pid, timeoutMs: WS_STOP_WAIT_DEFAULT_MS });
|
|
34
|
-
if (!exited) {
|
|
35
|
-
if (!force) {
|
|
36
|
-
return yield* Effect.fail(new CliError({
|
|
37
|
-
code: 'INTERNAL',
|
|
38
|
-
message: `WS 服务未在 ${WS_STOP_WAIT_DEFAULT_MS}ms 内退出,请使用 --force`,
|
|
39
|
-
exitCode: 1,
|
|
40
|
-
details: { pid: existing.pid, pid_file: pidFilePath },
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
yield* proc.kill(existing.pid, 'SIGKILL');
|
|
44
|
-
const killed = yield* proc.waitForExit({ pid: existing.pid, timeoutMs: WS_STOP_WAIT_DEFAULT_MS });
|
|
45
|
-
if (!killed) {
|
|
46
|
-
return yield* Effect.fail(new CliError({
|
|
47
|
-
code: 'INTERNAL',
|
|
48
|
-
message: '强制停止失败(进程仍存活)',
|
|
49
|
-
exitCode: 1,
|
|
50
|
-
details: { pid: existing.pid, pid_file: pidFilePath },
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
yield* daemonFiles.deletePidFile(pidFilePath);
|
|
55
|
-
yield* writeSuccess({
|
|
56
|
-
data: { stopped: true, pid: existing.pid, pid_file: pidFilePath },
|
|
57
|
-
md: `- stopped: true\n- pid: ${existing.pid}\n- pid_file: ${pidFilePath}\n`,
|
|
58
|
-
});
|
|
59
|
-
}).pipe(Effect.catchAll(writeFailure)));
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Command } from '@effect/cli';
|
|
2
|
-
import * as Options from '@effect/cli/Options';
|
|
3
|
-
import * as Effect from 'effect/Effect';
|
|
4
|
-
import { AppConfig } from '../../services/AppConfig.js';
|
|
5
|
-
import { WsClient } from '../../services/WsClient.js';
|
|
6
|
-
import { writeFailure, writeSuccess } from '../_shared.js';
|
|
7
|
-
import { WS_HEALTH_TIMEOUT_MS, WS_START_WAIT_DEFAULT_MS, ensureWsDaemon } from './_shared.js';
|
|
8
|
-
export const wsTriggerCommand = Command.make('trigger', { ensureWs: Options.boolean('ensure-ws') }, ({ ensureWs }) => Effect.gen(function* () {
|
|
9
|
-
const cfg = yield* AppConfig;
|
|
10
|
-
const ws = yield* WsClient;
|
|
11
|
-
if (ensureWs) {
|
|
12
|
-
yield* ensureWsDaemon({ waitMs: WS_START_WAIT_DEFAULT_MS });
|
|
13
|
-
}
|
|
14
|
-
const result = yield* ws.triggerStartSync({
|
|
15
|
-
url: cfg.wsUrl,
|
|
16
|
-
timeoutMs: WS_HEALTH_TIMEOUT_MS,
|
|
17
|
-
consumerId: cfg.consumerId,
|
|
18
|
-
});
|
|
19
|
-
yield* writeSuccess({ data: result, md: `- sent: ${result.sent}\n` });
|
|
20
|
-
}).pipe(Effect.catchAll(writeFailure)));
|