slapify 0.0.13 → 0.0.16
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 +342 -258
- package/dist/ai/interpreter.d.ts +13 -0
- package/dist/ai/interpreter.d.ts.map +1 -1
- package/dist/ai/interpreter.js +43 -5
- package/dist/ai/interpreter.js.map +1 -1
- package/dist/cli.js +500 -152
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/perf/audit.d.ts +215 -0
- package/dist/perf/audit.d.ts.map +1 -0
- package/dist/perf/audit.js +635 -0
- package/dist/perf/audit.js.map +1 -0
- package/dist/report/generator.d.ts +1 -0
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +92 -0
- package/dist/report/generator.js.map +1 -1
- package/dist/runner/index.d.ts +14 -1
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/index.js +195 -13
- package/dist/runner/index.js.map +1 -1
- package/dist/task/index.d.ts +5 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +4 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/report.d.ts +9 -0
- package/dist/task/report.d.ts.map +1 -0
- package/dist/task/report.js +740 -0
- package/dist/task/report.js.map +1 -0
- package/dist/task/runner.d.ts +3 -0
- package/dist/task/runner.d.ts.map +1 -0
- package/dist/task/runner.js +1362 -0
- package/dist/task/runner.js.map +1 -0
- package/dist/task/session.d.ts +18 -0
- package/dist/task/session.d.ts.map +1 -0
- package/dist/task/session.js +153 -0
- package/dist/task/session.js.map +1 -0
- package/dist/task/tools.d.ts +253 -0
- package/dist/task/tools.d.ts.map +1 -0
- package/dist/task/tools.js +258 -0
- package/dist/task/tools.js.map +1 -0
- package/dist/task/types.d.ts +153 -0
- package/dist/task/types.d.ts.map +1 -0
- package/dist/task/types.js +2 -0
- package/dist/task/types.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -13
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createOpenAI } from "@ai-sdk/openai";
|
|
|
16
16
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
17
17
|
import { createMistral } from "@ai-sdk/mistral";
|
|
18
18
|
import { createGroq } from "@ai-sdk/groq";
|
|
19
|
+
import yaml from "yaml";
|
|
19
20
|
// Load environment variables
|
|
20
21
|
dotenv.config();
|
|
21
22
|
/**
|
|
@@ -426,6 +427,7 @@ program
|
|
|
426
427
|
.option("--credentials <profile>", "Default credentials profile to use")
|
|
427
428
|
.option("-p, --parallel", "Run tests in parallel")
|
|
428
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")
|
|
429
431
|
.action(async (files, options) => {
|
|
430
432
|
try {
|
|
431
433
|
// Load configuration
|
|
@@ -573,12 +575,28 @@ program
|
|
|
573
575
|
console.log(chalk.red(` └─ ${stepResult.error}`));
|
|
574
576
|
}
|
|
575
577
|
// Show assumptions if any
|
|
576
|
-
if (stepResult.assumptions &&
|
|
578
|
+
if (stepResult.assumptions &&
|
|
579
|
+
stepResult.assumptions.length > 0) {
|
|
577
580
|
for (const assumption of stepResult.assumptions) {
|
|
578
581
|
console.log(chalk.gray(` └─ 💡 ${assumption}`));
|
|
579
582
|
}
|
|
580
583
|
}
|
|
581
|
-
});
|
|
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
|
+
}
|
|
582
600
|
results.push(result);
|
|
583
601
|
// Print test summary
|
|
584
602
|
console.log("");
|
|
@@ -692,9 +710,9 @@ program
|
|
|
692
710
|
program
|
|
693
711
|
.command("generate <prompt>")
|
|
694
712
|
.alias("gen")
|
|
695
|
-
.description("Generate a flow file
|
|
713
|
+
.description("Generate a verified .flow file by running the goal as a task and recording what worked")
|
|
696
714
|
.option("-d, --dir <directory>", "Directory to save flow", "tests")
|
|
697
|
-
.option("--headed", "Show browser while
|
|
715
|
+
.option("--headed", "Show browser window while running")
|
|
698
716
|
.action(async (prompt, options) => {
|
|
699
717
|
const configDir = getConfigDir();
|
|
700
718
|
if (!configDir) {
|
|
@@ -702,156 +720,42 @@ program
|
|
|
702
720
|
process.exit(1);
|
|
703
721
|
}
|
|
704
722
|
const config = loadConfig(configDir);
|
|
705
|
-
|
|
706
|
-
console.log(chalk.
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
},
|
|
717
752
|
});
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (url === "NO_URL" || !url.startsWith("http")) {
|
|
722
|
-
const rl = readline.createInterface({
|
|
723
|
-
input: process.stdin,
|
|
724
|
-
output: process.stdout,
|
|
725
|
-
});
|
|
726
|
-
url = await new Promise((resolve) => {
|
|
727
|
-
rl.question(chalk.cyan("Enter the URL to test: "), (answer) => {
|
|
728
|
-
rl.close();
|
|
729
|
-
resolve(answer.trim());
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
if (!url) {
|
|
733
|
-
console.log(chalk.red("No URL provided. Aborting."));
|
|
734
|
-
process.exit(1);
|
|
735
|
-
}
|
|
753
|
+
if (savedPath) {
|
|
754
|
+
console.log(chalk.green(`\n✓ Flow saved: ${savedPath}`));
|
|
755
|
+
console.log(chalk.gray(` Run with: slapify run ${savedPath}`));
|
|
736
756
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
spinner = ora("Opening browser and analyzing page...").start();
|
|
740
|
-
const browser = new BrowserAgent({
|
|
741
|
-
...config.browser,
|
|
742
|
-
headless: !options.headed,
|
|
743
|
-
});
|
|
744
|
-
try {
|
|
745
|
-
await browser.navigate(url);
|
|
746
|
-
await browser.wait(2000); // Wait for page to load
|
|
747
|
-
const snapshot = await browser.snapshot(true);
|
|
748
|
-
const pageTitle = await browser.getTitle();
|
|
749
|
-
spinner.succeed("Page analyzed");
|
|
750
|
-
// Step 4: Generate test steps using AI
|
|
751
|
-
spinner = ora("Generating test steps...").start();
|
|
752
|
-
const generationResponse = await generateText({
|
|
753
|
-
model: getModelFromConfig(config.llm),
|
|
754
|
-
system: `You are a test automation expert. Generate clear, actionable test steps for a .flow file.
|
|
755
|
-
|
|
756
|
-
Rules:
|
|
757
|
-
- Each step should be a single action or verification
|
|
758
|
-
- Use natural language that describes WHAT to do, not HOW
|
|
759
|
-
- Include [Optional] prefix for steps that might not always apply (popups, banners)
|
|
760
|
-
- Include verification steps to confirm actions worked
|
|
761
|
-
- Use "If X appears, do Y" for conditional handling
|
|
762
|
-
- Keep steps concise but clear
|
|
763
|
-
- Start with navigation to the URL
|
|
764
|
-
- Generate a suitable filename (lowercase, hyphenated, no extension)
|
|
765
|
-
|
|
766
|
-
Output format:
|
|
767
|
-
FILENAME: suggested-name
|
|
768
|
-
STEPS:
|
|
769
|
-
Go to <url>
|
|
770
|
-
Step 2 here
|
|
771
|
-
Step 3 here
|
|
772
|
-
...`,
|
|
773
|
-
prompt: `Generate test steps for this request:
|
|
774
|
-
|
|
775
|
-
"${prompt}"
|
|
776
|
-
|
|
777
|
-
Target URL: ${url}
|
|
778
|
-
Page Title: ${pageTitle}
|
|
779
|
-
|
|
780
|
-
Current page structure:
|
|
781
|
-
${snapshot}
|
|
782
|
-
|
|
783
|
-
Generate practical test steps that accomplish the user's goal.`,
|
|
784
|
-
maxTokens: 1500,
|
|
785
|
-
});
|
|
786
|
-
spinner.succeed("Test steps generated");
|
|
787
|
-
// Parse the response
|
|
788
|
-
const responseText = generationResponse.text;
|
|
789
|
-
const filenameMatch = responseText.match(/FILENAME:\s*(.+)/i);
|
|
790
|
-
const stepsMatch = responseText.match(/STEPS:\s*([\s\S]+)/i);
|
|
791
|
-
let filename = filenameMatch
|
|
792
|
-
? filenameMatch[1]
|
|
793
|
-
.trim()
|
|
794
|
-
.replace(/[^a-z0-9-]/gi, "-")
|
|
795
|
-
.toLowerCase()
|
|
796
|
-
: "generated-test";
|
|
797
|
-
if (!filename.endsWith(".flow")) {
|
|
798
|
-
filename += ".flow";
|
|
799
|
-
}
|
|
800
|
-
const steps = stepsMatch
|
|
801
|
-
? stepsMatch[1].trim()
|
|
802
|
-
: responseText.replace(/FILENAME:.+/i, "").trim();
|
|
803
|
-
// Close browser
|
|
804
|
-
await browser.close();
|
|
805
|
-
// Step 5: Show generated steps and confirm
|
|
806
|
-
console.log(chalk.blue("\n━━━ Generated Flow ━━━\n"));
|
|
807
|
-
console.log(chalk.white(steps));
|
|
808
|
-
console.log(chalk.blue("\n━━━━━━━━━━━━━━━━━━━━━━\n"));
|
|
809
|
-
const rl = readline.createInterface({
|
|
810
|
-
input: process.stdin,
|
|
811
|
-
output: process.stdout,
|
|
812
|
-
});
|
|
813
|
-
const confirm = await new Promise((resolve) => {
|
|
814
|
-
rl.question(chalk.cyan(`Save as ${options.dir}/${filename}? (Y/n/edit): `), (answer) => {
|
|
815
|
-
rl.close();
|
|
816
|
-
resolve(answer.trim().toLowerCase());
|
|
817
|
-
});
|
|
818
|
-
});
|
|
819
|
-
if (confirm === "n" || confirm === "no") {
|
|
820
|
-
console.log(chalk.yellow("Cancelled."));
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
if (confirm === "e" || confirm === "edit") {
|
|
824
|
-
// Let user edit the filename
|
|
825
|
-
const rl2 = readline.createInterface({
|
|
826
|
-
input: process.stdin,
|
|
827
|
-
output: process.stdout,
|
|
828
|
-
});
|
|
829
|
-
filename = await new Promise((resolve) => {
|
|
830
|
-
rl2.question(chalk.cyan("Filename: "), (answer) => {
|
|
831
|
-
rl2.close();
|
|
832
|
-
const name = answer.trim() || filename;
|
|
833
|
-
resolve(name.endsWith(".flow") ? name : name + ".flow");
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
// Step 6: Save the file
|
|
838
|
-
const dir = options.dir;
|
|
839
|
-
if (!fs.existsSync(dir)) {
|
|
840
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
841
|
-
}
|
|
842
|
-
const filepath = path.join(dir, filename);
|
|
843
|
-
const content = `# ${filename
|
|
844
|
-
.replace(".flow", "")
|
|
845
|
-
.replace(/-/g, " ")}\n# Generated from: ${prompt}\n\n${steps}\n`;
|
|
846
|
-
fs.writeFileSync(filepath, content);
|
|
847
|
-
console.log(chalk.green(`\n✓ Saved: ${filepath}`));
|
|
848
|
-
console.log(chalk.gray(`\nRun with: slapify run ${filepath}`));
|
|
849
|
-
}
|
|
850
|
-
catch (error) {
|
|
851
|
-
spinner.fail("Error");
|
|
852
|
-
await browser.close();
|
|
853
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
854
|
-
process.exit(1);
|
|
757
|
+
else {
|
|
758
|
+
console.log(chalk.yellow("\n⚠ No flow was saved. The agent may not have completed the goal."));
|
|
855
759
|
}
|
|
856
760
|
});
|
|
857
761
|
// Fix command - analyze and fix failing tests
|
|
@@ -1150,6 +1054,115 @@ program
|
|
|
1150
1054
|
}
|
|
1151
1055
|
console.log("");
|
|
1152
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
|
+
});
|
|
1153
1166
|
// Interactive mode
|
|
1154
1167
|
program
|
|
1155
1168
|
.command("interactive [url]")
|
|
@@ -1202,5 +1215,340 @@ program
|
|
|
1202
1215
|
};
|
|
1203
1216
|
prompt();
|
|
1204
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
|
+
});
|
|
1205
1553
|
program.parse();
|
|
1206
1554
|
//# sourceMappingURL=cli.js.map
|