remnote-mcp-server 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +82 -28
  3. package/dist/cli.d.ts +2 -1
  4. package/dist/cli.js +9 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config.d.ts +14 -2
  7. package/dist/config.js +210 -10
  8. package/dist/config.js.map +1 -1
  9. package/dist/daemon.d.ts +61 -0
  10. package/dist/daemon.js +735 -0
  11. package/dist/daemon.js.map +1 -0
  12. package/dist/index.js +6 -3
  13. package/dist/index.js.map +1 -1
  14. package/dist/remnote-cli/cli.js +4 -0
  15. package/dist/remnote-cli/cli.js.map +1 -1
  16. package/dist/remnote-cli/client/mcp-server-client.js +7 -1
  17. package/dist/remnote-cli/client/mcp-server-client.js.map +1 -1
  18. package/dist/remnote-cli/commands/content-input.d.ts +0 -12
  19. package/dist/remnote-cli/commands/content-input.js +0 -20
  20. package/dist/remnote-cli/commands/content-input.js.map +1 -1
  21. package/dist/remnote-cli/commands/create.js +3 -3
  22. package/dist/remnote-cli/commands/create.js.map +1 -1
  23. package/dist/remnote-cli/commands/journal.js +3 -0
  24. package/dist/remnote-cli/commands/journal.js.map +1 -1
  25. package/dist/remnote-cli/commands/read.js +20 -2
  26. package/dist/remnote-cli/commands/read.js.map +1 -1
  27. package/dist/remnote-cli/commands/search.js +24 -5
  28. package/dist/remnote-cli/commands/search.js.map +1 -1
  29. package/dist/remnote-cli/commands/update.js +5 -24
  30. package/dist/remnote-cli/commands/update.js.map +1 -1
  31. package/dist/remnote-cli/commands/write-actions.d.ts +4 -0
  32. package/dist/remnote-cli/commands/write-actions.js +123 -0
  33. package/dist/remnote-cli/commands/write-actions.js.map +1 -0
  34. package/dist/schemas/remnote-schemas.d.ts +33 -10
  35. package/dist/schemas/remnote-schemas.js +58 -16
  36. package/dist/schemas/remnote-schemas.js.map +1 -1
  37. package/dist/tools/index.d.ts +1264 -12
  38. package/dist/tools/index.js +157 -47
  39. package/dist/tools/index.js.map +1 -1
  40. package/dist/websocket-server.d.ts +5 -0
  41. package/dist/websocket-server.js +64 -10
  42. package/dist/websocket-server.js.map +1 -1
  43. package/mcpb/remnote-local/README.md +4 -3
  44. package/mcpb/remnote-local/manifest.json +30 -28
  45. package/mcpb/remnote-local/package.json +1 -1
  46. package/mcpb/remnote-local/remnote-local.mcpb +0 -0
  47. package/mcpb/remnote-local/server/fallback-tools.generated.js +323 -0
  48. package/mcpb/remnote-local/server/index.js +7 -122
  49. package/package.json +5 -3
