@arcadialdev/arcality 2.4.25 → 2.4.27
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/.agents/skills/security-qa/SKILL.md +253 -253
- package/README.md +2 -2
- package/package.json +7 -4
- package/scripts/gen-and-run.mjs +932 -932
- package/scripts/setup.mjs +166 -166
- package/src/arcalityClient.mjs +266 -266
- package/tests/_helpers/ArcalityReporter.js +706 -706
- package/tests/_helpers/agentic-runner.bundle.spec.js +83 -2806
- package/src/KnowledgeService.ts +0 -256
- package/src/consoleBanner.ts +0 -32
- package/src/envSetup.ts +0 -205
- package/src/index.ts +0 -25
- package/src/projectInspector.ts +0 -42
- package/src/services/collectiveMemoryService.ts +0 -178
- package/src/services/securityScanner.ts +0 -128
- package/src/testRunner.ts +0 -201
- package/tests/_helpers/ArcalityReporter.ts +0 -733
- package/tests/_helpers/agentic-runner.spec.ts +0 -783
- package/tests/_helpers/ai-agent-helper.ts +0 -1660
- package/tests/_helpers/discover-view.spec.ts +0 -238
- package/tests/_helpers/extract-view.spec.ts +0 -118
- package/tests/_helpers/qa-security-tools.ts +0 -265
- package/tests/_helpers/qa-tools.ts +0 -333
- package/tests/_helpers/smart-action.spec.ts +0 -1458
package/src/arcalityClient.mjs
CHANGED
|
@@ -1,266 +1,266 @@
|
|
|
1
|
-
// src/arcalityClient.mjs
|
|
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';
|
|
8
|
-
|
|
9
|
-
// ── Backend config (internal, not user-configurable) ──
|
|
10
|
-
const DAILY_MISSION_LIMIT = 50;
|
|
11
|
-
const USAGE_FILE = path.join(CONFIG_DIR, 'usage.json');
|
|
12
|
-
|
|
13
|
-
// ═══════════════════════════════════════════════════════
|
|
14
|
-
// MOCK MODE: Local simulation (no backend)
|
|
15
|
-
// Tracks daily usage in ~/.arcality/usage.json
|
|
16
|
-
// ═══════════════════════════════════════════════════════
|
|
17
|
-
|
|
18
|
-
function getTodayKey() {
|
|
19
|
-
return new Date().toISOString().slice(0, 10);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function loadLocalUsage() {
|
|
23
|
-
try {
|
|
24
|
-
if (fs.existsSync(USAGE_FILE)) {
|
|
25
|
-
return JSON.parse(fs.readFileSync(USAGE_FILE, 'utf8'));
|
|
26
|
-
}
|
|
27
|
-
} catch { }
|
|
28
|
-
return {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function saveLocalUsage(usage) {
|
|
32
|
-
try {
|
|
33
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
fs.writeFileSync(USAGE_FILE, JSON.stringify(usage, null, 2), 'utf8');
|
|
37
|
-
} catch { }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function getLocalDailyUsage() {
|
|
41
|
-
const usage = loadLocalUsage();
|
|
42
|
-
const today = getTodayKey();
|
|
43
|
-
return usage[today] || 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function incrementLocalUsage() {
|
|
47
|
-
const usage = loadLocalUsage();
|
|
48
|
-
const today = getTodayKey();
|
|
49
|
-
usage[today] = (usage[today] || 0) + 1;
|
|
50
|
-
|
|
51
|
-
const keys = Object.keys(usage).sort().reverse();
|
|
52
|
-
const cleaned = {};
|
|
53
|
-
for (const k of keys.slice(0, 7)) {
|
|
54
|
-
cleaned[k] = usage[k];
|
|
55
|
-
}
|
|
56
|
-
saveLocalUsage(cleaned);
|
|
57
|
-
return cleaned[today];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ═══════════════════════════════════════════════════════
|
|
61
|
-
// ARCALITY CONFIG LOADER (for reading arcality.config directly)
|
|
62
|
-
// ═══════════════════════════════════════════════════════
|
|
63
|
-
|
|
64
|
-
function loadArcalityConfig() {
|
|
65
|
-
try {
|
|
66
|
-
const configPath = path.join(process.cwd(), 'arcality.config');
|
|
67
|
-
if (fs.existsSync(configPath)) {
|
|
68
|
-
let raw = fs.readFileSync(configPath, 'utf8');
|
|
69
|
-
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
70
|
-
return JSON.parse(raw);
|
|
71
|
-
}
|
|
72
|
-
} catch { }
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Gets the effective API base URL.
|
|
78
|
-
* Uses the internal getApiUrl() — NOT configurable by user.
|
|
79
|
-
*/
|
|
80
|
-
function getEffectiveApiBase() {
|
|
81
|
-
return getApiUrl();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Gets the effective API Key.
|
|
86
|
-
* Priority: arcality.config > env > global config
|
|
87
|
-
*/
|
|
88
|
-
function getEffectiveApiKey() {
|
|
89
|
-
const localConfig = loadArcalityConfig();
|
|
90
|
-
if (localConfig?.apiKey) return localConfig.apiKey;
|
|
91
|
-
|
|
92
|
-
const { key } = getApiKey();
|
|
93
|
-
return key;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ═══════════════════════════════════════════════════════
|
|
97
|
-
// PUBLIC API
|
|
98
|
-
// ═══════════════════════════════════════════════════════
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* ArcalityClient class — used by KnowledgeService and other modules.
|
|
102
|
-
*/
|
|
103
|
-
export class ArcalityClient {
|
|
104
|
-
constructor(apiKey) {
|
|
105
|
-
this.apiUrl = getApiUrl();
|
|
106
|
-
this.apiKey = apiKey;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Validates the API Key.
|
|
112
|
-
* - If backend is available (ARCALITY_API_URL) → validates remotely
|
|
113
|
-
* - If no backend → mock mode with local validation
|
|
114
|
-
*/
|
|
115
|
-
export async function validateApiKey() {
|
|
116
|
-
const key = getEffectiveApiKey();
|
|
117
|
-
|
|
118
|
-
if (!key) {
|
|
119
|
-
return { valid: false, error: 'no_api_key', mode: 'mock' };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!key.startsWith('arc_k_')) {
|
|
123
|
-
return { valid: false, error: 'invalid_format', mode: 'mock' };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const apiBase = getEffectiveApiBase();
|
|
127
|
-
|
|
128
|
-
// ── LIVE mode: Backend available ──
|
|
129
|
-
if (apiBase) {
|
|
130
|
-
try {
|
|
131
|
-
const res = await fetch(`${apiBase}/api/v1/auth/validate`, {
|
|
132
|
-
method: 'POST',
|
|
133
|
-
headers: {
|
|
134
|
-
'Content-Type': 'application/json',
|
|
135
|
-
'x-api-key': key
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (res.status === 401) return { valid: false, error: 'invalid_api_key', mode: 'live' };
|
|
140
|
-
if (res.status === 403) return { valid: false, error: 'plan_expired', mode: 'live' };
|
|
141
|
-
if (!res.ok) return { valid: false, error: 'server_error', mode: 'live' };
|
|
142
|
-
|
|
143
|
-
const data = await res.json();
|
|
144
|
-
return { ...data, valid: true, mode: 'live' };
|
|
145
|
-
} catch {
|
|
146
|
-
console.warn('⚠️ Backend not available, using local mode');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ── MOCK mode: No backend ──
|
|
151
|
-
const dailyUsed = getLocalDailyUsage();
|
|
152
|
-
return {
|
|
153
|
-
valid: true,
|
|
154
|
-
mode: 'mock',
|
|
155
|
-
plan: 'internal',
|
|
156
|
-
daily_used: dailyUsed,
|
|
157
|
-
daily_limit: DAILY_MISSION_LIMIT,
|
|
158
|
-
remaining: Math.max(0, DAILY_MISSION_LIMIT - dailyUsed)
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Requests to start a mission.
|
|
164
|
-
* @param {string} prompt - The mission prompt (only a hash is sent, never the raw text)
|
|
165
|
-
* @param {string} targetUrl - Target portal URL
|
|
166
|
-
*/
|
|
167
|
-
export async function startMission(prompt, targetUrl) {
|
|
168
|
-
const key = getEffectiveApiKey();
|
|
169
|
-
if (!key) return { allowed: false, error: 'no_api_key' };
|
|
170
|
-
|
|
171
|
-
const apiBase = getEffectiveApiBase();
|
|
172
|
-
|
|
173
|
-
// ── LIVE mode ──
|
|
174
|
-
if (apiBase) {
|
|
175
|
-
try {
|
|
176
|
-
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
177
|
-
|
|
178
|
-
const res = await fetch(`${apiBase}/api/v1/missions/start`, {
|
|
179
|
-
method: 'POST',
|
|
180
|
-
headers: {
|
|
181
|
-
'Content-Type': 'application/json',
|
|
182
|
-
'x-api-key': key
|
|
183
|
-
},
|
|
184
|
-
body: JSON.stringify({
|
|
185
|
-
prompt_hash: simpleHash(prompt),
|
|
186
|
-
target_url: targetUrl,
|
|
187
|
-
project_id: projectId || undefined,
|
|
188
|
-
})
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
if (res.status === 429) {
|
|
192
|
-
const data = await res.json();
|
|
193
|
-
return { allowed: false, error: 'mission_limit_exceeded', ...data };
|
|
194
|
-
}
|
|
195
|
-
if (!res.ok) return { allowed: false, error: 'server_error' };
|
|
196
|
-
|
|
197
|
-
return { allowed: true, ...(await res.json()) };
|
|
198
|
-
} catch {
|
|
199
|
-
// Fallback to mock if backend is down
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ── MOCK mode ──
|
|
204
|
-
const dailyUsed = getLocalDailyUsage();
|
|
205
|
-
|
|
206
|
-
if (dailyUsed >= DAILY_MISSION_LIMIT) {
|
|
207
|
-
const tomorrow = new Date();
|
|
208
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
209
|
-
tomorrow.setHours(0, 0, 0, 0);
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
allowed: false,
|
|
213
|
-
error: 'mission_limit_exceeded',
|
|
214
|
-
daily_used: dailyUsed,
|
|
215
|
-
daily_limit: DAILY_MISSION_LIMIT,
|
|
216
|
-
remaining: 0,
|
|
217
|
-
resets_at: tomorrow.toISOString()
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const newCount = incrementLocalUsage();
|
|
222
|
-
return {
|
|
223
|
-
allowed: true,
|
|
224
|
-
mode: 'mock',
|
|
225
|
-
mission_id: `mock_${Date.now().toString(36)}`,
|
|
226
|
-
daily_used: newCount,
|
|
227
|
-
daily_limit: DAILY_MISSION_LIMIT,
|
|
228
|
-
remaining: DAILY_MISSION_LIMIT - newCount
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Marks a mission as completed.
|
|
234
|
-
* @param {string} missionId
|
|
235
|
-
* @param {'success'|'failed'|'cancelled'} result
|
|
236
|
-
*/
|
|
237
|
-
export async function endMission(missionId, result) {
|
|
238
|
-
const apiBase = getEffectiveApiBase();
|
|
239
|
-
if (!apiBase || !missionId) return { ok: true };
|
|
240
|
-
|
|
241
|
-
const key = getEffectiveApiKey();
|
|
242
|
-
try {
|
|
243
|
-
await fetch(`${apiBase}/api/v1/missions/${missionId}/end`, {
|
|
244
|
-
method: 'POST',
|
|
245
|
-
headers: {
|
|
246
|
-
'Content-Type': 'application/json',
|
|
247
|
-
'x-api-key': key
|
|
248
|
-
},
|
|
249
|
-
body: JSON.stringify({ result })
|
|
250
|
-
});
|
|
251
|
-
} catch { }
|
|
252
|
-
return { ok: true };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Simple hash of the prompt — we never send the raw text to backend
|
|
257
|
-
*/
|
|
258
|
-
function simpleHash(str) {
|
|
259
|
-
let hash = 0;
|
|
260
|
-
for (let i = 0; i < str.length; i++) {
|
|
261
|
-
const char = str.charCodeAt(i);
|
|
262
|
-
hash = ((hash << 5) - hash) + char;
|
|
263
|
-
hash |= 0;
|
|
264
|
-
}
|
|
265
|
-
return 'ph_' + Math.abs(hash).toString(36);
|
|
266
|
-
}
|
|
1
|
+
// src/arcalityClient.mjs
|
|
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';
|
|
8
|
+
|
|
9
|
+
// ── Backend config (internal, not user-configurable) ──
|
|
10
|
+
const DAILY_MISSION_LIMIT = 50;
|
|
11
|
+
const USAGE_FILE = path.join(CONFIG_DIR, 'usage.json');
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════
|
|
14
|
+
// MOCK MODE: Local simulation (no backend)
|
|
15
|
+
// Tracks daily usage in ~/.arcality/usage.json
|
|
16
|
+
// ═══════════════════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
function getTodayKey() {
|
|
19
|
+
return new Date().toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadLocalUsage() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(USAGE_FILE)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(USAGE_FILE, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
} catch { }
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveLocalUsage(usage) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(USAGE_FILE, JSON.stringify(usage, null, 2), 'utf8');
|
|
37
|
+
} catch { }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getLocalDailyUsage() {
|
|
41
|
+
const usage = loadLocalUsage();
|
|
42
|
+
const today = getTodayKey();
|
|
43
|
+
return usage[today] || 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function incrementLocalUsage() {
|
|
47
|
+
const usage = loadLocalUsage();
|
|
48
|
+
const today = getTodayKey();
|
|
49
|
+
usage[today] = (usage[today] || 0) + 1;
|
|
50
|
+
|
|
51
|
+
const keys = Object.keys(usage).sort().reverse();
|
|
52
|
+
const cleaned = {};
|
|
53
|
+
for (const k of keys.slice(0, 7)) {
|
|
54
|
+
cleaned[k] = usage[k];
|
|
55
|
+
}
|
|
56
|
+
saveLocalUsage(cleaned);
|
|
57
|
+
return cleaned[today];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════════════
|
|
61
|
+
// ARCALITY CONFIG LOADER (for reading arcality.config directly)
|
|
62
|
+
// ═══════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
function loadArcalityConfig() {
|
|
65
|
+
try {
|
|
66
|
+
const configPath = path.join(process.cwd(), 'arcality.config');
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
let raw = fs.readFileSync(configPath, 'utf8');
|
|
69
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
70
|
+
return JSON.parse(raw);
|
|
71
|
+
}
|
|
72
|
+
} catch { }
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets the effective API base URL.
|
|
78
|
+
* Uses the internal getApiUrl() — NOT configurable by user.
|
|
79
|
+
*/
|
|
80
|
+
function getEffectiveApiBase() {
|
|
81
|
+
return getApiUrl();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets the effective API Key.
|
|
86
|
+
* Priority: arcality.config > env > global config
|
|
87
|
+
*/
|
|
88
|
+
function getEffectiveApiKey() {
|
|
89
|
+
const localConfig = loadArcalityConfig();
|
|
90
|
+
if (localConfig?.apiKey) return localConfig.apiKey;
|
|
91
|
+
|
|
92
|
+
const { key } = getApiKey();
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ═══════════════════════════════════════════════════════
|
|
97
|
+
// PUBLIC API
|
|
98
|
+
// ═══════════════════════════════════════════════════════
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ArcalityClient class — used by KnowledgeService and other modules.
|
|
102
|
+
*/
|
|
103
|
+
export class ArcalityClient {
|
|
104
|
+
constructor(apiKey) {
|
|
105
|
+
this.apiUrl = getApiUrl();
|
|
106
|
+
this.apiKey = apiKey;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validates the API Key.
|
|
112
|
+
* - If backend is available (ARCALITY_API_URL) → validates remotely
|
|
113
|
+
* - If no backend → mock mode with local validation
|
|
114
|
+
*/
|
|
115
|
+
export async function validateApiKey() {
|
|
116
|
+
const key = getEffectiveApiKey();
|
|
117
|
+
|
|
118
|
+
if (!key) {
|
|
119
|
+
return { valid: false, error: 'no_api_key', mode: 'mock' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!key.startsWith('arc_k_')) {
|
|
123
|
+
return { valid: false, error: 'invalid_format', mode: 'mock' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const apiBase = getEffectiveApiBase();
|
|
127
|
+
|
|
128
|
+
// ── LIVE mode: Backend available ──
|
|
129
|
+
if (apiBase) {
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(`${apiBase}/api/v1/auth/validate`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'x-api-key': key
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (res.status === 401) return { valid: false, error: 'invalid_api_key', mode: 'live' };
|
|
140
|
+
if (res.status === 403) return { valid: false, error: 'plan_expired', mode: 'live' };
|
|
141
|
+
if (!res.ok) return { valid: false, error: 'server_error', mode: 'live' };
|
|
142
|
+
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
return { ...data, valid: true, mode: 'live' };
|
|
145
|
+
} catch {
|
|
146
|
+
console.warn('⚠️ Backend not available, using local mode');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── MOCK mode: No backend ──
|
|
151
|
+
const dailyUsed = getLocalDailyUsage();
|
|
152
|
+
return {
|
|
153
|
+
valid: true,
|
|
154
|
+
mode: 'mock',
|
|
155
|
+
plan: 'internal',
|
|
156
|
+
daily_used: dailyUsed,
|
|
157
|
+
daily_limit: DAILY_MISSION_LIMIT,
|
|
158
|
+
remaining: Math.max(0, DAILY_MISSION_LIMIT - dailyUsed)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Requests to start a mission.
|
|
164
|
+
* @param {string} prompt - The mission prompt (only a hash is sent, never the raw text)
|
|
165
|
+
* @param {string} targetUrl - Target portal URL
|
|
166
|
+
*/
|
|
167
|
+
export async function startMission(prompt, targetUrl) {
|
|
168
|
+
const key = getEffectiveApiKey();
|
|
169
|
+
if (!key) return { allowed: false, error: 'no_api_key' };
|
|
170
|
+
|
|
171
|
+
const apiBase = getEffectiveApiBase();
|
|
172
|
+
|
|
173
|
+
// ── LIVE mode ──
|
|
174
|
+
if (apiBase) {
|
|
175
|
+
try {
|
|
176
|
+
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
177
|
+
|
|
178
|
+
const res = await fetch(`${apiBase}/api/v1/missions/start`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
'x-api-key': key
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
prompt_hash: simpleHash(prompt),
|
|
186
|
+
target_url: targetUrl,
|
|
187
|
+
project_id: projectId || undefined,
|
|
188
|
+
})
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (res.status === 429) {
|
|
192
|
+
const data = await res.json();
|
|
193
|
+
return { allowed: false, error: 'mission_limit_exceeded', ...data };
|
|
194
|
+
}
|
|
195
|
+
if (!res.ok) return { allowed: false, error: 'server_error' };
|
|
196
|
+
|
|
197
|
+
return { allowed: true, ...(await res.json()) };
|
|
198
|
+
} catch {
|
|
199
|
+
// Fallback to mock if backend is down
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── MOCK mode ──
|
|
204
|
+
const dailyUsed = getLocalDailyUsage();
|
|
205
|
+
|
|
206
|
+
if (dailyUsed >= DAILY_MISSION_LIMIT) {
|
|
207
|
+
const tomorrow = new Date();
|
|
208
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
209
|
+
tomorrow.setHours(0, 0, 0, 0);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
allowed: false,
|
|
213
|
+
error: 'mission_limit_exceeded',
|
|
214
|
+
daily_used: dailyUsed,
|
|
215
|
+
daily_limit: DAILY_MISSION_LIMIT,
|
|
216
|
+
remaining: 0,
|
|
217
|
+
resets_at: tomorrow.toISOString()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const newCount = incrementLocalUsage();
|
|
222
|
+
return {
|
|
223
|
+
allowed: true,
|
|
224
|
+
mode: 'mock',
|
|
225
|
+
mission_id: `mock_${Date.now().toString(36)}`,
|
|
226
|
+
daily_used: newCount,
|
|
227
|
+
daily_limit: DAILY_MISSION_LIMIT,
|
|
228
|
+
remaining: DAILY_MISSION_LIMIT - newCount
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Marks a mission as completed.
|
|
234
|
+
* @param {string} missionId
|
|
235
|
+
* @param {'success'|'failed'|'cancelled'} result
|
|
236
|
+
*/
|
|
237
|
+
export async function endMission(missionId, result) {
|
|
238
|
+
const apiBase = getEffectiveApiBase();
|
|
239
|
+
if (!apiBase || !missionId) return { ok: true };
|
|
240
|
+
|
|
241
|
+
const key = getEffectiveApiKey();
|
|
242
|
+
try {
|
|
243
|
+
await fetch(`${apiBase}/api/v1/missions/${missionId}/end`, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
'x-api-key': key
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({ result })
|
|
250
|
+
});
|
|
251
|
+
} catch { }
|
|
252
|
+
return { ok: true };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Simple hash of the prompt — we never send the raw text to backend
|
|
257
|
+
*/
|
|
258
|
+
function simpleHash(str) {
|
|
259
|
+
let hash = 0;
|
|
260
|
+
for (let i = 0; i < str.length; i++) {
|
|
261
|
+
const char = str.charCodeAt(i);
|
|
262
|
+
hash = ((hash << 5) - hash) + char;
|
|
263
|
+
hash |= 0;
|
|
264
|
+
}
|
|
265
|
+
return 'ph_' + Math.abs(hash).toString(36);
|
|
266
|
+
}
|