baychat 0.11.4 → 0.13.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.
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ // Pure attachment helpers for the CLI — no network, no disk, no MCP.
3
+ //
4
+ // Everything here is a DECISION that the tool handlers around it depend on, kept
5
+ // separate so each one can be tested on its own and reused by the pieces that
6
+ // have nothing else in common:
7
+ //
8
+ // • `mimeForFile` — the stdio upload path, before it opens a socket.
9
+ // • `attachmentsFromMetadata` — the MCP message renderer AND the relay's wake
10
+ // prompt: two very different outputs, one reading of the wire format.
11
+ // • `filenameFromContentDisposition` — the download tool, which turns a header
12
+ // written by a server into a path on the caller's own disk.
13
+ //
14
+ // Zero dependencies on purpose (the package ships no MIME library and adds none).
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.ALLOWED_ATTACHMENT_EXTENSIONS = void 0;
50
+ exports.mimeForFile = mimeForFile;
51
+ exports.attachmentsFromMetadata = attachmentsFromMetadata;
52
+ exports.filenameFromContentDisposition = filenameFromContentDisposition;
53
+ exports.formatBytes = formatBytes;
54
+ const path = __importStar(require("path"));
55
+ // ─── Extension → MIME ───────────────────────────────────────────────────────
56
+ // The extensions covering the server's SECURE_MIME_ALLOWLIST
57
+ // (apps/api/src/lib/attachments.ts). This map is a fast local NO, never a yes:
58
+ // the server re-checks every upload, so an extension missing here costs a clear
59
+ // error instead of a wasted round trip, and one wrongly present costs a 400.
60
+ //
61
+ // The plain-text family is spelled out rather than left to `.txt` alone: the
62
+ // server accepts `text/plain` bytes whatever the file is called, and an agent's
63
+ // most ordinary attachment is a `.md` note or a `.log` excerpt it just wrote.
64
+ // Refusing those locally would be this map inventing a limit the server has not.
65
+ const EXTENSION_MIME = {
66
+ jpg: "image/jpeg",
67
+ jpeg: "image/jpeg",
68
+ png: "image/png",
69
+ gif: "image/gif",
70
+ webp: "image/webp",
71
+ pdf: "application/pdf",
72
+ doc: "application/msword",
73
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
74
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
75
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
76
+ txt: "text/plain",
77
+ md: "text/plain",
78
+ log: "text/plain",
79
+ json: "text/plain",
80
+ yaml: "text/plain",
81
+ yml: "text/plain",
82
+ csv: "text/csv",
83
+ zip: "application/zip",
84
+ };
85
+ /** Every extension an upload may carry, for the error message that names them. */
86
+ exports.ALLOWED_ATTACHMENT_EXTENSIONS = Object.keys(EXTENSION_MIME);
87
+ /**
88
+ * The MIME type to upload a local file as, from its extension alone — or
89
+ * `undefined` when the server would refuse it, so the caller errors before
90
+ * reading a byte. Never sniffs content: the server decides, this only spares an
91
+ * obviously-doomed request.
92
+ */
93
+ function mimeForFile(filePath) {
94
+ // `path.extname` returns "" for a dotfile with no second dot (".zip"), which is
95
+ // exactly right — that file is named `.zip`, it is not a zip archive.
96
+ const ext = path.extname(filePath).replace(/^\./, "").toLowerCase();
97
+ return ext ? EXTENSION_MIME[ext] : undefined;
98
+ }
99
+ /**
100
+ * A URL we are willing to put in front of a model as "fetch this".
101
+ *
102
+ * Everything this module reads came off a message, and a message body is written
103
+ * by whoever sent it. The server is the layer that guarantees an `attachmentUrl`
104
+ * is one it signed — this is the second one, because the output of
105
+ * `attachmentsFromMetadata` becomes a `curl` line in the relay's wake prompt, and
106
+ * a prompt is a place where a newline or a quote is not a cosmetic problem.
107
+ *
108
+ * Refused: anything unparseable, any scheme but http(s), and any string carrying
109
+ * a control character (a newline would break out of the untrusted-messages block
110
+ * the prompt fences the message in; a NUL or an escape would not survive being
111
+ * printed intact anyway).
112
+ */
113
+ function fetchableUrl(value) {
114
+ if (typeof value !== "string" || value.length === 0)
115
+ return null;
116
+ // eslint-disable-next-line no-control-regex
117
+ if (/[\u0000-\u001f\u007f]/.test(value))
118
+ return null;
119
+ let parsed;
120
+ try {
121
+ parsed = new URL(value);
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
127
+ return null;
128
+ return value;
129
+ }
130
+ function itemFrom(value, fallbackType) {
131
+ if (!value || typeof value !== "object")
132
+ return null;
133
+ const o = value;
134
+ // The id is what makes the URL believable. The server mints an `attachmentUrl`
135
+ // only from an `attachmentId` it resolved to a row, so an entry that carries a
136
+ // URL and no id was not written by the server — it was written by a sender, and
137
+ // honouring it would hand the model an errand chosen by whoever sent the message.
138
+ const id = o.attachmentId;
139
+ if (typeof id !== "string" || id.length === 0)
140
+ return null;
141
+ const url = fetchableUrl(o.attachmentUrl);
142
+ // No signed URL means nothing to hand the model: an unsigned read path (the
143
+ // human-facing one) never adds it, and a line pointing nowhere is worse than
144
+ // no line at all.
145
+ if (url === null)
146
+ return null;
147
+ const type = typeof o.type === "string" && o.type ? o.type : undefined;
148
+ const out = {
149
+ attachmentUrl: url,
150
+ type: type ?? (typeof fallbackType === "string" && fallbackType ? fallbackType : "file"),
151
+ };
152
+ if (typeof o.mimeType === "string" && o.mimeType)
153
+ out.mimeType = o.mimeType;
154
+ if (typeof o.sizeBytes === "number" && Number.isFinite(o.sizeBytes)) {
155
+ out.sizeBytes = o.sizeBytes;
156
+ }
157
+ return out;
158
+ }
159
+ /**
160
+ * The attachments a message carries, in render order — from EITHER wire shape.
161
+ *
162
+ * `metadata.attachments[]` is the current one and wins; the legacy single
163
+ * (`attachmentId`/`attachmentUrl`, or an older `fileUrl` upload) is the fallback
164
+ * for a message written before the array existed, or by a client that still only
165
+ * writes the mirror. Both are read because the server dual-publishes: reading
166
+ * only the array would silently lose old messages, reading only the mirror would
167
+ * lose every file but the first.
168
+ *
169
+ * Pure and total — junk metadata yields `[]`, never a throw. Shared with the
170
+ * relay's wake prompt, so a change here changes what BOTH surfaces show.
171
+ */
172
+ function attachmentsFromMetadata(metadata) {
173
+ if (!metadata || typeof metadata !== "object")
174
+ return [];
175
+ const m = metadata;
176
+ if (Array.isArray(m.attachments)) {
177
+ const items = m.attachments
178
+ .map((item) => itemFrom(item))
179
+ .filter((item) => item !== null);
180
+ // An array that yielded nothing fetchable falls through to the mirror rather
181
+ // than reporting "no attachments" for a message that plainly has one.
182
+ if (items.length > 0)
183
+ return items;
184
+ }
185
+ const legacy = itemFrom(m, m.type);
186
+ if (legacy)
187
+ return [legacy];
188
+ // Older uploads carried the file as a bare `fileUrl` with no attachment row —
189
+ // so there is no id to hold it to, and the URL check is all there is.
190
+ const fileUrl = fetchableUrl(m.fileUrl);
191
+ if (fileUrl !== null) {
192
+ const type = typeof m.type === "string" && m.type ? m.type : "file";
193
+ return [{ attachmentUrl: fileUrl, type }];
194
+ }
195
+ return [];
196
+ }
197
+ // ─── Content-Disposition → a filename we are willing to write ───────────────
198
+ /** Long enough for any real name, short enough to stay under every filesystem's
199
+ * limit once a de-duplication suffix is appended. */
200
+ const MAX_FILENAME_LENGTH = 120;
201
+ /**
202
+ * Reduce an arbitrary name to a BASENAME safe to create inside a chosen
203
+ * directory. The header is written by whatever answered the request, and the
204
+ * result becomes a real path on the caller's disk, so this is a security
205
+ * boundary, not tidying: path separators, `..`, control characters and quotes
206
+ * are removed, and anything left empty becomes `download`.
207
+ */
208
+ function sanitizeDownloadName(raw) {
209
+ // Basename first: `../../etc/passwd` must become `passwd`, never a traversal.
210
+ const base = raw.split(/[\\/]/).pop() ?? "";
211
+ const cleaned = base
212
+ // Control characters and quotes: a name must not carry an unprintable
213
+ // byte onto disk or break out of the line it will be printed in.
214
+ .replace(/[\u0000-\u001f\u007f"]/g, "")
215
+ // Trimmed BEFORE the leading dots are stripped, or a padded name climbs: the
216
+ // RFC 5987 form `%20%2e%2e` decodes to " ..", whose first character is a
217
+ // space, so a strip-then-trim would leave `..` standing.
218
+ .trim()
219
+ .replace(/^\.+/, "") // ".." / ".hidden" — never a name that hides or climbs
220
+ .trim();
221
+ if (!cleaned)
222
+ return "download";
223
+ return cleaned.slice(0, MAX_FILENAME_LENGTH);
224
+ }
225
+ /**
226
+ * The filename a download should be written under, from a `Content-Disposition`
227
+ * header — sanitized. Prefers the RFC 5987 `filename*=UTF-8''…` form (the only
228
+ * one that can carry non-ASCII), then a quoted or bare `filename=`.
229
+ *
230
+ * Always returns a usable name: a missing, empty or nameless header yields
231
+ * `"download"` rather than failing the transfer over a cosmetic detail.
232
+ */
233
+ function filenameFromContentDisposition(header) {
234
+ if (!header)
235
+ return "download";
236
+ const extended = /filename\*\s*=\s*([^;]+)/i.exec(header);
237
+ if (extended) {
238
+ // `UTF-8''name` / `iso-8859-1'en'name` — the value after the second quote.
239
+ const value = extended[1].trim().replace(/^[^']*'[^']*'/, "");
240
+ try {
241
+ // Only accepted when it survives sanitation as a real name; otherwise the
242
+ // plain `filename=` (often an ASCII fallback of the same file) gets its turn.
243
+ const decoded = sanitizeDownloadName(decodeURIComponent(value));
244
+ if (decoded !== "download")
245
+ return decoded;
246
+ }
247
+ catch {
248
+ // A malformed percent-escape is not fatal: fall through to `filename=`.
249
+ }
250
+ }
251
+ const plain = /filename\s*=\s*("([^"]*)"|[^;]+)/i.exec(header);
252
+ if (plain)
253
+ return sanitizeDownloadName((plain[2] ?? plain[1] ?? "").trim());
254
+ return "download";
255
+ }
256
+ // ─── Sizes ──────────────────────────────────────────────────────────────────
257
+ const SIZE_UNITS = ["B", "KB", "MB", "GB"];
258
+ /** A short human size for a render line ("12 KB", "1.5 KB", "5 MB"). Total: a
259
+ * negative or non-finite input renders as `0 B` rather than as nonsense. */
260
+ function formatBytes(bytes) {
261
+ if (!Number.isFinite(bytes) || bytes <= 0)
262
+ return "0 B";
263
+ let value = bytes;
264
+ let unit = 0;
265
+ while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
266
+ value /= 1024;
267
+ unit += 1;
268
+ }
269
+ // One decimal only where it carries information: "1.5 KB" is useful, "1.5 B"
270
+ // is not, and "12.3 KB" is more precision than a chat line needs.
271
+ const rounded = unit === 0 || value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
272
+ return `${rounded} ${SIZE_UNITS[unit]}`;
273
+ }
package/dist/commands.js CHANGED
@@ -12,6 +12,8 @@ exports.cmdContext = cmdContext;
12
12
  exports.cmdSummary = cmdSummary;
13
13
  exports.cmdOnboard = cmdOnboard;
14
14
  exports.cmdSend = cmdSend;
15
+ exports.cmdTyping = cmdTyping;
16
+ exports.cmdReact = cmdReact;
15
17
  exports.resetSessionState = resetSessionState;
16
18
  exports.cmdCheck = cmdCheck;
17
19
  exports.cmdWatch = cmdWatch;
@@ -243,10 +245,63 @@ async function cmdOnboard(conversationId, opts = {}) {
243
245
  }
244
246
  }
245
247
  }
246
- async function cmdSend(conversationId, text) {
248
+ async function cmdSend(conversationId, text, replyToMessageId) {
247
249
  const creds = requireCredentials();
248
- const message = await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/messages`, { content: text });
249
- console.log(`Sent ${message.id} at ${message.createdAt}`);
250
+ const message = await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/messages`, {
251
+ content: text,
252
+ // Left out entirely when absent — see handleSendMessage for why an explicit null is wrong.
253
+ ...(replyToMessageId ? { replyToMessageId } : {}),
254
+ });
255
+ console.log(replyToMessageId
256
+ ? `Sent ${message.id} at ${message.createdAt}, replying to ${replyToMessageId}.`
257
+ : `Sent ${message.id} at ${message.createdAt}`);
258
+ }
259
+ /**
260
+ * `baychat typing <conversationId>` — show the typing indicator while this agent
261
+ * works, for CLI-driven agents that have no MCP client.
262
+ *
263
+ * ONE CALL, at the start of a turn. The server entry carries a few-second TTL and
264
+ * a sweep clears it, so there is deliberately no `baychat typing --stop`: an
265
+ * agent that dies mid-turn stops appearing to type without anyone cleaning up
266
+ * after it. The printed line says so, because a caller told only "typing on"
267
+ * would reasonably wrap this in a `while` loop — which is exactly the heartbeat
268
+ * this design exists to avoid.
269
+ */
270
+ async function cmdTyping(conversationId) {
271
+ const creds = requireCredentials();
272
+ await (0, api_1.apiRequest)(creds, "POST", `/api/agent-api/conversations/${conversationId}/typing`);
273
+ console.log("Typing shown — it lapses on its own in a few seconds. Nothing to stop.");
274
+ }
275
+ /**
276
+ * `baychat react <conversationId> <messageId> <emoji>` — acknowledge one message,
277
+ * for CLI-driven agents that have no MCP client.
278
+ *
279
+ * The counterpart to `baychat typing`, and deliberately the opposite of it: typing
280
+ * lapses in seconds and says "alive right now", while this STAYS on the message
281
+ * and says "I read this one, and I am on it". A task that runs for minutes needs
282
+ * the second one — by the time it finishes, the first has long since cleared and
283
+ * the person waiting cannot tell work from a crash.
284
+ *
285
+ * One reaction per agent per message: calling again replaces it (👀 while working,
286
+ * ✅ when done), and calling twice with the same emoji changes nothing. `--remove`
287
+ * takes it back and succeeds even if there was nothing to take back.
288
+ */
289
+ async function cmdReact(conversationId, messageId, emoji, opts = {}) {
290
+ const creds = requireCredentials();
291
+ const path = `/api/agent-api/conversations/${conversationId}/messages/${messageId}/reaction`;
292
+ if (opts.remove) {
293
+ await (0, api_1.apiRequest)(creds, "DELETE", path);
294
+ console.log(`Reaction removed from ${messageId}.`);
295
+ return;
296
+ }
297
+ const trimmed = emoji?.trim();
298
+ if (!trimmed) {
299
+ throw new Error("Usage: baychat react <conversationId> <messageId> <emoji> (👀 for 'seen')");
300
+ }
301
+ const result = await (0, api_1.apiRequest)(creds, "PUT", path, {
302
+ emoji: trimmed,
303
+ });
304
+ console.log(`Reacted ${result.emoji} to ${messageId} — it stays on the message until you change it.`);
250
305
  }
251
306
  async function agentNameMap(creds) {
252
307
  try {
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commands_1 = require("./commands");
5
+ const approve_hook_1 = require("./approve-hook");
5
6
  const connect_1 = require("./connect");
6
7
  const mcp_1 = require("./mcp");
7
8
  const mcp_config_1 = require("./mcp-config");
@@ -29,6 +30,16 @@ Usage:
29
30
  baychat whoami Show the connected agent identity
30
31
  baychat conversations List conversations this agent is in
31
32
  baychat send <conversationId> <text> Send a message
33
+ baychat typing <conversationId> Show the typing indicator while you work, so a long
34
+ tool call doesn't look like a crash. ONE call at the
35
+ start of a turn — it lapses on its own after a few
36
+ seconds, so there is nothing to stop and no loop to run
37
+ baychat react <conversationId> <messageId> <emoji> [--remove]
38
+ Acknowledge one message so the person who sent it
39
+ knows you have read it and are on it. Unlike typing,
40
+ the reaction STAYS on the message — use 👀 the moment
41
+ you pick up work that will take more than a few
42
+ seconds. Reacting again replaces it; --remove clears it
32
43
  baychat check <conversationId> Print messages since the last check
33
44
  baychat context <conversationId> Show the roster + the group's agent instructions
34
45
  baychat summary <conversationId> [--refresh]
@@ -65,6 +76,16 @@ Usage:
65
76
  came from, so "we can wake this headlessly"
66
77
  is a claim you can check
67
78
  baychat relay stop Stop the relay and disable it at boot
79
+ baychat approve-hook [--session <name>] [--timeout <sec>]
80
+ Claude Code PermissionRequest hook: send the
81
+ permission prompt to BayChat as a decision card
82
+ and answer it from your phone. Reads the hook
83
+ JSON on stdin, writes ONE decision object to
84
+ stdout, and always exits 0. FAILS CLOSED — every
85
+ error denies. NOT enabled by default; see
86
+ docs/features/REMOTE_APPROVAL_HOOK.md before
87
+ switching it on, because it moves the last line
88
+ between an agent and this machine onto a phone
68
89
  baychat relay attach --session <name> [--runtime claude|codex|hermes]
69
90
  [--resume-id <id>] [--timeout <sec>]
70
91
  Register this session with the relay and block
@@ -132,11 +153,40 @@ async function main() {
132
153
  await (0, commands_1.cmdConversations)();
133
154
  return 0;
134
155
  case "send": {
135
- const [conversationId, ...words] = args;
156
+ // `--reply-to <id>` is pulled out before the positionals, so the message text can
157
+ // contain anything — including a word that looks like a flag.
158
+ const flagAt = args.indexOf("--reply-to");
159
+ let replyToMessageId;
160
+ let rest = args;
161
+ if (flagAt !== -1) {
162
+ replyToMessageId = args[flagAt + 1];
163
+ if (!replyToMessageId)
164
+ throw new Error("--reply-to needs a message id.");
165
+ rest = [...args.slice(0, flagAt), ...args.slice(flagAt + 2)];
166
+ }
167
+ const [conversationId, ...words] = rest;
136
168
  if (!conversationId || words.length === 0) {
137
- throw new Error("Usage: baychat send <conversationId> <text>");
169
+ throw new Error("Usage: baychat send [--reply-to <messageId>] <conversationId> <text>");
170
+ }
171
+ await (0, commands_1.cmdSend)(conversationId, words.join(" "), replyToMessageId);
172
+ return 0;
173
+ }
174
+ case "typing": {
175
+ if (!args[0])
176
+ throw new Error("Usage: baychat typing <conversationId>");
177
+ await (0, commands_1.cmdTyping)(args[0]);
178
+ return 0;
179
+ }
180
+ case "react": {
181
+ // `--remove` needs no emoji, so the positionals are read from the flag-free
182
+ // list rather than by index: `react c1 m1 --remove` must not read "--remove"
183
+ // as the emoji and try to store it.
184
+ const [conversationId, messageId, emoji] = args.filter((a) => !a.startsWith("--"));
185
+ const remove = args.includes("--remove");
186
+ if (!conversationId || !messageId || (!emoji && !remove)) {
187
+ throw new Error("Usage: baychat react <conversationId> <messageId> <emoji> [--remove]");
138
188
  }
139
- await (0, commands_1.cmdSend)(conversationId, words.join(" "));
189
+ await (0, commands_1.cmdReact)(conversationId, messageId, emoji, { remove });
140
190
  return 0;
141
191
  }
142
192
  case "check": {
@@ -211,6 +261,12 @@ async function main() {
211
261
  throw new Error("Usage: baychat relay <start|status|stop|attach> [options]");
212
262
  }
213
263
  }
264
+ case "approve-hook":
265
+ // Never throws and always returns 0 — see the file header in approve-hook.ts. A thrown
266
+ // error here would reach main()'s catch, print to stderr and exit 1, and to Claude Code an
267
+ // exit-1 hook with no JSON on stdout is a NON-BLOCKING error: the tool call proceeds. The
268
+ // guard in the catch below covers that anyway.
269
+ return await (0, approve_hook_1.cmdApproveHook)(args);
214
270
  case "connect": {
215
271
  // A bare `connect` prints the client menu; positional() skips a leading flag
216
272
  // so `connect --base x codex` still finds the client.
@@ -244,6 +300,18 @@ async function main() {
244
300
  main()
245
301
  .then((code) => process.exit(code))
246
302
  .catch((err) => {
247
- console.error(err instanceof Error ? err.message : String(err));
303
+ const message = err instanceof Error ? err.message : String(err);
304
+ console.error(message);
305
+ // The last fail-closed guard. For every other command, exit 1 is the right answer. For
306
+ // `approve-hook` it is the WRONG one twice over: Claude Code treats a non-zero exit with no
307
+ // JSON on stdout as a non-blocking error and lets the tool call through, so a crash in the
308
+ // approval gate would silently grant the permission it exists to withhold. Print a deny and
309
+ // exit 0 instead. Deliberately not sharing main()'s dispatch — this must still hold if the
310
+ // failure happened while parsing argv, or inside an import.
311
+ if (process.argv[2] === "approve-hook") {
312
+ process.stdout.write(`${JSON.stringify((0, approve_hook_1.denyDecision)(`Denied by BayChat remote approval: the approval gate crashed (${message}). ` +
313
+ `This gate fails closed, so the call was refused rather than allowed.`))}\n`);
314
+ process.exit(0);
315
+ }
248
316
  process.exit(1);
249
317
  });