@virsanghavi/axis-server 1.0.9 → 1.1.0

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.
Files changed (2) hide show
  1. package/dist/mcp-server.mjs +221 -102
  2. package/package.json +1 -1
@@ -13,6 +13,42 @@ import dotenv2 from "dotenv";
13
13
  import fs from "fs/promises";
14
14
  import path from "path";
15
15
  import { Mutex } from "async-mutex";
16
+
17
+ // ../../src/utils/logger.ts
18
+ var Logger = class {
19
+ level = "info" /* INFO */;
20
+ setLevel(level) {
21
+ this.level = level;
22
+ }
23
+ log(level, message, meta) {
24
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
25
+ console.error(JSON.stringify({
26
+ timestamp,
27
+ level,
28
+ message,
29
+ ...meta
30
+ }));
31
+ }
32
+ debug(message, meta) {
33
+ if (this.level === "debug" /* DEBUG */) this.log("debug" /* DEBUG */, message, meta);
34
+ }
35
+ info(message, meta) {
36
+ this.log("info" /* INFO */, message, meta);
37
+ }
38
+ warn(message, meta) {
39
+ this.log("warn" /* WARN */, message, meta);
40
+ }
41
+ error(message, error, meta) {
42
+ this.log("error" /* ERROR */, message, {
43
+ ...meta,
44
+ error: error instanceof Error ? error.message : String(error),
45
+ stack: error instanceof Error ? error.stack : void 0
46
+ });
47
+ }
48
+ };
49
+ var logger = new Logger();
50
+
51
+ // ../../src/local/context-manager.ts
16
52
  import * as fsSync from "fs";
