@arcadialdev/arcality 3.0.1 → 3.0.3

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.
@@ -1,10 +1,10 @@
1
1
  // src/arcalityClient.mjs
2
2
  // Client for communicating with Arcality Backend
3
- // Supports arcality.config (primary) and .env (fallback)
4
- // Includes MOCK MODE for development without backend
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
3
+ // Supports arcality.config (primary) and .env (fallback)
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
7
+ import { isRealProjectId } from './configManager.mjs';
8
8
 
9
9
  // ── Backend config (internal, not user-configurable) ──
10
10
  const DAILY_MISSION_LIMIT = 35;
@@ -97,11 +97,11 @@ function getEffectiveApiKey() {
97
97
  * Gets the effective Project ID.
98
98
  * Priority: arcality.config > ARCALITY_PROJECT_ID env var
99
99
  */
100
- function loadProjectId() {
101
- const localConfig = loadArcalityConfig();
102
- if (localConfig?.projectId) return localConfig.projectId;
103
- return process.env.ARCALITY_PROJECT_ID || null;
104
- }
100
+ function loadProjectId() {
101
+ const localConfig = loadArcalityConfig();
102
+ const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID || null;
103
+ return isRealProjectId(projectId) ? projectId : null;
104
+ }
105
105
 
106
106
  // ═══════════════════════════════════════════════════════
107
107
  // PUBLIC API
@@ -126,11 +126,11 @@ export async function validateApiKey() {
126
126
  const key = getEffectiveApiKey();
127
127
 
128
128
  if (!key) {
129
- return { valid: false, error: 'no_api_key', mode: 'mock' };
129
+ return { valid: false, error: 'no_api_key', mode: 'live' };
130
130
  }
131
131
 
132
132
  if (!key.startsWith('arc_')) {
133
- return { valid: false, error: 'invalid_format', mode: 'mock' };
133
+ return { valid: false, error: 'invalid_format', mode: 'live' };
134
134
  }
135
135
 
136
136
  const apiBase = getEffectiveApiBase();
@@ -162,20 +162,12 @@ export async function validateApiKey() {
162
162
  return { ...data, valid: true, mode: 'live' };
163
163
  } catch (e) {
164
164
  const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
165
- console.warn(`⚠️ Backend not available (${apiBase}): ${reason}. Using local mode.`);
165
+ console.warn(`Backend not available (${apiBase}): ${reason}.`);
166
+ return { valid: false, error: 'backend_unavailable', mode: 'live', reason };
166
167
  }
167
168
  }
168
169
 
169
- // ── MOCK mode: No backend ──
170
- const dailyUsed = getLocalDailyUsage();
171
- return {
172
- valid: true,
173
- mode: 'mock',
174
- plan: 'internal',
175
- daily_used: dailyUsed,
176
- daily_limit: DAILY_MISSION_LIMIT,
177
- remaining: Math.max(0, DAILY_MISSION_LIMIT - dailyUsed)
178
- };
170
+ return { valid: false, error: 'backend_unavailable', mode: 'live' };
179
171
  }
180
172
 
