@arcadialdev/arcality 2.4.37 → 2.4.48

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.48",
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",
@@ -54,6 +54,7 @@
54
54
  "esbuild": "^0.20.2"
55
55
  },
56
56
  "dependencies": {
57
+ "@azure/storage-blob": "^12.31.0",
57
58
  "@clack/prompts": "^1.0.1",
58
59
  "@inquirer/prompts": "^8.2.0",
59
60
  "@playwright/test": "^1.57.0",
@@ -62,9 +63,10 @@
62
63
  "dotenv": "^17.2.3",
63
64
  "figlet": "^1.9.4",
64
65
  "js-yaml": "^4.1.1",
66
+ "mime-types": "^3.0.2",
65
67
  "node-fetch": "^3.3.2",
66
68
  "prompts": "^2.4.2",
67
69
  "terminal-image": "^1.1.0",
68
70
  "tsx": "^4.21.0"
69
71
  }
70
- }
72
+ }
@@ -620,6 +620,14 @@ async function main() {
620
620
  const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
621
621
 
622
622
  const validation = await validateApiKey();
623
+ if (process.env.DEBUG || process.argv.includes('--debug')) {
624
+ console.log(chalk.gray(`[DEBUG] Validation result: ${JSON.stringify(validation, null, 2)}`));
625
+ }
626
+
627
+ const detectedOrgId = validation.organization_id || validation.organizationId || validation.OrganizationId;
628
+ if (detectedOrgId) {
629
+ process.env.ARCALITY_ORG_ID = detectedOrgId;
630
+ }
623
631
  if (!validation.valid) {
624
632
  const errorMessages = {
625
633
  'no_api_key': '🔑 API Key not found.\n Configure it via `arcality init` or in .env (ARCALITY_API_KEY)',
@@ -636,7 +644,7 @@ async function main() {
636
644
 
637
645
  // Show plan info
638
646
  const modeLabel = validation.mode === 'mock' ? chalk.gray('(local)') : chalk.green('(live)');
639
- const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 50);
647
+ const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
640
648
  const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
641
649
  note(
642
650
  chalk.bold.white('✅ Valid API Key ') + modeLabel + '\n' +
@@ -653,9 +661,13 @@ async function main() {
653
661
  if (!selectedProjectId) {
654
662
  // If no project ID from config, try to create/select from backend
655
663
  try {
664
+ const projController = new AbortController();
665
+ const projTimeout = setTimeout(() => projController.abort(), 5000);
656
666
  const projRes = await fetch(`${apiBase}/api/v1/projects`, {
657
- headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' }
667
+ headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' },
668
+ signal: projController.signal
658
669
  });
670
+ clearTimeout(projTimeout);
659
671
 
660
672
  if (projRes.ok) {
661
673
  const data = await projRes.json();
@@ -745,7 +757,8 @@ async function main() {
745
757
  ...process.env,
746
758
  ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
747
759
  ARCALITY_PROJECT_ID: finalProjectId,
748
- ARCALITY_MISSION_ID: mission.mission_id || '', // ← Propagate mission_id to every proxy call
760
+ ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
761
+ ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
749
762
  BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
750
763
  LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
751
764
  LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
@@ -758,7 +771,8 @@ async function main() {
758
771
  };
759
772
 
760
773
  if (finalProjectId) {
761
- console.log(chalk.gray(`\n>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
774
+ console.log(chalk.gray(`>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
775
+ console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
762
776
  }
763
777
 
764
778
  // ── Resolve execution paths ──
@@ -805,9 +819,6 @@ async function main() {
805
819
  if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
806
820
  else console.log(chalk.green('✅ Mission completed successfully.'));
807
821
 
808
- // ── End Mission ──
809
- const { endMission } = await import('../src/arcalityClient.mjs');
810
-
811
822
  let finalUsage = undefined;
812
823
  try {
813
824
  const ctxDir = process.env.CONTEXT_DIR;
@@ -822,6 +833,27 @@ async function main() {
822
833
  }
823
834
  } catch(e) {}
824
835
 
836
+ // ── Upload Evidence ──
837
+ const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
838
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
839
+
840
+ try {
841
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
842
+ if (sasData && sasData.sasUrl) {
843
+ if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
844
+ const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
845
+
846
+ await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
847
+
848
+ await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
849
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
850
+ }
851
+ } catch (e) {
852
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
853
+ if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Evidence upload failed.'));
854
+ }
855
+
856
+ // ── End Mission ──
825
857
  await endMission(mission.mission_id, 'success', finalUsage);
826
858
 
827
859
  // ── Ping Project ──
@@ -881,8 +913,8 @@ async function main() {
881
913
 
882
914
  // End mission with failure
883
915
  try {
884
- const { endMission } = await import('../src/arcalityClient.mjs');
885
916
  let finalUsage = undefined;
917
+ let failReason = "timeout_or_crash";
886
918
  try {
887
919
  const ctxDir = process.env.CONTEXT_DIR;
888
920
  if (fs.existsSync(ctxDir)) {
@@ -892,10 +924,29 @@ async function main() {
892
924
  const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
893
925
  const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
894
926
  finalUsage = logData.usage;
927
+ failReason = logData.fail_reason || failReason;
895
928
  }
896
929
  }
897
930
  } catch(e) {}
898
- await endMission(mission.mission_id, 'failed', finalUsage);
931
+
932
+ // ── Upload Evidence ──
933
+ const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
934
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
935
+
936
+ try {
937
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
938
+ if (sasData && sasData.sasUrl) {
939
+ if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
940
+ const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
941
+
942
+ await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
943
+
944
+ await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
945
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
946
+ }
947
+ } catch (e) {}
948
+
949
+ await endMission(mission.mission_id, 'failed', finalUsage, failReason);
899
950
  } catch { }
900
951
  } finally {
901
952
  const sRep = spinner();
@@ -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
  /**
package/src/index.ts CHANGED
@@ -2,13 +2,31 @@ import 'dotenv/config';
2
2
  import { showBanner } from './consoleBanner';
3
3
  import { promptAndRunPlaywrightTests } from './testRunner';
4
4
  import { KnowledgeService } from './KnowledgeService';
5
- import { ArcalityClient } from './arcalityClient';
5
+ import { ArcalityClient, validateApiKey } from './arcalityClient';
6
6
  import { loadConfig } from './configLoader';
7
+ import chalk from 'chalk';
7
8
 
8
9
  async function main() {
9
10
  showBanner();
10
11
 
11
12
  const config = loadConfig();
13
+
14
+ // Validar API Key y obtener contexto de organización
15
+ console.log(chalk.blue('Validating API Key...'));
16
+ const auth = await validateApiKey();
17
+ if (auth.valid) {
18
+ const orgId = auth.organization_id || auth.organizationId || auth.OrganizationId;
19
+ if (orgId) {
20
+ process.env.ARCALITY_ORG_ID = orgId;
21
+ console.log(chalk.green(`✅ Session initialized for organization: ${auth.organization_name || auth.organizationName || auth.OrganizationName || orgId}`));
22
+ } else {
23
+ console.warn(chalk.yellow('⚠️ API Key is valid but no organization_id was returned.'));
24
+ }
25
+ } else {
26
+ console.error(chalk.red(`❌ Invalid API Key: ${auth.error || 'Unknown error'}`));
27
+ process.exit(1);
28
+ }
29
+
12
30
  const arcalityClient = new ArcalityClient(config.ARCALITY_API_KEY);
13
31
  const knowledgeService = KnowledgeService.getInstance();
14
32
 
@@ -11,18 +11,32 @@
11
11
  */
12
12
 
13
13
  const getBase = () => process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1` : null;
14
- const getKey = () => process.env.ARCALITY_API_KEY || '';
15
- const getPid = () => process.env.ARCALITY_PROJECT_ID || '';
14
+ const getKey = () => process.env.ARCALITY_API_KEY || '';
15
+ const getPid = () => process.env.ARCALITY_PROJECT_ID || '';
16
+ const getOrgId = () => process.env.ARCALITY_ORG_ID;
17
+ const getMissionId = () => process.env.ARCALITY_MISSION_ID || EMPTY_GUID;
16
18
 
17
19
  const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
18
20
 
21
+ let configWarningLogged = false;
22
+
19
23
  function isConfigured(): boolean {
20
24
  const base = getBase();
21
- const key = getKey();
22
- const pid = getPid();
25
+ const key = getKey();
26
+ const pid = getPid();
27
+ const orgId = getOrgId();
23
28
 
24
- if (!base || !key || !pid || pid === EMPTY_GUID) {
25
- // Silencioso — no loguear en cada percepción para no hacer ruido
29
+ if (!base || !key || !pid || pid === EMPTY_GUID || !orgId) {
30
+ if (!configWarningLogged) {
31
+ const missing = [];
32
+ if (!base) missing.push('ARCALITY_API_URL');
33
+ if (!key) missing.push('ARCALITY_API_KEY');
34
+ if (!pid || pid === EMPTY_GUID) missing.push('ARCALITY_PROJECT_ID');
35
+ if (!orgId) missing.push('ARCALITY_ORG_ID');
36
+
37
+ console.warn(`[CollectiveMemory] Service disabled. Missing: ${missing.join(', ')}`);
38
+ configWarningLogged = true;
39
+ }
26
40
  return false;
27
41
  }
28
42
  return true;
@@ -48,6 +62,7 @@ export async function pushRule(rule: {
48
62
  'x-api-key': getKey()
49
63
  },
50
64
  body: JSON.stringify({
65
+ organization_id: getOrgId(),
51
66
  project_id: getPid(),
52
67
  rule_type: rule.rule_type,
53
68
  title: rule.title.substring(0, 100),
@@ -98,6 +113,7 @@ export async function pushField(field: {
98
113
  'x-api-key': getKey()
99
114
  },
100
115
  body: JSON.stringify({
116
+ organization_id: getOrgId(),
101
117
  project_id: getPid(),
102
118
  page_id: field.page_id ?? null,
103
119
  field_identifier: field.field_identifier.substring(0, 100),
@@ -138,6 +154,7 @@ export async function pushKnowledge(knowledge: {
138
154
  'x-api-key': getKey()
139
155
  },
140
156
  body: JSON.stringify({
157
+ organization_id: getOrgId(),
141
158
  project_id: getPid(),
142
159
  doc_type: knowledge.doc_type,
143
160
  title: knowledge.title.substring(0, 150),
@@ -160,13 +177,87 @@ export async function pushKnowledge(knowledge: {
160
177
  }
161
178
  }
162
179
 
180
+ // ─── 4. PROMPT EXECUTION PATTERNS ─────────────────────────────────────────────
181
+
182
+ export async function searchPromptPattern(prompt: string, limit: number = 5): Promise<any[]> {
183
+ if (!isConfigured()) return [];
184
+ try {
185
+ const apiUrl = process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1/prompt-execution-patterns/search` : `${getBase()}/prompt-execution-patterns/search`;
186
+
187
+ const payload = {
188
+ organization_id: getOrgId(),
189
+ project_id: getPid(),
190
+ prompt: prompt,
191
+ limit: limit
192
+ };
193
+
194
+ console.log(`[PatternSearch] 🔍 Buscando patrones en: ${apiUrl}`);
195
+ console.log(`[PatternSearch] 📦 Payload: ${JSON.stringify(payload, null, 2)}`);
196
+
197
+ const res = await fetch(apiUrl, {
198
+ method: 'POST',
199
+ headers: {
200
+ 'Content-Type': 'application/json',
201
+ 'x-api-key': getKey()
202
+ },
203
+ body: JSON.stringify(payload)
204
+ });
205
+
206
+ if (!res.ok) {
207
+ console.warn(`[PatternSearch] HTTP ${res.status} from ${apiUrl}`);
208
+ return [];
209
+ }
210
+ const data = await res.json();
211
+ const results = Array.isArray(data) ? data : (data.matches || data.results || []);
212
+ console.log(`[PatternSearch] ✅ ${results.length} patrones encontrados.`);
213
+ return results;
214
+ } catch (err: any) {
215
+ console.warn(`[PatternSearch] ⚠️ Error buscando patrones: ${err?.message || 'unknown'}`);
216
+ return [];
217
+ }
218
+ }
219
+
220
+ export async function savePromptPattern(patternData: any): Promise<boolean> {
221
+ if (!isConfigured()) return false;
222
+ try {
223
+ const apiUrl = process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1/prompt-execution-patterns` : `${getBase()}/prompt-execution-patterns`;
224
+
225
+ const payload = {
226
+ organization_id: getOrgId(),
227
+ project_id: getPid(),
228
+ mission_id: getMissionId(),
229
+ ...patternData
230
+ };
231
+ console.log(`[PatternSave] 📤 POST ${apiUrl} | org=${payload.organization_id} proj=${payload.project_id} mission=${payload.mission_id}`);
232
+
233
+ const res = await fetch(apiUrl, {
234
+ method: 'POST',
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ 'x-api-key': getKey()
238
+ },
239
+ body: JSON.stringify(payload)
240
+ });
241
+
242
+ if (!res.ok) {
243
+ console.warn(`[PatternSave] ⚠️ HTTP ${res.status} al guardar patrón en ${apiUrl}: ${await res.text().catch(() => '')}`);
244
+ } else {
245
+ console.log(`[PatternSave] ✅ Patrón guardado exitosamente.`);
246
+ }
247
+ return res.ok;
248
+ } catch (err: any) {
249
+ console.warn(`[PatternSave] ⚠️ Error guardando patrón: ${err?.message || 'unknown'}`);
250
+ return false;
251
+ }
252
+ }
253
+
163
254
  // ─── HELPER: Obtener campos ya conocidos para deduplicación ─────────────────
164
255
 
165
256
  export async function getKnownFieldIdentifiers(pathUrl: string): Promise<string[]> {
166
257
  if (!isConfigured()) return [];
167
258
  try {
168
259
  const res = await fetch(
169
- `${getBase()}/portal/context?project_id=${getPid()}&path=${encodeURIComponent(pathUrl)}`,
260
+ `${getBase()}/portal/context?organization_id=${getOrgId()}&project_id=${getPid()}&path=${encodeURIComponent(pathUrl)}`,
170
261
  { headers: { 'x-api-key': getKey() } }
171
262
  );
172
263
  if (!res.ok) return [];