17
53
  function getEffectiveInstructionsDir() {
18
54
  const cwd = process.cwd();
@@ -131,45 +167,105 @@ var ContextManager = class {
131
167
  throw new Error("SHARED_CONTEXT_API_URL not configured.");
132
168
  }
133
169
  const endpoint = this.apiUrl.endsWith("/v1") ? `${this.apiUrl}/search` : `${this.apiUrl}/v1/search`;
134
- const response = await fetch(endpoint, {
135
- method: "POST",
136
- headers: {
137
- "Content-Type": "application/json",
138
- "Authorization": `Bearer ${this.apiSecret || ""}`
139
- },
140
- body: JSON.stringify({ query, projectName })
141
- });
142
- if (!response.ok) {
143
- const text = await response.text();
144
- throw new Error(`API Error ${response.status}: ${text}`);
145
- }
146
- const result = await response.json();
147
- if (result.results && Array.isArray(result.results)) {
148
- return result.results.map(
149
- (r) => `[Similarity: ${(r.similarity * 100).toFixed(1)}%] ${r.content}`
150
- ).join("\n\n---\n\n") || "No results found.";
170
+ const maxRetries = 3;
171
+ const baseDelay = 1e3;
172
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
173
+ const controller = new AbortController();
174
+ const timeout = setTimeout(() => controller.abort(), 15e3);
175
+ try {
176
+ const response = await fetch(endpoint, {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ "Authorization": `Bearer ${this.apiSecret || ""}`
181
+ },
182
+ body: JSON.stringify({ query, projectName }),
183
+ signal: controller.signal
184
+ });
185
+ clearTimeout(timeout);
186
+ if (!response.ok) {
187
+ const text = await response.text();
188
+ if (response.status >= 400 && response.status < 500) {
189
+ throw new Error(`API Error ${response.status}: ${text}`);
190
+ }
191
+ if (attempt < maxRetries) {
192
+ const delay = baseDelay * Math.pow(2, attempt - 1);
193
+ logger.warn(`[searchContext] 5xx error, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
194
+ await new Promise((r) => setTimeout(r, delay));
195
+ continue;
196
+ }
197
+ throw new Error(`API Error ${response.status}: ${text}`);
198
+ }
199
+ const result = await response.json();
200
+ if (result.results && Array.isArray(result.results)) {
201
+ return result.results.map(
202
+ (r) => `[Similarity: ${(r.similarity * 100).toFixed(1)}%] ${r.content}`
203
+ ).join("\n\n---\n\n") || "No results found.";
204
+ }
205
+ throw new Error("No results format recognized.");
206
+ } catch (e) {
207
+ clearTimeout(timeout);
208
+ if (e.message.startsWith("API Error 4")) throw e;
209
+ if (attempt < maxRetries) {
210
+ const delay = baseDelay * Math.pow(2, attempt - 1);
211
+ logger.warn(`[searchContext] Network/timeout error, retrying in ${delay}ms (attempt ${attempt}/${maxRetries}): ${e.message}`);
212
+ await new Promise((r) => setTimeout(r, delay));
213
+ continue;
214
+ }
215
+ throw e;
216
+ }
151
217
  }
152
- throw new Error("No results format recognized.");
218
+ throw new Error("searchContext: unexpected end of retry loop");
153
219
  }
154
220
  async embedContent(items, projectName = "default") {
155
221
  if (!this.apiUrl) {
156
- console.warn("Skipping RAG embedding: SHARED_CONTEXT_API_URL not configured.");
222
+ logger.warn("Skipping RAG embedding: SHARED_CONTEXT_API_URL not configured.");
157
223
  return;
158
224
  }
159
225
  const endpoint = this.apiUrl.endsWith("/v1") ? `${this.apiUrl}/embed` : `${this.apiUrl}/v1/embed`;
160
- const response = await fetch(endpoint, {
161
- method: "POST",
162
- headers: {
163
- "Content-Type": "application/json",
164
- "Authorization": `Bearer ${this.apiSecret || ""}`
165
- },
166
- body: JSON.stringify({ items, projectName })
167
- });
168
- if (!response.ok) {
169
- const text = await response.text();
170
- throw new Error(`API Error ${response.status}: ${text}`);
226
+ const maxRetries = 3;
227
+ const baseDelay = 1e3;
228
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
229
+ const controller = new AbortController();
230
+ const timeout = setTimeout(() => controller.abort(), 15e3);
231
+ try {
232
+ const response = await fetch(endpoint, {
233
+ method: "POST",
234
+ headers: {
235
+ "Content-Type": "application/json",
236
+ "Authorization": `Bearer ${this.apiSecret || ""}`
237
+ },
238
+ body: JSON.stringify({ items, projectName }),
239
+ signal: controller.signal
240
+ });
241
+ clearTimeout(timeout);
242
+ if (!response.ok) {
243
+ const text = await response.text();
244
+ if (response.status >= 400 && response.status < 500) {
245
+ throw new Error(`API Error ${response.status}: ${text}`);
246
+ }
247
+ if (attempt < maxRetries) {
248
+ const delay = baseDelay * Math.pow(2, attempt - 1);
249
+ logger.warn(`[embedContent] 5xx error, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
250
+ await new Promise((r) => setTimeout(r, delay));
251
+ continue;
252
+ }
253
+ throw new Error(`API Error ${response.status}: ${text}`);
254
+ }
255
+ return await response.json();
256
+ } catch (e) {
257
+ clearTimeout(timeout);
258
+ if (e.message.startsWith("API Error 4")) throw e;
259
+ if (attempt < maxRetries) {
260
+ const delay = baseDelay * Math.pow(2, attempt - 1);
261
+ logger.warn(`[embedContent] Network/timeout error, retrying in ${delay}ms (attempt ${attempt}/${maxRetries}): ${e.message}`);
262
+ await new Promise((r) => setTimeout(r, delay));
263
+ continue;
264
+ }
265
+ logger.warn(`[embedContent] Failed after ${maxRetries} attempts: ${e.message}. Skipping embed.`);
266
+ return;
267
+ }
171
268
  }
172
- return await response.json();
173
269
  }
174
270
  };
175
271
 
@@ -178,44 +274,16 @@ import { Mutex as Mutex2 } from "async-mutex";
178
274
  import { createClient } from "@supabase/supabase-js";
179
275
  import fs2 from "fs/promises";
180
276
  import path2 from "path";