181
173
  /**
@@ -192,7 +184,10 @@ export async function startMission(prompt, targetUrl) {
192
184
  // ── LIVE mode ──
193
185
  if (apiBase) {
194
186
  try {
195
- const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
187
+ const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
188
+ if (!isRealProjectId(projectId)) {
189
+ return { allowed: false, error: 'no_project_id' };
190
+ }
196
191
 
197
192
  const controller = new AbortController();
198
193
  const timeout = setTimeout(() => controller.abort(), 5000);
@@ -221,38 +216,13 @@ export async function startMission(prompt, targetUrl) {
221
216
  return { allowed: true, ...(await res.json()) };
222
217
  } catch (e) {
223
218
  const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
224
- console.warn(`⚠️ startMission failed (${apiBase}): ${reason}. Falling back to mock mode.`);
219
+ console.warn(`startMission failed (${apiBase}): ${reason}.`);
220
+ return { allowed: false, error: 'backend_unavailable', reason };
225
221
  }
226
222
  }
227
223
 
228
- // ── MOCK mode ──
229
- const dailyUsed = getLocalDailyUsage();
230
-
231
- if (dailyUsed >= DAILY_MISSION_LIMIT) {
232
- const tomorrow = new Date();
233
- tomorrow.setDate(tomorrow.getDate() + 1);
234
- tomorrow.setHours(0, 0, 0, 0);
235
-
236
- return {
237
- allowed: false,
238
- error: 'mission_limit_exceeded',
239
- daily_used: dailyUsed,
240
- daily_limit: DAILY_MISSION_LIMIT,
241
- remaining: 0,
242
- resets_at: tomorrow.toISOString()
243
- };
244
- }
245
-
246
- const newCount = incrementLocalUsage();
247
- return {
248
- allowed: true,
249
- mode: 'mock',
250
- mission_id: `mock_${Date.now().toString(36)}`,
251
- daily_used: newCount,
252
- daily_limit: DAILY_MISSION_LIMIT,
253
- remaining: DAILY_MISSION_LIMIT - newCount
254
- };
255
- }
224
+ return { allowed: false, error: 'backend_unavailable' };
225
+ }
256
226
 
257
227
  /**
258
228
  * Fetches missions from the backend API.
@@ -264,11 +234,10 @@ export async function fetchMissions(projectId, tags) {
264
234
  const key = getEffectiveApiKey();
265
235
  if (!key) return { success: false, error: 'no_api_key', missions: [] };
266
236
 
267
- const apiBase = getEffectiveApiBase();
268
- if (!apiBase) {
269
- // MOCK MODE: Return empty array for now since there's no backend
270
- return { success: true, mode: 'mock', missions: [] };
271
- }
237
+ const apiBase = getEffectiveApiBase();
238
+ if (!apiBase) {
239
+ return { success: false, error: 'backend_unavailable', missions: [] };
240
+ }
272
241
 
273
242
  try {
274
243
  const url = new URL(`${apiBase}/api/v1/projects/${projectId}/missions`);
@@ -308,9 +277,10 @@ export async function fetchMissions(projectId, tags) {
308
277
  * @param {'success'|'failed'|'cancelled'} result
309
278
  * @param {object} usage - Optional usage statistics directly from the AI
310
279
  */
311
- export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
312
- const apiBase = getEffectiveApiBase();
313
- if (!apiBase || !missionId) return { ok: true };
280
+ export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
281
+ const apiBase = getEffectiveApiBase();
282
+ if (!apiBase) return { ok: false, error: 'backend_unavailable' };
283
+ if (!missionId) return { ok: false, error: 'no_mission_id' };
314
284
 
315
285
  const key = getEffectiveApiKey();
