@arcadialdev/arcality 2.4.37 → 2.4.49

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.37",
4
- "description": "AI-powered QA testing tool Autonomous web testing agent by Arcadial",
3
+ "version": "2.4.49",
4
+ "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "dev": "node scripts/gen-and-run.mjs",
@@ -51,9 +51,11 @@
51
51
  "@types/figlet": "^1.7.0",
52
52
  "@types/node": "^25.0.9",
53
53
  "@types/prompts": "^2.4.9",
54
- "esbuild": "^0.20.2"
54
+ "esbuild": "^0.20.2",
55
+ "typescript": "^6.0.3"
55
56
  },
56
57
  "dependencies": {
58
+ "@azure/storage-blob": "^12.31.0",
57
59
  "@clack/prompts": "^1.0.1",
58
60
  "@inquirer/prompts": "^8.2.0",
59
61
  "@playwright/test": "^1.57.0",
@@ -62,9 +64,10 @@
62
64
  "dotenv": "^17.2.3",
63
65
  "figlet": "^1.9.4",
64
66
  "js-yaml": "^4.1.1",
67
+ "mime-types": "^3.0.2",
65
68
  "node-fetch": "^3.3.2",
66
69
  "prompts": "^2.4.2",
67
70
  "terminal-image": "^1.1.0",
68
71
  "tsx": "^4.21.0"
69
72
  }
70
- }
73
+ }
@@ -34,7 +34,7 @@ const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
34
34
  const ORIGINAL_CWD = process.cwd();
35
35
 
36
36
  // ── Load arcality.config if present ──
37
- const projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
37
+ let projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
38
38
  if (projectConfig) {
39
39
  injectConfigToEnv(projectConfig);
40
40
  }
@@ -324,6 +324,9 @@ function run(cmd, args, options = {}) {
324
324
  env,
325
325
  });
326
326
 
327
+ // Exponer el proceso hijo para que el cancelHandler pueda matarlo
328
+ if (options.onSpawn) options.onSpawn(p);
329
+
327
330
  let outputBuffer = '';
328
331
  const handleData = (data) => {
329
332
  const str = data.toString();
@@ -467,7 +470,6 @@ async function main() {
467
470
  ...(savedMissions.length ? [{ label: '🚀 Launch Saved Mission (.yaml)', value: 'launch_saved' }] : []),
468
471
  ...(yamlOutputFiles.length ? [{ label: '♻️ Reuse YAML + Auto-Guide (if prev. run exists)', value: 'reuse_yaml' }] : []),
469
472
  ...(rootYamlFiles.length ? [{ label: '📂 Choose Root YAML (Templates)', value: 'yaml_list' }] : []),
470
- { label: '📋 View Current Configuration', value: 'view_config' },
471
473
  { label: '🔄 Reconfigure (arcality init)', value: 'reconfigure' },
472
474
  { label: '❌ Exit', value: 'exit' }
473
475
  ];
@@ -484,28 +486,6 @@ async function main() {
484
486
 
485
487
  if (action === 'agent') {
486
488
  agentMode = true;
487
- } else if (action === 'view_config') {
488
- if (projectConfig) {
489
- note(
490
- chalk.bold.white('📋 Project: ') + chalk.cyan(projectConfig.project?.name) + '\n' +
491
- chalk.bold.white('🌐 Base URL: ') + chalk.cyan(projectConfig.project?.baseUrl) + '\n' +
492
- chalk.bold.white('🛠️ Framework: ') + chalk.magenta(projectConfig.project?.frameworkDetected || 'N/A') + '\n' +
493
- chalk.bold.white('🆔 Project ID: ') + chalk.gray(projectConfig.projectId) + '\n' +
494
- chalk.bold.white('🏢 Org ID: ') + chalk.gray(projectConfig.organizationId || 'N/A') + '\n' +
495
- chalk.bold.white('👤 User: ') + chalk.cyan(projectConfig.auth?.username) + '\n' +
496
- chalk.bold.white('📂 YAML Dir: ') + chalk.yellow(projectConfig.runtime?.yamlOutputDir) + '\n' +
497
- chalk.bold.white('♻️ Reuse YAMLs: ') + chalk.cyan(projectConfig.runtime?.reuseSuccessfulYamls ? '✅' : '❌') + '\n' +
498
- chalk.bold.white('📦 Version: ') + chalk.yellow(projectConfig.meta?.arcalityVersion || 'N/A'),
499
- 'arcality.config'
500
- );
501
- } else {
502
- note(
503
- chalk.yellow('No arcality.config found.') + '\n' +
504
- chalk.gray('Run `arcality init` to configure this project.'),
505
- 'No Configuration'
506
- );
507
- }
508
- continue;
509
489
  } else if (action === 'reconfigure') {
510
490
  // Launch arcality init in the user's project directory
511
491
  const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
@@ -519,7 +499,20 @@ async function main() {
519
499
  child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
520
500
  child.on('error', reject);
521
501
  });
522
- } catch { /* user cancelled or error */ }
502
+
503
+ // ── REFRESH CONFIG ──
504
+ // Recargar el archivo que init.mjs acaba de escribir
505
+ projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
506
+ if (projectConfig) {
507
+ injectConfigToEnv(projectConfig);
508
+ }
509
+ setupEnvironment();
510
+
511
+ } catch (e) {
512
+ // Mostrar el error claramente antes de que console.clear() lo borre
513
+ console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
514
+ await new Promise(r => setTimeout(r, 3000));
515
+ }
523
516
  continue;
