@stevederico/dotbot 0.16.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 +136 -0
- package/README.md +380 -0
- package/bin/dotbot.js +461 -0
- package/core/agent.js +779 -0
- package/core/compaction.js +261 -0
- package/core/cron_handler.js +262 -0
- package/core/events.js +229 -0
- package/core/failover.js +193 -0
- package/core/gptoss_tool_parser.js +173 -0
- package/core/init.js +154 -0
- package/core/normalize.js +324 -0
- package/core/trigger_handler.js +148 -0
- package/docs/core.md +103 -0
- package/docs/protected-files.md +59 -0
- package/examples/sqlite-session-example.js +69 -0
- package/index.js +341 -0
- package/observer/index.js +164 -0
- package/package.json +42 -0
- package/storage/CronStore.js +145 -0
- package/storage/EventStore.js +71 -0
- package/storage/MemoryStore.js +175 -0
- package/storage/MongoAdapter.js +291 -0
- package/storage/MongoCronAdapter.js +347 -0
- package/storage/MongoTaskAdapter.js +242 -0
- package/storage/MongoTriggerAdapter.js +158 -0
- package/storage/SQLiteAdapter.js +382 -0
- package/storage/SQLiteCronAdapter.js +562 -0
- package/storage/SQLiteEventStore.js +300 -0
- package/storage/SQLiteMemoryAdapter.js +240 -0
- package/storage/SQLiteTaskAdapter.js +419 -0
- package/storage/SQLiteTriggerAdapter.js +262 -0
- package/storage/SessionStore.js +149 -0
- package/storage/TaskStore.js +100 -0
- package/storage/TriggerStore.js +90 -0
- package/storage/cron_constants.js +48 -0
- package/storage/index.js +21 -0
- package/tools/appgen.js +311 -0
- package/tools/browser.js +634 -0
- package/tools/code.js +101 -0
- package/tools/events.js +145 -0
- package/tools/files.js +201 -0
- package/tools/images.js +253 -0
- package/tools/index.js +97 -0
- package/tools/jobs.js +159 -0
- package/tools/memory.js +332 -0
- package/tools/messages.js +135 -0
- package/tools/notify.js +42 -0
- package/tools/tasks.js +404 -0
- package/tools/triggers.js +159 -0
- package/tools/weather.js +82 -0
- package/tools/web.js +283 -0
- package/utils/providers.js +136 -0
package/bin/dotbot.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dotbot CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* dotbot chat "What's the weather?" One-shot query
|
|
8
|
+
* dotbot repl Interactive chat session
|
|
9
|
+
* dotbot serve --port 3000 Start HTTP server
|
|
10
|
+
* dotbot --help Show help
|
|
11
|
+
*
|
|
12
|
+
* Requires Node.js 22.5+ with --experimental-sqlite flag, or Node.js 23+
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parseArgs } from 'node:util';
|
|
16
|
+
import * as readline from 'node:readline';
|
|
17
|
+
import { createServer } from 'node:http';
|
|
18
|
+
|
|
19
|
+
// Lazy-loaded modules (avoid SQLite import on --help)
|
|
20
|
+
let stores = null;
|
|
21
|
+
let coreTools = null;
|
|
22
|
+
let AI_PROVIDERS = null;
|
|
23
|
+
let agentLoop = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lazy-load dotbot modules.
|
|
27
|
+
*/
|
|
28
|
+
async function loadModules() {
|
|
29
|
+
if (stores) return;
|
|
30
|
+
const mod = await import('../index.js');
|
|
31
|
+
stores = {
|
|
32
|
+
SQLiteSessionStore: mod.SQLiteSessionStore,
|
|
33
|
+
SQLiteCronStore: mod.SQLiteCronStore,
|
|
34
|
+
SQLiteTaskStore: mod.SQLiteTaskStore,
|
|
35
|
+
SQLiteTriggerStore: mod.SQLiteTriggerStore,
|
|
36
|
+
SQLiteMemoryStore: mod.SQLiteMemoryStore,
|
|
37
|
+
SQLiteEventStore: mod.SQLiteEventStore,
|
|
38
|
+
};
|
|
39
|
+
coreTools = mod.coreTools;
|
|
40
|
+
AI_PROVIDERS = mod.AI_PROVIDERS;
|
|
41
|
+
agentLoop = mod.agentLoop;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const VERSION = '0.16.0';
|
|
45
|
+
const DEFAULT_PORT = 3000;
|
|
46
|
+
const DEFAULT_DB = './dotbot.db';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Print help message.
|
|
50
|
+
*/
|
|
51
|
+
function printHelp() {
|
|
52
|
+
console.log(`
|
|
53
|
+
dotbot v${VERSION} — AI agent CLI
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
dotbot chat "message" Send a one-shot message
|
|
57
|
+
dotbot repl Interactive chat session
|
|
58
|
+
dotbot serve [--port N] Start HTTP server (default: ${DEFAULT_PORT})
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--provider, -p AI provider: xai, anthropic, openai, ollama (default: xai)
|
|
62
|
+
--model, -m Model name (default: grok-3)
|
|
63
|
+
--db SQLite database path (default: ${DEFAULT_DB})
|
|
64
|
+
--port Server port for 'serve' command (default: ${DEFAULT_PORT})
|
|
65
|
+
--help, -h Show this help
|
|
66
|
+
--version, -v Show version
|
|
67
|
+
|
|
68
|
+
Environment Variables:
|
|
69
|
+
XAI_API_KEY API key for xAI
|
|
70
|
+
ANTHROPIC_API_KEY API key for Anthropic
|
|
71
|
+
OPENAI_API_KEY API key for OpenAI
|
|
72
|
+
OLLAMA_BASE_URL Base URL for Ollama (default: http://localhost:11434)
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
dotbot chat "What's the weather in SF?"
|
|
76
|
+
dotbot repl --provider anthropic --model claude-sonnet-4-5
|
|
77
|
+
dotbot serve --port 8080
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse CLI arguments.
|
|
83
|
+
*/
|
|
84
|
+
function parseCliArgs() {
|
|
85
|
+
try {
|
|
86
|
+
const { values, positionals } = parseArgs({
|
|
87
|
+
allowPositionals: true,
|
|
88
|
+
options: {
|
|
89
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
90
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
91
|
+
provider: { type: 'string', short: 'p', default: 'xai' },
|
|
92
|
+
model: { type: 'string', short: 'm', default: 'grok-3' },
|
|
93
|
+
db: { type: 'string', default: DEFAULT_DB },
|
|
94
|
+
port: { type: 'string', default: String(DEFAULT_PORT) },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return { ...values, positionals };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`Error: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get provider config with API key from environment.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} providerId - Provider ID
|
|
108
|
+
* @returns {Object} Provider config with headers
|
|
109
|
+
*/
|
|
110
|
+
async function getProviderConfig(providerId) {
|
|
111
|
+
await loadModules();
|
|
112
|
+
const base = AI_PROVIDERS[providerId];
|
|
113
|
+
if (!base) {
|
|
114
|
+
console.error(`Unknown provider: ${providerId}`);
|
|
115
|
+
console.error(`Available: ${Object.keys(AI_PROVIDERS).join(', ')}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const envKey = base.envKey;
|
|
120
|
+
const apiKey = process.env[envKey];
|
|
121
|
+
|
|
122
|
+
if (!apiKey && providerId !== 'ollama') {
|
|
123
|
+
console.error(`Missing ${envKey} environment variable`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (providerId === 'ollama') {
|
|
128
|
+
const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
129
|
+
return { ...base, apiUrl: `${baseUrl}/api/chat` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
headers: () => base.headers(apiKey),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Initialize stores.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} dbPath - Path to SQLite database
|
|
142
|
+
* @returns {Promise<Object>} Initialized stores
|
|
143
|
+
*/
|
|
144
|
+
async function initStores(dbPath) {
|
|
145
|
+
await loadModules();
|
|
146
|
+
|
|
147
|
+
const sessionStore = new stores.SQLiteSessionStore();
|
|
148
|
+
await sessionStore.init(dbPath, {
|
|
149
|
+
prefsFetcher: async () => ({ agentName: 'Dotbot', agentPersonality: '' }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const cronStore = new stores.SQLiteCronStore();
|
|
153
|
+
await cronStore.init({ dbPath });
|
|
154
|
+
|
|
155
|
+
const taskStore = new stores.SQLiteTaskStore();
|
|
156
|
+
await taskStore.init({ dbPath });
|
|
157
|
+
|
|
158
|
+
const triggerStore = new stores.SQLiteTriggerStore();
|
|
159
|
+
await triggerStore.init({ dbPath });
|
|
160
|
+
|
|
161
|
+
const memoryStore = new stores.SQLiteMemoryStore();
|
|
162
|
+
await memoryStore.init({ dbPath });
|
|
163
|
+
|
|
164
|
+
const eventStore = new stores.SQLiteEventStore();
|
|
165
|
+
await eventStore.init({ dbPath });
|
|
166
|
+
|
|
167
|
+
return { sessionStore, cronStore, taskStore, triggerStore, memoryStore, eventStore };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Run a single chat message and stream output.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} message - User message
|
|
174
|
+
* @param {Object} options - CLI options
|
|
175
|
+
*/
|
|
176
|
+
async function runChat(message, options) {
|
|
177
|
+
const storesObj = await initStores(options.db);
|
|
178
|
+
const provider = await getProviderConfig(options.provider);
|
|
179
|
+
|
|
180
|
+
const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
|
|
181
|
+
|
|
182
|
+
const context = {
|
|
183
|
+
userID: 'cli-user',
|
|
184
|
+
sessionId: session.id,
|
|
185
|
+
providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
|
|
186
|
+
...storesObj,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const messages = [{ role: 'user', content: message }];
|
|
190
|
+
|
|
191
|
+
process.stdout.write('\n');
|
|
192
|
+
|
|
193
|
+
for await (const event of agentLoop({
|
|
194
|
+
model: options.model,
|
|
195
|
+
messages,
|
|
196
|
+
tools: coreTools,
|
|
197
|
+
provider,
|
|
198
|
+
context,
|
|
199
|
+
})) {
|
|
200
|
+
switch (event.type) {
|
|
201
|
+
case 'text_delta':
|
|
202
|
+
process.stdout.write(event.text);
|
|
203
|
+
break;
|
|
204
|
+
case 'tool_start':
|
|
205
|
+
process.stdout.write(`\n[${event.name}] `);
|
|
206
|
+
break;
|
|
207
|
+
case 'tool_result':
|
|
208
|
+
process.stdout.write(`done\n`);
|
|
209
|
+
break;
|
|
210
|
+
case 'error':
|
|
211
|
+
console.error(`\nError: ${event.error}`);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
process.stdout.write('\n\n');
|
|
217
|
+
process.exit(0);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Run interactive REPL.
|
|
222
|
+
*
|
|
223
|
+
* @param {Object} options - CLI options
|
|
224
|
+
*/
|
|
225
|
+
async function runRepl(options) {
|
|
226
|
+
const storesObj = await initStores(options.db);
|
|
227
|
+
const provider = await getProviderConfig(options.provider);
|
|
228
|
+
|
|
229
|
+
const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
|
|
230
|
+
const messages = [];
|
|
231
|
+
|
|
232
|
+
const context = {
|
|
233
|
+
userID: 'cli-user',
|
|
234
|
+
sessionId: session.id,
|
|
235
|
+
providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
|
|
236
|
+
...storesObj,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const rl = readline.createInterface({
|
|
240
|
+
input: process.stdin,
|
|
241
|
+
output: process.stdout,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
|
|
245
|
+
console.log('Type /quit to exit, /clear to reset conversation\n');
|
|
246
|
+
|
|
247
|
+
const prompt = () => {
|
|
248
|
+
rl.question('> ', async (input) => {
|
|
249
|
+
const trimmed = input.trim();
|
|
250
|
+
|
|
251
|
+
if (!trimmed) {
|
|
252
|
+
prompt();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (trimmed === '/quit' || trimmed === '/exit') {
|
|
257
|
+
console.log('Goodbye!');
|
|
258
|
+
rl.close();
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (trimmed === '/clear') {
|
|
263
|
+
messages.length = 0;
|
|
264
|
+
console.log('Conversation cleared.\n');
|
|
265
|
+
prompt();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
messages.push({ role: 'user', content: trimmed });
|
|
270
|
+
|
|
271
|
+
process.stdout.write('\n');
|
|
272
|
+
let assistantContent = '';
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
for await (const event of agentLoop({
|
|
276
|
+
model: options.model,
|
|
277
|
+
messages: [...messages],
|
|
278
|
+
tools: coreTools,
|
|
279
|
+
provider,
|
|
280
|
+
context,
|
|
281
|
+
})) {
|
|
282
|
+
switch (event.type) {
|
|
283
|
+
case 'text_delta':
|
|
284
|
+
process.stdout.write(event.text);
|
|
285
|
+
assistantContent += event.text;
|
|
286
|
+
break;
|
|
287
|
+
case 'tool_start':
|
|
288
|
+
process.stdout.write(`\n[${event.name}] `);
|
|
289
|
+
break;
|
|
290
|
+
case 'tool_result':
|
|
291
|
+
process.stdout.write(`done\n`);
|
|
292
|
+
break;
|
|
293
|
+
case 'error':
|
|
294
|
+
console.error(`\nError: ${event.error}`);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (assistantContent) {
|
|
300
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error(`\nError: ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
process.stdout.write('\n\n');
|
|
307
|
+
prompt();
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
prompt();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Run HTTP server.
|
|
316
|
+
*
|
|
317
|
+
* @param {Object} options - CLI options
|
|
318
|
+
*/
|
|
319
|
+
async function runServer(options) {
|
|
320
|
+
const port = parseInt(options.port, 10);
|
|
321
|
+
const storesObj = await initStores(options.db);
|
|
322
|
+
|
|
323
|
+
const server = createServer(async (req, res) => {
|
|
324
|
+
// CORS headers
|
|
325
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
326
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
327
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
328
|
+
|
|
329
|
+
if (req.method === 'OPTIONS') {
|
|
330
|
+
res.writeHead(204);
|
|
331
|
+
res.end();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
336
|
+
|
|
337
|
+
// Health check
|
|
338
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
339
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({ status: 'ok', version: VERSION }));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Chat endpoint
|
|
345
|
+
if (req.method === 'POST' && url.pathname === '/chat') {
|
|
346
|
+
let body = '';
|
|
347
|
+
for await (const chunk of req) body += chunk;
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const { message, provider: providerId = 'anthropic', model = 'claude-sonnet-4-5', sessionId } = JSON.parse(body);
|
|
351
|
+
|
|
352
|
+
if (!message) {
|
|
353
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
354
|
+
res.end(JSON.stringify({ error: 'message required' }));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const provider = await getProviderConfig(providerId);
|
|
359
|
+
let session;
|
|
360
|
+
|
|
361
|
+
if (sessionId) {
|
|
362
|
+
session = await storesObj.sessionStore.getSessionInternal(sessionId);
|
|
363
|
+
}
|
|
364
|
+
if (!session) {
|
|
365
|
+
session = await storesObj.sessionStore.createSession('api-user', model, providerId);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const context = {
|
|
369
|
+
userID: 'api-user',
|
|
370
|
+
sessionId: session.id,
|
|
371
|
+
providers: { [providerId]: { apiKey: process.env[AI_PROVIDERS[providerId]?.envKey] } },
|
|
372
|
+
...storesObj,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const messages = [...(session.messages || []), { role: 'user', content: message }];
|
|
376
|
+
|
|
377
|
+
res.writeHead(200, {
|
|
378
|
+
'Content-Type': 'text/event-stream',
|
|
379
|
+
'Cache-Control': 'no-cache',
|
|
380
|
+
'Connection': 'keep-alive',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
for await (const event of agentLoop({
|
|
384
|
+
model,
|
|
385
|
+
messages,
|
|
386
|
+
tools: coreTools,
|
|
387
|
+
provider,
|
|
388
|
+
context,
|
|
389
|
+
})) {
|
|
390
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
res.end();
|
|
394
|
+
} catch (err) {
|
|
395
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
396
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 404
|
|
402
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
403
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
server.listen(port, () => {
|
|
407
|
+
console.log(`\ndotbot server v${VERSION}`);
|
|
408
|
+
console.log(`Listening on http://localhost:${port}`);
|
|
409
|
+
console.log(`\nEndpoints:`);
|
|
410
|
+
console.log(` GET /health Health check`);
|
|
411
|
+
console.log(` POST /chat Send message (SSE stream)\n`);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Main entry point.
|
|
417
|
+
*/
|
|
418
|
+
async function main() {
|
|
419
|
+
const args = parseCliArgs();
|
|
420
|
+
|
|
421
|
+
if (args.version) {
|
|
422
|
+
console.log(`dotbot v${VERSION}`);
|
|
423
|
+
process.exit(0);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (args.help || args.positionals.length === 0) {
|
|
427
|
+
printHelp();
|
|
428
|
+
process.exit(0);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const command = args.positionals[0];
|
|
432
|
+
|
|
433
|
+
switch (command) {
|
|
434
|
+
case 'chat':
|
|
435
|
+
const message = args.positionals.slice(1).join(' ');
|
|
436
|
+
if (!message) {
|
|
437
|
+
console.error('Usage: dotbot chat "your message"');
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
await runChat(message, args);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case 'repl':
|
|
444
|
+
await runRepl(args);
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case 'serve':
|
|
448
|
+
await runServer(args);
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
default:
|
|
452
|
+
console.error(`Unknown command: ${command}`);
|
|
453
|
+
printHelp();
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
main().catch((err) => {
|
|
459
|
+
console.error(`Fatal error: ${err.message}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
});
|