testdriverai 4.0.42 → 4.0.44

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/index.js CHANGED
@@ -1,50 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // disable depreciation warnings
4
- process.removeAllListeners('warning');
4
+ process.removeAllListeners("warning");
5
5
 
6
6
  // package.json is included to get the version number
7
- const package = require('./package.json')
7
+ const package = require("./package.json");
8
8
 
9
9
  // system modules
10
- const fs = require('fs');
11
- const readline = require('readline')
12
- const os = require('os');
13
- const http = require('http');
10
+ const fs = require("fs");
11
+ const readline = require("readline");
12
+ const os = require("os");
14
13
 
15
14
  // third party modules
16
- const path = require('path');
17
- const chalk = require('chalk')
18
- const yaml = require('js-yaml');
15
+ const path = require("path");
16
+ const chalk = require("chalk");
17
+ const yaml = require("js-yaml");
19
18
  const sanitizeFilename = require("sanitize-filename");
20
- const macScreenPerms = require('mac-screen-capture-permissions');
19
+ const macScreenPerms = require("mac-screen-capture-permissions");
21
20
 
22
21
  // local modules
23
- const speak = require('./lib/speak');
24
- const analytics = require('./lib/analytics');
25
- const log = require('./lib/logger');
26
- const parser = require('./lib/parser')
27
- const commander = require('./lib/commander')
28
- const system = require('./lib/system');
29
- const generator = require('./lib/generator')
30
- const sdk = require('./lib/sdk');
31
- const commands = require('./lib/commands');
32
- const init = require('./lib/init');
33
-
34
- const { showTerminal, hideTerminal } = require('./lib/focus-application');
35
- const isValidVersion = require('./lib/valid-version');
36
- const session = require('./lib/session');
37
- const notify = require('./lib/notify');
38
-
39
- let lastPrompt = '';
40
- let terminalApp = '';
22
+ const speak = require("./lib/speak");
23
+ const analytics = require("./lib/analytics");
24
+ const log = require("./lib/logger");
25
+ const parser = require("./lib/parser");
26
+ const commander = require("./lib/commander");
27
+ const system = require("./lib/system");
28
+ const generator = require("./lib/generator");
29
+ const sdk = require("./lib/sdk");
30
+ const commands = require("./lib/commands");
31
+ const init = require("./lib/init");
32
+
33
+ const { showTerminal, hideTerminal } = require("./lib/focus-application");
34
+ const isValidVersion = require("./lib/valid-version");
35
+ const session = require("./lib/session");
36
+ const notify = require("./lib/notify");
37
+
38
+ let lastPrompt = "";
39
+ let terminalApp = "";
41
40
  let commandHistory = [];
42
41
  let executionHistory = [];
43
42
  let errorCounts = {};
44
43
  let errorLimit = 3;
44
+ let checkCount = 0;
45
+ let checkLimit = 3;
45
46
  let rl;
46
47
 
47
- require('dotenv').config()
48
+ require("dotenv").config();
48
49
 
49
50
  // list of prompts that the user has given us
50
51
  let tasks = [];
@@ -52,344 +53,356 @@ let tasks = [];
52
53
  // get args from terminal
53
54
  const args = process.argv.slice(2);
54
55
 
55
- const commandHistoryFile = path.join(os.homedir(), '.testdriver_history');
56
+ const commandHistoryFile = path.join(os.homedir(), ".testdriver_history");
56
57
 
57
- const delay = t => new Promise(resolve => setTimeout(resolve, t));
58
+ const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
58
59
 
