@arcadialdev/arcality 2.4.32 → 2.4.34

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 CHANGED
@@ -55,6 +55,20 @@ npm run arcality:run
55
55
 
56
56
  ---
57
57
 
58
+ ## 💻 CLI Commands Cheatsheet
59
+
60
+ The Arcality flow is designed to be used globally or project-wide via \`npx\`. Below are all currently supported commands:
61
+
62
+ | Command | Description | Internal Behavior |
63
+ | :--- | :--- | :--- |
64
+ | \`npx arcality\` | **Interactive Menu**<br/>Launches a visual menu in the terminal to guide the user. | Prompts for the Mission to execute. Ideal for new users who want a guided experience. |
65
+ | \`npx arcality init\` | **Initial Configuration**<br/>Connects the current project to Arcality Cloud. | Prompts for your API Key and Project ID, generates \`arcality.config.json\`, and validates the ADO / Portal connection. |
66
+ | \`npx arcality setup\` | **Dependency Setup**<br/>Downloads browser binaries. | Silently executes the installation of internal Playwright browsers (Chromium) ensuring the local machine can run tests. |
67
+ | \`npx arcality run\` | **Mission Runner (Agent Mode)**<br/>Launches the QA Agent directly. | Skips the interactive menu, scans the _package.json_ ("Ghost Mode"), injects ephemeral _autoskills_, and unleashes the AI agent. Ideal for CI/CD. |
68
+ | \`npx arcality --logs\` | **History Viewer**<br/>Displays test performance and tokens spent. | Reads the local \`arcality-history.log\` and renders a colorful table listing each mission, token consumption, and success status. |
69
+
70
+ ---
71
+
58
72
  ## 🛠️ Internal Architecture
59
73
 
60
74
  Arcality operates on a highly decoupled modular system designed for maximum resilience:
package/bin/arcality.mjs CHANGED
@@ -14,15 +14,24 @@ const rawArgs = process.argv.slice(2);
14
14
  let isRun = false;
15
15
  let isInit = false;
16
16
  let isSetup = false;
17
+ let isLogs = false;
17
18
 
18
19
  // Buscamos los comandos en cualquier posición para evitar filtros de NPM en Windows
19
20
  rawArgs.forEach(arg => {
20
21
  if (arg === 'run') isRun = true;
21
22
  if (arg === 'init') isInit = true;
22
23
  if (arg === 'setup') isSetup = true;
24
+ if (arg === '--logs') isLogs = true;
23
25
  });
24
26
 
25
- if (isInit) {
27
+ if (isLogs) {
28
+ const logsScript = path.join(PACKAGE_ROOT, 'scripts', 'arcality-logs.mjs');
29
+ spawn('node', [logsScript], {
30
+ stdio: 'inherit',
31
+ cwd: process.cwd(),
32
+ env: { ...process.env, ARCALITY_ROOT: PACKAGE_ROOT }
33
+ }).on('exit', code => process.exit(code || 0));
34
+ } else if (isInit) {
26
35
  const initScript = path.join(PACKAGE_ROOT, 'scripts', 'init.mjs');
27
36
  spawn('node', [initScript, ...rawArgs.filter(a => a !== 'init')], {
28
37
  stdio: 'inherit',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.32",
3
+ "version": "2.4.34",
4
4
  "description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -7,6 +7,7 @@ module.exports = defineConfig({
7
7
  testDir: path.join(__dirname, 'tests', '_helpers'),
8
8
  testMatch: ['agentic-runner.bundle.spec.js'],
9
9
  reporter: [
10
+ ['line'],
10
11
  [path.join(__dirname, 'tests', '_helpers', 'ArcalityReporter.js'),
11
12
  { outputDir: process.env.REPORTS_DIR || path.join(__dirname, 'tests-report') }]
12
13
  ],
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
10
+
11
+ const contextDir = path.join(process.cwd(), "out");
12
+ const centralLog = path.join(contextDir, "arcality-history.log");
13
+
14
+ console.log(chalk.cyan(`\n🔍 Arcality Logs Viewer`));
15
+ console.log(chalk.gray(`=========================\n`));
16
+
17
+ if (!fs.existsSync(centralLog)) {
18
+ console.log(chalk.yellow(`No se encontró un historial de logs en este proyecto.`));
19
+ console.log(chalk.gray(`(Directorio actual: ${contextDir})\n`));
20
+ console.log(`Ejecuta algunas misiones de prueba primero usando el comando arcality.\n`);
21
+ process.exit(0);
22
+ }
23
+
24
+ try {
25
+ const rawData = fs.readFileSync(centralLog, 'utf8');
26
+ const lines = rawData.split('\n').filter(l => l.trim().length > 0);
27
+
28
+ if (lines.length === 0) {
29
+ console.log(chalk.yellow(`El archivo de historial de misiones está vacío.\n`));
30
+ process.exit(0);
31
+ }
32
+
33
+ console.log(chalk.bold(`Registros encontrados: `) + chalk.cyan(lines.length));
34
+ console.log(chalk.gray(`Mostrando los últimos 20 resultados:`));
35
+ console.log();
36
+
37
+ const recentLines = lines.slice(-20);
38
+ let totalInput = 0;
39
+ let totalOutput = 0;
40
+
41
+ recentLines.forEach((line, index) => {
42
+ try {
43
+ const data = JSON.parse(line);
44
+
45
+ // Icono según resultado
46
+ const statusIcon = data.success ? chalk.green('✅') : chalk.red('❌');
47
+
48
+ // Fecha bonita
49
+ const date = new Date(data.ts).toLocaleString();
50
+
51
+ // Contabilizar tokens
52
+ const inT = data.usage?.input_tokens || 0;
53
+ const caT = data.usage?.cache_creation_input_tokens || 0;
54
+ const crT = data.usage?.cache_read_input_tokens || 0;
55
+ const totalIn = inT + caT + crT;
56
+ const totalOut = data.usage?.output_tokens || 0;
57
+
58
+ totalInput += totalIn;
59
+ totalOutput += totalOut;
60
+
61
+ const costStr = chalk.yellow(`[IN: ${totalIn} | OUT: ${totalOut}]`);
62
+ const targetStr = data.target ? chalk.gray(` @ ${data.target}`) : '';
63
+
64
+ console.log(`${statusIcon} ${chalk.blue(date)} ${costStr}`);
65
+ console.log(` ${chalk.white(data.mission)}${targetStr}`);
66
+ console.log(` ${chalk.gray(`Pasos dados: ${data.steps}`)}\n`);
67
+ } catch (e) {
68
+ console.log(chalk.red(`⚠️ Error parseando línea: ${line.substring(0, 50)}...\n`));
69
+ }
70
+ });
71
+
72
+ console.log(chalk.gray(`--------------------------------------------------\n`));
73
+
74
+ } catch (err) {
75
+ console.error(chalk.red(`❌ Ocurrió un error al leer los logs:`), err.message);
76
+ }
@@ -0,0 +1,61 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ // Mapeo curado de tecnologías clave a sus skills de pruebas/qa/patrones de autoskills.sh
5
+ // Son recursos muy ligeros en formato Markdown
6
+ const SKILLS_MAP: Record<string, string> = {
7
+ 'next': 'https://skills.sh/vercel-labs/next-skills/next-best-practices/raw',
8
+ 'vue': 'https://skills.sh/antfu/skills/vue-best-practices/raw',
9
+ 'tailwindcss': 'https://skills.sh/giuseppe-trisciuoglio/developer-kit/tailwind-css-patterns/raw',
10
+ 'react': 'https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices/raw',
11
+ 'angular': 'https://skills.sh/angular/skills/angular-developer/raw',
12
+ 'nuxt': 'https://skills.sh/antfu/skills/nuxt/raw',
13
+ 'svelte': 'https://skills.sh/ejirocodes/agent-skills/svelte5-best-practices/raw',
14
+ 'astro': 'https://skills.sh/astrolicious/agent-skills/astro/raw',
15
+ 'prisma': 'https://skills.sh/prisma/skills/prisma-postgres/raw'
16
+ };
17
+
18
+ export async function detectAndFetchEphemeralSkills(projectRoot: string): Promise<string> {
19
+ const pkgPath = path.join(projectRoot, 'package.json');
20
+ if (!fs.existsSync(pkgPath)) return '';
21
+
22
+ try {
23
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
24
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
25
+
26
+ const matchedUrls: string[] = [];
27
+ const matchedTechs: string[] = [];
28
+
29
+ for (const [lib, url] of Object.entries(SKILLS_MAP)) {
30
+ // Buscamos coincidencia parcial (ej. si usan @angular/core, next, react-dom)
31
+ const hasLib = Object.keys(deps).some(dep => dep === lib || dep.includes(`/${lib}`));
32
+ if (hasLib) {
33
+ matchedUrls.push(url);
34
+ matchedTechs.push(lib);
35
+ }
36
+ }
37
+
38
+ if (matchedUrls.length === 0) return '';
39
+
40
+ console.log(`>>ARCALITY_STATUS>> ⚡ Stack detectado (${matchedTechs.join(', ')}). QA Skills inyectadas en memoria.`);
41
+
42
+ // Fetch strings in parallel with simple timeout
43
+ const skillsPromises = matchedUrls.map(url => {
44
+ const controller = new AbortController();
45
+ const id = setTimeout(() => controller.abort(), 2000); // Max 2s per request
46
+ return fetch(url, { signal: controller.signal })
47
+ .then(r => r.ok ? r.text() : '')
48
+ .catch(() => '')
49
+ .finally(() => clearTimeout(id));
50
+ });
51
+
52
+ const skillsTexts = await Promise.all(skillsPromises);
53
+
54
+ const validTexts = skillsTexts.filter(Boolean);
55
+ if (validTexts.length === 0) return '';
56
+
57
+ return `\n<TECH_STACK_CONTEXT>\nEl proyecto objetivo utiliza las siguientes tecnologías. Usa estas directrices técnicas para entender el DOM, rutas y posibles bugs visuales:\n\n${validTexts.join('\n\n---\n\n')}\n</TECH_STACK_CONTEXT>\n`;
58
+ } catch {
59
+ return ''; // Fails silently
60
+ }
61
+ }
@@ -1375,6 +1375,14 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1375
1375
  } catch (e) {
1376
1376
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
1377
1377
  }
1378
+ let autoskillsContext = "";
1379
+ try {
1380
+ const path3 = require("path");
1381
+ const toolsRoot = process.env.ARCALITY_ROOT || path3.join(__dirname, "..", "..");
1382
+ const { detectAndFetchEphemeralSkills } = require(path3.join(toolsRoot, "src", "services", "autoskillsService"));
1383
+ autoskillsContext = await detectAndFetchEphemeralSkills(process.cwd());
1384
+ } catch (e) {
1385
+ }
1378
1386
  const systemPromptBlocks = [
1379
1387
  {
1380
1388
  type: "text",
@@ -1386,6 +1394,7 @@ ${prompt}
1386
1394
 
1387
1395
  ${projectInfo}
1388
1396
  ${skillsContext}
1397
+ ${autoskillsContext}
1389
1398
  ${memoryContext}
1390
1399
  ${credentialsContext}
1391
1400
  ${memoryBackendContext}`