package/dist/daemon.js ADDED
@@ -0,0 +1,735 @@
1
+ import { spawn, execFile } from 'node:child_process';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:net';
5
+ import { homedir, platform as osPlatform } from 'node:os';
6
+ import { basename, dirname, join, resolve } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+ import { getConfig, loadConfigFileOptions } from './config.js';
9
+ const execFileAsync = promisify(execFile);
10
+ export const DEFAULT_DAEMON_DIR_NAME = '.remnote-mcp-server';
11
+ export const DAEMON_PID_FILE_NAME = 'remnote-mcp-server.pid';
12
+ export const DAEMON_STATE_FILE_NAME = 'daemon.json';
13
+ export const DAEMON_LOG_FILE_NAME = 'remnote-mcp-server.log';
14
+ export const LAUNCHD_LABEL = 'com.remnote.mcp-server';
15
+ const DEFAULT_START_TIMEOUT_MS = 5000;
16
+ const DEFAULT_STOP_TIMEOUT_MS = 5000;
17
+ const POLL_INTERVAL_MS = 100;
18
+ export function getDefaultDaemonPaths(homeDir = homedir(), stateDir) {
19
+ const resolvedStateDir = resolveTilde(stateDir ?? join(homeDir, DEFAULT_DAEMON_DIR_NAME), homeDir);
20
+ return {
21
+ stateDir: resolvedStateDir,
22
+ pidFile: join(resolvedStateDir, DAEMON_PID_FILE_NAME),
23
+ stateFile: join(resolvedStateDir, DAEMON_STATE_FILE_NAME),
24
+ logFile: join(resolvedStateDir, DAEMON_LOG_FILE_NAME),
25
+ lockFile: join(resolvedStateDir, `${DAEMON_PID_FILE_NAME}.lock`),
26
+ launchAgentFile: join(homeDir, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`),
27
+ };
28
+ }
29
+ export function isDaemonCommand(argv = process.argv) {
30
+ return argv[2] === 'daemon';
31
+ }
32
+ export async function handleDaemonCommand(argv = process.argv, runtime = {}) {
33
+ if (argv[3] === '--help' || argv[3] === '-h') {
34
+ (runtime.stdout ?? process.stdout).write(formatDaemonUsage());
35
+ return { exitCode: 0, handled: true };
36
+ }
37
+ try {
38
+ const parsed = parseDaemonCommand(argv);
39
+ const result = await runDaemonCommand(parsed, runtime);
40
+ return { exitCode: result, handled: true };
41
+ }
42
+ catch (error) {
43
+ (runtime.stderr ?? process.stderr).write(`${formatError(error)}\n`);
44
+ return { exitCode: 1, handled: true };
45
+ }
46
+ }
47
+ export function formatDaemonUsage() {
48
+ return `Usage:
49
+ remnote-mcp-server daemon start [options]
50
+ remnote-mcp-server daemon stop [--force] [--timeout-ms <ms>]
51
+ remnote-mcp-server daemon restart [options]
52
+ remnote-mcp-server daemon status [options]
53
+ remnote-mcp-server daemon logs [--lines <n>]
54
+ remnote-mcp-server daemon install-launchd [options]
55
+ remnote-mcp-server daemon uninstall-launchd
56
+
57
+ Daemon options:
58
+ --state-dir <path> State directory (default: ~/.remnote-mcp-server)
59
+ --log-file <path> Daemon stdout/stderr log path (default: ~/.remnote-mcp-server/remnote-mcp-server.log)
60
+ --timeout-ms <ms> Start/stop wait timeout
61
+ --force Force stop after graceful shutdown timeout
62
+ --lines, -n <n> Number of log lines to print
63
+
64
+ Server options accepted by start/restart/install-launchd:
65
+ --http-port <number> --ws-port <number> --http-host <host>
66
+ --config <path> --log-level <level> --verbose --request-log <path> --response-log <path>
67
+ `;
68
+ }
69
+ export async function runDaemonCommand(command, runtime = {}) {
70
+ const homeDir = runtime.homeDir ?? homedir();
71
+ const paths = getDefaultDaemonPaths(homeDir, command.stateDir);
72
+ const stdout = runtime.stdout ?? process.stdout;
73
+ const stderr = runtime.stderr ?? process.stderr;
74
+ const isProcessAlive = runtime.isProcessAlive ?? defaultIsProcessAlive;
75
+ const getProcessCommand = runtime.getProcessCommand ?? defaultGetProcessCommand;
76
+ try {
77
+ const useLaunchd = await shouldUseLaunchd(paths, runtime);
78
+ switch (command.action) {
79
+ case 'start':
80
+ if (useLaunchd) {
81
+ return await startLaunchd(paths, runtime);
82
+ }
83
+ return await startDaemon(command, paths, runtime);
84
+ case 'stop':
85
+ if (useLaunchd) {
86
+ return await stopLaunchd(paths, runtime);
87
+ }
88
+ return await stopDaemon(command, paths, {
89
+ isProcessAlive,
90
+ getProcessCommand,
91
+ stdout,
92
+ stderr,
93
+ killProcess: runtime.killProcess ?? process.kill,
94
+ });
95
+ case 'restart': {
96
+ if (useLaunchd) {
97
+ return await restartLaunchd(paths, runtime);
98
+ }
99
+ const stopCode = await stopDaemon({ ...command, action: 'stop' }, paths, {
100
+ isProcessAlive,
101
+ getProcessCommand,
102
+ stdout,
103
+ stderr,
104
+ killProcess: runtime.killProcess ?? process.kill,
105
+ });
106
+ if (stopCode !== 0) {
107
+ return stopCode;
108
+ }
109
+ return await startDaemon({ ...command, action: 'start' }, paths, runtime);
110
+ }
111
+ case 'status':
112
+ if (useLaunchd) {
113
+ return await printLaunchdStatus(paths, runtime);
114
+ }
115
+ return await printDaemonStatus(command, paths, {
116
+ isProcessAlive,
117
+ canBind: runtime.canBind ?? canBind,
118
+ stdout,
119
+ });
120
+ case 'logs':
121
+ return await printDaemonLogs(command, paths, stdout, stderr);
122
+ case 'install-launchd':
123
+ return await installLaunchd(command, paths, runtime);
124
+ case 'uninstall-launchd':
125
+ return await uninstallLaunchd(paths, runtime);
126
+ }
127
+ }
128
+ catch (error) {
129
+ stderr.write(`${formatError(error)}\n`);
130
+ return 1;
131
+ }
132
+ }
133
+ function parseDaemonCommand(argv) {
134
+ const [, , daemon, actionArg, ...rawArgs] = argv;
135
+ if (daemon !== 'daemon') {
136
+ throw new Error('Not a daemon command');
137
+ }
138
+ const action = parseDaemonAction(actionArg);
139
+ const cliOptions = {};
140
+ let stateDir;
141
+ let timeoutMs;
142
+ let force = false;
143
+ let lines;
144
+ for (let i = 0; i < rawArgs.length; i += 1) {
145
+ const arg = rawArgs[i];
146
+ const next = () => {
147
+ const value = rawArgs[i + 1];
148
+ if (!value || value.startsWith('--')) {
149
+ throw new Error(`Missing value for ${arg}`);
150
+ }
151
+ i += 1;
152
+ return value;
153
+ };
154
+ switch (arg) {
155
+ case '--http-port':
156
+ cliOptions.httpPort = parsePort(next(), arg);
157
+ break;
158
+ case '--ws-port':
159
+ cliOptions.wsPort = parsePort(next(), arg);
160
+ break;
161
+ case '--http-host':
162
+ cliOptions.httpHost = validateHost(next());
163
+ break;
164
+ case '--config':
165
+ cliOptions.config = next();
166
+ break;
167
+ case '--log-level':
168
+ cliOptions.logLevel = validateLogLevel(next());
169
+ break;
170
+ case '--log-file':
171
+ cliOptions.logFile = next();
172
+ break;
173
+ case '--request-log':
174
+ cliOptions.requestLog = next();
175
+ break;
176
+ case '--response-log':
177
+ cliOptions.responseLog = next();
178
+ break;
179
+ case '--verbose':
180
+ cliOptions.verbose = true;
181
+ break;
182
+ case '--state-dir':
183
+ stateDir = next();
184
+ break;
185
+ case '--timeout-ms':
186
+ timeoutMs = parsePositiveInteger(next(), arg);
187
+ break;
188
+ case '--force':
189
+ force = true;
190
+ break;
191
+ case '--lines':
192
+ lines = parsePositiveInteger(next(), arg);
193
+ break;
194
+ case '-n':
195
+ lines = parsePositiveInteger(next(), arg);
196
+ break;
197
+ default:
198
+ throw new Error(`Unknown daemon option: ${arg}`);
199
+ }
200
+ }
201
+ return { action, cliOptions, stateDir, timeoutMs, force, lines };
202
+ }
203
+ function parseDaemonAction(value) {
204
+ switch (value) {
205
+ case 'start':
206
+ case 'stop':
207
+ case 'restart':
208
+ case 'status':
209
+ case 'logs':
210
+ case 'install-launchd':
211
+ case 'uninstall-launchd':
212
+ return value;
213
+ default:
214
+ throw new Error('Usage: remnote-mcp-server daemon <start|stop|restart|status|logs|install-launchd|uninstall-launchd>');
215
+ }
216
+ }
217
+ async function startDaemon(command, paths, runtime) {
218
+ await mkdir(paths.stateDir, { recursive: true });
219
+ const lock = await acquireLock(paths.lockFile);
220
+ try {
221
+ const isProcessAlive = runtime.isProcessAlive ?? defaultIsProcessAlive;
222
+ const existingState = await readDaemonState(paths);
223
+ if (existingState && isProcessAlive(existingState.pid)) {
224
+ (runtime.stdout ?? process.stdout).write(`remnote-mcp-server daemon already running (pid ${existingState.pid})\n`);
225
+ return 0;
226
+ }
227
+ await cleanupStaleState(paths);
228
+ const config = getConfig({
229
+ ...command.cliOptions,
230
+ logFile: undefined,
231
+ logLevelFile: undefined,
232
+ }, { homeDir: runtime.homeDir });
233
+ const logFile = getDaemonLogFile(command, paths, runtime);
234
+ const bindCheck = runtime.canBind ?? canBind;
235
+ await assertPortAvailable(config.httpHost, config.httpPort, 'HTTP', bindCheck);
236
+ await assertPortAvailable(config.wsHost, config.wsPort, 'WebSocket', bindCheck);
237
+ await mkdir(dirname(logFile), { recursive: true });
238
+ const logHandle = await open(logFile, 'a');
239
+ const entrypointPath = resolveEntrypointPath(runtime);
240
+ const childArgs = [entrypointPath, ...serverArgsFromConfig(config)];
241
+ const child = (runtime.spawnProcess ?? spawn)(runtime.execPath ?? process.execPath, childArgs, {
242
+ detached: true,
243
+ stdio: ['ignore', logHandle.fd, logHandle.fd],
244
+ env: process.env,
245
+ windowsHide: true,
246
+ });
247
+ if (!child.pid) {
248
+ throw new Error('Failed to start daemon process: child pid was not assigned');
249
+ }
250
+ child.unref();
251
+ await logHandle.close();
252
+ const state = {
253
+ pid: child.pid,
254
+ startedAt: new Date().toISOString(),
255
+ entrypointPath,
256
+ logFile,
257
+ httpPort: config.httpPort,
258
+ httpHost: config.httpHost,
259
+ wsPort: config.wsPort,
260
+ wsHost: config.wsHost,
261
+ };
262
+ await writeDaemonState(paths, state);
263
+ try {
264
+ await waitForProcessOrPort(state, command.timeoutMs ?? DEFAULT_START_TIMEOUT_MS, runtime);
265
+ }
266
+ catch (error) {
267
+ try {
268
+ (runtime.killProcess ?? process.kill)(state.pid, 'SIGTERM');
269
+ }
270
+ catch {
271
+ // Startup failure cleanup is best-effort; preserve the original error.
272
+ }
273
+ await cleanupStaleState(paths);
274
+ throw error;
275
+ }
276
+ (runtime.stdout ?? process.stdout).write(`remnote-mcp-server daemon started (pid ${state.pid}, log ${state.logFile})\n`);
277
+ return 0;
278
+ }
279
+ finally {
280
+ await lock.release();
281
+ }
282
+ }
283
+ async function stopDaemon(command, paths, runtime) {
284
+ const state = await readDaemonState(paths);
285
+ if (!state || !runtime.isProcessAlive(state.pid)) {
286
+ await cleanupStaleState(paths);
287
+ runtime.stdout.write('remnote-mcp-server daemon is not running\n');
288
+ return 0;
289
+ }
290
+ await assertManagedProcess(state, runtime.getProcessCommand);
291
+ runtime.killProcess(state.pid, 'SIGTERM');
292
+ const stopped = await waitUntilStopped(state.pid, command.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS, runtime.isProcessAlive);
293
+ if (!stopped && command.force) {
294
+ runtime.killProcess(state.pid, 'SIGKILL');
295
+ await waitUntilStopped(state.pid, command.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS, runtime.isProcessAlive);
296
+ }
297
+ else if (!stopped) {
298
+ runtime.stderr.write(`Timed out waiting for daemon pid ${state.pid} to stop; retry with --force\n`);
299
+ return 1;
300
+ }
301
+ await cleanupStaleState(paths);
302
+ runtime.stdout.write(`remnote-mcp-server daemon stopped (pid ${state.pid})\n`);
303
+ return 0;
304
+ }
305
+ async function printDaemonStatus(command, paths, runtime) {
306
+ const state = await readDaemonState(paths);
307
+ const config = getConfig(command.cliOptions);
308
+ const httpOccupied = !(await runtime.canBind(config.httpHost, config.httpPort));
309
+ const wsOccupied = !(await runtime.canBind(config.wsHost, config.wsPort));
310
+ const launchdInstalled = await fileExists(paths.launchAgentFile);
311
+ if (state && runtime.isProcessAlive(state.pid)) {
312
+ runtime.stdout.write([
313
+ `running pid=${state.pid}`,
314
+ `http=${state.httpHost}:${state.httpPort}`,
315
+ `ws=${state.wsHost}:${state.wsPort}`,
316
+ `log=${state.logFile}`,
317
+ `launchd=${launchdInstalled ? 'installed' : 'not-installed'}`,
318
+ ].join(' ') + '\n');
319
+ return 0;
320
+ }
321
+ if (state) {
322
+ runtime.stdout.write(`not running (stale pid ${state.pid})\n`);
323
+ return 1;
324
+ }
325
+ runtime.stdout.write([
326
+ 'not running',
327
+ `http_port=${httpOccupied ? 'occupied' : 'free'}`,
328
+ `ws_port=${wsOccupied ? 'occupied' : 'free'}`,
329
+ `launchd=${launchdInstalled ? 'installed' : 'not-installed'}`,
330
+ ].join(' ') + '\n');
331
+ return 1;
332
+ }
333
+ async function printDaemonLogs(command, paths, stdout, stderr) {
334
+ const state = await readDaemonState(paths);
335
+ const logFile = resolveTilde(command.cliOptions.logFile ?? state?.logFile ?? paths.logFile);
336
+ try {
337
+ const contents = await readFile(logFile, 'utf8');
338
+ const lines = contents.trimEnd().split('\n');
339
+ const selected = lines.slice(-(command.lines ?? 80)).join('\n');
340
+ if (selected) {
341
+ stdout.write(`${selected}\n`);
342
+ }
343
+ return 0;
344
+ }
345
+ catch (error) {
346
+ stderr.write(`Cannot read daemon log at ${logFile}: ${formatError(error)}\n`);
347
+ return 1;
348
+ }
349
+ }
350
+ async function installLaunchd(command, paths, runtime) {
351
+ const currentPlatform = runtime.platform ?? osPlatform();
352
+ if (currentPlatform !== 'darwin') {
353
+ throw new Error('launchd permanence is only available on macOS');
354
+ }
355
+ const config = getConfig({ ...command.cliOptions, logFile: undefined, logLevelFile: undefined }, { homeDir: runtime.homeDir });
356
+ const logFile = getDaemonLogFile(command, paths, runtime);
357
+ await mkdir(paths.stateDir, { recursive: true });
358
+ await mkdir(dirname(paths.launchAgentFile), { recursive: true });
359
+ await mkdir(dirname(logFile), { recursive: true });
360
+ const entrypointPath = resolveEntrypointPath(runtime);
361
+ const plist = renderLaunchdPlist({
362
+ label: LAUNCHD_LABEL,
363
+ execPath: runtime.execPath ?? process.execPath,
364
+ entrypointPath,
365
+ args: serverArgsFromConfig(config),
366
+ logFile,
367
+ workingDirectory: dirname(entrypointPath),
368
+ });
369
+ await writeFile(paths.launchAgentFile, plist, 'utf8');
370
+ const exec = runtime.execFile ?? execFileAsync;
371
+ const domain = getLaunchdDomain(runtime);
372
+ await execLaunchctl(exec, ['bootout', domain, paths.launchAgentFile], true);
373
+ await execLaunchctl(exec, ['bootstrap', domain, paths.launchAgentFile], false);
374
+ await execLaunchctl(exec, ['enable', getLaunchdServiceTarget(runtime)], false);
375
+ await execLaunchctl(exec, ['kickstart', '-k', getLaunchdServiceTarget(runtime)], false);
376
+ (runtime.stdout ?? process.stdout).write(`Installed launchd agent ${LAUNCHD_LABEL} (${paths.launchAgentFile})\n`);
377
+ return 0;
378
+ }
379
+ async function uninstallLaunchd(paths, runtime) {
380
+ const currentPlatform = runtime.platform ?? osPlatform();
381
+ if (currentPlatform !== 'darwin') {
382
+ throw new Error('launchd permanence is only available on macOS');
383
+ }
384
+ const exec = runtime.execFile ?? execFileAsync;
385
+ const domain = getLaunchdDomain(runtime);
386
+ await execLaunchctl(exec, ['bootout', domain, paths.launchAgentFile], true);
387
+ await rm(paths.launchAgentFile, { force: true });
388
+ (runtime.stdout ?? process.stdout).write(`Uninstalled launchd agent ${LAUNCHD_LABEL}\n`);
389
+ return 0;
390
+ }
391
+ async function shouldUseLaunchd(paths, runtime) {
392
+ return ((runtime.platform ?? osPlatform()) === 'darwin' && (await fileExists(paths.launchAgentFile)));
393
+ }
394
+ async function startLaunchd(paths, runtime) {
395
+ const exec = runtime.execFile ?? execFileAsync;
396
+ const status = await getLaunchdStatus(paths, runtime);
397
+ if (status.running) {
398
+ const pidText = status.pid ? ` pid ${status.pid}` : '';
399
+ (runtime.stdout ?? process.stdout).write(`launchd service already running${pidText}\n`);
400
+ return 0;
401
+ }
402
+ if (!status.loaded) {
403
+ await execLaunchctl(exec, ['bootstrap', getLaunchdDomain(runtime), paths.launchAgentFile], false);
404
+ }
405
+ await execLaunchctl(exec, ['enable', getLaunchdServiceTarget(runtime)], false);
406
+ await execLaunchctl(exec, ['kickstart', getLaunchdServiceTarget(runtime)], false);
407
+ const updatedStatus = await getLaunchdStatus(paths, runtime);
408
+ const pidText = updatedStatus.pid ? ` pid ${updatedStatus.pid}` : '';
409
+ (runtime.stdout ?? process.stdout).write(`launchd service started${pidText}\n`);
410
+ return 0;
411
+ }
412
+ async function stopLaunchd(paths, runtime) {
413
+ await execLaunchctl(runtime.execFile ?? execFileAsync, ['bootout', getLaunchdDomain(runtime), paths.launchAgentFile], true);
414
+ (runtime.stdout ?? process.stdout).write(`launchd service stopped (${LAUNCHD_LABEL})\n`);
415
+ return 0;
416
+ }
417
+ async function restartLaunchd(paths, runtime) {
418
+ await stopLaunchd(paths, runtime);
419
+ return await startLaunchd(paths, runtime);
420
+ }
421
+ async function printLaunchdStatus(paths, runtime) {
422
+ const status = await getLaunchdStatus(paths, runtime);
423
+ const parts = [
424
+ 'launchd=installed',
425
+ `loaded=${status.loaded ? 'true' : 'false'}`,
426
+ `running=${status.running ? 'true' : 'false'}`,
427
+ ];
428
+ if (status.pid) {
429
+ parts.push(`pid=${status.pid}`);
430
+ }
431
+ parts.push(`plist=${paths.launchAgentFile}`);
432
+ parts.push(`log=${paths.logFile}`);
433
+ (runtime.stdout ?? process.stdout).write(`${parts.join(' ')}\n`);
434
+ return status.running ? 0 : 1;
435
+ }
436
+ async function getLaunchdStatus(paths, runtime) {
437
+ if (!(await fileExists(paths.launchAgentFile))) {
438
+ return { installed: false, loaded: false, running: false };
439
+ }
440
+ try {
441
+ const { stdout } = await (runtime.execFile ?? execFileAsync)('launchctl', [
442
+ 'print',
443
+ getLaunchdServiceTarget(runtime),
444
+ ]);
445
+ const pid = parseLaunchdPid(stdout);
446
+ return {
447
+ installed: true,
448
+ loaded: true,
449
+ running: pid !== undefined,
450
+ pid,
451
+ };
452
+ }
453
+ catch {
454
+ return { installed: true, loaded: false, running: false };
455
+ }
456
+ }
457
+ function renderLaunchdPlist({ label, execPath, entrypointPath, args, logFile, workingDirectory, }) {
458
+ const programArguments = [execPath, entrypointPath, ...args]
459
+ .map((arg) => ` <string>${escapeXml(arg)}</string>`)
460
+ .join('\n');
461
+ return `<?xml version="1.0" encoding="UTF-8"?>
462
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
463
+ <plist version="1.0">
464
+ <dict>
465
+ <key>Label</key>
466
+ <string>${escapeXml(label)}</string>
467
+ <key>ProgramArguments</key>
468
+ <array>
469
+ ${programArguments}
470
+ </array>
471
+ <key>RunAtLoad</key>
472
+ <true/>
473
+ <key>KeepAlive</key>
474
+ <true/>
475
+ <key>WorkingDirectory</key>
476
+ <string>${escapeXml(workingDirectory)}</string>
477
+ <key>StandardOutPath</key>
478
+ <string>${escapeXml(logFile)}</string>
479
+ <key>StandardErrorPath</key>
480
+ <string>${escapeXml(logFile)}</string>
481
+ </dict>
482
+ </plist>
483
+ `;
484
+ }
485
+ function serverArgsFromConfig(config) {
486
+ const args = [
487
+ '--http-port',
488
+ String(config.httpPort),
489
+ '--ws-port',
490
+ String(config.wsPort),
491
+ '--http-host',
492
+ config.httpHost,
493
+ '--log-level',
494
+ config.logLevel,
495
+ ];
496
+ if (config.requestLog) {
497
+ args.push('--request-log', config.requestLog);
498
+ }
499
+ if (config.responseLog) {
500
+ args.push('--response-log', config.responseLog);
501
+ }
502
+ return args;
503
+ }
504
+ function getDaemonLogFile(command, paths, runtime) {
505
+ const configFile = loadConfigFileOptions(command.cliOptions.config, { homeDir: runtime.homeDir });
506
+ return resolveTilde(command.cliOptions.logFile ?? configFile.daemon?.logFile ?? paths.logFile, runtime.homeDir);
507
+ }
508
+ async function acquireLock(lockFile) {
509
+ let handle;
510
+ try {
511
+ handle = await open(lockFile, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR);
512
+ await handle.writeFile(String(process.pid));
513
+ }
514
+ catch (error) {
515
+ if (error.code === 'EEXIST') {
516
+ throw new Error('Another remnote-mcp-server daemon command is already running', {
517
+ cause: error,
518
+ });
519
+ }
520
+ throw error;
521
+ }
522
+ return {
523
+ release: async () => {
524
+ await handle?.close();
525
+ await rm(lockFile, { force: true });
526
+ },
527
+ };
528
+ }
529
+ async function readDaemonState(paths) {
530
+ try {
531
+ const raw = await readFile(paths.stateFile, 'utf8');
532
+ const parsed = JSON.parse(raw);
533
+ if (typeof parsed.pid === 'number' && parsed.pid > 0) {
534
+ return parsed;
535
+ }
536
+ }
537
+ catch {
538
+ // Fall back to the plain pid file for compatibility with manual cleanup.
539
+ }
540
+ try {
541
+ const rawPid = (await readFile(paths.pidFile, 'utf8')).trim();
542
+ const pid = Number(rawPid);
543
+ if (Number.isInteger(pid) && pid > 0) {
544
+ return {
545
+ pid,
546
+ startedAt: '',
547
+ entrypointPath: '',
548
+ logFile: paths.logFile,
549
+ httpPort: 3001,
550
+ httpHost: '127.0.0.1',
551
+ wsPort: 3002,
552
+ wsHost: '127.0.0.1',
553
+ };
554
+ }
555
+ }
556
+ catch {
557
+ return null;
558
+ }
559
+ return null;
560
+ }
561
+ async function writeDaemonState(paths, state) {
562
+ await writeFile(paths.pidFile, `${state.pid}\n`, 'utf8');
563
+ await writeFile(paths.stateFile, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
564
+ }
565
+ async function cleanupStaleState(paths) {
566
+ await rm(paths.pidFile, { force: true });
567
+ await rm(paths.stateFile, { force: true });
568
+ }
569
+ async function waitForProcessOrPort(state, timeoutMs, runtime) {
570
+ const isProcessAlive = runtime.isProcessAlive ?? defaultIsProcessAlive;
571
+ const start = Date.now();
572
+ while (Date.now() - start < timeoutMs) {
573
+ if (!isProcessAlive(state.pid)) {
574
+ throw new Error(`Daemon process ${state.pid} exited during startup; inspect ${state.logFile}`);
575
+ }
576
+ const bindCheck = runtime.canBind ?? canBind;
577
+ const httpOccupied = !(await bindCheck(state.httpHost, state.httpPort));
578
+ const wsOccupied = !(await bindCheck(state.wsHost, state.wsPort));
579
+ if (httpOccupied && wsOccupied) {
580
+ return;
581
+ }
582
+ await delay(POLL_INTERVAL_MS);
583
+ }
584
+ throw new Error(`Timed out waiting for daemon startup; inspect ${state.logFile}`);
585
+ }
586
+ async function waitUntilStopped(pid, timeoutMs, isProcessAlive) {
587
+ const start = Date.now();
588
+ while (Date.now() - start < timeoutMs) {
589
+ if (!isProcessAlive(pid)) {
590
+ return true;
591
+ }
592
+ await delay(POLL_INTERVAL_MS);
593
+ }
594
+ return !isProcessAlive(pid);
595
+ }
596
+ async function assertManagedProcess(state, getProcessCommand) {
597
+ const command = await getProcessCommand(state.pid);
598
+ if (!command || !state.entrypointPath) {
599
+ return;
600
+ }
601
+ if (command.includes(state.entrypointPath) || command.includes(basename(state.entrypointPath))) {
602
+ return;
603
+ }
604
+ throw new Error(`Refusing to stop pid ${state.pid}; it does not look like the managed remnote-mcp-server process`);
605
+ }
606
+ async function assertPortAvailable(host, port, label, bindCheck) {
607
+ if (!(await bindCheck(host, port))) {
608
+ throw new Error(`${label} port ${host}:${port} is already occupied`);
609
+ }
610
+ }
611
+ async function canBind(host, port) {
612
+ return new Promise((resolveBind) => {
613
+ const server = createServer();
614
+ server.once('error', () => resolveBind(false));
615
+ server.listen(port, host, () => {
616
+ server.close(() => resolveBind(true));
617
+ });
618
+ });
619
+ }
620
+ function defaultIsProcessAlive(pid) {
621
+ try {
622
+ process.kill(pid, 0);
623
+ return true;
624
+ }
625
+ catch {
626
+ return false;
627
+ }
628
+ }
629
+ async function defaultGetProcessCommand(pid) {
630
+ try {
631
+ const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command=']);
632
+ return stdout.trim() || null;
633
+ }
634
+ catch {
635
+ return null;
636
+ }
637
+ }
638
+ async function execLaunchctl(exec, args, ignoreFailure) {
639
+ try {
640
+ await exec('launchctl', args);
641
+ }
642
+ catch (error) {
643
+ if (!ignoreFailure) {
644
+ throw error;
645
+ }
646
+ }
647
+ }
648
+ function getLaunchdDomain(runtime) {
649
+ return `gui/${runtime.uid ?? process.getuid?.() ?? 501}`;
650
+ }
651
+ function getLaunchdServiceTarget(runtime) {
652
+ return `${getLaunchdDomain(runtime)}/${LAUNCHD_LABEL}`;
653
+ }
654
+ function parseLaunchdPid(output) {
655
+ const match = output.match(/\bpid\s*=\s*(\d+)/);
656
+ if (!match) {
657
+ return undefined;
658
+ }
659
+ const pid = Number(match[1]);
660
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
661
+ }
662
+ async function fileExists(path) {
663
+ try {
664
+ await stat(path);
665
+ return true;
666
+ }
667
+ catch {
668
+ return false;
669
+ }
670
+ }
671
+ function resolveEntrypointPath(runtime) {
672
+ return resolve(runtime.entrypointPath ?? runtime.argv?.[1] ?? process.argv[1]);
673
+ }
674
+ function resolveTilde(path, homeDir = homedir()) {
675
+ if (path === '~') {
676
+ return homeDir;
677
+ }
678
+ if (path.startsWith('~/')) {
679
+ return join(homeDir, path.slice(2));
680
+ }
681
+ return resolve(path);
682
+ }
683
+ function parsePort(value, flag) {
684
+ const port = Number(value);
685
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
686
+ throw new Error(`Invalid value for ${flag}: ${value}. Must be between 1 and 65535.`);
687
+ }
688
+ return port;
689
+ }
690
+ function parsePositiveInteger(value, flag) {
691
+ const parsed = Number(value);
692
+ if (!Number.isInteger(parsed) || parsed < 1) {
693
+ throw new Error(`Invalid value for ${flag}: ${value}. Must be a positive integer.`);
694
+ }
695
+ return parsed;
696
+ }
697
+ function validateLogLevel(value) {
698
+ const normalized = value.toLowerCase();
699
+ if (!['debug', 'info', 'warn', 'error'].includes(normalized)) {
700
+ throw new Error(`Invalid log level: ${value}`);
701
+ }
702
+ return normalized;
703
+ }
704
+ function validateHost(value) {
705
+ if (value === 'localhost' || value === '127.0.0.1' || value === '0.0.0.0') {
706
+ return value;
707
+ }
708
+ const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
709
+ if (!ipv4Pattern.test(value)) {
710
+ throw new Error(`Invalid host: ${value}`);
711
+ }
712
+ const octets = value.split('.').map(Number);
713
+ if (octets.some((octet) => octet < 0 || octet > 255)) {
714
+ throw new Error(`Invalid host: ${value}`);
715
+ }
716
+ return value;
717
+ }
718
+ function escapeXml(value) {
719
+ return value
720
+ .replaceAll('&', '&amp;')
721
+ .replaceAll('<', '&lt;')
722
+ .replaceAll('>', '&gt;')
723
+ .replaceAll('"', '&quot;')
724
+ .replaceAll("'", '&apos;');
725
+ }
726
+ function delay(ms) {
727
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
728
+ }
729
+ function formatError(error) {
730
+ if (error instanceof Error) {
731
+ return error.message;
732
+ }
733
+ return String(error);
734
+ }
735
+ //# sourceMappingURL=daemon.js.map