524
517
  } else if (action === 'launch_saved') {
525
518
  const sel = await select({
@@ -620,6 +613,14 @@ async function main() {
620
613
  const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
621
614
 
622
615
  const validation = await validateApiKey();
616
+ if (process.env.DEBUG || process.argv.includes('--debug')) {
617
+ console.log(chalk.gray(`[DEBUG] Validation result: ${JSON.stringify(validation, null, 2)}`));
618
+ }
619
+
620
+ const detectedOrgId = validation.organization_id || validation.organizationId || validation.OrganizationId;
621
+ if (detectedOrgId) {
622
+ process.env.ARCALITY_ORG_ID = detectedOrgId;
623
+ }
623
624
  if (!validation.valid) {
624
625
  const errorMessages = {
625
626
  'no_api_key': '🔑 API Key not found.\n Configure it via `arcality init` or in .env (ARCALITY_API_KEY)',
@@ -636,7 +637,7 @@ async function main() {
636
637
 
637
638
  // Show plan info
638
639
  const modeLabel = validation.mode === 'mock' ? chalk.gray('(local)') : chalk.green('(live)');
639
- const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 50);
640
+ const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
640
641
  const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
641
642
  note(
642
643
  chalk.bold.white('✅ Valid API Key ') + modeLabel + '\n' +
@@ -653,9 +654,13 @@ async function main() {
653
654
  if (!selectedProjectId) {
654
655
  // If no project ID from config, try to create/select from backend
655
656
  try {
657
+ const projController = new AbortController();
658
+ const projTimeout = setTimeout(() => projController.abort(), 5000);
656
659
  const projRes = await fetch(`${apiBase}/api/v1/projects`, {
657
- headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' }
660
+ headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' },
661
+ signal: projController.signal
658
662
  });
663
+ clearTimeout(projTimeout);
659
664
 
660
665
  if (projRes.ok) {
661
666
  const data = await projRes.json();
@@ -735,6 +740,63 @@ async function main() {
735
740
 
736
741
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
737
742
 
743
+ // ── SIGINT Handler: Cancelación manual (Ctrl+C / ESC) ──
744
+ // Se registra AQUÍ, justo antes del spawn, para que solo aplique durante la ejecución.
745
+ // Al cancelar: mata el proceso hijo, guarda estado "canceled", llama endMission y genera reporte.
746
+ let activeChildProcess = null; // Referencia al proceso hijo de Playwright
747
+ let userCanceledMission = false;
748
+ const cancelHandler = async () => {
749
+ if (userCanceledMission) return; // Evitar doble disparo
750
+ userCanceledMission = true;
751
+
752
+ console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Misión cancelada por el usuario.'));
753
+ if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.'));
754
+
755
+ // 1. Escribir un agent-log de cancelación para que el reporte lo refleje
756
+ try {
757
+ const ctxDir = process.env.CONTEXT_DIR;
758
+ if (ctxDir && fs.existsSync(ctxDir)) {
759
+ const cancelLog = {
760
+ prompt,
761
+ history: ['[CANCELADO POR USUARIO] La prueba fue detenida manualmente antes de completarse.'],
762
+ steps: 0,
763
+ success: false,
764
+ error: true,
765
+ fail_reason: 'user_canceled',
766
+ canceled_by_user: true,
767
+ timestamp: new Date().toISOString()
768
+ };
769
+ fs.writeFileSync(path.join(ctxDir, `agent-log-${Date.now()}.json`), JSON.stringify(cancelLog, null, 2));
770
+ }
771
+ } catch { /* silencioso */ }
772
+
773
+ // 2. Llamar endMission con estado 'canceled'
774
+ try {
775
+ const { endMission: endM } = await import('../src/arcalityClient.mjs');
776
+ await endM(mission.mission_id, 'canceled', null, 'user_canceled');
777
+ console.log(chalk.gray(' >> Misión registrada como "cancelada" en el servidor.'));
778
+ } catch { /* silencioso si el backend no está disponible */ }
779
+
780
+ // 3. Matar el proceso hijo de Playwright — hacerlo AL FINAL para dar tiempo
781
+ // a que el agente-log y endMission terminen antes del kill.
782
+ // NO llamar process.exit() aquí: el finally del try/catch principal
783
+ // se encarga de generar el reporte visual y abrirlo en el browser.
784
+ if (activeChildProcess) {
785
+ try {
786
+ if (process.platform === 'win32') {
787
+ spawn('taskkill', ['/pid', activeChildProcess.pid, '/f', '/t'], { stdio: 'ignore', windowsHide: true });
788
+ } else {
789
+ activeChildProcess.kill('SIGTERM');
790
+ }
791
+ } catch { /* silencioso */ }
792
+ }
793
+ // El bloque finally del try/catch de run() tomará el control
794
+ // a partir de aquí (el proceso hijo ya terminó → run() rechaza → catch → finally).
795
+ };
796
+
797
+ process.once('SIGINT', cancelHandler);
798
+ process.once('SIGTERM', cancelHandler);
799
+
738
800
  // Build env for the runner — merge arcality.config values.
739
801
  // ARCALITY_API_URL must be explicit — it comes from the library's internal config,
740
802
  // not from the user's project .env, so we inject it directly via getApiUrl().
@@ -745,7 +807,8 @@ async function main() {
745
807
  ...process.env,
746
808
  ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
747
809
  ARCALITY_PROJECT_ID: finalProjectId,
748
- ARCALITY_MISSION_ID: mission.mission_id || '', // ← Propagate mission_id to every proxy call
810
+ ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
811
+ ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
749
812
  BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
750
813
  LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
751
814
  LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
@@ -758,7 +821,8 @@ async function main() {
758
821
  };
759
822
 
760
823
  if (finalProjectId) {
761
- console.log(chalk.gray(`\n>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
824
+ console.log(chalk.gray(`>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
825
+ console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
762
826
  }
763
827
 
764
828
  // ── Resolve execution paths ──
@@ -798,16 +862,20 @@ async function main() {
798
862
  '--headed',
799
863
  '--config', 'playwright.config.js'
800
864
  ], {
801
- cwd: PROJECT_ROOT, // Run from the arcality package root
865
+ cwd: PROJECT_ROOT,
802
866
  env: mergedEnv,
803
- onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); }
867
+ onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
868
+ onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
804
869
  });
870
+
871
+ // Eliminar handlers SIGINT ahora que el proceso terminó normalmente
872
+ process.off('SIGINT', cancelHandler);
873
+ process.off('SIGTERM', cancelHandler);
874
+
875
+ if (userCanceledMission) return; // El handler ya tomó el control
805
876
  if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
806
877
  else console.log(chalk.green('✅ Mission completed successfully.'));
807
878
 
808
- // ── End Mission ──
809
- const { endMission } = await import('../src/arcalityClient.mjs');
810
-
811
879
  let finalUsage = undefined;
812
880
  try {
813
881
  const ctxDir = process.env.CONTEXT_DIR;
@@ -822,6 +890,27 @@ async function main() {
822
890
  }
823
891
  } catch(e) {}
824
892
 
893
+ // ── Upload Evidence ──
894
+ const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
895
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
896
+
897
+ try {
898
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
899
+ if (sasData && sasData.sasUrl) {
900
+ if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
901
+ const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
902
+
903
+ await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
904
+
905
+ await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
906
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
907
+ }
908
+ } catch (e) {
909
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
910
+ if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Evidence upload failed.'));
911
+ }
912
+
913
+ // ── End Mission ──
825
914
  await endMission(mission.mission_id, 'success', finalUsage);
826
915
 
827
916
  // ── Ping Project ──
@@ -873,31 +962,66 @@ async function main() {
873
962
  }
874
963
  }
875
964
  } catch (e) {
876
- s.stop(chalk.red(`❌ Mission could not be completed.`));
877
- console.error(chalk.red(`\nError Details: ${e.message}`));
878
- if (e.stack && process.argv.includes('--debug')) {
879
- console.error(chalk.gray(e.stack));
880
- }
965
+ // Eliminar handlers SIGINT en el path de error también
966
+ process.off('SIGINT', cancelHandler);
967
+ process.off('SIGTERM', cancelHandler);
968
+
969
+ if (userCanceledMission) {
970
+ // El cancelHandler ya registró la cancelación en el servidor.
971
+ // Solo mostramos un mensaje y dejamos que el finally genere el reporte.
972
+ console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
973
+ } else {
974
+ s.stop(chalk.red(`❌ Mission could not be completed.`));
975
+ console.error(chalk.red(`\nError Details: ${e.message}`));
976
+ if (e.stack && process.argv.includes('--debug')) {
977
+ console.error(chalk.gray(e.stack));
978
+ }
881
979
 
882
- // End mission with failure
883
- try {
884
- const { endMission } = await import('../src/arcalityClient.mjs');
885
- let finalUsage = undefined;
980
+ // Si NO fue cancelación de usuario, cerrar la misión como fallida en el servidor
886
981
  try {
887
- const ctxDir = process.env.CONTEXT_DIR;
888
- if (fs.existsSync(ctxDir)) {
889
- const files = fs.readdirSync(ctxDir);
890
- const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
891
- if (logFiles.length > 0) {
892
- const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
893
- const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
894
- finalUsage = logData.usage;
982
+ let finalUsage = undefined;
983
+ let failReason = "timeout_or_crash";
984
+ try {
985
+ const ctxDir = process.env.CONTEXT_DIR;
986
+ if (fs.existsSync(ctxDir)) {
987
+ const files = fs.readdirSync(ctxDir);
988
+ const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
989
+ if (logFiles.length > 0) {
990
+ const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
991
+ const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
992
+ finalUsage = logData.usage;
993
+ failReason = logData.fail_reason || failReason;
994
+ }
895
995
  }
896
- }
897
- } catch(e) {}
898
- await endMission(mission.mission_id, 'failed', finalUsage);
899
- } catch { }
996
+ } catch(e) {}
997
+
998
+ // ── Upload Evidence ──
999
+ const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
1000
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
1001
+
1002
+ try {
1003
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
1004
+ if (sasData && sasData.sasUrl) {
1005
+ if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
1006
+ const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
1007
+
1008
+ await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
1009
+
1010
+ await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
1011
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
1012
+ }
1013
+ } catch (e) {}
1014
+
1015
+ await endMission(mission.mission_id, 'failed', finalUsage, failReason);
1016
+ } catch { }
1017
+ }
900
1018
  } finally {
1019
+ // Si fue cancelación de usuario, dar un breve margen para que el cancelHandler
1020
+ // async (endMission, escritura de log) termine antes de generar el reporte.
1021
+ if (userCanceledMission) {
1022
+ await new Promise(r => setTimeout(r, 800));
1023
+ }
1024
+
901
1025
  const sRep = spinner();
902
1026
  sRep.start('📊 Generating report...');
903
1027
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
package/scripts/init.mjs CHANGED
@@ -143,8 +143,12 @@ async function main() {
143
143
  401: '❌ Invalid API Key. Please check your key and try again.',
144
144
  403: `❌ Authentication Failed (HTTP 403). Server says: ${errText.slice(0, 150)}`,
145
145
  };
146
- s.stop(chalk.red(errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`));
147
- process.exit(1);
146
+ const msg = errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`;
147
+ s.stop(chalk.red(msg));
148
+ console.log(chalk.yellow('\n Press Enter to return to menu...'));
149
+ // Salida limpia (código 0) para que el menú padre no interprete esto como crash
150
+ await new Promise(r => setTimeout(r, 3000));
151
+ process.exit(0);
148
152
  }
149
153
 
150
154
  const data = await res.json();
@@ -162,7 +166,8 @@ async function main() {
162
166
  } catch (err) {
163
167
  s.stop(chalk.red(`❌ Could not connect to Arcality backend: ${err.message}`));
164
168
  console.log(chalk.yellow(' Make sure the backend is running and accessible.'));
165
- process.exit(1);
169
+ await new Promise(r => setTimeout(r, 3000));
170
+ process.exit(0);
166
171
  }
167
172
 
168
173
  // ── Step 3: Project details ──
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ContainerClient } from '@azure/storage-blob';
4
+ import mime from 'mime-types';
5
+
6
+ /**
7
+ * Subes la evidencia de prueba iterativamente a Azure Blob Storage usando una URL de SAS.
8
+ * @param {string} reportsDir Directorio local de reportes.
9
+ * @param {string} sasUrl URL de SAS para el ContainerClient.
10
+ * @param {string} basePath Path base dentro del contenedor de Azure.
11
+ */
12
+ export async function uploadEvidence(reportsDir, sasUrl, basePath) {
13
+ if (!fs.existsSync(reportsDir)) return;
14
+
15
+ const containerClient = new ContainerClient(sasUrl);
16
+
17
+ async function walkAndUpload(currentDir, relativePath = '') {
18
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
19
+ const uploadPromises = [];
20
+
21
+ for (const entry of entries) {
22
+ const fullPath = path.join(currentDir, entry.name);
23
+ const entryRelativePath = path.posix.join(relativePath, entry.name);
24
+
25
+ if (entry.isDirectory()) {
26
+ uploadPromises.push(walkAndUpload(fullPath, entryRelativePath));
27
+ } else {
28
+ // Blob path en Azure: basePath/ruta_relativa_del_archivo
29
+ const blobPath = `${basePath}/${entryRelativePath}`;
30
+ const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
31
+
32
+ const contentType = mime.lookup(entry.name) || 'application/octet-stream';
33
+
34
+ uploadPromises.push(
35
+ blockBlobClient.uploadFile(fullPath, {
36
+ blobHTTPHeaders: { blobContentType: contentType }
37
+ })
38
+ );
39
+ }
40
+ }
41
+ await Promise.all(uploadPromises);
42
+ }
43
+
44
+ await walkAndUpload(reportsDir);
45
+ }
@@ -41,12 +41,14 @@ export class KnowledgeService {
41
41
  private apiBase: string;
42
42
  private apiKey: string;
43
43
  private projectId: string | null = null;
44
+ private orgId: string | null = null;
44
45
 
45
46
  private constructor() {
46
47
  // API URL is internal-only, loaded from the tool's .env via dotenv
47
48
  this.apiBase = process.env.ARCALITY_API_URL || 'https://arcalityqadev.arcadial.lat';
48
49
  this.apiKey = process.env.ARCALITY_API_KEY || '';
49
50
  this.projectId = process.env.ARCALITY_PROJECT_ID || null;
51
+ this.orgId = process.env.ARCALITY_ORG_ID || null;
50
52
  }
51
53
 
52
54
  /**
@@ -140,7 +142,7 @@ export class KnowledgeService {
140
142
  if (res.status === 200) {
141
143
  const data = await res.json();
142
144
  if (data.status === 'skipped_duplicate_dom') {
143
- // console.log(chalk.gray(`[Knowledge] Ingesta omitida (Duplicado).`));
145
+ // console.log(chalk.gray(`[Knowledge] Ingesta omitida (Duplicado).`));
144
146
  } else {
145
147
  console.log(chalk.cyan(`[Knowledge] Ingesta exitosa (Project: ${payload.project_id})`));
146
148
  }
@@ -158,10 +160,13 @@ export class KnowledgeService {
158
160
  if (!pid || pid === 'undefined') return null;
159
161
  try {
160
162
  const pathUrl = new URL(url).pathname;
161
- const res = await this.fetchWithTimeout(
162
- `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`,
163
- { headers: { 'x-api-key': this.apiKey } }
164
- );
163
+ let contextUrl = `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`;
164
+ if (this.orgId) {
165
+ contextUrl += `&organization_id=${this.orgId}`;
166
+ }
167
+ const res = await this.fetchWithTimeout(contextUrl, {
168
+ headers: { 'x-api-key': this.apiKey }
169
+ });
165
170
  if (!res.ok) return null;
166
171
  return await res.json();
167
172
  } catch (e: any) {
@@ -185,6 +190,7 @@ export class KnowledgeService {
185
190
  'x-api-key': this.apiKey
186
191
  },
187
192
  body: JSON.stringify({
193
+ organization_id: this.orgId,
188
194
  project_id: pid,
189
195
  rule_type: "UI_UX", // Default según especificación
190
196
  title,
@@ -210,6 +216,7 @@ export class KnowledgeService {
210
216
  'x-api-key': this.apiKey
211
217
  },
212
218
  body: JSON.stringify({
219
+ organization_id: this.orgId,
213
220
  project_id: pid,
214
221
  field_identifier: identifier,
215
222
  field_type: type,
@@ -235,6 +242,7 @@ export class KnowledgeService {
235
242
  'x-api-key': this.apiKey
236
243
  },
237
244
  body: JSON.stringify({
245
+ organization_id: this.orgId,
238
246
  project_id: pid,
239
247
  doc_type: docType,
240
248
  title,
@@ -7,7 +7,7 @@ import path from 'node:path';
7
7
  import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
8
8
 
9
9
  // ── Backend config (internal, not user-configurable) ──
10
- const DAILY_MISSION_LIMIT = 50;
10
+ const DAILY_MISSION_LIMIT = 35;
11
11
  const USAGE_FILE = path.join(CONFIG_DIR, 'usage.json');
12
12
 
13
13
  // ═══════════════════════════════════════════════════════
@@ -128,22 +128,31 @@ export async function validateApiKey() {
128
128
  // ── LIVE mode: Backend available ──
129
129
  if (apiBase) {
130
130
  try {
131
+ const controller = new AbortController();
132
+ const timeout = setTimeout(() => controller.abort(), 5000);
131
133
  const res = await fetch(`${apiBase}/api/v1/auth/validate`, {
132
134
  method: 'POST',
133
135
  headers: {
134
136
  'Content-Type': 'application/json',
135
137
  'x-api-key': key
136
- }
138
+ },
139
+ signal: controller.signal
137
140
  });
141
+ clearTimeout(timeout);
138
142
 
139
143
  if (res.status === 401) return { valid: false, error: 'invalid_api_key', mode: 'live' };
140
144
  if (res.status === 403) return { valid: false, error: 'plan_expired', mode: 'live' };
141
145
  if (!res.ok) return { valid: false, error: 'server_error', mode: 'live' };
142
146
 
143
147
  const data = await res.json();
148
+ if (process.env.DEBUG || process.argv.includes('--debug')) {
149
+ console.log(`[DEBUG] Auth validation response keys:`, Object.keys(data));
150
+ console.log(`[DEBUG] Auth validation response:`, JSON.stringify(data, null, 2));
151
+ }
144
152
  return { ...data, valid: true, mode: 'live' };
145
- } catch {
146
- console.warn('⚠️ Backend not available, using local mode');
153
+ } catch (e) {
154
+ const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
155
+ console.warn(`⚠️ Backend not available (${apiBase}): ${reason}. Using local mode.`);
147
156
  }
148
157
  }
149
158
 
@@ -175,6 +184,8 @@ export async function startMission(prompt, targetUrl) {
175
184
  try {
176
185
  const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
177
186
 
187
+ const controller = new AbortController();
188
+ const timeout = setTimeout(() => controller.abort(), 5000);
178
189
  const res = await fetch(`${apiBase}/api/v1/missions/start`, {
179
190
  method: 'POST',
180
191
  headers: {
@@ -186,8 +197,10 @@ export async function startMission(prompt, targetUrl) {
186
197
  prompt_hash: simpleHash(prompt),
187
198
  target_url: targetUrl,
188
199
  project_id: projectId || undefined,
189
- })
200
+ }),
201
+ signal: controller.signal
190
202
  });
203
+ clearTimeout(timeout);
191
204
 
192
205
  if (res.status === 429) {
193
206
  const data = await res.json();
@@ -196,8 +209,9 @@ export async function startMission(prompt, targetUrl) {
196
209
  if (!res.ok) return { allowed: false, error: 'server_error' };
197
210
 
198
211
  return { allowed: true, ...(await res.json()) };
199
- } catch {
200
- // Fallback to mock if backend is down
212
+ } catch (e) {
213
+ const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
214
+ console.warn(`⚠️ startMission failed (${apiBase}): ${reason}. Falling back to mock mode.`);
201
215
  }
202
216
  }
203
217
 
@@ -236,7 +250,7 @@ export async function startMission(prompt, targetUrl) {
236
250
  * @param {'success'|'failed'|'cancelled'} result
237
251
  * @param {object} usage - Optional usage statistics directly from the AI
238
252
  */
239
- export async function endMission(missionId, result, usage = null) {
253
+ export async function endMission(missionId, result, usage = null, failReason = null) {
240
254
  const apiBase = getEffectiveApiBase();
241
255
  if (!apiBase || !missionId) return { ok: true };
242
256
 
@@ -248,7 +262,7 @@ export async function endMission(missionId, result, usage = null) {
248
262
  'Content-Type': 'application/json',
249
263
  'x-api-key': key
250
264
  },
251
- body: JSON.stringify({ result, usage })
265
+ body: JSON.stringify({ result, usage, failReason })
252
266
  });
253
267
  } catch { }
254
268
  return { ok: true };
@@ -266,3 +280,33 @@ function simpleHash(str) {
266
280
  }
267
281
  return 'ph_' + Math.abs(hash).toString(36);
268
282
  }
283
+
284
+ /**
285
+ * Obtiene el Token SAS y el path base para subir evidencias.
286
+ * @param {string} missionId
287
+ * @param {string} projectId
288
+ */
289
+ export async function getEvidenceSasToken(missionId, projectId) {
290
+ const apiBase = getEffectiveApiBase();
291
+ if (!apiBase || !missionId || !projectId) return null;
292
+
293
+ const key = getEffectiveApiKey();
294
+ if (!key) return null;
295
+
296
+ try {
297
+ const res = await fetch(`${apiBase}/api/v1/missions/${missionId}/evidence/sas`, {
298
+ method: 'POST',
299
+ headers: {
300
+ 'Content-Type': 'application/json',
301
+ 'Authorization': `Bearer ${key}`
302
+ },
303
+ body: JSON.stringify({ projectId })
304
+ });
305
+
306
+ if (!res.ok) return null;
307
+
308
+ return await res.json();
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
@@ -18,7 +18,8 @@ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
18
18
  * @returns {string}
19
19
  */
20
20
  export function getApiUrl() {
21
- return process.env.ARCALITY_API_URL || 'https://arcalityqadev.arcadial.lat';
21
+ // return 'https://arcalityqadev.arcadial.lat';
22
+ return 'https://arcalityqadev.arcadial.lat';
22
23
  }
23
24
 
24
25
  /**