@@ -2476,23 +2485,28 @@ function captureValidationRule(errorMessage, context) {
2476
2485
  }
2477
2486
  }
2478
2487
  }
2488
+ const effectiveLocalSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
2479
2489
  const missionData = {
2480
2490
  prompt,
2481
2491
  steps: history,
2482
- steps_data: stepsData,
2492
+ steps_data: effectiveLocalSteps,
2483
2493
  success: finalSuccess,
2484
2494
  error: hasCriticalError,
2485
2495
  timestamp: Date.now()
2486
2496
  };
2487
2497
  memories.push(missionData);
2488
2498
  fs2.writeFileSync(memoryFile, JSON.stringify(memories, null, 2));
2489
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${stepsData.length} pasos nuevos`);
2499
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${effectiveLocalSteps.length} pasos nuevos`);
2490
2500
  } catch (err) {
2491
2501
  console.error("\u274C Fall\xF3 al guardar memoria:", err);
2492
2502
  }
2493
2503
  try {
2494
2504
  const logPath = path2.join(contextDir, `agent-log-${Date.now()}.json`);
2495
- fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, usage: totalMissionUsage }, null, 2));
2505
+ fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2506
+ try {
2507
+ fs2.appendFileSync(path2.join(contextDir, "arcality-history.log"), JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), mission: prompt.substring(0, 150), success: finalSuccess, steps: stepCount, usage: totalMissionUsage }) + "\n");
2508
+ } catch (e) {
2509
+ }
2496
2510
  } catch (err) {
2497
2511
  }