316
286
  try {
@@ -356,7 +326,7 @@ export async function createAdoTask(title, description) {
356
326
  }
357
327
 
358
328
  const projectId = loadProjectId();
359
- if (!projectId) {
329
+ if (!isRealProjectId(projectId)) {
360
330
  if (process.env.DEBUG) console.log(`[ADO] No hay Project ID configurado. Omitiendo creación de Task en Azure DevOps.`);
361
331
  return { success: false, error: 'no_project_id' };
362
332
  }
@@ -415,7 +385,7 @@ export async function createAdoTask(title, description) {
415
385
  */
416
386
  export async function getEvidenceSasToken(missionId, projectId) {
417
387
  const apiBase = getEffectiveApiBase();
418
- if (!apiBase || !missionId) return null;
388
+ if (!apiBase || !missionId || !isRealProjectId(projectId)) return null;
419
389
 
420
390
  const key = getEffectiveApiKey();
421
391
  if (!key) return null;
@@ -1,9 +1,10 @@
1
1
  // src/configLoader.mjs
2
2
  // Configuration loader — supports arcality.config (primary) and .env / global config (fallback)
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import os from 'node:os';
6
- import dotenv from 'dotenv';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import dotenv from 'dotenv';
7
+ import { isRealProjectId } from './configManager.mjs';
7
8
 
8
9
  dotenv.config();
9
10
 
@@ -58,17 +59,22 @@ export function loadConfig() {
58
59
  ARCALITY_PROJECT_ID: localConfig?.projectId || process.env.ARCALITY_PROJECT_ID,
59
60
  };
60
61
 
61
- if (!config.ARCALITY_API_KEY) {
62
- throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
63
- }
64
-
65
- return config;
66
- }
62
+ if (!config.ARCALITY_API_KEY) {
63
+ throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
64
+ }
65
+
66
+ if (!isRealProjectId(config.ARCALITY_PROJECT_ID)) {
67
+ throw new Error('ARCALITY_PROJECT_ID is not connected to a real Arcality project. Run `arcality init`.');
68
+ }
69
+
70
+ return config;
71
+ }
67
72
 
68
- export function loadProjectId() {
69
- const localConfig = loadLocalArcalityConfig();
70
- return localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
71
- }
73
+ export function loadProjectId() {
74
+ const localConfig = loadLocalArcalityConfig();
75
+ const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
76
+ return isRealProjectId(projectId) ? projectId : null;
77
+ }
72
78
 
73
79
 
74
80
  /**
@@ -127,35 +133,25 @@ export function getApiKey() {
127
133
  return { key: null, source: 'none' };
128
134
  }
129
135
 
130
- /**
131
- * Loads additional secrets (like Anthropic) from global config
132
- */
133
- export function setupProcessEnv() {
134
- const config = loadGlobalConfig();
135
- if (!config) return;
136
-
137
- // Load Arcality API Key
138
- getApiKey();
139
-
140
- // Load Anthropic API Key if not already in env
141
- if (config.anthropic_api_key && !process.env.ANTHROPIC_API_KEY) {
142
- process.env.ANTHROPIC_API_KEY = config.anthropic_api_key;
143
- }
144
-
145
- // Load default model if exists
146
- if (config.model && !process.env.CLAUDE_MODEL) {
147
- process.env.CLAUDE_MODEL = config.model;
148
- }
149
-
150
- // Also inject arcality.config values into process.env for backward compat
151
- const localConfig = loadLocalArcalityConfig();
136
+ /**
137
+ * Loads supported Arcality runtime values into process.env.
138
+ */
139
+ export function setupProcessEnv() {
140
+ const config = loadGlobalConfig();
141
+ if (!config) return;
142
+
143
+ // Load Arcality API Key
144
+ getApiKey();
145
+
146
+ // Also inject arcality.config values into process.env for backward compat
147
+ const localConfig = loadLocalArcalityConfig();
152
148
  if (localConfig) {
153
149
  if (localConfig.apiKey && !process.env.ARCALITY_API_KEY) {
154
150
  process.env.ARCALITY_API_KEY = localConfig.apiKey;
155
151
  }
156
- if (localConfig.projectId && !process.env.ARCALITY_PROJECT_ID) {
157
- process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
158
- }
152
+ if (isRealProjectId(localConfig.projectId) && !process.env.ARCALITY_PROJECT_ID) {
153
+ process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
154
+ }
159
155
  if (localConfig.project?.baseUrl && !process.env.BASE_URL) {
160
156
  process.env.BASE_URL = localConfig.project.baseUrl;
161
157
  }
@@ -5,7 +5,16 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
 
8
- const CONFIG_FILENAME = 'arcality.config';
8
+ const CONFIG_FILENAME = 'arcality.config';
9
+
10
+ export function isRealProjectId(projectId) {
11
+ const id = String(projectId || '').trim();
12
+ if (!id) return false;
13
+ if (id === 'undefined' || id === 'null') return false;
14
+ if (/^(local|mock)_/i.test(id)) return false;
15
+ if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
16
+ return true;
17
+ }
9
18
 
10
19
  /**
11
20
  * Resolves the path to arcality.config in the project root.
@@ -59,21 +68,21 @@ export function saveProjectConfig(config, projectRoot) {
59
68
  * @param {ArcalityConfig} config
60
69
  * @returns {{ valid: boolean, missing: string[] }}
61
70
  */
62
- export function validateConfig(config) {
63
- const missing = [];
71
+ export function validateConfig(config) {
72
+ const missing = [];
64
73
 
65
74
  if (!config) {
66
75
  return { valid: false, missing: ['arcality.config file'] };
67
76
  }
68
77
 
69
- if (!config.apiKey) missing.push('apiKey');
70
- if (!config.projectId) missing.push('projectId');
71
- if (!config.project?.baseUrl) missing.push('project.baseUrl');
72
- if (!config.auth?.username) missing.push('auth.username');
73
- if (!config.auth?.password) missing.push('auth.password');
74
-
75
- return { valid: missing.length === 0, missing };
76
- }
78
+ if (!config.apiKey) missing.push('apiKey');
79
+ if (!isRealProjectId(config.projectId)) missing.push('projectId');
80
+ if (!config.project?.baseUrl) missing.push('project.baseUrl');
81
+ if (!config.auth?.username) missing.push('auth.username');
82
+ if (!config.auth?.password) missing.push('auth.password');
83
+
84
+ return { valid: missing.length === 0, missing };
85
+ }
77
86
 
78
87
  /**
79
88
  * Creates a fresh arcality.config structure.
@@ -84,30 +93,30 @@ export function createConfig({
84
93
  apiKey,
85
94
  organizationId,
86
95
  projectId,
87
- projectName,
88
- baseUrl,
89
- frameworkDetected,
90
- username,
91
- password,
92
- arcalityVersion,
93
- yamlOutputDir = './arcality',
94
- }) {
95
- return {
96
+ projectName,
97
+ baseUrl,
98
+ frameworkDetected,
99
+ username,
100
+ password,
101
+ arcalityVersion,
102
+ yamlOutputDir = './arcality',
103
+ }) {
104
+ return {
96
105
  version: '1',
97
106
  apiKey,
98
107
  organizationId: organizationId || null,
99
108
  projectId,
100
109
  project: {
101
110
  name: projectName,
102
- baseUrl,
103
- frameworkDetected: frameworkDetected || null,
104
- },
105
- auth: {
106
- username,
107
- password,
108
- },
109
- runtime: {
110
- yamlOutputDir,
111
+ baseUrl,
112
+ frameworkDetected: frameworkDetected || null,
113
+ },
114
+ auth: {
115
+ username,
116
+ password,
117
+ },
118
+ runtime: {
119
+ yamlOutputDir,
111
120
  reuseSuccessfulYamls: true,
112
121
  singleConfigurationMode: true,
113
122
  },
@@ -122,17 +131,17 @@ export function createConfig({
122
131
  * with existing modules that read from process.env.
123
132
  * @param {ArcalityConfig} config
124
133
  */
125
- export function injectConfigToEnv(config) {
126
- if (!config) return;
134
+ export function injectConfigToEnv(config) {
135
+ if (!config) return;
127
136
 
128
137
  if (config.apiKey) process.env.ARCALITY_API_KEY = config.apiKey;
129
- if (config.projectId) process.env.ARCALITY_PROJECT_ID = config.projectId;
130
- if (config.project?.baseUrl && !process.env.BASE_URL) {
131
- process.env.BASE_URL = config.project.baseUrl;
132
- }
133
- if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
134
- if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
135
- }
138
+ if (isRealProjectId(config.projectId)) process.env.ARCALITY_PROJECT_ID = config.projectId;
139
+ if (config.project?.baseUrl && !process.env.BASE_URL) {
140
+ process.env.BASE_URL = config.project.baseUrl;
141
+ }
142
+ if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
143
+ if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
144
+ }
136
145
 
137
146
  /**
138
147
  * Gets the YAML output directory from config, falling back to default.
@@ -166,7 +175,7 @@ export function ensureYamlOutputDir(config, projectRoot) {
166
175
  * @property {string|null} organizationId
167
176
  * @property {string} projectId
168
177
  * @property {{ name: string, baseUrl: string, frameworkDetected: string|null }} project
169
- * @property {{ username: string, password: string }} auth
178
+ * @property {{ username: string, password: string }} auth
170
179
  * @property {{ yamlOutputDir: string, reuseSuccessfulYamls: boolean, singleConfigurationMode: boolean }} runtime
171
180
  * @property {{ arcalityVersion: string }} meta
172
181
  */
@@ -0,0 +1,27 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export const PLAYWRIGHT_ASSETS = ['chromium', 'ffmpeg'];
4
+
5
+ export function getPlaywrightInstallCommand() {
6
+ return `npx playwright install ${PLAYWRIGHT_ASSETS.join(' ')}`;
7
+ }
8
+
9
+ export function installPlaywrightAssets({ cwd, stdio = 'inherit' } = {}) {
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn('npx', ['playwright', 'install', ...PLAYWRIGHT_ASSETS], {
12
+ stdio,
13
+ shell: true,
14
+ cwd,
15
+ });
16
+
17
+ child.on('exit', code => {
18
+ if (code === 0) {
19
+ resolve();
20
+ return;
21
+ }
22
+
23
+ reject(new Error(`Playwright install failed with exit code ${code}`));
24
+ });
25
+ child.on('error', reject);
26
+ });
27
+ }
@@ -16,25 +16,32 @@ const getPid = () => process.env.ARCALITY_PROJECT_ID || '';
16
16
  const getOrgId = () => process.env.ARCALITY_ORG_ID;
17
17
  const getMissionId = () => process.env.ARCALITY_MISSION_ID || EMPTY_GUID;
18
18
 
19
- const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
20
-
21
- let configWarningLogged = false;
22
-
23
- function isConfigured(): boolean {
19
+ const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
20
+
21
+ let configWarningLogged = false;
22
+
23
+ function hasRealProjectId(pid: string): boolean {
24
+ if (!pid || pid === EMPTY_GUID) return false;
25
+ if (/^(local|mock)_/i.test(pid)) return false;
26
+ if (pid === 'undefined' || pid === 'null') return false;
27
+ return true;
28
+ }
29
+
30
+ function isConfigured(): boolean {
24
31
  const base = getBase();
25
32
  const key = getKey();
26
33
  const pid = getPid();
27
34
  const orgId = getOrgId();
28
35
 
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');
36
+ if (!base || !key || !hasRealProjectId(pid) || !orgId) {
37
+ if (!configWarningLogged) {
38
+ const missing = [];
39
+ if (!base) missing.push('ARCALITY_API_URL');
40
+ if (!key) missing.push('ARCALITY_API_KEY');
41
+ if (!hasRealProjectId(pid)) missing.push('ARCALITY_PROJECT_ID');
35
42
  if (!orgId) missing.push('ARCALITY_ORG_ID');
36
43
 
37
- console.warn(`[CollectiveMemory] Service disabled. Missing: ${missing.join(', ')}`);
44
+ if (process.env.DEBUG) console.warn(`[CollectiveMemory] Service disabled. Missing: ${missing.join(', ')}`);
38
45
  configWarningLogged = true;
39
46
  }
40
47
  return false;
@@ -191,8 +198,10 @@ export async function searchPromptPattern(prompt: string, limit: number = 5): Pr
191
198
  limit: limit
192
199
  };
193
200
 
194
- console.log(`[PatternSearch] 🔍 Buscando patrones en: ${apiUrl}`);
195
- console.log(`[PatternSearch] 📦 Payload: ${JSON.stringify(payload, null, 2)}`);
201
+ const debugPatternSearch = process.env.DEBUG === 'true' || process.env.ARCALITY_DEBUG === 'true';
202
+ if (debugPatternSearch) {
203
+ console.log(`[PatternSearch] POST ${apiUrl} | prompt_chars=${prompt.length} | limit=${limit}`);
204
+ }
196
205
 
197
206
  const res = await fetch(apiUrl, {
198
207
  method: 'POST',
@@ -204,18 +213,20 @@ export async function searchPromptPattern(prompt: string, limit: number = 5): Pr
204
213
  });
205
214
 
206
215
  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
- }
216
+ if (debugPatternSearch) console.warn(`[PatternSearch] HTTP ${res.status} from ${apiUrl}`);
217
+ return [];
218
+ }
219
+ const data = await res.json();
220
+ const results = Array.isArray(data) ? data : (data.matches || data.results || []);
221
+ if (debugPatternSearch) console.log(`[PatternSearch] ${results.length} patrones encontrados.`);
222
+ return results;
223
+ } catch (err: any) {
224
+ if (process.env.DEBUG === 'true' || process.env.ARCALITY_DEBUG === 'true') {
225
+ console.warn(`[PatternSearch] Error buscando patrones: ${err?.message || 'unknown'}`);
226
+ }
227
+ return [];
228
+ }
229
+ }
219
230
 
220
231
  export async function savePromptPattern(patternData: any): Promise<boolean> {
221
232
  if (!isConfigured()) return false;