slapify 0.0.16 โ 0.0.18
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/README.md +38 -4
- package/dist/ai/interpreter.js +1 -331
- package/dist/browser/agent.js +1 -485
- package/dist/cli.js +1 -1553
- package/dist/config/loader.js +1 -305
- package/dist/index.js +1 -262
- package/dist/parser/flow.js +1 -117
- package/dist/perf/audit.js +1 -635
- package/dist/report/generator.js +1 -641
- package/dist/runner/index.js +1 -744
- package/dist/task/index.js +1 -4
- package/dist/task/report.js +1 -740
- package/dist/task/runner.js +1 -1362
- package/dist/task/session.js +1 -153
- package/dist/task/tools.d.ts +12 -0
- package/dist/task/tools.js +1 -258
- package/dist/task/types.d.ts +18 -0
- package/dist/task/types.js +1 -2
- package/dist/types.js +1 -2
- package/package.json +6 -3
- package/dist/ai/interpreter.d.ts.map +0 -1
- package/dist/ai/interpreter.js.map +0 -1
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parser/flow.d.ts.map +0 -1
- package/dist/parser/flow.js.map +0 -1
- package/dist/perf/audit.d.ts.map +0 -1
- package/dist/perf/audit.js.map +0 -1
- package/dist/report/generator.d.ts.map +0 -1
- package/dist/report/generator.js.map +0 -1
- package/dist/runner/index.d.ts.map +0 -1
- package/dist/runner/index.js.map +0 -1
- package/dist/task/index.d.ts.map +0 -1
- package/dist/task/index.js.map +0 -1
- package/dist/task/report.d.ts.map +0 -1
- package/dist/task/report.js.map +0 -1
- package/dist/task/runner.d.ts.map +0 -1
- package/dist/task/runner.js.map +0 -1
- package/dist/task/session.d.ts.map +0 -1
- package/dist/task/session.js.map +0 -1
- package/dist/task/tools.d.ts.map +0 -1
- package/dist/task/tools.js.map +0 -1
- package/dist/task/types.d.ts.map +0 -1
- package/dist/task/types.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,1554 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import ora from "ora";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import dotenv from "dotenv";
|
|
8
|
-
import { loadConfig, loadCredentials, getConfigDir, } from "./config/loader.js";
|
|
9
|
-
import { parseFlowFile, findFlowFiles, validateFlowFile, getFlowSummary, } from "./parser/flow.js";
|
|
10
|
-
import { TestRunner } from "./runner/index.js";
|
|
11
|
-
import { ReportGenerator } from "./report/generator.js";
|
|
12
|
-
import { BrowserAgent } from "./browser/agent.js";
|
|
13
|
-
import { generateText } from "ai";
|
|
14
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
15
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
16
|
-
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
17
|
-
import { createMistral } from "@ai-sdk/mistral";
|
|
18
|
-
import { createGroq } from "@ai-sdk/groq";
|
|
19
|
-
import yaml from "yaml";
|
|
20
|
-
// Load environment variables
|
|
21
|
-
dotenv.config();
|
|
22
|
-
/**
|
|
23
|
-
* Get AI model based on config
|
|
24
|
-
*/
|
|
25
|
-
function getModelFromConfig(llmConfig) {
|
|
26
|
-
switch (llmConfig.provider) {
|
|
27
|
-
case "anthropic": {
|
|
28
|
-
const anthropic = createAnthropic({ apiKey: llmConfig.api_key });
|
|
29
|
-
return anthropic(llmConfig.model);
|
|
30
|
-
}
|
|
31
|
-
case "openai": {
|
|
32
|
-
const openai = createOpenAI({ apiKey: llmConfig.api_key });
|
|
33
|
-
return openai(llmConfig.model);
|
|
34
|
-
}
|
|
35
|
-
case "google": {
|
|
36
|
-
const google = createGoogleGenerativeAI({ apiKey: llmConfig.api_key });
|
|
37
|
-
return google(llmConfig.model);
|
|
38
|
-
}
|
|
39
|
-
case "mistral": {
|
|
40
|
-
const mistral = createMistral({ apiKey: llmConfig.api_key });
|
|
41
|
-
return mistral(llmConfig.model);
|
|
42
|
-
}
|
|
43
|
-
case "groq": {
|
|
44
|
-
const groq = createGroq({ apiKey: llmConfig.api_key });
|
|
45
|
-
return groq(llmConfig.model);
|
|
46
|
-
}
|
|
47
|
-
case "ollama": {
|
|
48
|
-
const ollama = createOpenAI({
|
|
49
|
-
apiKey: "ollama",
|
|
50
|
-
baseURL: llmConfig.base_url || "http://localhost:11434/v1",
|
|
51
|
-
});
|
|
52
|
-
return ollama(llmConfig.model);
|
|
53
|
-
}
|
|
54
|
-
default:
|
|
55
|
-
throw new Error(`Unsupported provider: ${llmConfig.provider}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const program = new Command();
|
|
59
|
-
program
|
|
60
|
-
.name("slapify")
|
|
61
|
-
.description("AI-powered test automation using natural language flow files")
|
|
62
|
-
.version("0.1.0");
|
|
63
|
-
// Init command
|
|
64
|
-
program
|
|
65
|
-
.command("init")
|
|
66
|
-
.description("Initialize Slapify in the current directory")
|
|
67
|
-
.option("-y, --yes", "Skip prompts and use defaults")
|
|
68
|
-
.action(async (options) => {
|
|
69
|
-
const readline = await import("readline");
|
|
70
|
-
// Check if already initialized
|
|
71
|
-
if (fs.existsSync(".slapify")) {
|
|
72
|
-
console.log(chalk.yellow("Slapify is already initialized in this directory."));
|
|
73
|
-
console.log(chalk.gray("Delete .slapify folder to reinitialize."));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
console.log(chalk.blue.bold("\n๐๏ธ Welcome to Slapify!\n"));
|
|
77
|
-
console.log(chalk.gray("AI-powered E2E testing that slaps - by slaps.dev\n"));
|
|
78
|
-
// Import the new functions
|
|
79
|
-
const { findSystemBrowsers, initConfig: doInit } = await import("./config/loader.js");
|
|
80
|
-
let provider = "anthropic";
|
|
81
|
-
let model;
|
|
82
|
-
let browserPath;
|
|
83
|
-
let useSystemBrowser;
|
|
84
|
-
const providerInfo = {
|
|
85
|
-
anthropic: {
|
|
86
|
-
name: "Anthropic (Claude)",
|
|
87
|
-
envVar: "ANTHROPIC_API_KEY",
|
|
88
|
-
defaultModel: "claude-haiku-4-5-20251001",
|
|
89
|
-
models: [
|
|
90
|
-
{
|
|
91
|
-
id: "claude-haiku-4-5-20251001",
|
|
92
|
-
name: "Haiku 4.5 - fast & cheap ($1/5M tokens)",
|
|
93
|
-
recommended: true,
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "claude-sonnet-4-20250514",
|
|
97
|
-
name: "Sonnet 4 - more capable ($3/15M tokens)",
|
|
98
|
-
},
|
|
99
|
-
{ id: "custom", name: "Enter custom model ID" },
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
openai: {
|
|
103
|
-
name: "OpenAI",
|
|
104
|
-
envVar: "OPENAI_API_KEY",
|
|
105
|
-
defaultModel: "gpt-4o-mini",
|
|
106
|
-
models: [
|
|
107
|
-
{
|
|
108
|
-
id: "gpt-4o-mini",
|
|
109
|
-
name: "GPT-4o Mini - fast & cheap ($0.15/0.6M tokens)",
|
|
110
|
-
recommended: true,
|
|
111
|
-
},
|
|
112
|
-
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini - newer" },
|
|
113
|
-
{ id: "gpt-4o", name: "GPT-4o - more capable ($2.5/10M tokens)" },
|
|
114
|
-
{ id: "custom", name: "Enter custom model ID" },
|
|
115
|
-
],
|
|
116
|
-
},
|
|
117
|
-
google: {
|
|
118
|
-
name: "Google (Gemini)",
|
|
119
|
-
envVar: "GOOGLE_API_KEY",
|
|
120
|
-
defaultModel: "gemini-2.0-flash",
|
|
121
|
-
models: [
|
|
122
|
-
{
|
|
123
|
-
id: "gemini-2.0-flash",
|
|
124
|
-
name: "Gemini 2.0 Flash - fastest & cheapest",
|
|
125
|
-
recommended: true,
|
|
126
|
-
},
|
|
127
|
-
{ id: "gemini-1.5-flash", name: "Gemini 1.5 Flash - stable" },
|
|
128
|
-
{ id: "gemini-1.5-pro", name: "Gemini 1.5 Pro - more capable" },
|
|
129
|
-
{ id: "custom", name: "Enter custom model ID" },
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
mistral: {
|
|
133
|
-
name: "Mistral",
|
|
134
|
-
envVar: "MISTRAL_API_KEY",
|
|
135
|
-
askModel: true,
|
|
136
|
-
defaultModel: "mistral-small-latest",
|
|
137
|
-
},
|
|
138
|
-
groq: {
|
|
139
|
-
name: "Groq (Fast inference)",
|
|
140
|
-
envVar: "GROQ_API_KEY",
|
|
141
|
-
askModel: true,
|
|
142
|
-
defaultModel: "llama-3.3-70b-versatile",
|
|
143
|
-
},
|
|
144
|
-
ollama: {
|
|
145
|
-
name: "Ollama (Local)",
|
|
146
|
-
envVar: "",
|
|
147
|
-
askModel: true,
|
|
148
|
-
defaultModel: "llama3",
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
if (options.yes) {
|
|
152
|
-
// Use defaults
|
|
153
|
-
console.log(chalk.gray("Using default settings...\n"));
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
const rl = readline.createInterface({
|
|
157
|
-
input: process.stdin,
|
|
158
|
-
output: process.stdout,
|
|
159
|
-
});
|
|
160
|
-
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
161
|
-
// Step 1: LLM Provider
|
|
162
|
-
console.log(chalk.cyan("1. Choose your LLM provider:\n"));
|
|
163
|
-
console.log(" 1) Anthropic (Claude) " + chalk.green("- recommended"));
|
|
164
|
-
console.log(" 2) OpenAI (GPT-4)");
|
|
165
|
-
console.log(" 3) Google (Gemini)");
|
|
166
|
-
console.log(" 4) Mistral");
|
|
167
|
-
console.log(" 5) Groq " + chalk.gray("- fast & free tier"));
|
|
168
|
-
console.log(" 6) Ollama " + chalk.gray("- local, no API key"));
|
|
169
|
-
console.log("");
|
|
170
|
-
const providerChoice = await question(chalk.white(" Select [1]: "));
|
|
171
|
-
const providerMap = {
|
|
172
|
-
"1": "anthropic",
|
|
173
|
-
"2": "openai",
|
|
174
|
-
"3": "google",
|
|
175
|
-
"4": "mistral",
|
|
176
|
-
"5": "groq",
|
|
177
|
-
"6": "ollama",
|
|
178
|
-
};
|
|
179
|
-
provider = providerMap[providerChoice] || "anthropic";
|
|
180
|
-
const info = providerInfo[provider];
|
|
181
|
-
console.log(chalk.green(` โ Using ${info.name}\n`));
|
|
182
|
-
// Step 1b: Model Selection
|
|
183
|
-
if (info.models && info.models.length > 0) {
|
|
184
|
-
console.log(chalk.cyan(" Choose model:\n"));
|
|
185
|
-
info.models.forEach((m, i) => {
|
|
186
|
-
const rec = m.recommended ? chalk.green(" โ recommended") : "";
|
|
187
|
-
console.log(` ${i + 1}) ${m.name}${rec}`);
|
|
188
|
-
});
|
|
189
|
-
console.log("");
|
|
190
|
-
const modelChoice = await question(chalk.white(" Select [1]: "));
|
|
191
|
-
const modelIdx = parseInt(modelChoice) - 1 || 0;
|
|
192
|
-
const selectedModel = info.models[modelIdx];
|
|
193
|
-
if (selectedModel?.id === "custom") {
|
|
194
|
-
const customModel = await question(chalk.white(" Enter model ID: "));
|
|
195
|
-
model = customModel.trim() || info.defaultModel;
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
model = selectedModel?.id || info.defaultModel;
|
|
199
|
-
}
|
|
200
|
-
console.log(chalk.green(` โ Using model: ${model}\n`));
|
|
201
|
-
}
|
|
202
|
-
else if (info.askModel) {
|
|
203
|
-
// For Mistral, Groq, Ollama - ask for model ID directly
|
|
204
|
-
console.log(chalk.gray(` Enter model ID (default: ${info.defaultModel})`));
|
|
205
|
-
if (provider === "ollama") {
|
|
206
|
-
console.log(chalk.gray(" Common models: llama3, mistral, codellama, phi3"));
|
|
207
|
-
}
|
|
208
|
-
else if (provider === "groq") {
|
|
209
|
-
console.log(chalk.gray(" Common models: llama-3.3-70b-versatile, mixtral-8x7b-32768"));
|
|
210
|
-
}
|
|
211
|
-
else if (provider === "mistral") {
|
|
212
|
-
console.log(chalk.gray(" Common models: mistral-small-latest, mistral-large-latest"));
|
|
213
|
-
}
|
|
214
|
-
console.log("");
|
|
215
|
-
const modelInput = await question(chalk.white(` Model [${info.defaultModel}]: `));
|
|
216
|
-
model = modelInput.trim() || info.defaultModel;
|
|
217
|
-
console.log(chalk.green(` โ Using model: ${model}\n`));
|
|
218
|
-
if (provider === "ollama") {
|
|
219
|
-
console.log(chalk.gray(" Make sure Ollama is running: ollama serve"));
|
|
220
|
-
console.log("");
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
// Step 2: API Key Verification (skip for Ollama)
|
|
224
|
-
if (provider !== "ollama") {
|
|
225
|
-
console.log(chalk.cyan("2. API Key verification:\n"));
|
|
226
|
-
const envVar = info.envVar;
|
|
227
|
-
let apiKey = process.env[envVar];
|
|
228
|
-
if (apiKey) {
|
|
229
|
-
console.log(chalk.gray(` Found ${envVar} in environment`));
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
console.log(chalk.yellow(` ${envVar} not found in environment`));
|
|
233
|
-
console.log(chalk.gray(" You can set it now or add it to your shell config later.\n"));
|
|
234
|
-
const keyInput = await question(chalk.white(` Enter API key (or press Enter to skip): `));
|
|
235
|
-
if (keyInput.trim()) {
|
|
236
|
-
apiKey = keyInput.trim();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// API key verification loop
|
|
240
|
-
let verified = false;
|
|
241
|
-
while (!verified) {
|
|
242
|
-
if (apiKey) {
|
|
243
|
-
const verifyChoice = await question(chalk.white(" Verify API key with test call? (Y/n): "));
|
|
244
|
-
if (verifyChoice.toLowerCase() !== "n") {
|
|
245
|
-
// Close readline temporarily - ora spinner can interfere with readline
|
|
246
|
-
rl.pause();
|
|
247
|
-
const verifySpinner = ora(" Verifying API key...").start();
|
|
248
|
-
try {
|
|
249
|
-
// Make a minimal test call
|
|
250
|
-
const testConfig = {
|
|
251
|
-
provider,
|
|
252
|
-
model: model || info.defaultModel || "test",
|
|
253
|
-
api_key: apiKey,
|
|
254
|
-
};
|
|
255
|
-
const testModel = getModelFromConfig(testConfig);
|
|
256
|
-
const response = await generateText({
|
|
257
|
-
model: testModel,
|
|
258
|
-
prompt: "Reply with only the word 'pong'",
|
|
259
|
-
maxTokens: 10,
|
|
260
|
-
});
|
|
261
|
-
if (response.text.toLowerCase().includes("pong")) {
|
|
262
|
-
verifySpinner.succeed(chalk.green("API key verified! โ"));
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
verifySpinner.succeed(chalk.green("API key works! (got response)"));
|
|
266
|
-
}
|
|
267
|
-
verified = true;
|
|
268
|
-
}
|
|
269
|
-
catch (error) {
|
|
270
|
-
verifySpinner.fail(chalk.red("API key verification failed"));
|
|
271
|
-
console.log(chalk.red(` Error: ${error.message}\n`));
|
|
272
|
-
}
|
|
273
|
-
// Resume readline after spinner is done
|
|
274
|
-
rl.resume();
|
|
275
|
-
if (!verified) {
|
|
276
|
-
const retryChoice = await question(chalk.white(" Try a different API key? (Y/n): "));
|
|
277
|
-
if (retryChoice.toLowerCase() === "n") {
|
|
278
|
-
console.log(chalk.yellow(` Remember to set ${envVar} correctly before running tests.`));
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
const newKey = await question(chalk.white(` Enter API key: `));
|
|
282
|
-
if (newKey.trim()) {
|
|
283
|
-
apiKey = newKey.trim();
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
console.log(chalk.yellow(" No key entered, skipping.\n"));
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
console.log(chalk.gray(" Skipping verification\n"));
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
console.log(chalk.yellow(`\n Remember to set ${envVar} before running tests.`));
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
console.log("");
|
|
302
|
-
}
|
|
303
|
-
// Step 3: Browser Setup
|
|
304
|
-
console.log(chalk.cyan(`${provider === "ollama" ? "2" : "3"}. Browser setup:\n`));
|
|
305
|
-
const systemBrowsers = findSystemBrowsers();
|
|
306
|
-
if (systemBrowsers.length > 0) {
|
|
307
|
-
console.log(" Found browsers on your system:");
|
|
308
|
-
systemBrowsers.forEach((b, i) => {
|
|
309
|
-
console.log(chalk.gray(` ${i + 1}) ${b.name}`));
|
|
310
|
-
});
|
|
311
|
-
console.log(chalk.gray(` ${systemBrowsers.length + 1}) Download Chromium (~170MB)`));
|
|
312
|
-
console.log(chalk.gray(` ${systemBrowsers.length + 2}) Enter custom path`));
|
|
313
|
-
console.log("");
|
|
314
|
-
const browserChoice = await question(chalk.white(` Select [1]: `));
|
|
315
|
-
const choiceNum = parseInt(browserChoice) || 1;
|
|
316
|
-
if (choiceNum <= systemBrowsers.length) {
|
|
317
|
-
browserPath = systemBrowsers[choiceNum - 1].path;
|
|
318
|
-
useSystemBrowser = true;
|
|
319
|
-
console.log(chalk.green(` โ Using ${systemBrowsers[choiceNum - 1].name}\n`));
|
|
320
|
-
}
|
|
321
|
-
else if (choiceNum === systemBrowsers.length + 2) {
|
|
322
|
-
const customPath = await question(chalk.white(" Enter browser path: "));
|
|
323
|
-
if (customPath.trim()) {
|
|
324
|
-
browserPath = customPath.trim();
|
|
325
|
-
useSystemBrowser = true;
|
|
326
|
-
console.log(chalk.green(` โ Using custom browser\n`));
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
useSystemBrowser = false;
|
|
331
|
-
console.log(chalk.green(" โ Will download Chromium on first run\n"));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
console.log(" No browsers found. Options:");
|
|
336
|
-
console.log(chalk.gray(" 1) Download Chromium automatically (~170MB)"));
|
|
337
|
-
console.log(chalk.gray(" 2) Enter custom browser path"));
|
|
338
|
-
console.log("");
|
|
339
|
-
const browserChoice = await question(chalk.white(" Select [1]: "));
|
|
340
|
-
if (browserChoice === "2") {
|
|
341
|
-
const customPath = await question(chalk.white(" Enter browser path: "));
|
|
342
|
-
if (customPath.trim()) {
|
|
343
|
-
browserPath = customPath.trim();
|
|
344
|
-
useSystemBrowser = true;
|
|
345
|
-
console.log(chalk.green(" โ Using custom browser\n"));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
useSystemBrowser = false;
|
|
350
|
-
console.log(chalk.green(" โ Will download Chromium on first run\n"));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
rl.close();
|
|
354
|
-
}
|
|
355
|
-
// Initialize
|
|
356
|
-
const spinner = ora("Creating configuration...").start();
|
|
357
|
-
try {
|
|
358
|
-
doInit(process.cwd(), {
|
|
359
|
-
provider,
|
|
360
|
-
model,
|
|
361
|
-
browserPath,
|
|
362
|
-
useSystemBrowser,
|
|
363
|
-
});
|
|
364
|
-
spinner.succeed("Slapify initialized!");
|
|
365
|
-
console.log("");
|
|
366
|
-
console.log(chalk.green("Created:"));
|
|
367
|
-
console.log(" ๐ .slapify/config.yaml - Configuration");
|
|
368
|
-
console.log(" ๐ .slapify/credentials.yaml - Credentials (gitignored)");
|
|
369
|
-
console.log(" ๐ tests/example.flow - Sample test");
|
|
370
|
-
console.log("");
|
|
371
|
-
const info = providerInfo[provider];
|
|
372
|
-
console.log(chalk.yellow("Next steps:"));
|
|
373
|
-
console.log("");
|
|
374
|
-
if (provider === "ollama") {
|
|
375
|
-
console.log(chalk.white(` 1. Make sure Ollama is running:`));
|
|
376
|
-
console.log(chalk.cyan(` ollama serve`));
|
|
377
|
-
console.log(chalk.cyan(` ollama pull llama3`));
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
console.log(chalk.white(` 1. Set your API key:`));
|
|
381
|
-
console.log(chalk.cyan(` export ${info.envVar}=your-key-here`));
|
|
382
|
-
}
|
|
383
|
-
console.log("");
|
|
384
|
-
console.log(chalk.white(` 2. Run the example test:`));
|
|
385
|
-
console.log(chalk.cyan(` slapify run tests/example.flow`));
|
|
386
|
-
console.log("");
|
|
387
|
-
console.log(chalk.white(` 3. Create your own tests:`));
|
|
388
|
-
console.log(chalk.cyan(` slapify create my-first-test`));
|
|
389
|
-
console.log(chalk.cyan(` slapify generate "test login for myapp.com"`));
|
|
390
|
-
console.log("");
|
|
391
|
-
console.log(chalk.gray(" Config can be modified anytime in .slapify/config.yaml"));
|
|
392
|
-
console.log("");
|
|
393
|
-
}
|
|
394
|
-
catch (error) {
|
|
395
|
-
spinner.fail(error.message);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
// Install command (for agent-browser)
|
|
400
|
-
program
|
|
401
|
-
.command("install")
|
|
402
|
-
.description("Install browser dependencies")
|
|
403
|
-
.action(() => {
|
|
404
|
-
const spinner = ora("Checking agent-browser...").start();
|
|
405
|
-
if (BrowserAgent.isInstalled()) {
|
|
406
|
-
spinner.succeed("agent-browser is already installed");
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
spinner.text = "Installing agent-browser...";
|
|
410
|
-
try {
|
|
411
|
-
BrowserAgent.install();
|
|
412
|
-
spinner.succeed("Browser dependencies installed!");
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
spinner.fail(`Installation failed: ${error.message}`);
|
|
416
|
-
process.exit(1);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
// Run command
|
|
421
|
-
program
|
|
422
|
-
.command("run [files...]")
|
|
423
|
-
.description("Run flow test files")
|
|
424
|
-
.option("--headed", "Run browser in headed mode (visible)")
|
|
425
|
-
.option("--report [format]", "Generate report folder (html, markdown, json)")
|
|
426
|
-
.option("--output <dir>", "Output directory for reports", "./test-reports")
|
|
427
|
-
.option("--credentials <profile>", "Default credentials profile to use")
|
|
428
|
-
.option("-p, --parallel", "Run tests in parallel")
|
|
429
|
-
.option("-w, --workers <n>", "Number of parallel workers (default: 4)", "4")
|
|
430
|
-
.option("--performance", "Run performance audit (scores, real-user metrics, framework & re-render analysis) and include in report")
|
|
431
|
-
.action(async (files, options) => {
|
|
432
|
-
try {
|
|
433
|
-
// Load configuration
|
|
434
|
-
const configDir = getConfigDir();
|
|
435
|
-
if (!configDir) {
|
|
436
|
-
console.log(chalk.red('No .slapify directory found. Run "slapify init" first.'));
|
|
437
|
-
process.exit(1);
|
|
438
|
-
}
|
|
439
|
-
const config = loadConfig(configDir);
|
|
440
|
-
const credentials = loadCredentials(configDir);
|
|
441
|
-
// Apply CLI options
|
|
442
|
-
if (options.headed) {
|
|
443
|
-
config.browser = { ...config.browser, headless: false };
|
|
444
|
-
}
|
|
445
|
-
// Only enable screenshots if report is requested
|
|
446
|
-
const generateReport = options.report !== undefined;
|
|
447
|
-
config.report = {
|
|
448
|
-
...config.report,
|
|
449
|
-
format: typeof options.report === "string" ? options.report : "html",
|
|
450
|
-
output_dir: options.output,
|
|
451
|
-
screenshots: generateReport, // Only take screenshots for reports
|
|
452
|
-
};
|
|
453
|
-
// Find flow files
|
|
454
|
-
let flowFiles = [];
|
|
455
|
-
if (files.length === 0) {
|
|
456
|
-
// Run all flow files in tests directory
|
|
457
|
-
const testsDir = path.join(process.cwd(), "tests");
|
|
458
|
-
if (fs.existsSync(testsDir)) {
|
|
459
|
-
flowFiles = await findFlowFiles(testsDir);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
for (const file of files) {
|
|
464
|
-
if (fs.statSync(file).isDirectory()) {
|
|
465
|
-
const found = await findFlowFiles(file);
|
|
466
|
-
flowFiles.push(...found);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
flowFiles.push(file);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
if (flowFiles.length === 0) {
|
|
474
|
-
console.log(chalk.yellow("No .flow files found to run."));
|
|
475
|
-
process.exit(0);
|
|
476
|
-
}
|
|
477
|
-
const reporter = new ReportGenerator(config.report);
|
|
478
|
-
const results = [];
|
|
479
|
-
const isParallel = options.parallel && flowFiles.length > 1;
|
|
480
|
-
const workers = parseInt(options.workers) || 4;
|
|
481
|
-
if (isParallel) {
|
|
482
|
-
// Parallel execution
|
|
483
|
-
console.log(chalk.blue.bold(`\nโโโ Running ${flowFiles.length} tests in parallel (${workers} workers) โโโ\n`));
|
|
484
|
-
const pending = [...flowFiles];
|
|
485
|
-
const running = new Map();
|
|
486
|
-
const testStatus = new Map();
|
|
487
|
-
// Initialize status
|
|
488
|
-
for (const file of flowFiles) {
|
|
489
|
-
const name = path.basename(file, ".flow");
|
|
490
|
-
testStatus.set(name, chalk.gray("โณ pending"));
|
|
491
|
-
}
|
|
492
|
-
const printStatus = () => {
|
|
493
|
-
// Clear and reprint status
|
|
494
|
-
process.stdout.write("\x1B[" + flowFiles.length + "A"); // Move cursor up
|
|
495
|
-
for (const file of flowFiles) {
|
|
496
|
-
const name = path.basename(file, ".flow");
|
|
497
|
-
const status = testStatus.get(name) || "";
|
|
498
|
-
process.stdout.write("\x1B[2K"); // Clear line
|
|
499
|
-
console.log(` ${name}: ${status}`);
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
// Print initial status
|
|
503
|
-
for (const file of flowFiles) {
|
|
504
|
-
const name = path.basename(file, ".flow");
|
|
505
|
-
console.log(` ${name}: ${testStatus.get(name)}`);
|
|
506
|
-
}
|
|
507
|
-
const runTest = async (file) => {
|
|
508
|
-
const flow = parseFlowFile(file);
|
|
509
|
-
const name = flow.name;
|
|
510
|
-
testStatus.set(name, chalk.cyan("โถ running..."));
|
|
511
|
-
printStatus();
|
|
512
|
-
try {
|
|
513
|
-
const runner = new TestRunner(config, credentials);
|
|
514
|
-
const result = await runner.runFlow(flow);
|
|
515
|
-
results.push(result);
|
|
516
|
-
if (result.status === "passed") {
|
|
517
|
-
testStatus.set(name, chalk.green(`โ passed (${result.passedSteps}/${result.totalSteps} steps, ${(result.duration / 1000).toFixed(1)}s)`));
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
testStatus.set(name, chalk.red(`โ failed (${result.failedSteps} failed, ${result.passedSteps} passed)`));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
catch (error) {
|
|
524
|
-
testStatus.set(name, chalk.red(`โ error: ${error.message}`));
|
|
525
|
-
}
|
|
526
|
-
printStatus();
|
|
527
|
-
};
|
|
528
|
-
// Process with worker limit
|
|
529
|
-
while (pending.length > 0 || running.size > 0) {
|
|
530
|
-
// Start new tasks up to worker limit
|
|
531
|
-
while (pending.length > 0 && running.size < workers) {
|
|
532
|
-
const file = pending.shift();
|
|
533
|
-
const promise = runTest(file).then(() => {
|
|
534
|
-
running.delete(file);
|
|
535
|
-
});
|
|
536
|
-
running.set(file, promise);
|
|
537
|
-
}
|
|
538
|
-
// Wait for at least one to complete
|
|
539
|
-
if (running.size > 0) {
|
|
540
|
-
await Promise.race(running.values());
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
console.log(""); // Extra newline after parallel run
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
// Sequential execution
|
|
547
|
-
for (const file of flowFiles) {
|
|
548
|
-
const flow = parseFlowFile(file);
|
|
549
|
-
const summary = getFlowSummary(flow);
|
|
550
|
-
// Print test header
|
|
551
|
-
console.log("");
|
|
552
|
-
console.log(chalk.blue.bold(`โโโ ${flow.name} โโโ`));
|
|
553
|
-
console.log(chalk.gray(` ${summary.totalSteps} steps (${summary.requiredSteps} required, ${summary.optionalSteps} optional)`));
|
|
554
|
-
console.log("");
|
|
555
|
-
try {
|
|
556
|
-
const runner = new TestRunner(config, credentials);
|
|
557
|
-
const result = await runner.runFlow(flow, (stepResult) => {
|
|
558
|
-
// Real-time step output
|
|
559
|
-
const step = stepResult.step;
|
|
560
|
-
const statusIcon = stepResult.status === "passed"
|
|
561
|
-
? chalk.green("โ")
|
|
562
|
-
: stepResult.status === "failed"
|
|
563
|
-
? chalk.red("โ")
|
|
564
|
-
: chalk.yellow("โ");
|
|
565
|
-
const optionalTag = step.optional
|
|
566
|
-
? chalk.gray(" [optional]")
|
|
567
|
-
: "";
|
|
568
|
-
const retriedTag = stepResult.retried
|
|
569
|
-
? chalk.yellow(" [retried]")
|
|
570
|
-
: "";
|
|
571
|
-
const duration = chalk.gray(`(${(stepResult.duration / 1000).toFixed(1)}s)`);
|
|
572
|
-
console.log(` ${statusIcon} ${step.text}${optionalTag}${retriedTag} ${duration}`);
|
|
573
|
-
// Show error inline if failed
|
|
574
|
-
if (stepResult.status === "failed" && stepResult.error) {
|
|
575
|
-
console.log(chalk.red(` โโ ${stepResult.error}`));
|
|
576
|
-
}
|
|
577
|
-
// Show assumptions if any
|
|
578
|
-
if (stepResult.assumptions &&
|
|
579
|
-
stepResult.assumptions.length > 0) {
|
|
580
|
-
for (const assumption of stepResult.assumptions) {
|
|
581
|
-
console.log(chalk.gray(` โโ ๐ก ${assumption}`));
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}, !!options.performance);
|
|
585
|
-
// Show perf summary inline if audit was run
|
|
586
|
-
if (result.perfAudit) {
|
|
587
|
-
const p = result.perfAudit;
|
|
588
|
-
const parts = [];
|
|
589
|
-
if (p.vitals.fcp)
|
|
590
|
-
parts.push(`FCP ${p.vitals.fcp}ms`);
|
|
591
|
-
if (p.vitals.lcp)
|
|
592
|
-
parts.push(`LCP ${p.vitals.lcp}ms`);
|
|
593
|
-
if (p.vitals.cls != null)
|
|
594
|
-
parts.push(`CLS ${p.vitals.cls}`);
|
|
595
|
-
const s = p.scores ?? p.lighthouse;
|
|
596
|
-
if (s)
|
|
597
|
-
parts.push(`Perf ${s.performance}/100`);
|
|
598
|
-
console.log(chalk.cyan(` โก Perf: ${parts.join(" ยท ")}`));
|
|
599
|
-
}
|
|
600
|
-
results.push(result);
|
|
601
|
-
// Print test summary
|
|
602
|
-
console.log("");
|
|
603
|
-
if (result.status === "passed") {
|
|
604
|
-
console.log(chalk.green.bold(` โ PASSED`) +
|
|
605
|
-
chalk.gray(` (${result.passedSteps}/${result.totalSteps} steps in ${(result.duration / 1000).toFixed(1)}s)`));
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
console.log(chalk.red.bold(` โ FAILED`) +
|
|
609
|
-
chalk.gray(` (${result.failedSteps} failed, ${result.passedSteps} passed)`));
|
|
610
|
-
}
|
|
611
|
-
// Auto-handled info
|
|
612
|
-
if (result.autoHandled.length > 0) {
|
|
613
|
-
console.log(chalk.gray(` โน Auto-handled: ${result.autoHandled.join(", ")}`));
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
console.log(chalk.red(` โ ERROR: ${error.message}`));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
// Final summary
|
|
622
|
-
console.log("");
|
|
623
|
-
console.log(chalk.blue.bold("โโโ Summary โโโ"));
|
|
624
|
-
const passed = results.filter((r) => r.status === "passed").length;
|
|
625
|
-
const failed = results.filter((r) => r.status === "failed").length;
|
|
626
|
-
const totalSteps = results.reduce((sum, r) => sum + r.totalSteps, 0);
|
|
627
|
-
const passedSteps = results.reduce((sum, r) => sum + r.passedSteps, 0);
|
|
628
|
-
console.log(chalk.gray(` ${results.length} test file(s), ${totalSteps} total steps`));
|
|
629
|
-
if (failed === 0) {
|
|
630
|
-
console.log(chalk.green.bold(` โ All ${passed} test(s) passed! (${passedSteps}/${totalSteps} steps)`));
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
console.log(chalk.red.bold(` โ ${failed}/${results.length} test(s) failed`));
|
|
634
|
-
}
|
|
635
|
-
// Generate suite report if requested and multiple files
|
|
636
|
-
if (generateReport && results.length > 0) {
|
|
637
|
-
let reportPath;
|
|
638
|
-
if (results.length === 1) {
|
|
639
|
-
reportPath = reporter.saveAsFolder(results[0]);
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
reportPath = reporter.saveSuiteAsFolder(results);
|
|
643
|
-
}
|
|
644
|
-
console.log(chalk.cyan(`\n ๐ Report: ${reportPath}`));
|
|
645
|
-
}
|
|
646
|
-
console.log("");
|
|
647
|
-
if (failed > 0) {
|
|
648
|
-
process.exit(1);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
catch (error) {
|
|
652
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
653
|
-
process.exit(1);
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
// Create command
|
|
657
|
-
program
|
|
658
|
-
.command("create <name>")
|
|
659
|
-
.description("Create a new flow file")
|
|
660
|
-
.option("-d, --dir <directory>", "Directory to create flow in", "tests")
|
|
661
|
-
.action(async (name, options) => {
|
|
662
|
-
const readline = await import("readline");
|
|
663
|
-
// Ensure directory exists
|
|
664
|
-
const dir = options.dir;
|
|
665
|
-
if (!fs.existsSync(dir)) {
|
|
666
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
667
|
-
}
|
|
668
|
-
// Generate filename
|
|
669
|
-
const filename = name.endsWith(".flow") ? name : `${name}.flow`;
|
|
670
|
-
const filepath = path.join(dir, filename);
|
|
671
|
-
if (fs.existsSync(filepath)) {
|
|
672
|
-
console.log(chalk.red(`File already exists: ${filepath}`));
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
675
|
-
console.log(chalk.blue(`\nCreating: ${filepath}`));
|
|
676
|
-
console.log(chalk.gray("Enter your test steps (one per line). Empty line to finish.\n"));
|
|
677
|
-
const rl = readline.createInterface({
|
|
678
|
-
input: process.stdin,
|
|
679
|
-
output: process.stdout,
|
|
680
|
-
});
|
|
681
|
-
const lines = [`# ${name}`, ""];
|
|
682
|
-
let lineNum = 1;
|
|
683
|
-
const askLine = () => {
|
|
684
|
-
return new Promise((resolve) => {
|
|
685
|
-
rl.question(chalk.cyan(`${lineNum}. `), (answer) => {
|
|
686
|
-
resolve(answer);
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
};
|
|
690
|
-
while (true) {
|
|
691
|
-
const line = await askLine();
|
|
692
|
-
if (line === "") {
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
lines.push(line);
|
|
696
|
-
lineNum++;
|
|
697
|
-
}
|
|
698
|
-
rl.close();
|
|
699
|
-
if (lines.length <= 2) {
|
|
700
|
-
console.log(chalk.yellow("\nNo steps entered. File not created."));
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
// Write file
|
|
704
|
-
fs.writeFileSync(filepath, lines.join("\n") + "\n");
|
|
705
|
-
console.log(chalk.green(`\nโ Created: ${filepath}`));
|
|
706
|
-
console.log(chalk.gray(` ${lineNum - 1} steps`));
|
|
707
|
-
console.log(chalk.gray(`\nRun with: slapify run ${filepath}`));
|
|
708
|
-
});
|
|
709
|
-
// Generate command - AI-powered flow generation
|
|
710
|
-
program
|
|
711
|
-
.command("generate <prompt>")
|
|
712
|
-
.alias("gen")
|
|
713
|
-
.description("Generate a verified .flow file by running the goal as a task and recording what worked")
|
|
714
|
-
.option("-d, --dir <directory>", "Directory to save flow", "tests")
|
|
715
|
-
.option("--headed", "Show browser window while running")
|
|
716
|
-
.action(async (prompt, options) => {
|
|
717
|
-
const configDir = getConfigDir();
|
|
718
|
-
if (!configDir) {
|
|
719
|
-
console.log(chalk.red('No .slapify directory found. Run "slapify init" first.'));
|
|
720
|
-
process.exit(1);
|
|
721
|
-
}
|
|
722
|
-
const config = loadConfig(configDir);
|
|
723
|
-
console.log(chalk.blue("\n๐ค Flow Generator\n"));
|
|
724
|
-
console.log(chalk.gray(" Running the goal in the browser to discover the real path...\n"));
|
|
725
|
-
// Delegate to the task agent with save-flow enabled.
|
|
726
|
-
// The agent actually executes every step, handles login/captcha/popups,
|
|
727
|
-
// and writes only steps that are proven to work.
|
|
728
|
-
const { runTask } = await import("./task/runner.js");
|
|
729
|
-
let savedPath;
|
|
730
|
-
await runTask({
|
|
731
|
-
goal: prompt,
|
|
732
|
-
headed: options.headed,
|
|
733
|
-
saveFlow: true,
|
|
734
|
-
flowOutputDir: options.dir,
|
|
735
|
-
onEvent: (event) => {
|
|
736
|
-
if (event.type === "status_update") {
|
|
737
|
-
process.stdout.write(chalk.gray(` โ ${event.message}\n`));
|
|
738
|
-
}
|
|
739
|
-
if (event.type === "message") {
|
|
740
|
-
console.log(chalk.white(`\n${event.text}`));
|
|
741
|
-
}
|
|
742
|
-
if (event.type === "flow_saved") {
|
|
743
|
-
savedPath = event.path;
|
|
744
|
-
}
|
|
745
|
-
if (event.type === "done") {
|
|
746
|
-
console.log(chalk.green(`\nโ
Done`));
|
|
747
|
-
}
|
|
748
|
-
if (event.type === "error") {
|
|
749
|
-
console.log(chalk.red(`\nโ ${event.error}`));
|
|
750
|
-
}
|
|
751
|
-
},
|
|
752
|
-
});
|
|
753
|
-
if (savedPath) {
|
|
754
|
-
console.log(chalk.green(`\nโ Flow saved: ${savedPath}`));
|
|
755
|
-
console.log(chalk.gray(` Run with: slapify run ${savedPath}`));
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
console.log(chalk.yellow("\nโ No flow was saved. The agent may not have completed the goal."));
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
// Fix command - analyze and fix failing tests
|
|
762
|
-
program
|
|
763
|
-
.command("fix <file>")
|
|
764
|
-
.description("Analyze a failing test and suggest/apply fixes")
|
|
765
|
-
.option("--auto", "Automatically apply suggested fixes without confirmation")
|
|
766
|
-
.option("--headed", "Run browser in headed mode for debugging")
|
|
767
|
-
.action(async (file, options) => {
|
|
768
|
-
const configDir = getConfigDir();
|
|
769
|
-
if (!configDir) {
|
|
770
|
-
console.log(chalk.red('No .slapify directory found. Run "slapify init" first.'));
|
|
771
|
-
process.exit(1);
|
|
772
|
-
}
|
|
773
|
-
if (!fs.existsSync(file)) {
|
|
774
|
-
console.log(chalk.red(`File not found: ${file}`));
|
|
775
|
-
process.exit(1);
|
|
776
|
-
}
|
|
777
|
-
const config = loadConfig(configDir);
|
|
778
|
-
const credentials = loadCredentials(configDir);
|
|
779
|
-
if (options.headed) {
|
|
780
|
-
config.browser = { ...config.browser, headless: false };
|
|
781
|
-
}
|
|
782
|
-
// Enable screenshots for diagnosis
|
|
783
|
-
config.report = { ...config.report, screenshots: true };
|
|
784
|
-
const readline = await import("readline");
|
|
785
|
-
let spinner = ora("Running test to identify failures...").start();
|
|
786
|
-
try {
|
|
787
|
-
// Step 1: Run the test to see what fails
|
|
788
|
-
const flow = parseFlowFile(file);
|
|
789
|
-
const runner = new TestRunner(config, credentials);
|
|
790
|
-
const failedSteps = [];
|
|
791
|
-
const result = await runner.runFlow(flow, (stepResult) => {
|
|
792
|
-
if (stepResult.status === "failed" && stepResult.error) {
|
|
793
|
-
failedSteps.push({
|
|
794
|
-
step: stepResult.step.text,
|
|
795
|
-
error: stepResult.error,
|
|
796
|
-
line: stepResult.step.line,
|
|
797
|
-
screenshot: stepResult.screenshot,
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
});
|
|
801
|
-
if (result.status === "passed") {
|
|
802
|
-
spinner.succeed("Test passed! No fixes needed.");
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
spinner.info(`Found ${failedSteps.length} failing step(s)`);
|
|
806
|
-
if (failedSteps.length === 0) {
|
|
807
|
-
console.log(chalk.yellow("No specific step failures to fix."));
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
// Step 2: Read the original flow file
|
|
811
|
-
const originalContent = fs.readFileSync(file, "utf-8");
|
|
812
|
-
const lines = originalContent.split("\n");
|
|
813
|
-
// Step 3: Use AI to analyze and suggest fixes
|
|
814
|
-
spinner = ora("Analyzing failures and generating fixes...").start();
|
|
815
|
-
const failureDetails = failedSteps
|
|
816
|
-
.map((f) => `Line ${f.line}: "${f.step}"\n Error: ${f.error}`)
|
|
817
|
-
.join("\n\n");
|
|
818
|
-
const analysisResponse = await generateText({
|
|
819
|
-
model: getModelFromConfig(config.llm),
|
|
820
|
-
system: `You are a test automation expert. Analyze failing test steps and suggest fixes.
|
|
821
|
-
|
|
822
|
-
Original flow file:
|
|
823
|
-
\`\`\`
|
|
824
|
-
${originalContent}
|
|
825
|
-
\`\`\`
|
|
826
|
-
|
|
827
|
-
Failing steps:
|
|
828
|
-
${failureDetails}
|
|
829
|
-
|
|
830
|
-
Based on the errors, suggest fixes for the flow file. Common issues and fixes:
|
|
831
|
-
1. Element not found โ Try more descriptive text, add wait, or make step optional
|
|
832
|
-
2. Timeout โ Add explicit wait or increase timeout
|
|
833
|
-
3. Navigation error โ Add wait after navigation, or split into smaller steps
|
|
834
|
-
4. Element obscured โ Add step to close popup/modal first
|
|
835
|
-
5. Stale element โ Add wait for page to stabilize
|
|
836
|
-
|
|
837
|
-
Respond with JSON:
|
|
838
|
-
{
|
|
839
|
-
"analysis": "Brief explanation of what's wrong",
|
|
840
|
-
"fixes": [
|
|
841
|
-
{
|
|
842
|
-
"line": 5,
|
|
843
|
-
"original": "Click the submit button",
|
|
844
|
-
"fixed": "Click the Submit button",
|
|
845
|
-
"reason": "Button text is capitalized"
|
|
846
|
-
}
|
|
847
|
-
],
|
|
848
|
-
"additions": [
|
|
849
|
-
{
|
|
850
|
-
"afterLine": 4,
|
|
851
|
-
"step": "[Optional] Wait for page to load",
|
|
852
|
-
"reason": "Page might still be loading"
|
|
853
|
-
}
|
|
854
|
-
]
|
|
855
|
-
}`,
|
|
856
|
-
prompt: "Analyze the failures and suggest specific fixes.",
|
|
857
|
-
maxTokens: 1500,
|
|
858
|
-
});
|
|
859
|
-
spinner.succeed("Analysis complete");
|
|
860
|
-
// Parse the response
|
|
861
|
-
const jsonMatch = analysisResponse.text.match(/\{[\s\S]*\}/);
|
|
862
|
-
if (!jsonMatch) {
|
|
863
|
-
console.log(chalk.red("Could not parse AI response"));
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const suggestions = JSON.parse(jsonMatch[0]);
|
|
867
|
-
// Step 4: Display suggestions
|
|
868
|
-
console.log(chalk.blue("\nโโโ Analysis โโโ\n"));
|
|
869
|
-
console.log(chalk.white(suggestions.analysis));
|
|
870
|
-
if (suggestions.fixes?.length > 0 || suggestions.additions?.length > 0) {
|
|
871
|
-
console.log(chalk.blue("\nโโโ Suggested Fixes โโโ\n"));
|
|
872
|
-
if (suggestions.fixes?.length > 0) {
|
|
873
|
-
for (const fix of suggestions.fixes) {
|
|
874
|
-
console.log(chalk.yellow(`Line ${fix.line}:`));
|
|
875
|
-
console.log(chalk.red(` - ${fix.original}`));
|
|
876
|
-
console.log(chalk.green(` + ${fix.fixed}`));
|
|
877
|
-
console.log(chalk.gray(` Reason: ${fix.reason}\n`));
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (suggestions.additions?.length > 0) {
|
|
881
|
-
console.log(chalk.yellow("New steps to add:"));
|
|
882
|
-
for (const add of suggestions.additions) {
|
|
883
|
-
console.log(chalk.green(` + After line ${add.afterLine}: ${add.step}`));
|
|
884
|
-
console.log(chalk.gray(` Reason: ${add.reason}\n`));
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// Step 5: Apply fixes
|
|
888
|
-
let shouldApply = options.auto;
|
|
889
|
-
if (!shouldApply) {
|
|
890
|
-
const rl = readline.createInterface({
|
|
891
|
-
input: process.stdin,
|
|
892
|
-
output: process.stdout,
|
|
893
|
-
});
|
|
894
|
-
const answer = await new Promise((resolve) => {
|
|
895
|
-
rl.question(chalk.cyan("Apply these fixes? (y/N): "), (answer) => {
|
|
896
|
-
rl.close();
|
|
897
|
-
resolve(answer.trim().toLowerCase());
|
|
898
|
-
});
|
|
899
|
-
});
|
|
900
|
-
shouldApply = answer === "y" || answer === "yes";
|
|
901
|
-
}
|
|
902
|
-
if (shouldApply) {
|
|
903
|
-
// Apply fixes to lines
|
|
904
|
-
let newLines = [...lines];
|
|
905
|
-
const lineOffsets = new Map(); // Track line number changes
|
|
906
|
-
// Apply modifications first
|
|
907
|
-
if (suggestions.fixes) {
|
|
908
|
-
for (const fix of suggestions.fixes) {
|
|
909
|
-
const lineIdx = fix.line - 1;
|
|
910
|
-
if (lineIdx >= 0 && lineIdx < newLines.length) {
|
|
911
|
-
newLines[lineIdx] = fix.fixed;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
// Apply additions (in reverse order to maintain line numbers)
|
|
916
|
-
if (suggestions.additions) {
|
|
917
|
-
const sortedAdditions = [...suggestions.additions].sort((a, b) => b.afterLine - a.afterLine);
|
|
918
|
-
for (const add of sortedAdditions) {
|
|
919
|
-
const afterIdx = add.afterLine;
|
|
920
|
-
newLines.splice(afterIdx, 0, add.step);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
// Write the fixed file
|
|
924
|
-
const newContent = newLines.join("\n");
|
|
925
|
-
// Backup original
|
|
926
|
-
const backupPath = file + ".backup";
|
|
927
|
-
fs.writeFileSync(backupPath, originalContent);
|
|
928
|
-
// Write fixed version
|
|
929
|
-
fs.writeFileSync(file, newContent);
|
|
930
|
-
console.log(chalk.green(`\nโ Fixes applied to ${file}`));
|
|
931
|
-
console.log(chalk.gray(` Backup saved to ${backupPath}`));
|
|
932
|
-
console.log(chalk.gray(`\nRun again with: slapify run ${file}`));
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
console.log(chalk.yellow("No changes made."));
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
console.log(chalk.yellow("\nNo automatic fixes suggested."));
|
|
940
|
-
console.log(chalk.gray("The failures may require manual investigation."));
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
catch (error) {
|
|
944
|
-
spinner.fail("Error");
|
|
945
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
946
|
-
process.exit(1);
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
// Validate command
|
|
950
|
-
program
|
|
951
|
-
.command("validate [files...]")
|
|
952
|
-
.description("Validate flow files for syntax issues")
|
|
953
|
-
.action(async (files) => {
|
|
954
|
-
let flowFiles = [];
|
|
955
|
-
if (files.length === 0) {
|
|
956
|
-
const testsDir = path.join(process.cwd(), "tests");
|
|
957
|
-
if (fs.existsSync(testsDir)) {
|
|
958
|
-
flowFiles = await findFlowFiles(testsDir);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
flowFiles = files;
|
|
963
|
-
}
|
|
964
|
-
if (flowFiles.length === 0) {
|
|
965
|
-
console.log(chalk.yellow("No .flow files found."));
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
let hasWarnings = false;
|
|
969
|
-
for (const file of flowFiles) {
|
|
970
|
-
try {
|
|
971
|
-
const flow = parseFlowFile(file);
|
|
972
|
-
const warnings = validateFlowFile(flow);
|
|
973
|
-
const summary = getFlowSummary(flow);
|
|
974
|
-
if (warnings.length > 0) {
|
|
975
|
-
hasWarnings = true;
|
|
976
|
-
console.log(chalk.yellow(`โ ๏ธ ${file}`));
|
|
977
|
-
for (const warning of warnings) {
|
|
978
|
-
console.log(chalk.yellow(` ${warning}`));
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
console.log(chalk.green(`โ
${file}`));
|
|
983
|
-
console.log(chalk.gray(` ${summary.totalSteps} steps (${summary.requiredSteps} required, ${summary.optionalSteps} optional)`));
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
catch (error) {
|
|
987
|
-
console.log(chalk.red(`โ ${file}`));
|
|
988
|
-
console.log(chalk.red(` ${error.message}`));
|
|
989
|
-
hasWarnings = true;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
if (hasWarnings) {
|
|
993
|
-
process.exit(1);
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
// List command
|
|
997
|
-
program
|
|
998
|
-
.command("list")
|
|
999
|
-
.description("List all flow files")
|
|
1000
|
-
.action(async () => {
|
|
1001
|
-
const testsDir = path.join(process.cwd(), "tests");
|
|
1002
|
-
if (!fs.existsSync(testsDir)) {
|
|
1003
|
-
console.log(chalk.yellow("No tests directory found."));
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
const flowFiles = await findFlowFiles(testsDir);
|
|
1007
|
-
if (flowFiles.length === 0) {
|
|
1008
|
-
console.log(chalk.yellow("No .flow files found."));
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
console.log(chalk.blue(`\nFound ${flowFiles.length} flow file(s):\n`));
|
|
1012
|
-
for (const file of flowFiles) {
|
|
1013
|
-
const flow = parseFlowFile(file);
|
|
1014
|
-
const summary = getFlowSummary(flow);
|
|
1015
|
-
const relativePath = path.relative(process.cwd(), file);
|
|
1016
|
-
console.log(` ${chalk.white(relativePath)}`);
|
|
1017
|
-
console.log(chalk.gray(` ${summary.totalSteps} steps (${summary.requiredSteps} required, ${summary.optionalSteps} optional)`));
|
|
1018
|
-
}
|
|
1019
|
-
console.log("");
|
|
1020
|
-
});
|
|
1021
|
-
// Credentials command
|
|
1022
|
-
program
|
|
1023
|
-
.command("credentials")
|
|
1024
|
-
.description("List configured credential profiles")
|
|
1025
|
-
.action(() => {
|
|
1026
|
-
const configDir = getConfigDir();
|
|
1027
|
-
if (!configDir) {
|
|
1028
|
-
console.log(chalk.red('No .slapify directory found. Run "slapify init" first.'));
|
|
1029
|
-
process.exit(1);
|
|
1030
|
-
}
|
|
1031
|
-
const credentials = loadCredentials(configDir);
|
|
1032
|
-
const profiles = Object.keys(credentials.profiles);
|
|
1033
|
-
if (profiles.length === 0) {
|
|
1034
|
-
console.log(chalk.yellow("No credential profiles configured."));
|
|
1035
|
-
console.log(chalk.gray("Edit .slapify/credentials.yaml to add profiles."));
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
console.log(chalk.blue(`\nConfigured credential profiles:\n`));
|
|
1039
|
-
for (const name of profiles) {
|
|
1040
|
-
const profile = credentials.profiles[name];
|
|
1041
|
-
console.log(` ${chalk.white(name)} (${profile.type})`);
|
|
1042
|
-
if (profile.username) {
|
|
1043
|
-
console.log(chalk.gray(` username: ${profile.username}`));
|
|
1044
|
-
}
|
|
1045
|
-
if (profile.email) {
|
|
1046
|
-
console.log(chalk.gray(` email: ${profile.email}`));
|
|
1047
|
-
}
|
|
1048
|
-
if (profile.totp_secret) {
|
|
1049
|
-
console.log(chalk.gray(` 2FA: TOTP configured`));
|
|
1050
|
-
}
|
|
1051
|
-
if (profile.fixed_otp) {
|
|
1052
|
-
console.log(chalk.gray(` 2FA: Fixed OTP configured`));
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
console.log("");
|
|
1056
|
-
});
|
|
1057
|
-
// Fix credentials YAML (localStorage/sessionStorage saved as JSON strings)
|
|
1058
|
-
function normalizeStorage(v) {
|
|
1059
|
-
if (typeof v === "string") {
|
|
1060
|
-
try {
|
|
1061
|
-
v = JSON.parse(v);
|
|
1062
|
-
}
|
|
1063
|
-
catch {
|
|
1064
|
-
return {};
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
1068
|
-
return {};
|
|
1069
|
-
const out = {};
|
|
1070
|
-
for (const [k, val] of Object.entries(v)) {
|
|
1071
|
-
out[String(k)] = typeof val === "string" ? val : JSON.stringify(val);
|
|
1072
|
-
}
|
|
1073
|
-
return out;
|
|
1074
|
-
}
|
|
1075
|
-
function fixCredentialsFile(filePath, dryRun) {
|
|
1076
|
-
const resolved = path.resolve(filePath);
|
|
1077
|
-
if (!fs.existsSync(resolved)) {
|
|
1078
|
-
console.log(chalk.yellow(` Skip (not found): ${resolved}`));
|
|
1079
|
-
return false;
|
|
1080
|
-
}
|
|
1081
|
-
const content = fs.readFileSync(resolved, "utf-8");
|
|
1082
|
-
let data;
|
|
1083
|
-
try {
|
|
1084
|
-
data = yaml.parse(content);
|
|
1085
|
-
}
|
|
1086
|
-
catch (e) {
|
|
1087
|
-
console.log(chalk.red(` Invalid YAML: ${resolved}`));
|
|
1088
|
-
console.log(chalk.gray(` ${e.message}`));
|
|
1089
|
-
return false;
|
|
1090
|
-
}
|
|
1091
|
-
if (!data || !data.profiles || typeof data.profiles !== "object") {
|
|
1092
|
-
console.log(chalk.yellow(` No profiles in: ${resolved}`));
|
|
1093
|
-
return false;
|
|
1094
|
-
}
|
|
1095
|
-
let changed = false;
|
|
1096
|
-
for (const [name, profile] of Object.entries(data.profiles)) {
|
|
1097
|
-
if (profile.type !== "inject")
|
|
1098
|
-
continue;
|
|
1099
|
-
const needLocal = typeof profile.localStorage === "string" ||
|
|
1100
|
-
(profile.localStorage &&
|
|
1101
|
-
(Array.isArray(profile.localStorage) ||
|
|
1102
|
-
typeof profile.localStorage !== "object"));
|
|
1103
|
-
const needSession = typeof profile.sessionStorage === "string" ||
|
|
1104
|
-
(profile.sessionStorage &&
|
|
1105
|
-
(Array.isArray(profile.sessionStorage) ||
|
|
1106
|
-
typeof profile.sessionStorage !== "object"));
|
|
1107
|
-
if (!needLocal && !needSession)
|
|
1108
|
-
continue;
|
|
1109
|
-
data.profiles[name] = {
|
|
1110
|
-
...profile,
|
|
1111
|
-
...(needLocal && {
|
|
1112
|
-
localStorage: normalizeStorage(profile.localStorage),
|
|
1113
|
-
}),
|
|
1114
|
-
...(needSession && {
|
|
1115
|
-
sessionStorage: normalizeStorage(profile.sessionStorage),
|
|
1116
|
-
}),
|
|
1117
|
-
};
|
|
1118
|
-
changed = true;
|
|
1119
|
-
}
|
|
1120
|
-
if (!changed) {
|
|
1121
|
-
console.log(chalk.gray(` No changes needed: ${resolved}`));
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
if (dryRun) {
|
|
1125
|
-
console.log(chalk.cyan(` Would fix: ${resolved}`));
|
|
1126
|
-
return true;
|
|
1127
|
-
}
|
|
1128
|
-
const backupPath = resolved + ".backup";
|
|
1129
|
-
fs.copyFileSync(resolved, backupPath);
|
|
1130
|
-
fs.writeFileSync(resolved, yaml.stringify(data, { indent: 2, lineWidth: 0 }));
|
|
1131
|
-
console.log(chalk.green(` Fixed: ${resolved}`));
|
|
1132
|
-
console.log(chalk.gray(` Backup: ${backupPath}`));
|
|
1133
|
-
return true;
|
|
1134
|
-
}
|
|
1135
|
-
program
|
|
1136
|
-
.command("fix-credentials [files...]")
|
|
1137
|
-
.description("Fix credential YAML files where localStorage/sessionStorage were saved as JSON strings")
|
|
1138
|
-
.option("--dry-run", "Only print what would be fixed")
|
|
1139
|
-
.action((files, options) => {
|
|
1140
|
-
const toFix = [];
|
|
1141
|
-
if (files && files.length > 0) {
|
|
1142
|
-
toFix.push(...files.map((f) => path.resolve(f)));
|
|
1143
|
-
}
|
|
1144
|
-
else {
|
|
1145
|
-
const cwd = process.cwd();
|
|
1146
|
-
toFix.push(path.join(cwd, "temp_credentials.yaml"));
|
|
1147
|
-
const configDir = getConfigDir();
|
|
1148
|
-
if (configDir) {
|
|
1149
|
-
toFix.push(path.join(configDir, "credentials.yaml"));
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
console.log(chalk.blue("\n๐ง Fix credential YAML files\n"));
|
|
1153
|
-
if (options.dryRun) {
|
|
1154
|
-
console.log(chalk.gray(" (dry run โ no files will be modified)\n"));
|
|
1155
|
-
}
|
|
1156
|
-
let fixed = 0;
|
|
1157
|
-
for (const f of toFix) {
|
|
1158
|
-
if (fixCredentialsFile(f, !!options.dryRun))
|
|
1159
|
-
fixed++;
|
|
1160
|
-
}
|
|
1161
|
-
if (fixed === 0 && toFix.length > 0) {
|
|
1162
|
-
console.log(chalk.gray("\n No files needed fixing."));
|
|
1163
|
-
}
|
|
1164
|
-
console.log("");
|
|
1165
|
-
});
|
|
1166
|
-
// Interactive mode
|
|
1167
|
-
program
|
|
1168
|
-
.command("interactive [url]")
|
|
1169
|
-
.alias("i")
|
|
1170
|
-
.description("Run steps interactively")
|
|
1171
|
-
.option("--headed", "Run browser in headed mode")
|
|
1172
|
-
.action(async (url, options) => {
|
|
1173
|
-
console.log(chalk.blue("\n๐งช Slapify Interactive Mode"));
|
|
1174
|
-
console.log(chalk.gray('Type test steps and press Enter to execute. Type "exit" to quit.\n'));
|
|
1175
|
-
const configDir = getConfigDir();
|
|
1176
|
-
if (!configDir) {
|
|
1177
|
-
console.log(chalk.red('No .slapify directory found. Run "slapify init" first.'));
|
|
1178
|
-
process.exit(1);
|
|
1179
|
-
}
|
|
1180
|
-
const config = loadConfig(configDir);
|
|
1181
|
-
const credentials = loadCredentials(configDir);
|
|
1182
|
-
if (options.headed) {
|
|
1183
|
-
config.browser = { ...config.browser, headless: false };
|
|
1184
|
-
}
|
|
1185
|
-
const runner = new TestRunner(config, credentials);
|
|
1186
|
-
if (url) {
|
|
1187
|
-
console.log(chalk.gray(`Navigating to ${url}...`));
|
|
1188
|
-
// Would navigate here
|
|
1189
|
-
}
|
|
1190
|
-
// Simple readline interface
|
|
1191
|
-
const readline = await import("readline");
|
|
1192
|
-
const rl = readline.createInterface({
|
|
1193
|
-
input: process.stdin,
|
|
1194
|
-
output: process.stdout,
|
|
1195
|
-
});
|
|
1196
|
-
const prompt = () => {
|
|
1197
|
-
rl.question(chalk.cyan("> "), async (input) => {
|
|
1198
|
-
const trimmed = input.trim();
|
|
1199
|
-
if (trimmed.toLowerCase() === "exit" ||
|
|
1200
|
-
trimmed.toLowerCase() === "quit") {
|
|
1201
|
-
console.log(chalk.gray("\nGoodbye!"));
|
|
1202
|
-
rl.close();
|
|
1203
|
-
process.exit(0);
|
|
1204
|
-
}
|
|
1205
|
-
if (!trimmed) {
|
|
1206
|
-
prompt();
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
// Execute the step
|
|
1210
|
-
console.log(chalk.gray(`Executing: ${trimmed}`));
|
|
1211
|
-
// Would execute step here
|
|
1212
|
-
console.log(chalk.green("โ Done"));
|
|
1213
|
-
prompt();
|
|
1214
|
-
});
|
|
1215
|
-
};
|
|
1216
|
-
prompt();
|
|
1217
|
-
});
|
|
1218
|
-
// โโโ Task command โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1219
|
-
program
|
|
1220
|
-
.command("task [goal]")
|
|
1221
|
-
.description("Run an autonomous AI agent task in plain English.\n" +
|
|
1222
|
-
" The agent decides everything: what to do, when to schedule, when to sleep.\n" +
|
|
1223
|
-
" Examples:\n" +
|
|
1224
|
-
' slapify task "Go to linkedin.com and like the latest 3 posts"\n' +
|
|
1225
|
-
' slapify task "Monitor my Gmail for new emails every 30 min and log subjects"\n' +
|
|
1226
|
-
' slapify task "Order breakfast from Swiggy every day at 8am"')
|
|
1227
|
-
.option("--headed", "Show the browser window")
|
|
1228
|
-
.option("--debug", "Show all tool calls and internal steps")
|
|
1229
|
-
.option("--report", "Generate an HTML report after the task completes")
|
|
1230
|
-
.option("--save-flow", "Save agent steps as a reusable .flow file when done")
|
|
1231
|
-
.option("--session <id>", "Resume an existing task session")
|
|
1232
|
-
.option("--list-sessions", "List all task sessions")
|
|
1233
|
-
.option("--logs <id>", "Show logs for a task session")
|
|
1234
|
-
.option("--max-iterations <n>", "Safety cap on agent iterations (default 200)", parseInt)
|
|
1235
|
-
.action(async (goal, options) => {
|
|
1236
|
-
// Sub-command: list sessions
|
|
1237
|
-
if (options.listSessions) {
|
|
1238
|
-
const { listSessions } = await import("./task/index.js");
|
|
1239
|
-
const sessions = listSessions();
|
|
1240
|
-
if (sessions.length === 0) {
|
|
1241
|
-
console.log(chalk.gray("\nNo task sessions found.\n"));
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
console.log(chalk.blue(`\n๐ Task Sessions (${sessions.length})\n`));
|
|
1245
|
-
for (const s of sessions) {
|
|
1246
|
-
const statusColor = s.status === "completed"
|
|
1247
|
-
? chalk.green
|
|
1248
|
-
: s.status === "failed"
|
|
1249
|
-
? chalk.red
|
|
1250
|
-
: s.status === "scheduled"
|
|
1251
|
-
? chalk.blue
|
|
1252
|
-
: chalk.yellow;
|
|
1253
|
-
console.log(` ${statusColor("โ")} ${chalk.bold(s.id)}\n` +
|
|
1254
|
-
` Goal: ${s.goal.slice(0, 70)}${s.goal.length > 70 ? "โฆ" : ""}\n` +
|
|
1255
|
-
` Status: ${statusColor(s.status)} Iterations: ${s.iteration}\n` +
|
|
1256
|
-
` Updated: ${new Date(s.updatedAt).toLocaleString()}\n`);
|
|
1257
|
-
}
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
// Sub-command: show logs
|
|
1261
|
-
if (options.logs) {
|
|
1262
|
-
const { loadSession } = await import("./task/index.js");
|
|
1263
|
-
const { loadEvents } = await import("./task/session.js");
|
|
1264
|
-
const session = loadSession(options.logs);
|
|
1265
|
-
if (!session) {
|
|
1266
|
-
console.log(chalk.red(`Session '${options.logs}' not found.`));
|
|
1267
|
-
process.exit(1);
|
|
1268
|
-
}
|
|
1269
|
-
console.log(chalk.blue(`\n๐ Logs: ${session.id}\n`));
|
|
1270
|
-
console.log(chalk.gray(`Goal: ${session.goal}\n`));
|
|
1271
|
-
const events = loadEvents(options.logs);
|
|
1272
|
-
for (const event of events) {
|
|
1273
|
-
const ts = chalk.gray(new Date(event.ts).toLocaleTimeString());
|
|
1274
|
-
if (event.type === "llm_response") {
|
|
1275
|
-
if (event.text)
|
|
1276
|
-
console.log(`${ts} ๐ค ${chalk.cyan(event.text.slice(0, 120))}`);
|
|
1277
|
-
}
|
|
1278
|
-
else if (event.type === "tool_call") {
|
|
1279
|
-
console.log(`${ts} ๐ง ${chalk.yellow(event.toolName)} โ ${chalk.gray(JSON.stringify(event.result).slice(0, 80))}`);
|
|
1280
|
-
}
|
|
1281
|
-
else if (event.type === "tool_error") {
|
|
1282
|
-
console.log(`${ts} โ ${chalk.red(event.toolName)} โ ${chalk.red(event.error.slice(0, 80))}`);
|
|
1283
|
-
}
|
|
1284
|
-
else if (event.type === "memory_update") {
|
|
1285
|
-
console.log(`${ts} ๐ง ${chalk.magenta("remember")} ${event.key} = ${event.value.slice(0, 60)}`);
|
|
1286
|
-
}
|
|
1287
|
-
else if (event.type === "scheduled") {
|
|
1288
|
-
console.log(`${ts} โฐ ${chalk.blue("schedule")} ${event.cron} โ ${event.task}`);
|
|
1289
|
-
}
|
|
1290
|
-
else if (event.type === "sleeping_until") {
|
|
1291
|
-
console.log(`${ts} ๐ด ${chalk.blue("sleep")} until ${event.until}`);
|
|
1292
|
-
}
|
|
1293
|
-
else if (event.type === "session_end") {
|
|
1294
|
-
console.log(`${ts} โ
${chalk.green("done")} ${event.summary.slice(0, 120)}`);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
console.log("");
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
// Determine goal
|
|
1301
|
-
let taskGoal = goal || options.session ? goal : undefined;
|
|
1302
|
-
// Resume without goal is fine โ goal is stored in session
|
|
1303
|
-
if (!taskGoal && !options.session) {
|
|
1304
|
-
console.log(chalk.red('\nPlease provide a goal. Example:\n slapify task "Go to example.com and check the title"\n'));
|
|
1305
|
-
process.exit(1);
|
|
1306
|
-
}
|
|
1307
|
-
// If resuming, load the goal from session
|
|
1308
|
-
if (!taskGoal && options.session) {
|
|
1309
|
-
const { loadSession } = await import("./task/index.js");
|
|
1310
|
-
const s = loadSession(options.session);
|
|
1311
|
-
if (!s) {
|
|
1312
|
-
console.log(chalk.red(`Session '${options.session}' not found.`));
|
|
1313
|
-
process.exit(1);
|
|
1314
|
-
}
|
|
1315
|
-
taskGoal = s.goal;
|
|
1316
|
-
}
|
|
1317
|
-
// Check config
|
|
1318
|
-
const configDir = getConfigDir();
|
|
1319
|
-
if (!configDir) {
|
|
1320
|
-
console.log(chalk.red('\nNo .slapify directory found. Run "slapify init" first.\n'));
|
|
1321
|
-
process.exit(1);
|
|
1322
|
-
}
|
|
1323
|
-
// Track current session so SIGINT can generate report
|
|
1324
|
-
let activeSession = null;
|
|
1325
|
-
const generateAndPrintReport = async (session) => {
|
|
1326
|
-
if (!options.report)
|
|
1327
|
-
return;
|
|
1328
|
-
try {
|
|
1329
|
-
const { loadEvents, saveTaskReport } = await import("./task/index.js");
|
|
1330
|
-
const events = loadEvents(session.id);
|
|
1331
|
-
const reportPath = saveTaskReport(session, events);
|
|
1332
|
-
console.log(chalk.cyan(`\n ๐ Report: ${reportPath}`));
|
|
1333
|
-
}
|
|
1334
|
-
catch (e) {
|
|
1335
|
-
console.log(chalk.yellow(` โ Could not generate report: ${e?.message}`));
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
const debug = !!options.debug;
|
|
1339
|
-
// Clear the "thinking..." spinner line
|
|
1340
|
-
const clearLine = () => process.stdout.write("\x1b[2K\r");
|
|
1341
|
-
const printEvent = (event) => {
|
|
1342
|
-
switch (event.type) {
|
|
1343
|
-
// โโ Debug-only (verbose internal steps) โโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1344
|
-
case "thinking":
|
|
1345
|
-
if (debug)
|
|
1346
|
-
process.stdout.write(chalk.gray(" โณ thinking...\r"));
|
|
1347
|
-
break;
|
|
1348
|
-
case "message":
|
|
1349
|
-
if (debug) {
|
|
1350
|
-
clearLine();
|
|
1351
|
-
console.log(chalk.gray(` ๐ฌ ${event.text}`));
|
|
1352
|
-
}
|
|
1353
|
-
break;
|
|
1354
|
-
case "tool_start":
|
|
1355
|
-
if (debug) {
|
|
1356
|
-
clearLine();
|
|
1357
|
-
const argStr = JSON.stringify(event.args);
|
|
1358
|
-
console.log(chalk.dim(` โบ ${chalk.cyan(event.toolName)} `) +
|
|
1359
|
-
chalk.gray(argStr.slice(0, 100) + (argStr.length > 100 ? "โฆ" : "")));
|
|
1360
|
-
}
|
|
1361
|
-
break;
|
|
1362
|
-
case "tool_done":
|
|
1363
|
-
if (debug) {
|
|
1364
|
-
console.log(chalk.dim(` โ ${event.result.slice(0, 120)}`));
|
|
1365
|
-
}
|
|
1366
|
-
break;
|
|
1367
|
-
case "tool_error":
|
|
1368
|
-
// Always show errors
|
|
1369
|
-
clearLine();
|
|
1370
|
-
console.log(chalk.red(` โ ${event.toolName}: ${event.error.slice(0, 120)}`));
|
|
1371
|
-
break;
|
|
1372
|
-
// โโ Always visible โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1373
|
-
case "status_update":
|
|
1374
|
-
clearLine();
|
|
1375
|
-
console.log(chalk.white(` ${event.message}`));
|
|
1376
|
-
break;
|
|
1377
|
-
case "human_input_needed":
|
|
1378
|
-
// spinner is stopped by the trigger check above
|
|
1379
|
-
console.log("\n" + chalk.yellow("โ".repeat(60)));
|
|
1380
|
-
console.log(chalk.yellow.bold(" ๐ Agent needs your input"));
|
|
1381
|
-
console.log(chalk.white(`\n ${event.question}`));
|
|
1382
|
-
if (event.hint)
|
|
1383
|
-
console.log(chalk.gray(` ${event.hint}`));
|
|
1384
|
-
// Answer is handled via onHumanInput callback โ just show the prompt here
|
|
1385
|
-
break;
|
|
1386
|
-
case "credentials_saved":
|
|
1387
|
-
clearLine();
|
|
1388
|
-
console.log(chalk.green(` ๐พ Credentials saved: '${event.profileName}' (${event.credType}) โ .slapify/credentials.yaml`));
|
|
1389
|
-
break;
|
|
1390
|
-
case "scheduled":
|
|
1391
|
-
if (debug) {
|
|
1392
|
-
clearLine();
|
|
1393
|
-
console.log(chalk.dim(` โฐ scheduled: ${event.cron} โ ${event.task}`));
|
|
1394
|
-
}
|
|
1395
|
-
break;
|
|
1396
|
-
case "sleeping":
|
|
1397
|
-
if (debug) {
|
|
1398
|
-
clearLine();
|
|
1399
|
-
console.log(chalk.dim(` ๐ด sleeping until ${new Date(event.until).toLocaleString()}`));
|
|
1400
|
-
}
|
|
1401
|
-
break;
|
|
1402
|
-
case "done":
|
|
1403
|
-
clearLine();
|
|
1404
|
-
console.log("\n" + chalk.green("โ".repeat(60)));
|
|
1405
|
-
console.log(chalk.green.bold(" โ
Task complete!"));
|
|
1406
|
-
console.log(chalk.white(`\n ${event.summary}`));
|
|
1407
|
-
console.log(chalk.green("โ".repeat(60)));
|
|
1408
|
-
break;
|
|
1409
|
-
case "error":
|
|
1410
|
-
clearLine();
|
|
1411
|
-
console.log(chalk.red(`\n โ Error: ${event.error}`));
|
|
1412
|
-
break;
|
|
1413
|
-
}
|
|
1414
|
-
};
|
|
1415
|
-
console.log(chalk.blue("\n๐ค Slapify Task Agent\n"));
|
|
1416
|
-
console.log(chalk.white(` Goal: ${taskGoal}`));
|
|
1417
|
-
if (options.session)
|
|
1418
|
-
console.log(chalk.gray(` Resuming session: ${options.session}`));
|
|
1419
|
-
console.log(chalk.gray([
|
|
1420
|
-
options.report ? " --report: HTML report on exit" : "",
|
|
1421
|
-
debug ? " --debug: verbose output" : "",
|
|
1422
|
-
" Ctrl+C to stop",
|
|
1423
|
-
]
|
|
1424
|
-
.filter(Boolean)
|
|
1425
|
-
.join(" ยท ") + "\n"));
|
|
1426
|
-
console.log(chalk.gray("โ".repeat(60)) + "\n");
|
|
1427
|
-
// Thinking spinner for default (non-debug) mode
|
|
1428
|
-
let spinnerInterval = null;
|
|
1429
|
-
let spinnerPaused = false; // true while waiting for human input
|
|
1430
|
-
const startSpinner = () => {
|
|
1431
|
-
if (debug || spinnerPaused || spinnerInterval)
|
|
1432
|
-
return;
|
|
1433
|
-
const frames = ["โ ", "โ ", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ", "โ "];
|
|
1434
|
-
let fi = 0;
|
|
1435
|
-
spinnerInterval = setInterval(() => {
|
|
1436
|
-
process.stdout.write(chalk.gray(`\r ${frames[fi++ % frames.length]} working...`));
|
|
1437
|
-
}, 80);
|
|
1438
|
-
};
|
|
1439
|
-
if (!debug)
|
|
1440
|
-
startSpinner();
|
|
1441
|
-
const { runTask } = await import("./task/index.js");
|
|
1442
|
-
// SIGINT handler โ generate report then exit gracefully
|
|
1443
|
-
let sigintHandled = false;
|
|
1444
|
-
const onSigint = async () => {
|
|
1445
|
-
if (sigintHandled)
|
|
1446
|
-
return;
|
|
1447
|
-
sigintHandled = true;
|
|
1448
|
-
clearInterval(spinnerInterval);
|
|
1449
|
-
spinnerInterval = null;
|
|
1450
|
-
spinnerPaused = true;
|
|
1451
|
-
process.stdout.write("\x1b[2K\r");
|
|
1452
|
-
console.log(chalk.yellow("\n โก Interrupted" +
|
|
1453
|
-
(options.report ? " โ generating report..." : "")));
|
|
1454
|
-
if (activeSession) {
|
|
1455
|
-
activeSession.status = "failed";
|
|
1456
|
-
activeSession.finalSummary = "Task interrupted by user (Ctrl+C).";
|
|
1457
|
-
const { saveSessionMeta } = await import("./task/session.js");
|
|
1458
|
-
saveSessionMeta(activeSession);
|
|
1459
|
-
await generateAndPrintReport(activeSession);
|
|
1460
|
-
}
|
|
1461
|
-
console.log(chalk.gray(" Goodbye.\n"));
|
|
1462
|
-
process.exit(0);
|
|
1463
|
-
};
|
|
1464
|
-
process.once("SIGINT", onSigint);
|
|
1465
|
-
try {
|
|
1466
|
-
const stopSpinner = () => {
|
|
1467
|
-
if (spinnerInterval) {
|
|
1468
|
-
clearInterval(spinnerInterval);
|
|
1469
|
-
spinnerInterval = null;
|
|
1470
|
-
}
|
|
1471
|
-
process.stdout.write("\x1b[2K\r");
|
|
1472
|
-
};
|
|
1473
|
-
const session = await runTask({
|
|
1474
|
-
goal: taskGoal,
|
|
1475
|
-
sessionId: options.session,
|
|
1476
|
-
headed: options.headed,
|
|
1477
|
-
saveFlow: options.saveFlow,
|
|
1478
|
-
maxIterations: options.maxIterations,
|
|
1479
|
-
onHumanInput: async (question, hint) => {
|
|
1480
|
-
// Spinner is already stopped; block it from restarting while we read input
|
|
1481
|
-
spinnerPaused = true;
|
|
1482
|
-
stopSpinner();
|
|
1483
|
-
// Read a full line from stdin cleanly
|
|
1484
|
-
const readline = await import("readline");
|
|
1485
|
-
const rl = readline.createInterface({
|
|
1486
|
-
input: process.stdin,
|
|
1487
|
-
output: process.stdout,
|
|
1488
|
-
terminal: true,
|
|
1489
|
-
});
|
|
1490
|
-
const answer = await new Promise((resolve) => {
|
|
1491
|
-
rl.question(` ${chalk.cyan("โบ")} `, (ans) => {
|
|
1492
|
-
rl.close();
|
|
1493
|
-
resolve(ans.trim());
|
|
1494
|
-
});
|
|
1495
|
-
});
|
|
1496
|
-
console.log(chalk.yellow("โ".repeat(60)) + "\n");
|
|
1497
|
-
// Unblock and restart spinner
|
|
1498
|
-
spinnerPaused = false;
|
|
1499
|
-
startSpinner();
|
|
1500
|
-
return answer;
|
|
1501
|
-
},
|
|
1502
|
-
onEvent: (event) => {
|
|
1503
|
-
const isVisible = event.type === "status_update" ||
|
|
1504
|
-
event.type === "human_input_needed" ||
|
|
1505
|
-
event.type === "credentials_saved" ||
|
|
1506
|
-
event.type === "done" ||
|
|
1507
|
-
event.type === "error" ||
|
|
1508
|
-
event.type === "tool_error";
|
|
1509
|
-
if (isVisible)
|
|
1510
|
-
stopSpinner();
|
|
1511
|
-
printEvent(event);
|
|
1512
|
-
// Restart spinner after visible output (but not if waiting for input or finished)
|
|
1513
|
-
if (isVisible &&
|
|
1514
|
-
event.type !== "done" &&
|
|
1515
|
-
event.type !== "error" &&
|
|
1516
|
-
event.type !== "human_input_needed" // onHumanInput restarts it after input
|
|
1517
|
-
) {
|
|
1518
|
-
startSpinner();
|
|
1519
|
-
}
|
|
1520
|
-
},
|
|
1521
|
-
onSessionUpdate: (s) => {
|
|
1522
|
-
activeSession = s;
|
|
1523
|
-
},
|
|
1524
|
-
});
|
|
1525
|
-
stopSpinner();
|
|
1526
|
-
process.removeListener("SIGINT", onSigint);
|
|
1527
|
-
console.log(chalk.gray(`\n Session: ${session.id}`));
|
|
1528
|
-
if (session.savedFlowPath) {
|
|
1529
|
-
console.log(chalk.cyan(` Flow saved: ${session.savedFlowPath}`));
|
|
1530
|
-
}
|
|
1531
|
-
if (Object.keys(session.memory).length > 0) {
|
|
1532
|
-
console.log(chalk.gray(` Memory (${Object.keys(session.memory).length} items):`));
|
|
1533
|
-
for (const [k, v] of Object.entries(session.memory)) {
|
|
1534
|
-
console.log(chalk.gray(` โข ${k}: ${v.slice(0, 80)}`));
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
// Always generate report after task completes
|
|
1538
|
-
await generateAndPrintReport(session);
|
|
1539
|
-
console.log("");
|
|
1540
|
-
}
|
|
1541
|
-
catch (err) {
|
|
1542
|
-
process.removeListener("SIGINT", onSigint);
|
|
1543
|
-
clearInterval(spinnerInterval);
|
|
1544
|
-
spinnerInterval = null;
|
|
1545
|
-
process.stdout.write("\x1b[2K\r");
|
|
1546
|
-
console.error(chalk.red(`\n Task failed: ${err?.message || err}`));
|
|
1547
|
-
if (activeSession) {
|
|
1548
|
-
await generateAndPrintReport(activeSession);
|
|
1549
|
-
}
|
|
1550
|
-
process.exit(1);
|
|
1551
|
-
}
|
|
1552
|
-
});
|
|
1553
|
-
program.parse();
|
|
1554
|
-
//# sourceMappingURL=cli.js.map
|
|
2
|
+
import{Command as e}from"commander";import o from"chalk";import s from"ora";import t from"path";import n from"fs";import l from"dotenv";import{loadConfig as r,loadCredentials as a,getConfigDir as i}from"./config/loader.js";import{parseFlowFile as c,findFlowFiles as d,validateFlowFile as g,getFlowSummary as p}from"./parser/flow.js";import{TestRunner as f}from"./runner/index.js";import{ReportGenerator as u}from"./report/generator.js";import{BrowserAgent as m}from"./browser/agent.js";import{generateText as y}from"ai";import{createAnthropic as w}from"@ai-sdk/anthropic";import{createOpenAI as h}from"@ai-sdk/openai";import{createGoogleGenerativeAI as $}from"@ai-sdk/google";import{createMistral as b}from"@ai-sdk/mistral";import{createGroq as S}from"@ai-sdk/groq";import k from"yaml";function x(e){switch(e.provider){case"anthropic":return w({apiKey:e.api_key})(e.model);case"openai":return h({apiKey:e.api_key})(e.model);case"google":return $({apiKey:e.api_key})(e.model);case"mistral":return b({apiKey:e.api_key})(e.model);case"groq":return S({apiKey:e.api_key})(e.model);case"ollama":return h({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported provider: ${e.provider}`)}}l.config();const v=new e;function A(e){if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const o={};for(const[s,t]of Object.entries(e))o[String(s)]="string"==typeof t?t:JSON.stringify(t);return o}function I(e,s){const l=t.resolve(e);if(!n.existsSync(l))return console.log(o.yellow(` Skip (not found): ${l}`)),!1;const r=n.readFileSync(l,"utf-8");let a;try{a=k.parse(r)}catch(e){return console.log(o.red(` Invalid YAML: ${l}`)),console.log(o.gray(` ${e.message}`)),!1}if(!a||!a.profiles||"object"!=typeof a.profiles)return console.log(o.yellow(` No profiles in: ${l}`)),!1;let i=!1;for(const[e,o]of Object.entries(a.profiles)){if("inject"!==o.type)continue;const s="string"==typeof o.localStorage||o.localStorage&&(Array.isArray(o.localStorage)||"object"!=typeof o.localStorage),t="string"==typeof o.sessionStorage||o.sessionStorage&&(Array.isArray(o.sessionStorage)||"object"!=typeof o.sessionStorage);(s||t)&&(a.profiles[e]={...o,...s&&{localStorage:A(o.localStorage)},...t&&{sessionStorage:A(o.sessionStorage)}},i=!0)}if(!i)return console.log(o.gray(` No changes needed: ${l}`)),!1;if(s)return console.log(o.cyan(` Would fix: ${l}`)),!0;const c=l+".backup";return n.copyFileSync(l,c),n.writeFileSync(l,k.stringify(a,{indent:2,lineWidth:0})),console.log(o.green(` Fixed: ${l}`)),console.log(o.gray(` Backup: ${c}`)),!0}v.name("slapify").description("AI-powered test automation using natural language flow files").version("0.1.0"),v.command("init").description("Initialize Slapify in the current directory").option("-y, --yes","Skip prompts and use defaults").action(async e=>{const t=await import("readline");if(n.existsSync(".slapify"))return console.log(o.yellow("Slapify is already initialized in this directory.")),void console.log(o.gray("Delete .slapify folder to reinitialize."));console.log(o.blue.bold("\n๐๏ธ Welcome to Slapify!\n")),console.log(o.gray("AI-powered E2E testing that slaps - by slaps.dev\n"));const{findSystemBrowsers:l,initConfig:r}=await import("./config/loader.js");let a,i,c,d="anthropic";const g={anthropic:{name:"Anthropic (Claude)",envVar:"ANTHROPIC_API_KEY",defaultModel:"claude-haiku-4-5-20251001",models:[{id:"claude-haiku-4-5-20251001",name:"Haiku 4.5 - fast & cheap ($1/5M tokens)",recommended:!0},{id:"claude-sonnet-4-20250514",name:"Sonnet 4 - more capable ($3/15M tokens)"},{id:"custom",name:"Enter custom model ID"}]},openai:{name:"OpenAI",envVar:"OPENAI_API_KEY",defaultModel:"gpt-4o-mini",models:[{id:"gpt-4o-mini",name:"GPT-4o Mini - fast & cheap ($0.15/0.6M tokens)",recommended:!0},{id:"gpt-4.1-mini",name:"GPT-4.1 Mini - newer"},{id:"gpt-4o",name:"GPT-4o - more capable ($2.5/10M tokens)"},{id:"custom",name:"Enter custom model ID"}]},google:{name:"Google (Gemini)",envVar:"GOOGLE_API_KEY",defaultModel:"gemini-2.0-flash",models:[{id:"gemini-2.0-flash",name:"Gemini 2.0 Flash - fastest & cheapest",recommended:!0},{id:"gemini-1.5-flash",name:"Gemini 1.5 Flash - stable"},{id:"gemini-1.5-pro",name:"Gemini 1.5 Pro - more capable"},{id:"custom",name:"Enter custom model ID"}]},mistral:{name:"Mistral",envVar:"MISTRAL_API_KEY",askModel:!0,defaultModel:"mistral-small-latest"},groq:{name:"Groq (Fast inference)",envVar:"GROQ_API_KEY",askModel:!0,defaultModel:"llama-3.3-70b-versatile"},ollama:{name:"Ollama (Local)",envVar:"",askModel:!0,defaultModel:"llama3"}};if(e.yes)console.log(o.gray("Using default settings...\n"));else{const e=t.createInterface({input:process.stdin,output:process.stdout}),n=o=>new Promise(s=>e.question(o,s));console.log(o.cyan("1. Choose your LLM provider:\n")),console.log(" 1) Anthropic (Claude) "+o.green("- recommended")),console.log(" 2) OpenAI (GPT-4)"),console.log(" 3) Google (Gemini)"),console.log(" 4) Mistral"),console.log(" 5) Groq "+o.gray("- fast & free tier")),console.log(" 6) Ollama "+o.gray("- local, no API key")),console.log("");d={1:"anthropic",2:"openai",3:"google",4:"mistral",5:"groq",6:"ollama"}[await n(o.white(" Select [1]: "))]||"anthropic";const r=g[d];if(console.log(o.green(` โ Using ${r.name}\n`)),r.models&&r.models.length>0){console.log(o.cyan(" Choose model:\n")),r.models.forEach((e,s)=>{const t=e.recommended?o.green(" โ recommended"):"";console.log(` ${s+1}) ${e.name}${t}`)}),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)-1||0,t=r.models[s];if("custom"===t?.id){a=(await n(o.white(" Enter model ID: "))).trim()||r.defaultModel}else a=t?.id||r.defaultModel;console.log(o.green(` โ Using model: ${a}\n`))}else if(r.askModel){console.log(o.gray(` Enter model ID (default: ${r.defaultModel})`)),"ollama"===d?console.log(o.gray(" Common models: llama3, mistral, codellama, phi3")):"groq"===d?console.log(o.gray(" Common models: llama-3.3-70b-versatile, mixtral-8x7b-32768")):"mistral"===d&&console.log(o.gray(" Common models: mistral-small-latest, mistral-large-latest")),console.log("");a=(await n(o.white(` Model [${r.defaultModel}]: `))).trim()||r.defaultModel,console.log(o.green(` โ Using model: ${a}\n`)),"ollama"===d&&(console.log(o.gray(" Make sure Ollama is running: ollama serve")),console.log(""))}if("ollama"!==d){console.log(o.cyan("2. API Key verification:\n"));const t=r.envVar;let l=process.env[t];if(l)console.log(o.gray(` Found ${t} in environment`));else{console.log(o.yellow(` ${t} not found in environment`)),console.log(o.gray(" You can set it now or add it to your shell config later.\n"));const e=await n(o.white(" Enter API key (or press Enter to skip): "));e.trim()&&(l=e.trim())}let i=!1;for(;!i;){if(!l){console.log(o.yellow(`\n Remember to set ${t} before running tests.`));break}if("n"===(await n(o.white(" Verify API key with test call? (Y/n): "))).toLowerCase()){console.log(o.gray(" Skipping verification\n"));break}{e.pause();const c=s(" Verifying API key...").start();try{const e=x({provider:d,model:a||r.defaultModel||"test",api_key:l});(await y({model:e,prompt:"Reply with only the word 'pong'",maxTokens:10})).text.toLowerCase().includes("pong")?c.succeed(o.green("API key verified! โ")):c.succeed(o.green("API key works! (got response)")),i=!0}catch(e){c.fail(o.red("API key verification failed")),console.log(o.red(` Error: ${e.message}\n`))}if(e.resume(),!i){if("n"===(await n(o.white(" Try a different API key? (Y/n): "))).toLowerCase()){console.log(o.yellow(` Remember to set ${t} correctly before running tests.`));break}const e=await n(o.white(" Enter API key: "));if(!e.trim()){console.log(o.yellow(" No key entered, skipping.\n"));break}l=e.trim()}}}console.log("")}console.log(o.cyan(("ollama"===d?"2":"3")+". Browser setup:\n"));const p=l();if(p.length>0){console.log(" Found browsers on your system:"),p.forEach((e,s)=>{console.log(o.gray(` ${s+1}) ${e.name}`))}),console.log(o.gray(` ${p.length+1}) Download Chromium (~170MB)`)),console.log(o.gray(` ${p.length+2}) Enter custom path`)),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)||1;if(s<=p.length)i=p[s-1].path,c=!0,console.log(o.green(` โ Using ${p[s-1].name}\n`));else if(s===p.length+2){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" โ Using custom browser\n")))}else c=!1,console.log(o.green(" โ Will download Chromium on first run\n"))}else{console.log(" No browsers found. Options:"),console.log(o.gray(" 1) Download Chromium automatically (~170MB)")),console.log(o.gray(" 2) Enter custom browser path")),console.log("");if("2"===await n(o.white(" Select [1]: "))){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" โ Using custom browser\n")))}else c=!1,console.log(o.green(" โ Will download Chromium on first run\n"))}e.close()}const p=s("Creating configuration...").start();try{r(process.cwd(),{provider:d,model:a,browserPath:i,useSystemBrowser:c}),p.succeed("Slapify initialized!"),console.log(""),console.log(o.green("Created:")),console.log(" ๐ .slapify/config.yaml - Configuration"),console.log(" ๐ .slapify/credentials.yaml - Credentials (gitignored)"),console.log(" ๐ tests/example.flow - Sample test"),console.log("");const e=g[d];console.log(o.yellow("Next steps:")),console.log(""),"ollama"===d?(console.log(o.white(" 1. Make sure Ollama is running:")),console.log(o.cyan(" ollama serve")),console.log(o.cyan(" ollama pull llama3"))):(console.log(o.white(" 1. Set your API key:")),console.log(o.cyan(` export ${e.envVar}=your-key-here`))),console.log(""),console.log(o.white(" 2. Run the example test:")),console.log(o.cyan(" slapify run tests/example.flow")),console.log(""),console.log(o.white(" 3. Create your own tests:")),console.log(o.cyan(" slapify create my-first-test")),console.log(o.cyan(' slapify generate "test login for myapp.com"')),console.log(""),console.log(o.gray(" Config can be modified anytime in .slapify/config.yaml")),console.log("")}catch(e){p.fail(e.message),process.exit(1)}}),v.command("install").description("Install browser dependencies").action(()=>{const e=s("Checking agent-browser...").start();if(m.isInstalled())e.succeed("agent-browser is already installed");else{e.text="Installing agent-browser...";try{m.install(),e.succeed("Browser dependencies installed!")}catch(o){e.fail(`Installation failed: ${o.message}`),process.exit(1)}}}),v.command("run [files...]").description("Run flow test files").option("--headed","Run browser in headed mode (visible)").option("--report [format]","Generate report folder (html, markdown, json)").option("--output <dir>","Output directory for reports","./test-reports").option("--credentials <profile>","Default credentials profile to use").option("-p, --parallel","Run tests in parallel").option("-w, --workers <n>","Number of parallel workers (default: 4)","4").option("--performance","Run performance audit (scores, real-user metrics, framework & re-render analysis) and include in report").action(async(e,s)=>{try{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const g=r(l),m=a(l);s.headed&&(g.browser={...g.browser,headless:!1});const y=void 0!==s.report;g.report={...g.report,format:"string"==typeof s.report?s.report:"html",output_dir:s.output,screenshots:y};let w=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(w=await d(e))}else for(const o of e)if(n.statSync(o).isDirectory()){const e=await d(o);w.push(...e)}else w.push(o);0===w.length&&(console.log(o.yellow("No .flow files found to run.")),process.exit(0));const h=new u(g.report),$=[],b=s.parallel&&w.length>1,S=parseInt(s.workers)||4;if(b){console.log(o.blue.bold(`\nโโโ Running ${w.length} tests in parallel (${S} workers) โโโ\n`));const e=[...w],s=new Map,n=new Map;for(const e of w){const s=t.basename(e,".flow");n.set(s,o.gray("โณ pending"))}const l=()=>{process.stdout.write("["+w.length+"A");for(const e of w){const o=t.basename(e,".flow"),s=n.get(o)||"";process.stdout.write("[2K"),console.log(` ${o}: ${s}`)}};for(const e of w){const o=t.basename(e,".flow");console.log(` ${o}: ${n.get(o)}`)}const r=async e=>{const s=c(e),t=s.name;n.set(t,o.cyan("โถ running...")),l();try{const e=new f(g,m),l=await e.runFlow(s);$.push(l),"passed"===l.status?n.set(t,o.green(`โ passed (${l.passedSteps}/${l.totalSteps} steps, ${(l.duration/1e3).toFixed(1)}s)`)):n.set(t,o.red(`โ failed (${l.failedSteps} failed, ${l.passedSteps} passed)`))}catch(e){n.set(t,o.red(`โ error: ${e.message}`))}l()};for(;e.length>0||s.size>0;){for(;e.length>0&&s.size<S;){const o=e.shift(),t=r(o).then(()=>{s.delete(o)});s.set(o,t)}s.size>0&&await Promise.race(s.values())}console.log("")}else for(const e of w){const t=c(e),n=p(t);console.log(""),console.log(o.blue.bold(`โโโ ${t.name} โโโ`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`)),console.log("");try{const e=new f(g,m),n=await e.runFlow(t,e=>{const s=e.step,t="passed"===e.status?o.green("โ"):"failed"===e.status?o.red("โ"):o.yellow("โ"),n=s.optional?o.gray(" [optional]"):"",l=e.retried?o.yellow(" [retried]"):"",r=o.gray(`(${(e.duration/1e3).toFixed(1)}s)`);if(console.log(` ${t} ${s.text}${n}${l} ${r}`),"failed"===e.status&&e.error&&console.log(o.red(` โโ ${e.error}`)),e.assumptions&&e.assumptions.length>0)for(const s of e.assumptions)console.log(o.gray(` โโ ๐ก ${s}`))},!!s.performance);if(n.perfAudit){const e=n.perfAudit,s=[];e.vitals.fcp&&s.push(`FCP ${e.vitals.fcp}ms`),e.vitals.lcp&&s.push(`LCP ${e.vitals.lcp}ms`),null!=e.vitals.cls&&s.push(`CLS ${e.vitals.cls}`);const t=e.scores??e.lighthouse;t&&s.push(`Perf ${t.performance}/100`),console.log(o.cyan(` โก Perf: ${s.join(" ยท ")}`))}$.push(n),console.log(""),"passed"===n.status?console.log(o.green.bold(" โ PASSED")+o.gray(` (${n.passedSteps}/${n.totalSteps} steps in ${(n.duration/1e3).toFixed(1)}s)`)):console.log(o.red.bold(" โ FAILED")+o.gray(` (${n.failedSteps} failed, ${n.passedSteps} passed)`)),n.autoHandled.length>0&&console.log(o.gray(` โน Auto-handled: ${n.autoHandled.join(", ")}`))}catch(e){console.log(o.red(` โ ERROR: ${e.message}`))}}console.log(""),console.log(o.blue.bold("โโโ Summary โโโ"));const k=$.filter(e=>"passed"===e.status).length,x=$.filter(e=>"failed"===e.status).length,v=$.reduce((e,o)=>e+o.totalSteps,0),A=$.reduce((e,o)=>e+o.passedSteps,0);if(console.log(o.gray(` ${$.length} test file(s), ${v} total steps`)),0===x?console.log(o.green.bold(` โ All ${k} test(s) passed! (${A}/${v} steps)`)):console.log(o.red.bold(` โ ${x}/${$.length} test(s) failed`)),y&&$.length>0){let e;e=1===$.length?h.saveAsFolder($[0]):h.saveSuiteAsFolder($),console.log(o.cyan(`\n ๐ Report: ${e}`))}console.log(""),x>0&&process.exit(1)}catch(e){console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),v.command("create <name>").description("Create a new flow file").option("-d, --dir <directory>","Directory to create flow in","tests").action(async(e,s)=>{const l=await import("readline"),r=s.dir;n.existsSync(r)||n.mkdirSync(r,{recursive:!0});const a=e.endsWith(".flow")?e:`${e}.flow`,i=t.join(r,a);n.existsSync(i)&&(console.log(o.red(`File already exists: ${i}`)),process.exit(1)),console.log(o.blue(`\nCreating: ${i}`)),console.log(o.gray("Enter your test steps (one per line). Empty line to finish.\n"));const c=l.createInterface({input:process.stdin,output:process.stdout}),d=[`# ${e}`,""];let g=1;const p=()=>new Promise(e=>{c.question(o.cyan(`${g}. `),o=>{e(o)})});for(;;){const e=await p();if(""===e)break;d.push(e),g++}c.close(),d.length<=2?console.log(o.yellow("\nNo steps entered. File not created.")):(n.writeFileSync(i,d.join("\n")+"\n"),console.log(o.green(`\nโ Created: ${i}`)),console.log(o.gray(` ${g-1} steps`)),console.log(o.gray(`\nRun with: slapify run ${i}`)))}),v.command("generate <prompt>").alias("gen").description("Generate a verified .flow file by running the goal as a task and recording what worked").option("-d, --dir <directory>","Directory to save flow","tests").option("--headed","Show browser window while running").action(async(e,s)=>{const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));r(t);console.log(o.blue("\n๐ค Flow Generator\n")),console.log(o.gray(" Running the goal in the browser to discover the real path...\n"));const{runTask:n}=await import("./task/runner.js");let l;await n({goal:e,headed:s.headed,saveFlow:!0,flowOutputDir:s.dir,onEvent:e=>{"status_update"===e.type&&process.stdout.write(o.gray(` โ ${e.message}\n`)),"message"===e.type&&console.log(o.white(`\n${e.text}`)),"flow_saved"===e.type&&(l=e.path),"done"===e.type&&console.log(o.green("\nโ
Done")),"error"===e.type&&console.log(o.red(`\nโ ${e.error}`))}}),l?(console.log(o.green(`\nโ Flow saved: ${l}`)),console.log(o.gray(` Run with: slapify run ${l}`))):console.log(o.yellow("\nโ No flow was saved. The agent may not have completed the goal."))}),v.command("fix <file>").description("Analyze a failing test and suggest/apply fixes").option("--auto","Automatically apply suggested fixes without confirmation").option("--headed","Run browser in headed mode for debugging").action(async(e,t)=>{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1)),n.existsSync(e)||(console.log(o.red(`File not found: ${e}`)),process.exit(1));const d=r(l),g=a(l);t.headed&&(d.browser={...d.browser,headless:!1}),d.report={...d.report,screenshots:!0};const p=await import("readline");let u=s("Running test to identify failures...").start();try{const l=c(e),r=new f(d,g),a=[];if("passed"===(await r.runFlow(l,e=>{"failed"===e.status&&e.error&&a.push({step:e.step.text,error:e.error,line:e.step.line,screenshot:e.screenshot})})).status)return void u.succeed("Test passed! No fixes needed.");if(u.info(`Found ${a.length} failing step(s)`),0===a.length)return void console.log(o.yellow("No specific step failures to fix."));const i=n.readFileSync(e,"utf-8"),m=i.split("\n");u=s("Analyzing failures and generating fixes...").start();const w=a.map(e=>`Line ${e.line}: "${e.step}"\n Error: ${e.error}`).join("\n\n"),h=await y({model:x(d.llm),system:`You are a test automation expert. Analyze failing test steps and suggest fixes.\n\nOriginal flow file:\n\`\`\`\n${i}\n\`\`\`\n\nFailing steps:\n${w}\n\nBased on the errors, suggest fixes for the flow file. Common issues and fixes:\n1. Element not found โ Try more descriptive text, add wait, or make step optional\n2. Timeout โ Add explicit wait or increase timeout\n3. Navigation error โ Add wait after navigation, or split into smaller steps\n4. Element obscured โ Add step to close popup/modal first\n5. Stale element โ Add wait for page to stabilize\n\nRespond with JSON:\n{\n "analysis": "Brief explanation of what's wrong",\n "fixes": [\n {\n "line": 5,\n "original": "Click the submit button",\n "fixed": "Click the Submit button",\n "reason": "Button text is capitalized"\n }\n ],\n "additions": [\n {\n "afterLine": 4,\n "step": "[Optional] Wait for page to load",\n "reason": "Page might still be loading"\n }\n ]\n}`,prompt:"Analyze the failures and suggest specific fixes.",maxTokens:1500});u.succeed("Analysis complete");const $=h.text.match(/\{[\s\S]*\}/);if(!$)return void console.log(o.red("Could not parse AI response"));const b=JSON.parse($[0]);if(console.log(o.blue("\nโโโ Analysis โโโ\n")),console.log(o.white(b.analysis)),b.fixes?.length>0||b.additions?.length>0){if(console.log(o.blue("\nโโโ Suggested Fixes โโโ\n")),b.fixes?.length>0)for(const e of b.fixes)console.log(o.yellow(`Line ${e.line}:`)),console.log(o.red(` - ${e.original}`)),console.log(o.green(` + ${e.fixed}`)),console.log(o.gray(` Reason: ${e.reason}\n`));if(b.additions?.length>0){console.log(o.yellow("New steps to add:"));for(const e of b.additions)console.log(o.green(` + After line ${e.afterLine}: ${e.step}`)),console.log(o.gray(` Reason: ${e.reason}\n`))}let s=t.auto;if(!s){const e=p.createInterface({input:process.stdin,output:process.stdout}),t=await new Promise(s=>{e.question(o.cyan("Apply these fixes? (y/N): "),o=>{e.close(),s(o.trim().toLowerCase())})});s="y"===t||"yes"===t}if(s){let s=[...m];new Map;if(b.fixes)for(const e of b.fixes){const o=e.line-1;o>=0&&o<s.length&&(s[o]=e.fixed)}if(b.additions){const e=[...b.additions].sort((e,o)=>o.afterLine-e.afterLine);for(const o of e){const e=o.afterLine;s.splice(e,0,o.step)}}const t=s.join("\n"),l=e+".backup";n.writeFileSync(l,i),n.writeFileSync(e,t),console.log(o.green(`\nโ Fixes applied to ${e}`)),console.log(o.gray(` Backup saved to ${l}`)),console.log(o.gray(`\nRun again with: slapify run ${e}`))}else console.log(o.yellow("No changes made."))}else console.log(o.yellow("\nNo automatic fixes suggested.")),console.log(o.gray("The failures may require manual investigation."))}catch(e){u.fail("Error"),console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),v.command("validate [files...]").description("Validate flow files for syntax issues").action(async e=>{let s=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(s=await d(e))}else s=e;if(0===s.length)return void console.log(o.yellow("No .flow files found."));let l=!1;for(const e of s)try{const s=c(e),t=g(s),n=p(s);if(t.length>0){l=!0,console.log(o.yellow(`โ ๏ธ ${e}`));for(const e of t)console.log(o.yellow(` ${e}`))}else console.log(o.green(`โ
${e}`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}catch(s){console.log(o.red(`โ ${e}`)),console.log(o.red(` ${s.message}`)),l=!0}l&&process.exit(1)}),v.command("list").description("List all flow files").action(async()=>{const e=t.join(process.cwd(),"tests");if(!n.existsSync(e))return void console.log(o.yellow("No tests directory found."));const s=await d(e);if(0!==s.length){console.log(o.blue(`\nFound ${s.length} flow file(s):\n`));for(const e of s){const s=c(e),n=p(s),l=t.relative(process.cwd(),e);console.log(` ${o.white(l)}`),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}console.log("")}else console.log(o.yellow("No .flow files found."))}),v.command("credentials").description("List configured credential profiles").action(()=>{const e=i();e||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const s=a(e),t=Object.keys(s.profiles);if(0===t.length)return console.log(o.yellow("No credential profiles configured.")),void console.log(o.gray("Edit .slapify/credentials.yaml to add profiles."));console.log(o.blue("\nConfigured credential profiles:\n"));for(const e of t){const t=s.profiles[e];console.log(` ${o.white(e)} (${t.type})`),t.username&&console.log(o.gray(` username: ${t.username}`)),t.email&&console.log(o.gray(` email: ${t.email}`)),t.totp_secret&&console.log(o.gray(" 2FA: TOTP configured")),t.fixed_otp&&console.log(o.gray(" 2FA: Fixed OTP configured"))}console.log("")}),v.command("fix-credentials [files...]").description("Fix credential YAML files where localStorage/sessionStorage were saved as JSON strings").option("--dry-run","Only print what would be fixed").action((e,s)=>{const n=[];if(e&&e.length>0)n.push(...e.map(e=>t.resolve(e)));else{const e=process.cwd();n.push(t.join(e,"temp_credentials.yaml"));const o=i();o&&n.push(t.join(o,"credentials.yaml"))}console.log(o.blue("\n๐ง Fix credential YAML files\n")),s.dryRun&&console.log(o.gray(" (dry run โ no files will be modified)\n"));let l=0;for(const e of n)I(e,!!s.dryRun)&&l++;0===l&&n.length>0&&console.log(o.gray("\n No files needed fixing.")),console.log("")}),v.command("interactive [url]").alias("i").description("Run steps interactively").option("--headed","Run browser in headed mode").action(async(e,s)=>{console.log(o.blue("\n๐งช Slapify Interactive Mode")),console.log(o.gray('Type test steps and press Enter to execute. Type "exit" to quit.\n'));const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const n=r(t),l=a(t);s.headed&&(n.browser={...n.browser,headless:!1});new f(n,l);e&&console.log(o.gray(`Navigating to ${e}...`));const c=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),d=()=>{c.question(o.cyan("> "),async e=>{const s=e.trim();"exit"!==s.toLowerCase()&&"quit"!==s.toLowerCase()||(console.log(o.gray("\nGoodbye!")),c.close(),process.exit(0)),s?(console.log(o.gray(`Executing: ${s}`)),console.log(o.green("โ Done")),d()):d()})};d()}),v.command("task [goal]").description('Run an autonomous AI agent task in plain English.\n The agent decides everything: what to do, when to schedule, when to sleep.\n Examples:\n slapify task "Go to linkedin.com and like the latest 3 posts"\n slapify task "Monitor my Gmail for new emails every 30 min and log subjects"\n slapify task "Order breakfast from Swiggy every day at 8am"').option("--headed","Show the browser window").option("--debug","Show all tool calls and internal steps").option("--report","Generate an HTML report after the task completes").option("--save-flow","Save agent steps as a reusable .flow file when done").option("--session <id>","Resume an existing task session").option("--list-sessions","List all task sessions").option("--logs <id>","Show logs for a task session").option("--max-iterations <n>","Safety cap on agent loop iterations (default 400)",parseInt).option("--schema <json-or-file>","JSON Schema (inline JSON string or path to a .json file) the agent should use to structure its output").option("--output <file>","File path to write structured JSON output to (used together with --schema)").action(async(e,s)=>{if(s.listSessions){const{listSessions:e}=await import("./task/index.js"),s=e();if(0===s.length)return void console.log(o.gray("\nNo task sessions found.\n"));console.log(o.blue(`\n๐ Task Sessions (${s.length})\n`));for(const e of s){const s="completed"===e.status?o.green:"failed"===e.status?o.red:"scheduled"===e.status?o.blue:o.yellow;console.log(` ${s("โ")} ${o.bold(e.id)}\n Goal: ${e.goal.slice(0,70)}${e.goal.length>70?"โฆ":""}\n Status: ${s(e.status)} Iterations: ${e.iteration}\n Updated: ${new Date(e.updatedAt).toLocaleString()}\n`)}return}if(s.logs){const{loadSession:e}=await import("./task/index.js"),{loadEvents:t}=await import("./task/session.js"),n=e(s.logs);n||(console.log(o.red(`Session '${s.logs}' not found.`)),process.exit(1)),console.log(o.blue(`\n๐ Logs: ${n.id}\n`)),console.log(o.gray(`Goal: ${n.goal}\n`));const l=t(s.logs);for(const e of l){const s=o.gray(new Date(e.ts).toLocaleTimeString());"llm_response"===e.type?e.text&&console.log(`${s} ๐ค ${o.cyan(e.text.slice(0,120))}`):"tool_call"===e.type?console.log(`${s} ๐ง ${o.yellow(e.toolName)} โ ${o.gray(JSON.stringify(e.result).slice(0,80))}`):"tool_error"===e.type?console.log(`${s} โ ${o.red(e.toolName)} โ ${o.red(e.error.slice(0,80))}`):"memory_update"===e.type?console.log(`${s} ๐ง ${o.magenta("remember")} ${e.key} = ${e.value.slice(0,60)}`):"scheduled"===e.type?console.log(`${s} โฐ ${o.blue("schedule")} ${e.cron} โ ${e.task}`):"sleeping_until"===e.type?console.log(`${s} ๐ด ${o.blue("sleep")} until ${e.until}`):"session_end"===e.type&&console.log(`${s} โ
${o.green("done")} ${e.summary.slice(0,120)}`)}return void console.log("")}let l=e||s.session?e:void 0;if(l||s.session||(console.log(o.red('\nPlease provide a goal. Example:\n slapify task "Go to example.com and check the title"\n')),process.exit(1)),!l&&s.session){const{loadSession:e}=await import("./task/index.js"),t=e(s.session);t||(console.log(o.red(`Session '${s.session}' not found.`)),process.exit(1)),l=t.goal}i()||(console.log(o.red('\nNo .slapify directory found. Run "slapify init" first.\n')),process.exit(1));let r=null;const a=async e=>{if(s.report)try{const{loadEvents:s,saveTaskReport:t}=await import("./task/index.js"),n=t(e,s(e.id));console.log(o.cyan(`\n ๐ Report: ${n}`))}catch(e){console.log(o.yellow(` โ Could not generate report: ${e?.message}`))}},c=!!s.debug,d=()=>process.stdout.write("[2K\r");console.log(o.blue("\n๐ค Slapify Task Agent\n")),console.log(o.white(` Goal: ${l}`)),s.session&&console.log(o.gray(` Resuming session: ${s.session}`)),console.log(o.gray([s.report?" --report: HTML report on exit":"",c?" --debug: verbose output":""," Ctrl+C to stop"].filter(Boolean).join(" ยท ")+"\n")),console.log(o.gray("โ".repeat(60))+"\n");let g=null,p=!1;const f=()=>{if(c||p||g)return;const e=["โ ","โ ","โ น","โ ธ","โ ผ","โ ด","โ ฆ","โ ง","โ ","โ "];let s=0;g=setInterval(()=>{process.stdout.write(o.gray(`\r ${e[s++%e.length]} working...`))},80)};c||f();const{runTask:u}=await import("./task/index.js");let m=!1;const y=async()=>{if(!m){if(m=!0,clearInterval(g),g=null,p=!0,process.stdout.write("[2K\r"),console.log(o.yellow("\n โก Interrupted"+(s.report?" โ generating report...":""))),r){r.status="failed",r.finalSummary="Task interrupted by user (Ctrl+C).";const{saveSessionMeta:e}=await import("./task/session.js");e(r),await a(r)}console.log(o.gray(" Goodbye.\n")),process.exit(0)}};process.once("SIGINT",y);try{const e=()=>{g&&(clearInterval(g),g=null),process.stdout.write("[2K\r")};let i;if(s.schema)try{i=JSON.parse(s.schema)}catch{try{const e=n.readFileSync(t.resolve(s.schema),"utf8");i=JSON.parse(e)}catch{console.log(o.red("Could not parse --schema: expected inline JSON or a valid .json file path.")),process.exit(1)}}const m=await u({goal:l,sessionId:s.session,headed:s.headed,saveFlow:s.saveFlow,maxIterations:s.maxIterations,schema:i,outputFile:s.output,onHumanInput:async(s,t)=>{p=!0,e();const n=(await import("readline")).createInterface({input:process.stdin,output:process.stdout,terminal:!0}),l=await new Promise(e=>{n.question(` ${o.cyan("โบ")} `,o=>{n.close(),e(o.trim())})});return console.log(o.yellow("โ".repeat(60))+"\n"),p=!1,f(),l},onEvent:s=>{const t="status_update"===s.type||"human_input_needed"===s.type||"credentials_saved"===s.type||"done"===s.type||"error"===s.type||"tool_error"===s.type;t&&e(),(e=>{switch(e.type){case"thinking":c&&process.stdout.write(o.gray(" โณ thinking...\r"));break;case"message":c&&(d(),console.log(o.gray(` ๐ฌ ${e.text}`)));break;case"tool_start":if(c){d();const s=JSON.stringify(e.args);console.log(o.dim(` โบ ${o.cyan(e.toolName)} `)+o.gray(s.slice(0,100)+(s.length>100?"โฆ":"")))}break;case"tool_done":c&&console.log(o.dim(` โ ${e.result.slice(0,120)}`));break;case"tool_error":d(),console.log(o.red(` โ ${e.toolName}: ${e.error.slice(0,120)}`));break;case"status_update":d(),console.log(o.white(` ${e.message}`));break;case"human_input_needed":console.log("\n"+o.yellow("โ".repeat(60))),console.log(o.yellow.bold(" ๐ Agent needs your input")),console.log(o.white(`\n ${e.question}`)),e.hint&&console.log(o.gray(` ${e.hint}`));break;case"credentials_saved":d(),console.log(o.green(` ๐พ Credentials saved: '${e.profileName}' (${e.credType}) โ .slapify/credentials.yaml`));break;case"scheduled":c&&(d(),console.log(o.dim(` โฐ scheduled: ${e.cron} โ ${e.task}`)));break;case"sleeping":c&&(d(),console.log(o.dim(` ๐ด sleeping until ${new Date(e.until).toLocaleString()}`)));break;case"done":d(),console.log("\n"+o.green("โ".repeat(60))),console.log(o.green.bold(" โ
Task complete!")),console.log(o.white(`\n ${e.summary}`)),console.log(o.green("โ".repeat(60)));break;case"error":d(),console.log(o.red(`\n โ Error: ${e.error}`))}})(s),t&&"done"!==s.type&&"error"!==s.type&&"human_input_needed"!==s.type&&f()},onSessionUpdate:e=>{r=e}});if(e(),process.removeListener("SIGINT",y),console.log(o.gray(`\n Session: ${m.id}`)),m.savedFlowPath&&console.log(o.cyan(` Flow saved: ${m.savedFlowPath}`)),s.output&&null!=m.structuredOutput&&console.log(o.cyan(` Output: ${t.resolve(s.output)}`)),Object.keys(m.memory).length>0){console.log(o.gray(` Memory (${Object.keys(m.memory).length} items):`));for(const[e,s]of Object.entries(m.memory))console.log(o.gray(` โข ${e}: ${s.slice(0,80)}`))}await a(m),console.log("")}catch(e){process.removeListener("SIGINT",y),clearInterval(g),g=null,process.stdout.write("[2K\r"),console.error(o.red(`\n Task failed: ${e?.message||e}`)),r&&await a(r),process.exit(1)}}),v.parse();
|