2498
2512
  };
@@ -2513,7 +2527,12 @@ function captureValidationRule(errorMessage, context) {
2513
2527
  console.warn(`[CollectiveMemory] \u26A0\uFE0F stepsData vac\xEDo en path de \xE9xito \u2014 intentando usar backup...`);
2514
2528
  }
2515
2529
  const effectiveSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
2516
- console.log(`\u{1F9E0} [COLLECTIVE MEMORY] Persistiendo conocimiento (project: ${pid}, steps: ${effectiveSteps.length}, isFailed: ${isFailed})...`);
2530
+ console.log(`
2531
+ ======================================================`);
2532
+ console.log(`\u{1F9E0} [MEMORIA COLECTIVA - INGESTA DE APRENDIZAJE]`);
2533
+ console.log(`>>arcality>> Persistiendo conocimiento (project: ${pid}, steps: ${effectiveSteps.length}, isFailed: ${isFailed})...`);
2534
+ console.log(`======================================================
2535
+ `);
2517
2536
  try {
2518
2537
  const stepsNarrative = effectiveSteps.map((s) => `${s.action_data?.action || "action"} "${s.componentName}" en ${s.url}`).join(" \u2192 ").substring(0, 500);
2519
2538
  if (!isFailed) {
@@ -2703,7 +2722,12 @@ function captureValidationRule(errorMessage, context) {
2703
2722
  response = null;
2704
2723
  } else {
2705
2724
  guideStepCount++;
2706
- console.log(`>>ARCALITY_STATUS>> \u2728 Gu\xEDa de \xE9xito: Paso validado (Gu\xEDa #${guideStepCount}, NO consume turno IA)`);
2725
+ console.log(`
2726
+ ======================================================`);
2727
+ console.log(`\u{1F680} [USANDO GU\xCDA DE \xC9XITO - MEMORIA COLECTIVA AHORRANDO TOKENS]`);
2728
+ console.log(`>>arcality>> Paso validado (Gu\xEDa #${guideStepCount}, NO consume turno IA)`);
2729
+ console.log(`======================================================
2730
+ `);
2707
2731
  }
2708
2732
  }
2709
2733
  if (!response) {
@@ -2733,7 +2757,12 @@ function captureValidationRule(errorMessage, context) {
2733
2757
  }
2734
2758
  }
2735
2759
  }
2736
- console.log(`\u{1F9E0} Pensamiento: ${response.thought}`);
2760
+ console.log(`
2761
+ ======================================================`);
2762
+ console.log(`\u{1F916} [RAZONAMIENTO DEL AGENTE]`);
2763
+ console.log(`${response.thought}`);
2764
+ console.log(`======================================================
2765
+ `);
2737
2766
  if (!response.finish && history.length >= 6) {
2738
2767
  const lastSixActions = history.slice(-6);
2739
2768
  const clickActions = lastSixActions.filter((h) => h.includes("click en"));