@virsanghavi/axis-server 1.0.9 → 1.1.1
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/.axis/instructions/context.md +30 -1
- package/.axis/instructions/conventions.md +61 -1
- package/dist/mcp-server.mjs +221 -102
- package/package.json +1 -1
|
@@ -1,2 +1,31 @@
|
|
|
1
|
-
# Project Context
|
|
1
|
+
# Axis — Project Context
|
|
2
2
|
|
|
3
|
+
## Overview
|
|
4
|
+
Axis is a distributed orchestration layer for parallel AI coding agents. It enables multiple agents (Cursor, Claude Code, Windsurf, Codex, Antigravity, etc.) to work on the same codebase simultaneously without collisions or context drift.
|
|
5
|
+
|
|
6
|
+
The core value proposition: **agents that coordinate like a team, not individuals who overwrite each other.**
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
- **Frontend**: Next.js 14 (App Router) deployed on Vercel. Tailwind CSS, Framer Motion. Auth via Supabase.
|
|
10
|
+
- **Backend**: Next.js API routes + Supabase (Postgres, RLS, RPC functions). Stripe for billing.
|
|
11
|
+
- **MCP Server**: `@virsanghavi/axis-server` — an npm package that exposes Axis tools via the Model Context Protocol. Runs locally in each agent's IDE.
|
|
12
|
+
- **State**: All shared state (locks, jobs, notepad, sessions, embeddings) lives in Supabase, scoped per project. The MCP server syncs local state with the remote API.
|
|
13
|
+
- **File structure**:
|
|
14
|
+
- `shared-context/frontend/` — the web app (dashboard, billing, docs, auth)
|
|
15
|
+
- `shared-context/packages/axis-server/` — the MCP server package
|
|
16
|
+
- `shared-context/supabase/` — schema, migrations, RPC functions
|
|
17
|
+
- `.axis/instructions/` — soul files read by all agents via MCP
|
|
18
|
+
|
|
19
|
+
## Core Features
|
|
20
|
+
1. **Job Board**: Agents post, claim, and complete tasks. Priority-based, dependency-aware. Prevents duplicate work.
|
|
21
|
+
2. **File Locking**: Atomic, per-file locks with 30-minute TTL. Agents call `propose_file_access` before editing. Prevents merge conflicts.
|
|
22
|
+
3. **Live Notepad**: Real-time shared memory. Agents log progress so others know what's happening. Cleared on `finalize_session`.
|
|
23
|
+
4. **Context Mirroring**: `get_project_soul` returns this file + conventions to ground agents in project reality.
|
|
24
|
+
5. **RAG Search**: `search_codebase` and `search_docs` for semantic search over indexed files and documentation.
|
|
25
|
+
6. **Session Management**: `finalize_session` archives the notepad, clears locks, resets for new work.
|
|
26
|
+
7. **Billing**: Stripe-based Pro tier ($25/mo) with API key management, usage tracking, and retention flow.
|
|
27
|
+
|
|
28
|
+
## Deployment
|
|
29
|
+
- Frontend: Vercel (auto-deploy from `shared-context/frontend/`)
|
|
30
|
+
- Database: Supabase (hosted Postgres)
|
|
31
|
+
- MCP Server: Published to npm, run locally via `npx @virsanghavi/axis-server`
|
|
@@ -1,2 +1,62 @@
|
|
|
1
|
-
# Coding Conventions
|
|
1
|
+
# Axis — Coding Conventions & Agent Norms
|
|
2
2
|
|
|
3
|
+
## Language Standards
|
|
4
|
+
- **TypeScript** for all frontend and API code. Strict mode. No `any` unless absolutely necessary.
|
|
5
|
+
- **SQL** for Supabase migrations. Use `IF NOT EXISTS` / `IF EXISTS` for idempotency.
|
|
6
|
+
- **HTML/CSS/JS** for standalone tools (e.g. sandbox apps). Single-file, no frameworks, no build step.
|
|
7
|
+
|
|
8
|
+
## Styling
|
|
9
|
+
- Tailwind CSS exclusively. No custom CSS files unless for animations.
|
|
10
|
+
- Dark theme: `bg-[#050505]`, `text-white`, `border-white/5`. Light panels: `bg-white/95`, `text-neutral-900`.
|
|
11
|
+
- Typography: `lowercase` class on page wrappers. `font-mono` for technical content. `tracking-tight` default.
|
|
12
|
+
- Components: Minimal, no component library. Custom components in `components/`.
|
|
13
|
+
|
|
14
|
+
## Testing
|
|
15
|
+
- Manual testing via browser and MCP tool calls.
|
|
16
|
+
- Health endpoint at `/api/health` checks Supabase and Stripe connectivity.
|
|
17
|
+
|
|
18
|
+
## Code Patterns
|
|
19
|
+
- API routes use `getSessionFromRequest` for auth, `getClientIp` + `rateLimit` for rate limiting.
|
|
20
|
+
- Supabase queries use `.ilike('email', ...)` for case-insensitive email matching.
|
|
21
|
+
- Stripe customer IDs come from DB (`profiles.stripe_customer_id`). Never hardcode customer IDs.
|
|
22
|
+
- All Stripe routes have "no such customer" self-healing: look up by email, update DB, retry.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Agent Behavioral Norms
|
|
27
|
+
|
|
28
|
+
### Plan Before Write — The Core Invariant
|
|
29
|
+
|
|
30
|
+
**No agent writes code unless it either owns a file lock OR has explicitly declined the job board for a scoped reason.**
|
|
31
|
+
|
|
32
|
+
On non-trivial tasks (2+ files, new features, refactors):
|
|
33
|
+
1. Break work into jobs → `post_job`
|
|
34
|
+
2. Claim before editing → `claim_next_job`
|
|
35
|
+
3. Lock before writing → `propose_file_access` with a **descriptive intent**
|
|
36
|
+
4. Complete when done → `complete_job` with outcome
|
|
37
|
+
|
|
38
|
+
Direct edits without a job are allowed only for:
|
|
39
|
+
- Single-line fixes, typos, config tweaks
|
|
40
|
+
- Clearly scoped changes the user asked for directly
|
|
41
|
+
|
|
42
|
+
### Force Unlock Policy
|
|
43
|
+
|
|
44
|
+
`force_unlock` is a **last resort, not a convenience tool.**
|
|
45
|
+
|
|
46
|
+
Rules:
|
|
47
|
+
1. **Never** call `force_unlock` on a file you didn't lock unless:
|
|
48
|
+
- The lock has been held for >25 minutes (close to TTL expiry), AND
|
|
49
|
+
- The locking agent is clearly not responding or has crashed
|
|
50
|
+
2. **Always** provide a specific reason (e.g. "Agent claude-code crashed 20 minutes ago, lock on auth.ts is blocking progress")
|
|
51
|
+
3. **Never** force-unlock to skip coordination. If another agent holds a lock, work on something else.
|
|
52
|
+
4. Prefer waiting for TTL expiry (30 min) over force-unlocking.
|
|
53
|
+
|
|
54
|
+
### Lock Hygiene
|
|
55
|
+
- Always provide descriptive `intent` when locking (e.g. "Refactor auth middleware to use JWT validation" — not "editing file")
|
|
56
|
+
- Release locks early by completing jobs when done
|
|
57
|
+
- Call `finalize_session` at end of session to clean up all locks
|
|
58
|
+
|
|
59
|
+
### Shared Memory
|
|
60
|
+
- Call `update_shared_context` after completing meaningful steps
|
|
61
|
+
- Log decisions, not just actions (e.g. "Chose JWT over session tokens because...")
|
|
62
|
+
- Other agents read the notepad in real-time — write for them
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
(
|
|
150
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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://
|
|
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://
|
|
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) {
|