@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.
@@ -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
+ }