59
60
  let getArgs = () => {
60
-
61
61
  let command = 0;
62
62
  let file = 1;
63
63
 
64
-
65
64
  // TODO use a arg parser library to simplify this
66
- if (args[command] == "--help" || args[command] == '-h'
67
- || args[file] == "--help" || args[file] == '-h') {
68
- console.log(
69
- "Command: testdriverai [init, run, edit] [yaml filepath]"
70
- );
71
- process.exit(0);
72
- }
73
-
74
- if (args[command] == 'init') {
75
- } else if (args[command] !== 'run' && !args[file]) {
65
+ if (
66
+ args[command] == "--help" ||
67
+ args[command] == "-h" ||
68
+ args[file] == "--help" ||
69
+ args[file] == "-h"
70
+ ) {
71
+ console.log("Command: testdriverai [init, run, edit] [yaml filepath]");
72
+ process.exit(0);
73
+ }
74
+
75
+ if (args[command] == "init") {
76
+ args[command] = "init";
77
+ } else if (args[command] !== "run" && !args[file]) {
76
78
  args[file] = args[command];
77
- args[command] = 'edit';
79
+ args[command] = "edit";
78
80
  } else if (!args[command]) {
79
- args[command] = 'edit'
81
+ args[command] = "edit";
80
82
  }
81
83
 
82
84
  if (!args[file]) {
83
-
84
85
  // make testdriver directory if it doesn't exist
85
- let testdriverFolder = path.join(process.cwd(), 'testdriver');
86
+ let testdriverFolder = path.join(process.cwd(), "testdriver");
86
87
  if (!fs.existsSync(testdriverFolder)) {
87
88
  fs.mkdirSync(testdriverFolder);
88
89
  }
89
90
 
90
- args[file] = 'testdriver/testdriver.yml'
91
+ args[file] = "testdriver/testdriver.yml";
91
92
  }
92
93
 
93
94
  // turn args[file] into local path
94
95
  if (args[file]) {
95
- args[file] = `${process.cwd()}/${args[file]}`
96
- if (!args[file].endsWith('.yml')) {
97
- args[file] += '.yml';
96
+ args[file] = `${process.cwd()}/${args[file]}`;
97
+ if (!args[file].endsWith(".yml")) {
98
+ args[file] += ".yml";
98
99
  }
99
100
  }
100
101
 
101
- return {command: args[command], file: args[file]};
102
- }
102
+ return { command: args[command], file: args[file] };
103
+ };
103
104
 
104
105
  let a = getArgs();
105
106
 
106
107
  const thisFile = a.file;
107
108
  const thisCommand = a.command;
108
109
 
109
- log.log('info', chalk.green(`Howdy! I'm TestDriver v${package.version}`))
110
- log.log('info', chalk.dim(`Working on ${thisFile}`))
111
- console.log('')
112
- log.log('info', chalk.yellow(`This is beta software!`))
113
- log.log('info', `Join our Discord for help`);
114
- log.log('info', `https://discord.com/invite/cWDFW8DzPm`);
115
- console.log('')
110
+ log.log("info", chalk.green(`Howdy! I'm TestDriver v${package.version}`));
111
+ log.log("info", chalk.dim(`Working on ${thisFile}`));
112
+ console.log("");
113
+ log.log("info", chalk.yellow(`This is beta software!`));
114
+ log.log("info", `Join our Discord for help`);
115
+ log.log("info", `https://discord.com/invite/cWDFW8DzPm`);
116
+ console.log("");
116
117
 
117
118
  // individual run ID for this session
118
- let runID = new Date().getTime();
119
+ // let runID = new Date().getTime();
119
120
 
120
121
  function fileCompleter(line) {
121
- line = line.slice(5); // remove /run
122
+ line = line.slice(5); // remove /run
122
123
  const lastSepIndex = line.lastIndexOf(path.sep);
123
124
  let dir;
124
125
  let partial;
125
126
  if (lastSepIndex === -1) {
126
- dir = '.';
127
+ dir = ".";
127
128
  partial = line;
129
+ } else {
130
+ dir = line.slice(0, lastSepIndex + 1);
131
+ partial = line.slice(lastSepIndex + 1);
128
132
  }
129
- else{
130
- dir = line.slice(0, lastSepIndex + 1);
131
- partial = line.slice(lastSepIndex + 1);
132
- }
133
- try{
134
- const dirPath = path.resolve(process.cwd(), dir);
135
-
136
- let files = fs.readdirSync(dirPath);
137
- files = files.map(file => {
138
- const fullFilePath = path.join(dirPath, file);
139
- const fileStats = fs.statSync(fullFilePath);
140
- return file + (fileStats.isDirectory() ? path.sep : ''); // add path.sep for dir
141
- });
142
- const matches = files.filter(file => file.startsWith(partial));
133
+ try {
134
+ const dirPath = path.resolve(process.cwd(), dir);
143
135
 
144
- return [matches.length ? matches : files, partial];
145
- }
146
- catch(err){
147
- return [[], partial];
148
- }
136
+ let files = fs.readdirSync(dirPath);
137
+ files = files.map((file) => {
138
+ const fullFilePath = path.join(dirPath, file);
139
+ const fileStats = fs.statSync(fullFilePath);
140
+ return file + (fileStats.isDirectory() ? path.sep : ""); // add path.sep for dir
141
+ });
142
+ const matches = files.filter((file) => file.startsWith(partial));
143
+
144
+ return [matches.length ? matches : files, partial];
145
+ } catch (e) {
146
+ console.log(e);
147
+ return [[], partial];
148
+ }
149
149
  }
150
150
 
151
151
  function completer(line) {
152
- let completions = '/summarize /save /run /quit /explore /assert /undo /manual'.split(' ')
153
- if(line.startsWith('/run ')){
152
+ let completions =
153
+ "/summarize /save /run /quit /explore /assert /undo /manual".split(" ");
154
+ if (line.startsWith("/run ")) {
154
155
  return fileCompleter(line);
155
- }
156
- else{
157
- completions.concat(tasks)
156
+ } else {
157
+ completions.concat(tasks);
158
158
 
159
- var hits = completions.filter(function(c) { return c.indexOf(line) == 0 })
160
- // show all completions if none found
161
- return [hits.length ? hits : completions, line]
159
+ var hits = completions.filter(function (c) {
160
+ return c.indexOf(line) == 0;
161
+ });
162
+ // show all completions if none found
163
+ return [hits.length ? hits : completions, line];
162
164
  }
163
165
  }
164
166
 
165
167
  if (!fs.existsSync(commandHistoryFile)) {
166
168
  // make the file
167
- fs.writeFileSync(commandHistoryFile, '');
169
+ fs.writeFileSync(commandHistoryFile, "");
168
170
  } else {
169
- commandHistory = fs.readFileSync(commandHistoryFile, 'utf-8').split('\n').filter((line) => {
170
- return line.trim() !== '';
171
- }).reverse();
171
+ commandHistory = fs
172
+ .readFileSync(commandHistoryFile, "utf-8")
173
+ .split("\n")
174
+ .filter((line) => {
175
+ return line.trim() !== "";
176
+ })
177
+ .reverse();
172
178
  }
173
179
 
174
180
  if (!commandHistory.length) {
175
- commandHistory = ['open google chrome', 'type hello world', 'click on the current time'];
181
+ commandHistory = [
182
+ "open google chrome",
183
+ "type hello world",
184
+ "click on the current time",
185
+ ];
176
186
  }
177
187
 
178
188
  const exit = async (failed = true) => {
179
-
180
189
  await save();
181
190
 
182
- analytics.track('exit', {failed});
191
+ analytics.track("exit", { failed });
183
192
 
184
193
  // we purposly never resolve this promise so the process will hang
185
- return new Promise(async () => {
186
- rl?.close()
187
- rl?.removeAllListeners()
194
+ return new Promise(() => {
195
+ rl?.close();
196
+ rl?.removeAllListeners();
188
197
  process.exit(failed ? 1 : 0);
189
198
  });
190
- }
199
+ };
191
200
 
192
201
  // creates a new "thread" in which the AI is given an error
193
202
  // and responds. notice `actOnMarkdown` which will continue
194
203
  // the thread until there are no more codeblocks to execute
195
204
  const haveAIResolveError = async (error, markdown, depth = 0, undo = false) => {
196
-
197
205
  let eMessage = error.message ? error.message : error;
198
206
 
199
207
  let safeKey = JSON.stringify(eMessage);
200
208
  errorCounts[safeKey] = errorCounts[safeKey] ? errorCounts[safeKey] + 1 : 1;
201
209
 
202
- log.log('error', eMessage)
210
+ log.log("error", eMessage);
203
211
 
204
212
  if (process.env["DEV"]) {
205
- console.log(error, eMessage)
206
- console.log(error.stack)
213
+ console.log(error, eMessage);
214
+ console.log(error.stack);
207
215
  }
208
216
 
209
- log.prettyMarkdown(eMessage)
217
+ log.prettyMarkdown(eMessage);
210
218
 
211
219
  // if we get the same error 3 times in `run` mode, we exit
212
- if (errorCounts[safeKey] > (errorLimit - 1)) {
213
- console.log(chalk.red('Error loop detected. Exiting.'));
214
- console.log(eMessage)
215
- await summarize(eMessage)
220
+ if (errorCounts[safeKey] > errorLimit - 1) {
221
+ console.log(chalk.red("Error loop detected. Exiting."));
222
+ console.log(eMessage);
223
+ await summarize(eMessage);
216
224
  return await exit(true);
217
225
  }
218
-
226
+
219
227
  if (undo) {
220
- await popFromHistory()
228
+ await popFromHistory();
221
229
  }
222
230
 
223
231
  let image;
224
232
  if (error.attachScreenshot) {
225
- image = await system.captureScreenBase64(.5);
233
+ image = await system.captureScreenBase64(0.5);
226
234
  } else {
227
235
  image = null;
228
236
  }
229
237
 
230
- speak('thinking...');
231
- notify('thinking...');
232
- log.log('info', chalk.dim('thinking...'), true)
238
+ speak("thinking...");
239
+ notify("thinking...");
240
+ log.log("info", chalk.dim("thinking..."), true);
233
241
 
234
- let response = await sdk.req('error', {description: eMessage, markdown, image});
242
+ let response = await sdk.req("error", {
243
+ description: eMessage,
244
+ markdown,
245
+ image,
246
+ });
235
247
 
236
248
  if (response) {
237
249
  return await actOnMarkdown(response, depth, true);
238
250
  }
239
-
240
251
  };
241
252
 
242
253
  // this is run after all possible codeblocks have been executed, but only at depth 0, which is the top level
243
254
  // this checks that the task is "really done" using a screenshot of the desktop state
244
255
  // it's likely that the task will not be complete and the AI will respond with more codeblocks to execute
245
- const check = async () => {
256
+ const check = async () => {
257
+
258
+ checkCount++;
259
+
260
+ if (checkCount >= checkLimit) {
261
+ log.log("info", chalk.red("Exploratory loop detected. Exiting."));
262
+ await summarize("Check loop detected.");
263
+ return await exit(true);
264
+ }
246
265
 
247
- await delay(3000)
266
+ await delay(3000);
248
267
 
249
- console.log('')
250
- log.log('info', chalk.dim('checking...'), 'testdriver')
251
- let image = await system.captureScreenBase64(.5);
268
+ console.log("");
269
+ log.log("info", chalk.dim("checking..."), "testdriver");
270
+ let image = await system.captureScreenBase64(0.5);
252
271
  let mousePosition = await system.getMousePosition();
253
272
  let activeWindow = await system.activeWin();
254
- return await sdk.req('check', {tasks, image, mousePosition, activeWindow});
255
273
 
256
- }
274
+ return await sdk.req("check", { tasks, image, mousePosition, activeWindow });
275
+ };
257
276
 
258
277
  // command is transformed from a single yml entry generated by the AI into a JSON object
259
278
  // it is mapped via `commander` to the `commands` module so the yaml
260
279
  // parameters can be mapped to actual functions
261
280
  const runCommand = async (command, depth) => {
262
-
263
281
  let yml = await yaml.dump(command);
264
282
 
265
- log.log('debug', `running command: \n\n${yml}`)
283
+ log.log("debug", `running command: \n\n${yml}`);
266
284
 
267
285
  try {
268
-
269
286
  let response;
270
287
 
271
- if (command.command == 'embed') {
288
+ if (command.command == "embed") {
272
289
  response = await embed(command.file, depth);
273
- } else if (command.command == 'if') {
274
- response = await iffy(command.condition, command.then, command.else, depth);
290
+ } else if (command.command == "if") {
291
+ response = await iffy(
292
+ command.condition,
293
+ command.then,
294
+ command.else,
295
+ depth,
296
+ );
275
297
  } else {
276
298
  response = await commander.run(command, depth);
277
299
  }
278
300
 
279
- if (response && typeof response === 'string') {
301
+ if (response && typeof response === "string") {
280
302
  return await actOnMarkdown(response, depth);
281
303
  }
282
-
283
304
  } catch (error) {
284
-
285
305
  if (error.fatal) {
286
- console.log('');
287
- log.log('info', chalk.red('Fatal Error') + `\n${error.message}`)
288
- await summarize(error.message)
289
- return await exit(true)
306
+ console.log("");
307
+ log.log("info", chalk.red("Fatal Error") + `\n${error.message}`);
308
+ await summarize(error.message);
309
+ return await exit(true);
290
310
  } else {
291
- return await haveAIResolveError(error, yaml.dump({commands: [yml]}), depth, true)
311
+ return await haveAIResolveError(
312
+ error,
313
+ yaml.dump({ commands: [yml] }),
314
+ depth,
315
+ true,
316
+ );
292
317
  }
293
318
  }
294
-
295
- };
319
+ };
296
320
 
297
321
  let lastCommand = new Date().getTime();
298
- let csv = [['command,time']];
322
+ let csv = [["command,time"]];
299
323
 
300
324
  const executeCommands = async (commands, depth, pushToHistory = false) => {
301
-
302
325
  if (commands?.length) {
303
-
304
326
  for (const command of commands) {
305
-
306
327
  if (pushToHistory) {
307
- executionHistory[executionHistory.length - 1]?.commands.push(command)
328
+ executionHistory[executionHistory.length - 1]?.commands.push(command);
308
329
  }
309
330
 
310
- await runCommand(command, depth)
331
+ await runCommand(command, depth);
311
332
 
312
333
  let timeToComplete = (new Date().getTime() - lastCommand) / 1000;
313
334
  // console.log(timeToComplete, 'seconds')
314
335
 
315
- csv.push([command.command, timeToComplete])
336
+ csv.push([command.command, timeToComplete]);
316
337
  lastCommand = new Date().getTime();
317
-
318
338
  }
319
-
320
339
  }
321
340
  };
322
341
 
323
342
  // note that commands are run in a recursive loop, so that the AI can respond to the output of the commands
324
343
  // like `click-image` and `click-text` for example
325
344
  const executeCodeBlocks = async (codeblocks, depth, pushToHistory = false) => {
345
+ depth = depth + 1;
326
346
 
327
- depth = depth+1;
328
-
329
- log.log('debug', {message: 'execute code blocks', depth})
347
+ log.log("debug", { message: "execute code blocks", depth });
330
348
 
331
349
  for (const codeblock of codeblocks) {
332
-
333
350
  let commands;
334
351
 
335
- let steps = [];
336
352
  try {
337
- commands = await parser.getCommands(codeblock)
353
+ commands = await parser.getCommands(codeblock);
338
354
  } catch (e) {
339
- return await haveAIResolveError(e, yaml.dump(parser.getYAMLFromCodeBlock(codeblock)), depth)
355
+ return await haveAIResolveError(
356
+ e,
357
+ yaml.dump(parser.getYAMLFromCodeBlock(codeblock)),
358
+ depth,
359
+ );
340
360
  }
341
361
 
342
362
  await executeCommands(commands, depth, pushToHistory);
343
-
344
363
  }
345
-
346
- }
364
+ };
347
365
 
348
366
  // this is the main function that interacts with the ai, runs commands, and checks the results
349
367
  // notice that depth is 0 here. when this function resolves, the task is considered complete
350
368
  // notice the call to `check()` which validates the prompt is complete
351
- const aiExecute = async(message, validateAndLoop = false) => {
369
+ const aiExecute = async (message, validateAndLoop = false) => {
370
+ executionHistory.push({ prompt: lastPrompt, commands: [] });
352
371
 
353
- executionHistory.push({prompt: lastPrompt, commands: []})
354
-
355
- log.log('debug', 'kicking off exploratory loop')
372
+ log.log("debug", "kicking off exploratory loop");
356
373
 
357
374
  // kick everything off
358
- await actOnMarkdown(message, 0, true)
375
+ await actOnMarkdown(message, 0, true);
359
376
 
360
377
  if (validateAndLoop) {
361
-
362
- log.log('debug', 'exploratory loop resolved, check your work')
378
+ log.log("debug", "exploratory loop resolved, check your work");
363
379
 
364
380
  let response = await check();
365
381
 
366
- console.log('');
382
+ console.log("");
367
383
  log.prettyMarkdown(response);
368
384
 
369
385
  let checkCodeblocks = [];
370
386
  try {
371
387
  checkCodeblocks = await parser.findCodeBlocks(response);
372
388
  } catch (error) {
373
- return await haveAIResolveError(error, response, 0)
389
+ return await haveAIResolveError(error, response, 0);
374
390
  }
375
-
376
- log.log('debug', `found ${checkCodeblocks.length} codeblocks`)
391
+
392
+ log.log("debug", `found ${checkCodeblocks.length} codeblocks`);
377
393
 
378
394
  if (checkCodeblocks.length > 0) {
379
- log.log('debug', 'check thinks more needs to be done')
395
+ log.log("debug", "check thinks more needs to be done");
380
396
  return await aiExecute(response, validateAndLoop);
381
397
  } else {
382
- log.log('debug', 'seems complete, returning')
398
+ log.log("debug", "seems complete, returning");
383
399
  return response;
384
400
  }
385
-
386
401
  }
387
-
388
- }
402
+ };
389
403
 
390
404
  const assert = async (expect) => {
391
-
392
- analytics.track('assert');
405
+ analytics.track("assert");
393
406
 
394
407
  let task = expect;
395
408
  if (!task) {
@@ -398,123 +411,124 @@ const assert = async (expect) => {
398
411
 
399
412
  // throw error if no task
400
413
  if (!task) {
401
- throw new Error('No task to assert')
414
+ throw new Error("No task to assert");
402
415
  }
403
416
  }
404
417
 
405
- speak('thinking...');
406
- notify('thinking...');
407
- log.log('info', chalk.dim('thinking...'), true)
418
+ speak("thinking...");
419
+ notify("thinking...");
420
+ log.log("info", chalk.dim("thinking..."), true);
408
421
 
409
422
  let response = `\`\`\`yml
410
423
  commands:
411
424
  - command: assert
412
425
  expect: ${expect}
413
- \`\`\``
426
+ \`\`\``;
414
427
 
415
- await aiExecute(response)
428
+ await aiExecute(response);
416
429
 
417
430
  await save({ silent: true });
418
-
419
- }
431
+ };
420
432
 
421
433
  // this function responds to the result of `promptUser()` which is the user input
422
434
  // it kicks off the exploratory loop, which is the main function that interacts with the AI
423
435
  const humanInput = async (currentTask, validateAndLoop = false) => {
424
436
 
425
437
  lastPrompt = currentTask;
438
+ checkCount = 0;
426
439
 
427
- log.log('debug', 'humanInput called')
440
+ log.log("debug", "humanInput called");
428
441
 
429
442
  tasks.push(currentTask);
430
443
 
431
- speak('thinking...');
432
- notify('thinking...');
433
- log.log('info', chalk.dim('thinking...'), true);
444
+ speak("thinking...");
445
+ notify("thinking...");
446
+ log.log("info", chalk.dim("thinking..."), true);
434
447
 
435
- log.log('info', '');
448
+ log.log("info", "");
436
449
 
437
- let image = await system.captureScreenBase64(.5);
438
- let message = await sdk.req('input', {
439
- input: currentTask,
450
+ let image = await system.captureScreenBase64(0.5);
451
+ let message = await sdk.req("input", {
452
+ input: currentTask,
440
453
  mousePosition: await system.getMousePosition(),
441
454
  activeWindow: await system.activeWin(),
442
- image});
455
+ image,
456
+ });
443
457
 
444
- log.prettyMarkdown(message)
458
+ log.prettyMarkdown(message);
445
459
 
446
460
  await aiExecute(message, validateAndLoop);
447
461
 
448
- log.log('debug', 'showing prompt from humanInput response check')
462
+ log.log("debug", "showing prompt from humanInput response check");
449
463
 
450
- await save({ silent: true })
451
-
452
- }
464
+ await save({ silent: true });
465
+ };
453
466
 
454
467
  const generate = async (type) => {
468
+ log.log("debug", "generate called", type);
455
469
 
456
- log.log('debug', 'generate called', type)
457
-
458
- speak('thinking...');
459
- notify('thinking...');
460
- log.log('info', chalk.dim('thinking...'), true);
470
+ speak("thinking...");
471
+ notify("thinking...");
472
+ log.log("info", chalk.dim("thinking..."), true);
461
473
 
462
- log.log('info', '');
474
+ log.log("info", "");
463
475
 
464
- let image = await system.captureScreenBase64(.5);
465
- let message = await sdk.req('generate', {
476
+ let image = await system.captureScreenBase64(0.5);
477
+ let message = await sdk.req("generate", {
466
478
  type,
467
- image});
479
+ image,
480
+ });
468
481
 
469
- log.prettyMarkdown(message)
482
+ log.prettyMarkdown(message);
470
483
 
471
484
  let testPrompts = await parser.findGenerativePrompts(message);
472
485
 
473
486
  // for each testPrompt
474
487
  for (const testPrompt of testPrompts) {
475
488
  // with the contents of the testPrompt
476
- let fileName = sanitizeFilename(testPrompt.headings[0]).trim().replace(/ /g, '-').toLowerCase() + '.md';
477
- let path1 = path.join(process.cwd(), 'testdriver', 'generate', fileName);
489
+ let fileName =
490
+ sanitizeFilename(testPrompt.headings[0])
491
+ .trim()
492
+ .replace(/ /g, "-")
493
+ .toLowerCase() + ".md";
494
+ let path1 = path.join(process.cwd(), "testdriver", "generate", fileName);
478
495
  // create generate directory if it doesn't exist
479
- if (!fs.existsSync(path.join(process.cwd(), 'testdriver', 'generate'))) {
480
- fs.mkdirSync(path.join(process.cwd(), 'testdriver', 'generate'));
496
+ if (!fs.existsSync(path.join(process.cwd(), "testdriver", "generate"))) {
497
+ fs.mkdirSync(path.join(process.cwd(), "testdriver", "generate"));
481
498
  }
482
499
  let list = testPrompt.listsOrdered[0];
483
500
  list.append(`/save testdriver/${fileName}`);
484
- let contents = list.map((item, index) => `${index + 1}. /explore ${item}`).join('\n');
501
+ let contents = list
502
+ .map((item, index) => `${index + 1}. /explore ${item}`)
503
+ .join("\n");
485
504
  fs.writeFileSync(path1, contents);
486
505
  }
487
- }
506
+ };
488
507
 
489
508
  const popFromHistory = async (fullStep) => {
490
-
491
- log.log('info', chalk.dim('undoing...'), true)
509
+ log.log("info", chalk.dim("undoing..."), true);
492
510
 
493
511
  if (executionHistory.length) {
494
512
  if (fullStep) {
495
- removed = executionHistory.pop();
513
+ executionHistory.pop();
496
514
  } else {
497
- removed = executionHistory[executionHistory.length -1].commands.pop();
515
+ executionHistory[executionHistory.length - 1].commands.pop();
498
516
  }
499
- if (!executionHistory[executionHistory.length -1].commands.length) {
517
+ if (!executionHistory[executionHistory.length - 1].commands.length) {
500
518
  executionHistory.pop();
501
519
  }
502
520
  }
503
-
504
- }
521
+ };
505
522
 
506
523
  const undo = async () => {
524
+ analytics.track("undo");
507
525
 
508
- analytics.track('undo');
509
-
510
526
  popFromHistory();
511
527
  await save();
528
+ };
512
529
 
513
- }
514
-
515
- const manualInput = async(commandString) => {
516
-
517
- analytics.track('manual input');
530
+ const manualInput = async (commandString) => {
531
+ analytics.track("manual input");
518
532
 
519
533
  let yml = await generator.manualToYml(commandString);
520
534
 
@@ -522,41 +536,37 @@ const manualInput = async(commandString) => {
522
536
  ${yml}
523
537
  \`\`\``;
524
538
 
525
- await aiExecute(message, false);
526
-
527
- await save({ silent: true })
539
+ await aiExecute(message, false);
528
540
 
529
- }
541
+ await save({ silent: true });
542
+ };
530
543
 
531
544
  // this function is responsible for starting the recursive process of executing codeblocks
532
545
  const actOnMarkdown = async (content, depth, pushToHistory = false) => {
533
-
534
- log.log('debug', {
546
+ log.log("debug", {
535
547
  message: "actOnMarkdown called",
536
- depth
537
- })
548
+ depth,
549
+ });
538
550
 
539
551
  let codeblocks = [];
540
552
  try {
541
553
  codeblocks = await parser.findCodeBlocks(content);
542
554
  } catch (error) {
543
555
  pushToHistory = false;
544
- return await haveAIResolveError(error, content, depth)
556
+ return await haveAIResolveError(error, content, depth);
545
557
  }
546
558
 
547
559
  if (codeblocks.length) {
548
- let executions = await executeCodeBlocks(codeblocks, depth, pushToHistory)
560
+ let executions = await executeCodeBlocks(codeblocks, depth, pushToHistory);
549
561
  return executions;
550
562
  } else {
551
563
  return true;
552
564
  }
553
-
554
565
  };
555
566
 
556
567
  // simple function to backfill the chat history with a prompt and
557
568
  // then call `promptUser()` to get the user input
558
- const firstPrompt = async (text) => {
559
-
569
+ const firstPrompt = async () => {
560
570
  // readline is what allows us to get user input
561
571
  rl = readline.createInterface({
562
572
  terminal: true,
@@ -564,139 +574,136 @@ const firstPrompt = async (text) => {
564
574
  removeHistoryDuplicates: true,
565
575
  input: process.stdin,
566
576
  output: process.stdout,
567
- completer
577
+ completer,
568
578
  });
569
579
 
570
- analytics.track('first prompt');
580
+ analytics.track("first prompt");
571
581
 
572
- rl.on('SIGINT', async () => {
573
- analytics.track('sigint');
574
- await exit(false)
582
+ rl.on("SIGINT", async () => {
583
+ analytics.track("sigint");
584
+ await exit(false);
575
585
  });
576
586
 
577
587
  // this is how we parse user input
578
588
  // notice that the AI is only called if the input is not a command
579
- rl.on('line', async (input) => {
580
-
589
+ rl.on("line", async (input) => {
581
590
  await setTerminalApp();
582
591
 
583
- setTerminalWindowTransparency(true)
592
+ setTerminalWindowTransparency(true);
584
593
  errorCounts = {};
585
594
 
586
595
  // append this to commandHistoryFile
587
- fs.appendFileSync(commandHistoryFile, input + '\n');
596
+ fs.appendFileSync(commandHistoryFile, input + "\n");
588
597
 
589
- analytics.track('input', {input});
598
+ analytics.track("input", { input });
590
599
 
591
- console.log('') // adds a nice break between submissions
600
+ console.log(""); // adds a nice break between submissions
592
601
 
593
- let commands = input.split(' ');
602
+ let commands = input.split(" ");
594
603
 
595
604
  // if last character is a question mark, we assume the user is asking a question
596
- if (input.indexOf('/summarize') == 0) {
605
+ if (input.indexOf("/summarize") == 0) {
597
606
  await summarize();
598
- } else if (input.indexOf('/quit') == 0) {
607
+ } else if (input.indexOf("/quit") == 0) {
599
608
  await exit();
600
- } else if (input.indexOf('/save') == 0) {
609
+ } else if (input.indexOf("/save") == 0) {
601
610
  await save({ filepath: commands[1] });
602
- } else if (input.indexOf('/explore') == 0) {
603
- await humanInput(commands.slice(1).join(' '), true)
604
- } else if (input.indexOf('/undo') == 0) {
611
+ } else if (input.indexOf("/explore") == 0) {
612
+ await humanInput(commands.slice(1).join(" "), true);
613
+ } else if (input.indexOf("/undo") == 0) {
605
614
  await undo();
606
- } else if (input.indexOf('/assert') == 0) {
607
- await assert(commands.slice(1).join(' '))
608
- } else if (input.indexOf('/manual') == 0) {
609
- await manualInput(commands.slice(1).join(' '))
610
- } else if (input.indexOf('/run') == 0) {
615
+ } else if (input.indexOf("/assert") == 0) {
616
+ await assert(commands.slice(1).join(" "));
617
+ } else if (input.indexOf("/manual") == 0) {
618
+ await manualInput(commands.slice(1).join(" "));
619
+ } else if (input.indexOf("/run") == 0) {
611
620
  await run(commands[1], commands[2]);
612
- } else if (input.indexOf('/generate') == 0) {
621
+ } else if (input.indexOf("/generate") == 0) {
613
622
  await generate(commands[1]);
614
623
  } else {
615
- await humanInput(input, false)
624
+ await humanInput(input, false);
616
625
  }
617
626
 
618
- setTerminalWindowTransparency(false)
627
+ setTerminalWindowTransparency(false);
619
628
  promptUser();
620
-
621
629
  });
622
-
630
+
623
631
  // if file exists, load it
624
632
  if (fs.existsSync(thisFile)) {
625
- analytics.track('load');
626
- let object = await generator.ymlToHistory(fs.readFileSync(thisFile, 'utf-8'));
633
+ analytics.track("load");
634
+ let object = await generator.ymlToHistory(
635
+ fs.readFileSync(thisFile, "utf-8"),
636
+ );
627
637
 
628
638
  if (!object?.steps) {
629
- analytics.track('load invalid yaml');
630
- log.log('error', 'Invalid YAML. No steps found.')
631
- console.log('Invalid YAML: ' + thisFile)
639
+ analytics.track("load invalid yaml");
640
+ log.log("error", "Invalid YAML. No steps found.");
641
+ console.log("Invalid YAML: " + thisFile);
632
642
  return await exit(true);
633
643
  }
634
644
 
635
645
  // push each step to executionHistory from { commands: {steps: [ { commands: [Array] } ] } }
636
646
  object.steps.forEach((step) => {
637
647
  executionHistory.push(step);
638
- })
648
+ });
639
649
 
640
- let yml = fs.readFileSync(thisFile, 'utf-8');
650
+ let yml = fs.readFileSync(thisFile, "utf-8");
641
651
 
642
652
  let markdown = `\`\`\`yaml
643
653
  ${yml}
644
654
  \`\`\``;
645
655
 
646
- log.log('info', `Loaded test script ${thisFile}\n`)
656
+ log.log("info", `Loaded test script ${thisFile}\n`);
647
657
 
648
- log.prettyMarkdown(`
658
+ log.prettyMarkdown(`
649
659
 
650
660
  ${markdown}
651
661
 
652
662
  New commands will be appended.
653
- `)
654
-
663
+ `);
655
664
  }
656
665
 
657
666
  promptUser();
658
-
659
667
  };
660
668
 
661
669
  let setTerminalWindowTransparency = async (hide) => {
662
-
663
670
  try {
664
671
  if (hide) {
665
-
666
672
  if (terminalApp) {
667
- hideTerminal(terminalApp)
668
- };
669
-
673
+ hideTerminal(terminalApp);
674
+ }
670
675
  } else {
671
-
672
- if(terminalApp) {
676
+ if (terminalApp) {
673
677
  showTerminal(terminalApp);
674
678
  }
675
679
  }
676
680
  } catch (e) {
677
681
  // Suppress error
678
- console.error('Caught exception:', e);
682
+ console.error("Caught exception:", e);
679
683
  }
680
- }
684
+ };
681
685
 
682
686
  // this function is responsible for summarizing the test script that has already executed
683
687
  // it is what is saved to the `/tmp/oiResult.log.log` file and output to the action as a summary
684
688
  let summarize = async (error = null) => {
685
-
686
- analytics.track('summarize');
689
+ analytics.track("summarize");
687
690
 
688
- log.log('info', '');
689
-
690
- log.log('info', chalk.cyan('summarizing'))
691
- log.log('info', chalk.dim('reviewing test...'), true);
691
+ log.log("info", "");
692
+
693
+ log.log("info", chalk.cyan("summarizing"));
694
+ log.log("info", chalk.dim("reviewing test..."), true);
692
695
 
693
696
  // let text = prompts.summarize(tasks, error);
694
- let image = await system.captureScreenBase64(.5);
697
+ let image = await system.captureScreenBase64(0.5);
695
698
 
696
- log.log('info', chalk.dim('summarizing...'), true);
699
+ log.log("info", chalk.dim("summarizing..."), true);
700
+
701
+ let reply = await sdk.req("summarize", {
702
+ image,
703
+ error: error?.toString(),
704
+ tasks,
705
+ });
697
706
 
698
- let reply = await sdk.req('summarize', {image, error: error?.toString(), tasks});
699
-
700
707
  let resultFile = "/tmp/oiResult.log.log";
701
708
  if (process.platform === "win32") {
702
709
  resultFile = "/Windows/Temp/oiResult.log.log";
@@ -704,114 +711,113 @@ let summarize = async (error = null) => {
704
711
  // write reply to /tmp/oiResult.log.log
705
712
  fs.writeFileSync(resultFile, reply);
706
713
 
707
- log.prettyMarkdown(reply)
708
- }
714
+ log.prettyMarkdown(reply);
715
+ };
709
716
 
710
717
  // this function is responsible for saving the regression test script to a file
711
718
  let save = async ({ filepath = thisFile, silent = false } = {}) => {
712
-
713
- analytics.track('save', {silent});
719
+ analytics.track("save", { silent });
714
720
 
715
721
  if (!silent) {
716
- log.log('info', chalk.dim('saving...'), true);
717
- console.log('')
722
+ log.log("info", chalk.dim("saving..."), true);
723
+ console.log("");
718
724
  }
719
-
725
+
720
726
  if (!executionHistory.length) {
721
727
  return;
722
728
  }
723
729
 
724
730
  // write reply to /tmp/oiResult.log.log
725
- let regression = await generator.historyToYml(executionHistory);
731
+ let regression = await generator.historyToYml(executionHistory);
726
732
  try {
727
- fs.writeFileSync(filepath, regression);
733
+ fs.writeFileSync(filepath, regression);
728
734
  } catch (e) {
729
- log.log('error', e.message)
730
- console.log(e)
735
+ log.log("error", e.message);
736
+ console.log(e);
731
737
  }
732
738
 
733
739
  if (!silent) {
734
-
735
740
  log.prettyMarkdown(`Current test script:
736
741
 
737
742
  \`\`\`yaml
738
743
  ${regression}
739
- \`\`\``
740
- )
744
+ \`\`\``);
741
745
 
742
746
  // console.log(csv.join('\n'))
743
747
 
744
- const fileName = filepath.split('/').pop();
748
+ const fileName = filepath.split("/").pop();
745
749
  if (!silent) {
746
- log.log('info', chalk.dim(`saved as ${fileName}`));
750
+ log.log("info", chalk.dim(`saved as ${fileName}`));
747
751
  }
748
752
  }
749
-
750
753
  };
751
754
 
752
755
  // this will load a regression test from a file location
753
756
  // it parses the markdown file and executes the codeblocks exactly as if they were
754
757
  // generated by the AI in a single prompt
755
758
  let run = async (file, overwrite = false) => {
756
- setTerminalWindowTransparency(true)
759
+ setTerminalWindowTransparency(true);
757
760
 
758
- log.log('info', chalk.cyan(`running ${file}...`))
761
+ log.log("info", chalk.cyan(`running ${file}...`));
759
762
 
760
763
  executionHistory = [];
761
764
  let yml;
762
765
 
763
766
  //wrap this in try/catch so if the file doesn't exist output an error message to the user
764
767
  try {
765
- yml = fs.readFileSync(file, 'utf-8');
768
+ yml = fs.readFileSync(file, "utf-8");
766
769
  } catch (e) {
767
- log.log('error', 'File not found. Please try again.')
768
- console.log(`File not found: ${file}`)
769
- console.log(`Current directory: ${process.cwd()}`)
770
-
771
- await summarize('File not found');
770
+ console.log(e);
771
+ log.log("error", "File not found. Please try again.");
772
+ console.log(`File not found: ${file}`);
773
+ console.log(`Current directory: ${process.cwd()}`);
774
+
775
+ await summarize("File not found");
772
776
  await exit(true);
773
- }
777
+ }
774
778
 
775
779
  let ymlObj = null;
776
780
  try {
777
781
  ymlObj = await yaml.load(yml);
778
782
  } catch (e) {
779
- log.log('error', 'Invalid YAML. Please try again.')
780
- console.log(`Invalid YAML: ${file}`)
781
-
782
- await summarize('Invalid YAML');
783
+ console.log(e);
784
+ log.log("error", "Invalid YAML. Please try again.");
785
+ console.log(`Invalid YAML: ${file}`);
786
+
787
+ await summarize("Invalid YAML");
783
788
  await exit(true);
784
789
  }
785
790
 
786
791
  if (ymlObj.version) {
787
792
  let valid = isValidVersion(ymlObj.version);
788
793
  if (!valid) {
789
- log.log('error', 'Version mismatch. Please try again.')
790
- console.log(`Version mismatch: ${file}. Trying to run a test with v${ymlObj.version} test when this package is v${package.version}.`)
791
-
792
- await summarize('Version mismatch');
794
+ log.log("error", "Version mismatch. Please try again.");
795
+ console.log(
796
+ `Version mismatch: ${file}. Trying to run a test with v${ymlObj.version} test when this package is v${package.version}.`,
797
+ );
798
+
799
+ await summarize("Version mismatch");
793
800
  await exit(true);
794
- }
801
+ }
795
802
  }
796
803
 
797
804
  executionHistory = [];
798
805
 
799
806
  for (const step of ymlObj.steps) {
800
-
801
- log.log('info', ``, null);
802
- log.log('info', chalk.cyan(`${step.prompt || 'no prompt'}`), null);
807
+ log.log("info", ``, null);
808
+ log.log("info", chalk.cyan(`${step.prompt || "no prompt"}`), null);
803
809
 
804
810
  executionHistory.push({
805
811
  prompt: step.prompt,
806
- commands: [] // run will overwrite the commands
812
+ commands: [], // run will overwrite the commands
807
813
  });
808
814
 
809
815
  let markdown = `\`\`\`yaml
810
816
  ${yaml.dump(step)}
811
817
  \`\`\``;
812
818
 
813
- log.log('debug', markdown)
814
- log.log('debug', 'load calling actOnMarkdown')
819
+ log.log("debug", markdown);
820
+ log.log("debug", "load calling actOnMarkdown");
815
821
 
816
822
  lastPrompt = step.prompt;
817
823
  await actOnMarkdown(markdown, 0, true);
@@ -822,32 +828,28 @@ ${yaml.dump(step)}
822
828
  }
823
829
 
824
830
  setTerminalWindowTransparency(false);
825
-
826
- await summarize();
827
- await exit(false)
828
831
 
832
+ await summarize();
833
+ await exit(false);
829
834
  };
830
835
 
831
836
  const promptUser = () => {
832
837
  rl.prompt(true);
833
- }
838
+ };
834
839
 
835
840
  const setTerminalApp = async () => {
836
-
837
841
  let win = await system.activeWin();
838
- if (process.platform === 'win32') {
842
+ if (process.platform === "win32") {
839
843
  terminalApp = win?.title || "";
840
844
  } else {
841
845
  terminalApp = win?.owner?.name || "";
842
846
  }
843
-
844
- }
847
+ };
845
848
 
846
849
  const iffy = async (condition, then, otherwise, depth) => {
850
+ analytics.track("if", { condition });
847
851
 
848
- analytics.track('if', {condition});
849
-
850
- log.log('info', generator.jsonToManual({command: 'if', condition}));
852
+ log.log("info", generator.jsonToManual({ command: "if", condition }));
851
853
 
852
854
  let response = await commands.assert(condition);
853
855
 
@@ -858,18 +860,16 @@ const iffy = async (condition, then, otherwise, depth) => {
858
860
  } else {
859
861
  return await executeCommands(otherwise, depth);
860
862
  }
861
-
862
863
  };
863
864
 
864
865
  const embed = async (file, depth) => {
866
+ analytics.track("embed", { file });
865
867
 
866
- analytics.track('embed', {file});
867
-
868
- log.log('info', generator.jsonToManual({command: 'embed', file}));
868
+ log.log("info", generator.jsonToManual({ command: "embed", file }));
869
869
 
870
870
  depth = depth + 1;
871
871
 
872
- log.log('info', `${file} (start)`)
872
+ log.log("info", `${file} (start)`);
873
873
 
874
874
  // get the current wowrking directory where this file is being executed
875
875
  let cwd = process.cwd();
@@ -881,26 +881,24 @@ const embed = async (file, depth) => {
881
881
 
882
882
  // check if the file exists
883
883
  if (!fs.existsSync(file)) {
884
- throw new AiError(`Embedded file not found: ${file}`, true);
884
+ throw `Embedded file not found: ${file}`;
885
885
  }
886
886
 
887
887
  // read the file contents
888
- let contents = fs.readFileSync(file, 'utf8');
888
+ let contents = fs.readFileSync(file, "utf8");
889
889
 
890
890
  // for each step, run each command
891
891
  let steps = yaml.load(contents).steps;
892
892
  // for each step, execute the commands
893
893
 
894
- for(const step of steps) {
894
+ for (const step of steps) {
895
895
  await executeCommands(step.commands, depth);
896
- };
897
-
898
- log.log('info', `${file} (end)`)
896
+ }
899
897
 
900
- }
898
+ log.log("info", `${file} (end)`);
899
+ };
901
900
 
902
901
  (async () => {
903
-
904
902
  // @todo add-auth
905
903
  // if (!process.env.DASHCAM_API_KEY) {
906
904
  // log.log('info', chalk.red(`You must supply an API key`), 'system')
@@ -911,59 +909,70 @@ const embed = async (file, depth) => {
911
909
 
912
910
  // await sdk.auth();
913
911
 
914
-
915
912
  // if os is mac, check for screen capture permissions
916
- if (process.platform === 'darwin' && !macScreenPerms.hasScreenCapturePermission()) {
917
- log.log('info', chalk.red('Screen capture permissions not enabled.'))
918
- log.log('info', 'You must enable screen capture permissions for the application calling `testdriverai`.')
919
- log.log('info', 'Read More: https://docs.testdriver.ai/faq/screen-recording-permissions-mac-only')
920
- analytics.track('noMacPermissions');
913
+ if (
914
+ process.platform === "darwin" &&
915
+ !macScreenPerms.hasScreenCapturePermission()
916
+ ) {
917
+ log.log("info", chalk.red("Screen capture permissions not enabled."));
918
+ log.log(
919
+ "info",
920
+ "You must enable screen capture permissions for the application calling `testdriverai`.",
921
+ );
922
+ log.log(
923
+ "info",
924
+ "Read More: https://docs.testdriver.ai/faq/screen-recording-permissions-mac-only",
925
+ );
926
+ analytics.track("noMacPermissions");
921
927
  return exit();
922
928
  }
923
929
 
924
- if (thisCommand !== 'run') {
925
- speak('Howdy! I am TestDriver version ' + package.version);
926
-
927
- console.log(chalk.red('Warning!') + chalk.dim(' TestDriver sends screenshots of the desktop to our API.'));
928
- console.log(chalk.dim('https://docs.testdriver.ai/security-and-privacy/agent'));
929
- console.log('')
930
+ if (thisCommand !== "run") {
931
+ speak("Howdy! I am TestDriver version " + package.version);
930
932
 
933
+ console.log(
934
+ chalk.red("Warning!") +
935
+ chalk.dim(" TestDriver sends screenshots of the desktop to our API."),
936
+ );
937
+ console.log(
938
+ chalk.dim("https://docs.testdriver.ai/security-and-privacy/agent"),
939
+ );
940
+ console.log("");
931
941
  }
932
942
 
933
943
  await setTerminalApp();
934
944
 
935
945
  // should be start of new session
936
- sessionRes = await sdk.req('session/start', {
946
+ let sessionRes = await sdk.req("session/start", {
937
947
  systemInformationOsInfo: await system.getSystemInformationOsInfo(),
938
948
  mousePosition: await system.getMousePosition(),
939
- activeWindow: await system.activeWin()
949
+ activeWindow: await system.activeWin(),
940
950
  });
941
951
 
942
952
  session.set(sessionRes);
943
953
 
944
- analytics.track('command', {command: thisCommand, file: thisFile});
954
+ analytics.track("command", { command: thisCommand, file: thisFile });
945
955
 
946
- if (thisCommand == 'edit') {
956
+ if (thisCommand == "edit") {
947
957
  firstPrompt();
948
- } else if (thisCommand == 'run') {
958
+ } else if (thisCommand == "run") {
949
959
  errorLimit = 100;
950
960
  run(thisFile);
951
- } else if (thisCommand == 'init') {
952
- init()
961
+ } else if (thisCommand == "init") {
962
+ init();
953
963
  }
964
+ })();
954
965
 
955
- })()
956
-
957
- process.on('uncaughtException', async (err) => {
958
- analytics.track('uncaughtException', {err});
959
- console.error('Uncaught Exception:', err);
966
+ process.on("uncaughtException", async (err) => {
967
+ analytics.track("uncaughtException", { err });
968
+ console.error("Uncaught Exception:", err);
960
969
  // You might want to exit the process after handling the error
961
970
  await exit(true);
962
971
  });
963
972
 
964
- process.on('unhandledRejection', async (reason, promise) => {
965
- analytics.track('unhandledRejection', {reason, promise});
966
- console.error('Unhandled Rejection at:', promise, 'reason:', reason);
973
+ process.on("unhandledRejection", async (reason, promise) => {
974
+ analytics.track("unhandledRejection", { reason, promise });
975
+ console.error("Unhandled Rejection at:", promise, "reason:", reason);
967
976
  // Optionally, you might want to exit the process
968
977
  await exit(true);
969
978
  });