testdriverai 4.1.1 → 4.1.4

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