181
-
182
- // ../../src/utils/logger.ts
183
- var Logger = class {
184
- level = "info" /* INFO */;
185
- setLevel(level) {
186
- this.level = level;
187
- }
188
- log(level, message, meta) {
189
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
190
- console.error(JSON.stringify({
191
- timestamp,
192
- level,
193
- message,
194
- ...meta
195
- }));
196
- }
197
- debug(message, meta) {
198
- if (this.level === "debug" /* DEBUG */) this.log("debug" /* DEBUG */, message, meta);
199
- }
200
- info(message, meta) {
201
- this.log("info" /* INFO */, message, meta);
202
- }
203
- warn(message, meta) {
204
- this.log("warn" /* WARN */, message, meta);
205
- }
206
- error(message, error, meta) {
207
- this.log("error" /* ERROR */, message, {
208
- ...meta,
209
- error: error instanceof Error ? error.message : String(error),
210
- stack: error instanceof Error ? error.stack : void 0
211
- });
212
- }
213
- };
214
- var logger = new Logger();
215
-
216
- // ../../src/local/nerve-center.ts
217
277
  var STATE_FILE = process.env.NERVE_CENTER_STATE_FILE || path2.join(process.cwd(), "history", "nerve-center-state.json");
218
278
  var LOCK_TIMEOUT_DEFAULT = 30 * 60 * 1e3;
