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/.github/workflows/format.yml +17 -0
- package/.github/workflows/npm-publish.yml +1 -1
- package/.prettierignore +2 -0
- package/.prettierrc +1 -0
- package/README.md +9 -10
- package/eslint.config.js +21 -0
- package/index.js +403 -394
- package/lib/analytics.js +8 -10
- package/lib/commander.js +120 -90
- package/lib/commands.js +259 -217
- package/lib/config.js +18 -18
- package/lib/focus-application.js +12 -15
- package/lib/generator.js +35 -35
- package/lib/history.js +10 -10
- package/lib/init.js +125 -120
- package/lib/keymap.js +114 -115
- package/lib/logger.js +42 -45
- package/lib/notify.js +1 -5
- package/lib/parser.js +28 -42
- package/lib/redraw.js +19 -23
- package/lib/sdk.js +46 -47
- package/lib/session.js +1 -1
- package/lib/speak.js +5 -7
- package/lib/subimage/index.js +9 -8
- package/lib/subimage/opencv.js +1 -0
- package/lib/system.js +38 -34
- package/lib/valid-version.js +10 -8
- package/package.json +7 -4
- package/postinstall.js +5 -5
package/index.js
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// disable depreciation warnings
|
|
4
|
-
process.removeAllListeners(
|
|
4
|
+
process.removeAllListeners("warning");
|
|
5
5
|
|
|
6
6
|
// package.json is included to get the version number
|
|
7
|
-
const package = require(
|
|
7
|
+
const package = require("./package.json");
|
|
8
8
|
|
|
9
9
|
// system modules
|
|
10
|
-
const fs = require(
|
|
11
|
-
const readline = require(
|
|
12
|
-
const os = require(
|
|
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(
|
|
17
|
-
const chalk = require(
|
|
18
|
-
const yaml = require(
|
|
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(
|
|
19
|
+
const macScreenPerms = require("mac-screen-capture-permissions");
|
|
21
20
|
|
|
22
21
|
// local modules
|
|
23
|
-
const speak = require(
|
|
24
|
-
const analytics = require(
|
|
25
|
-
const log = require(
|
|
26
|
-
const parser = require(
|
|
27
|
-
const commander = require(
|
|
28
|
-
const system = require(
|
|
29
|
-
const generator = require(
|
|
30
|
-
const sdk = require(
|
|
31
|
-
const commands = require(
|
|
32
|
-
const init = require(
|
|
33
|
-
|
|
34
|
-
const { showTerminal, hideTerminal } = require(
|
|
35
|
-
const isValidVersion = require(
|
|
36
|
-
const session = require(
|
|
37
|
-
const notify = require(
|
|
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(
|
|
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(),
|
|
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 (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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] =
|
|
79
|
+
args[command] = "edit";
|
|
78
80
|
} else if (!args[command]) {
|
|
79
|
-
args[command] =
|
|
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(),
|
|
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] =
|
|
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(
|
|
97
|
-
args[file] +=
|
|
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(
|
|
110
|
-
log.log(
|
|
111
|
-
console.log(
|
|
112
|
-
log.log(
|
|
113
|
-
log.log(
|
|
114
|
-
log.log(
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 =
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
completions.concat(tasks)
|
|
156
|
+
} else {
|
|
157
|
+
completions.concat(tasks);
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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 = [
|
|
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(
|
|
191
|
+
analytics.track("exit", { failed });
|
|
183
192
|
|
|
184
193
|
// we purposly never resolve this promise so the process will hang
|
|
185
|
-
return new Promise(
|
|
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(
|
|
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] >
|
|
213
|
-
console.log(chalk.red(
|
|
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(
|
|
231
|
-
notify(
|
|
232
|
-
log.log(
|
|
238
|
+
speak("thinking...");
|
|
239
|
+
notify("thinking...");
|
|
240
|
+
log.log("info", chalk.dim("thinking..."), true);
|
|
233
241
|
|
|
234
|
-
let response = await sdk.req(
|
|
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(
|
|
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(
|
|
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 ==
|
|
288
|
+
if (command.command == "embed") {
|
|
272
289
|
response = await embed(command.file, depth);
|
|
273
|
-
} else if (command.command ==
|
|
274
|
-
response = await iffy(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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 = [[
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
391
|
+
|
|
392
|
+
log.log("debug", `found ${checkCodeblocks.length} codeblocks`);
|
|
377
393
|
|
|
378
394
|
if (checkCodeblocks.length > 0) {
|
|
379
|
-
log.log(
|
|
395
|
+
log.log("debug", "check thinks more needs to be done");
|
|
380
396
|
return await aiExecute(response, validateAndLoop);
|
|
381
397
|
} else {
|
|
382
|
-
log.log(
|
|
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(
|
|
414
|
+
throw new Error("No task to assert");
|
|
402
415
|
}
|
|
403
416
|
}
|
|
404
417
|
|
|
405
|
-
speak(
|
|
406
|
-
notify(
|
|
407
|
-
log.log(
|
|
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(
|
|
440
|
+
log.log("debug", "humanInput called");
|
|
428
441
|
|
|
429
442
|
tasks.push(currentTask);
|
|
430
443
|
|
|
431
|
-
speak(
|
|
432
|
-
notify(
|
|
433
|
-
log.log(
|
|
444
|
+
speak("thinking...");
|
|
445
|
+
notify("thinking...");
|
|
446
|
+
log.log("info", chalk.dim("thinking..."), true);
|
|
434
447
|
|
|
435
|
-
log.log(
|
|
448
|
+
log.log("info", "");
|
|
436
449
|
|
|
437
|
-
let image = await system.captureScreenBase64(.5);
|
|
438
|
-
let message = await sdk.req(
|
|
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(
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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(
|
|
474
|
+
log.log("info", "");
|
|
463
475
|
|
|
464
|
-
let image = await system.captureScreenBase64(.5);
|
|
465
|
-
let message = await sdk.req(
|
|
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 =
|
|
477
|
-
|
|
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(),
|
|
480
|
-
fs.mkdirSync(path.join(process.cwd(),
|
|
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
|
|
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
|
-
|
|
513
|
+
executionHistory.pop();
|
|
496
514
|
} else {
|
|
497
|
-
|
|
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 (
|
|
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(
|
|
580
|
+
analytics.track("first prompt");
|
|
571
581
|
|
|
572
|
-
rl.on(
|
|
573
|
-
analytics.track(
|
|
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(
|
|
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 +
|
|
596
|
+
fs.appendFileSync(commandHistoryFile, input + "\n");
|
|
588
597
|
|
|
589
|
-
analytics.track(
|
|
598
|
+
analytics.track("input", { input });
|
|
590
599
|
|
|
591
|
-
console.log(
|
|
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(
|
|
605
|
+
if (input.indexOf("/summarize") == 0) {
|
|
597
606
|
await summarize();
|
|
598
|
-
} else if (input.indexOf(
|
|
607
|
+
} else if (input.indexOf("/quit") == 0) {
|
|
599
608
|
await exit();
|
|
600
|
-
} else if (input.indexOf(
|
|
609
|
+
} else if (input.indexOf("/save") == 0) {
|
|
601
610
|
await save({ filepath: commands[1] });
|
|
602
|
-
} else if (input.indexOf(
|
|
603
|
-
await humanInput(commands.slice(1).join(
|
|
604
|
-
} else if (input.indexOf(
|
|
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(
|
|
607
|
-
await assert(commands.slice(1).join(
|
|
608
|
-
} else if (input.indexOf(
|
|
609
|
-
await manualInput(commands.slice(1).join(
|
|
610
|
-
} else if (input.indexOf(
|
|
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(
|
|
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(
|
|
626
|
-
let object = await generator.ymlToHistory(
|
|
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(
|
|
630
|
-
log.log(
|
|
631
|
-
console.log(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
689
|
-
|
|
690
|
-
log.log(
|
|
691
|
-
log.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
748
|
+
const fileName = filepath.split("/").pop();
|
|
745
749
|
if (!silent) {
|
|
746
|
-
log.log(
|
|
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(
|
|
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,
|
|
768
|
+
yml = fs.readFileSync(file, "utf-8");
|
|
766
769
|
} catch (e) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
console.log(`
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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(
|
|
790
|
-
console.log(
|
|
791
|
-
|
|
792
|
-
|
|
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(
|
|
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(
|
|
814
|
-
log.log(
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
884
|
+
throw `Embedded file not found: ${file}`;
|
|
885
885
|
}
|
|
886
886
|
|
|
887
887
|
// read the file contents
|
|
888
|
-
let contents = fs.readFileSync(file,
|
|
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 (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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 !==
|
|
925
|
-
speak(
|
|
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(
|
|
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(
|
|
954
|
+
analytics.track("command", { command: thisCommand, file: thisFile });
|
|
945
955
|
|
|
946
|
-
if (thisCommand ==
|
|
956
|
+
if (thisCommand == "edit") {
|
|
947
957
|
firstPrompt();
|
|
948
|
-
} else if (thisCommand ==
|
|
958
|
+
} else if (thisCommand == "run") {
|
|
949
959
|
errorLimit = 100;
|
|
950
960
|
run(thisFile);
|
|
951
|
-
} else if (thisCommand ==
|
|
952
|
-
init()
|
|
961
|
+
} else if (thisCommand == "init") {
|
|
962
|
+
init();
|
|
953
963
|
}
|
|
964
|
+
})();
|
|
954
965
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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(
|
|
965
|
-
analytics.track(
|
|
966
|
-
console.error(
|
|
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
|
});
|