@sym-bot/sym 0.1.0 → 0.2.1
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/PRD.md +1 -1
- package/README.md +237 -70
- package/TECHNICAL-SPEC.md +250 -197
- package/bin/setup-claude.sh +31 -1
- package/bin/sym-daemon.js +437 -0
- package/docs/mesh-memory-protocol.md +563 -0
- package/docs/mmp-architecture-image-prompt.txt +12 -0
- package/docs/p2p-protocol-research.md +907 -0
- package/docs/protocol-wake.md +242 -0
- package/integrations/claude-code/mcp-server.js +264 -41
- package/integrations/telegram/bot.js +418 -0
- package/lib/ipc-client.js +241 -0
- package/lib/node.js +489 -39
- package/lib/transport.js +88 -0
- package/package.json +5 -3
- package/sym-relay/Dockerfile +7 -0
- package/sym-relay/lib/logger.js +28 -0
- package/sym-relay/lib/relay.js +388 -0
- package/sym-relay/package-lock.json +40 -0
- package/sym-relay/package.json +18 -0
- package/sym-relay/render.yaml +14 -0
- package/sym-relay/server.js +67 -0
- package/.mcp.json +0 -12
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ── EPIPE/EIO Safety (must be first — OpenClaw issue #4632) ────
|
|
5
|
+
// launchd may close stdout/stderr pipes during restart. Without this,
|
|
6
|
+
// Node.js throws uncaught EPIPE and enters a crash loop with exponential
|
|
7
|
+
// throttle, causing hours-long outages.
|
|
8
|
+
function suppressEpipe(stream) {
|
|
9
|
+
stream.on('error', (err) => {
|
|
10
|
+
if (err.code === 'EPIPE' || err.code === 'EIO') process.exit(0);
|
|
11
|
+
throw err;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
suppressEpipe(process.stdout);
|
|
15
|
+
suppressEpipe(process.stderr);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* sym-daemon — persistent physical mesh node for macOS/Linux.
|
|
19
|
+
*
|
|
20
|
+
* Runs as a background service (launchd LaunchAgent on macOS, systemd on Linux).
|
|
21
|
+
* Maintains relay connection, Bonjour discovery, peer state, and wake channels
|
|
22
|
+
* independently of any application. Virtual nodes (Claude Code, MeloTune Mac, etc.)
|
|
23
|
+
* connect via Unix socket IPC.
|
|
24
|
+
*
|
|
25
|
+
* MMP v0.2.0: The daemon IS the device's mesh presence.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* sym-daemon # Run in foreground
|
|
29
|
+
* sym-daemon --install # Install as launchd LaunchAgent (macOS)
|
|
30
|
+
* sym-daemon --uninstall # Remove LaunchAgent
|
|
31
|
+
* sym-daemon --status # Show daemon status
|
|
32
|
+
*
|
|
33
|
+
* Copyright (c) 2026 SYM.BOT Ltd. Apache 2.0 License.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const net = require('net');
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const os = require('os');
|
|
40
|
+
const { SymNode } = require('../lib/node');
|
|
41
|
+
|
|
42
|
+
// ── Configuration ──────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const SOCKET_PATH = process.env.SYM_SOCKET || '/tmp/sym.sock';
|
|
45
|
+
const NODE_NAME = process.env.SYM_NODE_NAME || os.hostname().split('.')[0].toLowerCase();
|
|
46
|
+
const LOG_DIR = path.join(os.homedir(), 'Library', 'Logs', 'sym-daemon');
|
|
47
|
+
|
|
48
|
+
// Load relay config from ~/.sym/relay.env if env vars not set
|
|
49
|
+
if (!process.env.SYM_RELAY_URL) {
|
|
50
|
+
const envFile = path.join(os.homedir(), '.sym', 'relay.env');
|
|
51
|
+
if (fs.existsSync(envFile)) {
|
|
52
|
+
for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) {
|
|
53
|
+
const m = line.match(/^(\w+)=(.*)$/);
|
|
54
|
+
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const relayUrl = process.env.SYM_RELAY_URL || null;
|
|
60
|
+
const relayToken = process.env.SYM_RELAY_TOKEN || null;
|
|
61
|
+
|
|
62
|
+
// ── CLI Commands ───────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const args = process.argv.slice(2);
|
|
65
|
+
|
|
66
|
+
if (args.includes('--install')) {
|
|
67
|
+
installLaunchAgent();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (args.includes('--uninstall')) {
|
|
72
|
+
uninstallLaunchAgent();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (args.includes('--status')) {
|
|
77
|
+
showStatus();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── SYM Node ───────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const node = new SymNode({
|
|
84
|
+
name: NODE_NAME,
|
|
85
|
+
cognitiveProfile: `Physical mesh node for ${os.hostname()}. Routes frames between virtual nodes and the mesh.`,
|
|
86
|
+
relay: relayUrl,
|
|
87
|
+
relayToken: relayToken,
|
|
88
|
+
silent: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── IPC Server (Unix Socket) ───────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Connected virtual nodes. socketId → { socket, name, cognitiveProfile } */
|
|
94
|
+
const virtualNodes = new Map();
|
|
95
|
+
let nextSocketId = 1;
|
|
96
|
+
|
|
97
|
+
function startIPCServer() {
|
|
98
|
+
// Clean up stale socket
|
|
99
|
+
if (fs.existsSync(SOCKET_PATH)) {
|
|
100
|
+
try { fs.unlinkSync(SOCKET_PATH); } catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const server = net.createServer((socket) => {
|
|
104
|
+
const socketId = nextSocketId++;
|
|
105
|
+
let buffer = '';
|
|
106
|
+
|
|
107
|
+
socket.on('data', (data) => {
|
|
108
|
+
buffer += data.toString();
|
|
109
|
+
let idx;
|
|
110
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
111
|
+
const line = buffer.slice(0, idx);
|
|
112
|
+
buffer = buffer.slice(idx + 1);
|
|
113
|
+
if (line.trim()) {
|
|
114
|
+
try {
|
|
115
|
+
handleIPCMessage(socketId, socket, JSON.parse(line));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log(`IPC parse error: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
socket.on('close', () => {
|
|
124
|
+
const vn = virtualNodes.get(socketId);
|
|
125
|
+
if (vn) {
|
|
126
|
+
log(`Virtual node disconnected: ${vn.name}`);
|
|
127
|
+
virtualNodes.delete(socketId);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
socket.on('error', (err) => {
|
|
132
|
+
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
133
|
+
log(`IPC socket error: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
virtualNodes.delete(socketId);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
server.listen(SOCKET_PATH, () => {
|
|
140
|
+
fs.chmodSync(SOCKET_PATH, 0o700);
|
|
141
|
+
log(`IPC server listening: ${SOCKET_PATH}`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
server.on('error', (err) => {
|
|
145
|
+
log(`IPC server error: ${err.message}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return server;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleIPCMessage(socketId, socket, msg) {
|
|
153
|
+
switch (msg.type) {
|
|
154
|
+
case 'register': {
|
|
155
|
+
virtualNodes.set(socketId, {
|
|
156
|
+
socket,
|
|
157
|
+
name: msg.name || `virtual-${socketId}`,
|
|
158
|
+
cognitiveProfile: msg.cognitiveProfile || null,
|
|
159
|
+
});
|
|
160
|
+
sendIPC(socket, {
|
|
161
|
+
type: 'registered',
|
|
162
|
+
nodeId: node._identity?.nodeId,
|
|
163
|
+
name: node.name,
|
|
164
|
+
relay: relayUrl,
|
|
165
|
+
});
|
|
166
|
+
log(`Virtual node registered: ${msg.name}`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'mood':
|
|
171
|
+
if (msg.mood) {
|
|
172
|
+
node.broadcastMood(msg.mood, { context: msg.context });
|
|
173
|
+
sendIPC(socket, { type: 'result', action: 'mood', peers: node.peers().length });
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case 'message':
|
|
178
|
+
if (msg.content) {
|
|
179
|
+
node.send(msg.content, msg.to ? { to: msg.to } : {});
|
|
180
|
+
sendIPC(socket, { type: 'result', action: 'message', peers: node.peers().length });
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'remember':
|
|
185
|
+
if (msg.content) {
|
|
186
|
+
const entry = node.remember(msg.content, { tags: msg.tags });
|
|
187
|
+
sendIPC(socket, { type: 'result', action: 'remember', key: entry.key });
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'recall': {
|
|
192
|
+
const results = node.recall(msg.query || '');
|
|
193
|
+
sendIPC(socket, { type: 'result', action: 'recall', results });
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'send':
|
|
198
|
+
if (msg.message) {
|
|
199
|
+
node.send(msg.message);
|
|
200
|
+
sendIPC(socket, { type: 'result', action: 'send', peers: node.peers().length });
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'peers':
|
|
205
|
+
sendIPC(socket, { type: 'result', action: 'peers', peers: node.peers() });
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case 'status':
|
|
209
|
+
sendIPC(socket, {
|
|
210
|
+
type: 'result',
|
|
211
|
+
action: 'status',
|
|
212
|
+
status: node.status(),
|
|
213
|
+
virtualNodes: Array.from(virtualNodes.values()).map(v => v.name),
|
|
214
|
+
});
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
default:
|
|
218
|
+
log(`Unknown IPC message type: ${msg.type}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sendIPC(socket, msg) {
|
|
223
|
+
try { socket.write(JSON.stringify(msg) + '\n'); } catch {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Forward mesh events to all registered virtual nodes. */
|
|
227
|
+
function forwardEventsToVirtualNodes() {
|
|
228
|
+
const events = [
|
|
229
|
+
['mood-accepted', (d) => ({ type: 'event', event: 'mood-accepted', data: d })],
|
|
230
|
+
['mood-rejected', (d) => ({ type: 'event', event: 'mood-rejected', data: d })],
|
|
231
|
+
['peer-joined', (d) => ({ type: 'event', event: 'peer-joined', data: d })],
|
|
232
|
+
['peer-left', (d) => ({ type: 'event', event: 'peer-left', data: d })],
|
|
233
|
+
['coupling-decision', (d) => ({ type: 'event', event: 'coupling-decision', data: d })],
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
for (const [event, formatter] of events) {
|
|
237
|
+
node.on(event, (data) => broadcastToVirtualNodes(formatter(data)));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
node.on('message', (from, content) => {
|
|
241
|
+
broadcastToVirtualNodes({ type: 'event', event: 'message', data: { from, content } });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
node.on('memory-received', ({ from, entry, decision }) => {
|
|
245
|
+
broadcastToVirtualNodes({ type: 'event', event: 'memory-received', data: { from, content: entry.content, decision } });
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function broadcastToVirtualNodes(msg) {
|
|
250
|
+
const data = JSON.stringify(msg) + '\n';
|
|
251
|
+
for (const [id, vn] of virtualNodes) {
|
|
252
|
+
try { vn.socket.write(data); } catch { virtualNodes.delete(id); }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── launchd Install/Uninstall ──────────────────────────────────
|
|
257
|
+
|
|
258
|
+
function launchAgentPlist() {
|
|
259
|
+
// Resolve node binary — prefer stable symlink over Cellar path
|
|
260
|
+
const nodePath = fs.existsSync('/opt/homebrew/bin/node')
|
|
261
|
+
? '/opt/homebrew/bin/node'
|
|
262
|
+
: fs.existsSync('/usr/local/bin/node')
|
|
263
|
+
? '/usr/local/bin/node'
|
|
264
|
+
: process.execPath;
|
|
265
|
+
|
|
266
|
+
const scriptPath = path.resolve(__dirname, 'sym-daemon.js');
|
|
267
|
+
const symDir = path.resolve(__dirname, '..');
|
|
268
|
+
|
|
269
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
270
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
271
|
+
<plist version="1.0">
|
|
272
|
+
<dict>
|
|
273
|
+
<key>Label</key>
|
|
274
|
+
<string>bot.sym.daemon</string>
|
|
275
|
+
<key>ProgramArguments</key>
|
|
276
|
+
<array>
|
|
277
|
+
<string>${nodePath}</string>
|
|
278
|
+
<string>${scriptPath}</string>
|
|
279
|
+
</array>
|
|
280
|
+
<key>WorkingDirectory</key>
|
|
281
|
+
<string>${symDir}</string>
|
|
282
|
+
<key>EnvironmentVariables</key>
|
|
283
|
+
<dict>
|
|
284
|
+
<key>PATH</key>
|
|
285
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
286
|
+
<key>HOME</key>
|
|
287
|
+
<string>${os.homedir()}</string>
|
|
288
|
+
<key>NODE_ENV</key>
|
|
289
|
+
<string>production</string>
|
|
290
|
+
</dict>
|
|
291
|
+
<key>RunAtLoad</key>
|
|
292
|
+
<true/>
|
|
293
|
+
<key>KeepAlive</key>
|
|
294
|
+
<true/>
|
|
295
|
+
<key>ThrottleInterval</key>
|
|
296
|
+
<integer>5</integer>
|
|
297
|
+
<key>ProcessType</key>
|
|
298
|
+
<string>Background</string>
|
|
299
|
+
<key>ExitTimeOut</key>
|
|
300
|
+
<integer>15</integer>
|
|
301
|
+
<key>StandardOutPath</key>
|
|
302
|
+
<string>${LOG_DIR}/stdout.log</string>
|
|
303
|
+
<key>StandardErrorPath</key>
|
|
304
|
+
<string>${LOG_DIR}/stderr.log</string>
|
|
305
|
+
</dict>
|
|
306
|
+
</plist>`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function installLaunchAgent() {
|
|
310
|
+
if (process.platform !== 'darwin') {
|
|
311
|
+
console.error('--install is macOS only. On Linux, create a systemd service.');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
316
|
+
const plistPath = path.join(plistDir, 'bot.sym.daemon.plist');
|
|
317
|
+
|
|
318
|
+
// Ensure directories exist
|
|
319
|
+
if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
|
|
320
|
+
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
321
|
+
|
|
322
|
+
// Write plist with correct permissions (644 — launchd rejects writable plists)
|
|
323
|
+
fs.writeFileSync(plistPath, launchAgentPlist(), { mode: 0o644 });
|
|
324
|
+
console.log(`Installed: ${plistPath}`);
|
|
325
|
+
|
|
326
|
+
// Load using modern launchctl API
|
|
327
|
+
const { execSync } = require('child_process');
|
|
328
|
+
const uid = process.getuid();
|
|
329
|
+
try { execSync(`launchctl bootout gui/${uid}/bot.sym.daemon 2>/dev/null`); } catch {}
|
|
330
|
+
execSync(`launchctl bootstrap gui/${uid} "${plistPath}"`);
|
|
331
|
+
console.log(`sym-daemon started. Logs: ${LOG_DIR}/`);
|
|
332
|
+
console.log('Check status: sym-daemon --status');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function uninstallLaunchAgent() {
|
|
336
|
+
if (process.platform !== 'darwin') {
|
|
337
|
+
console.error('--uninstall is macOS only.');
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'bot.sym.daemon.plist');
|
|
342
|
+
const { execSync } = require('child_process');
|
|
343
|
+
|
|
344
|
+
try { execSync(`launchctl bootout gui/${process.getuid()}/bot.sym.daemon`); } catch {}
|
|
345
|
+
|
|
346
|
+
if (fs.existsSync(plistPath)) {
|
|
347
|
+
fs.unlinkSync(plistPath);
|
|
348
|
+
console.log('sym-daemon uninstalled.');
|
|
349
|
+
} else {
|
|
350
|
+
console.log('sym-daemon is not installed.');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (fs.existsSync(SOCKET_PATH)) {
|
|
354
|
+
try { fs.unlinkSync(SOCKET_PATH); } catch {}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function showStatus() {
|
|
359
|
+
if (!fs.existsSync(SOCKET_PATH)) {
|
|
360
|
+
console.log('sym-daemon: not running (no socket)');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const client = net.createConnection(SOCKET_PATH, () => {
|
|
365
|
+
client.write(JSON.stringify({ type: 'status' }) + '\n');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
let data = '';
|
|
369
|
+
client.on('data', (chunk) => {
|
|
370
|
+
data += chunk;
|
|
371
|
+
if (data.includes('\n')) {
|
|
372
|
+
try {
|
|
373
|
+
const msg = JSON.parse(data.split('\n')[0]);
|
|
374
|
+
if (msg.type === 'result' && msg.status) {
|
|
375
|
+
const s = msg.status;
|
|
376
|
+
console.log('sym-daemon: running');
|
|
377
|
+
console.log(` node: ${s.name} (${s.nodeId})`);
|
|
378
|
+
console.log(` relay: ${s.relayConnected ? 'connected' : 'disconnected'} (${s.relay || 'none'})`);
|
|
379
|
+
console.log(` peers: ${s.peerCount}`);
|
|
380
|
+
console.log(` memories: ${s.memoryCount}`);
|
|
381
|
+
console.log(` virtual: ${(msg.virtualNodes || []).join(', ') || 'none'}`);
|
|
382
|
+
console.log(` socket: ${SOCKET_PATH}`);
|
|
383
|
+
}
|
|
384
|
+
} catch {}
|
|
385
|
+
client.end();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
client.on('error', () => {
|
|
390
|
+
console.log('sym-daemon: socket exists but not responding');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
setTimeout(() => client.destroy(), 3000);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Logging ────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
function log(msg) {
|
|
399
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
400
|
+
console.log(`[${ts}] ${msg}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Startup ────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
async function main() {
|
|
406
|
+
log(`sym-daemon starting: ${NODE_NAME}`);
|
|
407
|
+
log(` relay: ${relayUrl || 'none'}`);
|
|
408
|
+
log(` socket: ${SOCKET_PATH}`);
|
|
409
|
+
|
|
410
|
+
await node.start();
|
|
411
|
+
log(`SYM node started (${node._identity?.nodeId?.slice(0, 8)})`);
|
|
412
|
+
|
|
413
|
+
forwardEventsToVirtualNodes();
|
|
414
|
+
|
|
415
|
+
const ipcServer = startIPCServer();
|
|
416
|
+
|
|
417
|
+
log('sym-daemon ready');
|
|
418
|
+
|
|
419
|
+
// Graceful shutdown (launchd sends SIGTERM, then SIGKILL after ExitTimeOut)
|
|
420
|
+
const shutdown = () => {
|
|
421
|
+
log('Shutting down...');
|
|
422
|
+
node.stop();
|
|
423
|
+
ipcServer.close();
|
|
424
|
+
if (fs.existsSync(SOCKET_PATH)) {
|
|
425
|
+
try { fs.unlinkSync(SOCKET_PATH); } catch {}
|
|
426
|
+
}
|
|
427
|
+
process.exit(0);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
process.on('SIGTERM', shutdown);
|
|
431
|
+
process.on('SIGINT', shutdown);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
main().catch((err) => {
|
|
435
|
+
log(`Fatal: ${err.message}`);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
});
|