279
+ var CIRCUIT_FAILURE_THRESHOLD = 5;
280
+ var CIRCUIT_COOLDOWN_MS = 6e4;
281
+ var CircuitOpenError = class extends Error {
282
+ constructor() {
283
+ super("Circuit breaker open \u2014 remote API temporarily unavailable, falling back to local");
284
+ this.name = "CircuitOpenError";
285
+ }
286
+ };
219
287
  var NerveCenter = class {
220
288
  mutex;
221
289
  state;
@@ -227,6 +295,8 @@ var NerveCenter = class {
227
295
  // Renamed backing field
228
296
  projectName;
229
297
  useSupabase;
298
+ _circuitFailures = 0;
299
+ _circuitOpenUntil = 0;
230
300
  /**
231
301
  * @param contextManager - Instance of ContextManager for legacy operations
232
302
  * @param options - Configuration options for state persistence and timeouts
@@ -335,40 +405,88 @@ var NerveCenter = class {
335
405
  logger.error("[callCoordination] Remote API not configured - apiUrl is:", this.contextManager.apiUrl);
336
406
  throw new Error("Remote API not configured");
337
407
  }
408
+ if (this._circuitFailures >= CIRCUIT_FAILURE_THRESHOLD && Date.now() < this._circuitOpenUntil) {
409
+ logger.warn(`[callCoordination] Circuit breaker OPEN \u2014 skipping remote call (resets at ${new Date(this._circuitOpenUntil).toISOString()})`);
410
+ throw new CircuitOpenError();
411
+ }
412
+ if (this._circuitFailures >= CIRCUIT_FAILURE_THRESHOLD && Date.now() >= this._circuitOpenUntil) {
413
+ logger.info("[callCoordination] Circuit breaker half-open \u2014 allowing probe request");
414
+ }
338
415
  const url = this.contextManager.apiUrl.endsWith("/v1") ? `${this.contextManager.apiUrl}/${endpoint}` : `${this.contextManager.apiUrl}/v1/${endpoint}`;
339
416
  logger.info(`[callCoordination] Full URL: ${method} ${url}`);
340
417
  logger.info(`[callCoordination] Request body: ${body ? JSON.stringify({ ...body, projectName: this.projectName }) : "none"}`);
341
- try {
342
- const response = await fetch(url, {
343
- method,
344
- headers: {
345
- "Content-Type": "application/json",
346
- "Authorization": `Bearer ${this.contextManager.apiSecret || ""}`
347
- },
348
- body: body ? JSON.stringify({ ...body, projectName: this.projectName }) : void 0
349
- });
350
- logger.info(`[callCoordination] Response status: ${response.status} ${response.statusText}`);
351
- if (!response.ok) {
352
- const text = await response.text();
353
- logger.error(`[callCoordination] API Error Response (${response.status}): ${text}`);
354
- if (response.status === 401) {
355
- throw new Error(`Authentication failed (401): ${text}. Check if API key is valid and exists in api_keys table.`);
356
- } else if (response.status === 500) {
357
- throw new Error(`Server error (500): ${text}. Check Vercel logs for details.`);
358
- } else {
359
- throw new Error(`API Error (${response.status}): ${text}`);
418
+ const maxRetries = 3;
419
+ const baseDelay = 1e3;
420
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
421
+ const controller = new AbortController();
422
+ const timeout = setTimeout(() => controller.abort(), 1e4);
423
+ try {
424
+ const response = await fetch(url, {
425
+ method,
426
+ headers: {
427
+ "Content-Type": "application/json",
428
+ "Authorization": `Bearer ${this.contextManager.apiSecret || ""}`
429
+ },
430
+ body: body ? JSON.stringify({ ...body, projectName: this.projectName }) : void 0,
431
+ signal: controller.signal
432
+ });
433
+ clearTimeout(timeout);
434
+ logger.info(`[callCoordination] Response status: ${response.status} ${response.statusText}`);
435
+ if (!response.ok) {
436
+ const text = await response.text();
437
+ logger.error(`[callCoordination] API Error Response (${response.status}): ${text}`);
438
+ if (response.status >= 400 && response.status < 500) {
439
+ if (response.status === 401) {
440
+ throw new Error(`Authentication failed (401): ${text}. Check if API key is valid and exists in api_keys table.`);
441
+ }
442
+ throw new Error(`API Error (${response.status}): ${text}`);
443
+ }
444
+ if (attempt < maxRetries) {
445
+ const delay = baseDelay * Math.pow(2, attempt - 1);
446
+ logger.warn(`[callCoordination] 5xx error, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
447
+ await new Promise((r) => setTimeout(r, delay));
448
+ continue;
449
+ }
450
+ this._circuitFailures++;
451
+ if (this._circuitFailures >= CIRCUIT_FAILURE_THRESHOLD) {
452
+ this._circuitOpenUntil = Date.now() + CIRCUIT_COOLDOWN_MS;
453
+ logger.error(`[callCoordination] Circuit breaker OPENED after ${this._circuitFailures} consecutive failures`);
454
+ }
455
+ throw new Error(`Server error (${response.status}): ${text}. Check Vercel logs for details.`);
360
456
  }
457
+ if (this._circuitFailures > 0) {
458
+ logger.info(`[callCoordination] Request succeeded, resetting circuit breaker (was at ${this._circuitFailures} failures)`);
459
+ this._circuitFailures = 0;
460
+ this._circuitOpenUntil = 0;
461
+ }
462
+ const jsonResult = await response.json();
463
+ logger.info(`[callCoordination] Success - Response: ${JSON.stringify(jsonResult).substring(0, 200)}...`);
464
+ return jsonResult;
465
+ } catch (e) {
466
+ clearTimeout(timeout);
467
+ if (e instanceof CircuitOpenError) throw e;
468
+ if (e.message.includes("Authentication failed") || e.message.includes("API Error (4")) {
469
+ throw e;
470
+ }
471
+ if (attempt < maxRetries) {
472
+ const delay = baseDelay * Math.pow(2, attempt - 1);
473
+ logger.warn(`[callCoordination] Network/timeout error, retrying in ${delay}ms (attempt ${attempt}/${maxRetries}): ${e.message}`);
474
+ await new Promise((r) => setTimeout(r, delay));
475
+ continue;
476
+ }
477
+ this._circuitFailures++;
478
+ if (this._circuitFailures >= CIRCUIT_FAILURE_THRESHOLD) {
479
+ this._circuitOpenUntil = Date.now() + CIRCUIT_COOLDOWN_MS;
480
+ logger.error(`[callCoordination] Circuit breaker OPENED after ${this._circuitFailures} consecutive failures`);
481
+ }
482
+ logger.error(`[callCoordination] Fetch failed after ${maxRetries} attempts: ${e.message}`, e);
483
+ if (e.message.includes("401")) {
484
+ throw new Error(`API Authentication Error: ${e.message}. Verify AXIS_API_KEY in MCP config matches a key in the api_keys table.`);
485
+ }
486
+ throw e;
361
487
  }
362
- const jsonResult = await response.json();
363
- logger.info(`[callCoordination] Success - Response: ${JSON.stringify(jsonResult).substring(0, 200)}...`);
364
- return jsonResult;
365
- } catch (e) {
366
- logger.error(`[callCoordination] Fetch failed: ${e.message}`, e);
367
- if (e.message.includes("Authentication failed") || e.message.includes("401")) {
368
- throw new Error(`API Authentication Error: ${e.message}. Verify AXIS_API_KEY in MCP config matches a key in the api_keys table.`);
369
- }
370
- throw e;
371
488
  }
489
+ throw new Error("callCoordination: unexpected end of retry loop");
372
490
  }
373
491
  jobFromRecord(record) {
374
492
  return {
@@ -479,6 +597,7 @@ var NerveCenter = class {
479
597
  p_text: text
480
598
  });
481
599
  } catch (e) {
600
+ logger.warn("Notepad RPC append failed", e);
482
601
  }
483
602
  }
484
603
  if (this.contextManager.apiUrl) {
@@ -955,7 +1074,7 @@ var RagEngine = class {
955
1074
  }
956
1075
  async indexContent(filePath, content) {
957
1076
  if (!this.projectId) {
958
- console.error("RAG: Project ID missing.");
1077
+ logger.error("RAG: Project ID missing.");
959
1078
  return false;
960
1079
  }
961
1080
  try {
@@ -973,13 +1092,13 @@ var RagEngine = class {
973
1092
  metadata: { filePath }
974
1093
  });
975
1094
  if (error) {
976
- console.error("RAG Insert Error:", error);
1095
+ logger.error("RAG Insert Error:", error);
977
1096
  return false;
978
1097
  }
979
1098
  logger.info(`Indexed ${filePath}`);
980
1099
  return true;
981
1100
  } catch (e) {
982
- console.error("RAG Error:", e);
1101
+ logger.error("RAG Error:", e);
983
1102
  return false;
984
1103
  }
985
1104
  }
@@ -998,12 +1117,12 @@ var RagEngine = class {
998
1117
  p_project_id: this.projectId
999
1118
  });
1000
1119
  if (error || !data) {
1001
- console.error("RAG Search DB Error:", error);
1120
+ logger.error("RAG Search DB Error:", error);
1002
1121
  return [];
1003
1122
  }
1004
1123
  return data.map((d) => d.content);
1005
1124
  } catch (e) {
1006
- console.error("RAG Search Fail:", e);
1125
+ logger.error("RAG Search Fail:", e);
1007
1126
  return [];
1008
1127
  }
1009
1128
  }
@@ -1037,7 +1156,7 @@ if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
1037
1156
  }
1038
1157
  if (!envLoaded) {
1039
1158
  logger.warn("No configuration found from MCP client (mcp.json) or .env.local");
1040
- logger.warn("MCP server will use default API URL: https://aicontext.vercel.app/api/v1");
1159
+ logger.warn("MCP server will use default API URL: https://useaxis.dev/api/v1");
1041
1160
  }
1042
1161
  }
1043
1162
  logger.info("=== Axis MCP Server Starting ===");
@@ -1049,7 +1168,7 @@ logger.info("Environment check:", {
1049
1168
  hasSUPABASE_SERVICE_ROLE_KEY: !!process.env.SUPABASE_SERVICE_ROLE_KEY,
1050
1169
  PROJECT_NAME: process.env.PROJECT_NAME || "default"
1051
1170
  });
1052
- var apiUrl = process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_URL || "https://aicontext.vercel.app/api/v1";
1171
+ var apiUrl = process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_URL || "https://useaxis.dev/api/v1";
1053
1172
  var apiSecret = process.env.AXIS_API_KEY || process.env.SHARED_CONTEXT_API_SECRET || process.env.AXIS_API_SECRET;
1054
1173
  var useRemoteApiOnly = !!process.env.SHARED_CONTEXT_API_URL || !!process.env.AXIS_API_KEY;
1055
1174
  if (useRemoteApiOnly) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virsanghavi/axis-server",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "Axis MCP Server CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {