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.
- package/agent.js +1082 -0
- package/index.js +30 -1075
- package/package.json +1 -1
- 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
|
+
});
|