@virsanghavi/axis-server 1.0.8 → 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.
- package/.axis/instructions/activity.md +2 -0
- package/.axis/instructions/context.md +2 -0
- package/.axis/instructions/conventions.md +2 -0
- package/.axis-server.log +9 -0
- package/bin/cli.ts +2 -2
- package/dist/cli.js +0 -1
- package/dist/mcp-server.mjs +971 -514
- package/package.json +1 -1
package/dist/mcp-server.mjs
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// ../../src/local/mcp-server.ts
|
|
9
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -20,25 +13,83 @@ import dotenv2 from "dotenv";
|
|
|
20
13
|
import fs from "fs/promises";
|
|
21
14
|
import path from "path";
|
|
22
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
|
|
52
|
+
import * as fsSync from "fs";
|
|
23
53
|
function getEffectiveInstructionsDir() {
|
|
24
54
|
const cwd = process.cwd();
|
|
25
55
|
const axisDir = path.resolve(cwd, ".axis");
|
|
26
56
|
const instructionsDir = path.resolve(axisDir, "instructions");
|
|
27
57
|
const legacyDir = path.resolve(cwd, "agent-instructions");
|
|
58
|
+
const sharedContextDir = path.resolve(cwd, "shared-context", "agent-instructions");
|
|
59
|
+
try {
|
|
60
|
+
if (fsSync.existsSync(instructionsDir)) {
|
|
61
|
+
console.error(`[ContextManager] Using instructions dir: ${instructionsDir}`);
|
|
62
|
+
return instructionsDir;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
if (fsSync.existsSync(legacyDir)) {
|
|
68
|
+
console.error(`[ContextManager] Using legacy dir: ${legacyDir}`);
|
|
69
|
+
return legacyDir;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
28
73
|
try {
|
|
29
|
-
if (
|
|
74
|
+
if (fsSync.existsSync(sharedContextDir)) {
|
|
75
|
+
console.error(`[ContextManager] Using shared-context dir: ${sharedContextDir}`);
|
|
76
|
+
return sharedContextDir;
|
|
77
|
+
}
|
|
30
78
|
} catch {
|
|
31
79
|
}
|
|
80
|
+
console.error(`[ContextManager] Fallback to legacy dir: ${legacyDir}`);
|
|
32
81
|
return legacyDir;
|
|
33
82
|
}
|
|
34
83
|
var ContextManager = class {
|
|
35
84
|
mutex;
|
|
36
85
|
apiUrl;
|
|
86
|
+
// Made public so NerveCenter can access it
|
|
37
87
|
apiSecret;
|
|
38
|
-
|
|
88
|
+
// Made public so NerveCenter can access it
|
|
89
|
+
constructor(apiUrl2, apiSecret2) {
|
|
39
90
|
this.mutex = new Mutex();
|
|
40
|
-
this.apiUrl =
|
|
41
|
-
this.apiSecret =
|
|
91
|
+
this.apiUrl = apiUrl2;
|
|
92
|
+
this.apiSecret = apiSecret2;
|
|
42
93
|
}
|
|
43
94
|
resolveFilePath(filename) {
|
|
44
95
|
if (!filename || filename.includes("\0")) {
|
|
@@ -116,45 +167,105 @@ var ContextManager = class {
|
|
|
116
167
|
throw new Error("SHARED_CONTEXT_API_URL not configured.");
|
|
117
168
|
}
|
|
118
169
|
const endpoint = this.apiUrl.endsWith("/v1") ? `${this.apiUrl}/search` : `${this.apiUrl}/v1/search`;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
(
|
|
135
|
-
|
|
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
|
+
}
|
|
136
217
|
}
|
|
137
|
-
throw new Error("
|
|
218
|
+
throw new Error("searchContext: unexpected end of retry loop");
|
|
138
219
|
}
|
|
139
220
|
async embedContent(items, projectName = "default") {
|
|
140
221
|
if (!this.apiUrl) {
|
|
141
|
-
|
|
222
|
+
logger.warn("Skipping RAG embedding: SHARED_CONTEXT_API_URL not configured.");
|
|
142
223
|
return;
|
|
143
224
|
}
|
|
144
225
|
const endpoint = this.apiUrl.endsWith("/v1") ? `${this.apiUrl}/embed` : `${this.apiUrl}/v1/embed`;
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|
|
156
268
|
}
|
|
157
|
-
return await response.json();
|
|
158
269
|
}
|
|
159
270
|
};
|
|
160
271
|
|
|
@@ -163,44 +274,16 @@ import { Mutex as Mutex2 } from "async-mutex";
|
|
|
163
274
|
import { createClient } from "@supabase/supabase-js";
|
|
164
275
|
import fs2 from "fs/promises";
|
|
165
276
|
import path2 from "path";
|
|
166
|
-
|
|
167
|
-
// ../../src/utils/logger.ts
|
|
168
|
-
var Logger = class {
|
|
169
|
-
level = "info" /* INFO */;
|
|
170
|
-
setLevel(level) {
|
|
171
|
-
this.level = level;
|
|
172
|
-
}
|
|
173
|
-
log(level, message, meta) {
|
|
174
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
175
|
-
console.error(JSON.stringify({
|
|
176
|
-
timestamp,
|
|
177
|
-
level,
|
|
178
|
-
message,
|
|
179
|
-
...meta
|
|
180
|
-
}));
|
|
181
|
-
}
|
|
182
|
-
debug(message, meta) {
|
|
183
|
-
if (this.level === "debug" /* DEBUG */) this.log("debug" /* DEBUG */, message, meta);
|
|
184
|
-
}
|
|
185
|
-
info(message, meta) {
|
|
186
|
-
this.log("info" /* INFO */, message, meta);
|
|
187
|
-
}
|
|
188
|
-
warn(message, meta) {
|
|
189
|
-
this.log("warn" /* WARN */, message, meta);
|
|
190
|
-
}
|
|
191
|
-
error(message, error, meta) {
|
|
192
|
-
this.log("error" /* ERROR */, message, {
|
|
193
|
-
...meta,
|
|
194
|
-
error: error instanceof Error ? error.message : String(error),
|
|
195
|
-
stack: error instanceof Error ? error.stack : void 0
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
var logger = new Logger();
|
|
200
|
-
|
|
201
|
-
// ../../src/local/nerve-center.ts
|
|
202
277
|
var STATE_FILE = process.env.NERVE_CENTER_STATE_FILE || path2.join(process.cwd(), "history", "nerve-center-state.json");
|
|
203
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
|
+
};
|
|
204
287
|
var NerveCenter = class {
|
|
205
288
|
mutex;
|
|
206
289
|
state;
|
|
@@ -212,6 +295,8 @@ var NerveCenter = class {
|
|
|
212
295
|
// Renamed backing field
|
|
213
296
|
projectName;
|
|
214
297
|
useSupabase;
|
|
298
|
+
_circuitFailures = 0;
|
|
299
|
+
_circuitOpenUntil = 0;
|
|
215
300
|
/**
|
|
216
301
|
* @param contextManager - Instance of ContextManager for legacy operations
|
|
217
302
|
* @param options - Configuration options for state persistence and timeouts
|
|
@@ -221,16 +306,27 @@ var NerveCenter = class {
|
|
|
221
306
|
this.contextManager = contextManager;
|
|
222
307
|
this.stateFilePath = options.stateFilePath || STATE_FILE;
|
|
223
308
|
this.lockTimeout = options.lockTimeout || LOCK_TIMEOUT_DEFAULT;
|
|
224
|
-
|
|
225
|
-
const supabaseUrl = options.supabaseUrl
|
|
226
|
-
const supabaseKey = options.supabaseServiceRoleKey
|
|
227
|
-
if (
|
|
228
|
-
|
|
309
|
+
const hasRemoteApi = !!this.contextManager.apiUrl;
|
|
310
|
+
const supabaseUrl = options.supabaseUrl !== void 0 ? options.supabaseUrl : hasRemoteApi ? null : process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
311
|
+
const supabaseKey = options.supabaseServiceRoleKey !== void 0 ? options.supabaseServiceRoleKey : hasRemoteApi ? null : process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
312
|
+
if (supabaseUrl && supabaseKey) {
|
|
313
|
+
this.supabase = createClient(supabaseUrl, supabaseKey);
|
|
314
|
+
this.useSupabase = true;
|
|
315
|
+
logger.info("NerveCenter: Using direct Supabase persistence.");
|
|
316
|
+
} else if (this.contextManager.apiUrl) {
|
|
229
317
|
this.supabase = void 0;
|
|
230
318
|
this.useSupabase = false;
|
|
319
|
+
logger.info(`NerveCenter: Using Remote API persistence (${this.contextManager.apiUrl})`);
|
|
231
320
|
} else {
|
|
232
|
-
this.supabase =
|
|
233
|
-
this.useSupabase =
|
|
321
|
+
this.supabase = void 0;
|
|
322
|
+
this.useSupabase = false;
|
|
323
|
+
logger.warn("NerveCenter: Running in local-only mode. Coordination restricted to this machine.");
|
|
324
|
+
}
|
|
325
|
+
const explicitProjectName = options.projectName || process.env.PROJECT_NAME;
|
|
326
|
+
if (explicitProjectName) {
|
|
327
|
+
this.projectName = explicitProjectName;
|
|
328
|
+
} else {
|
|
329
|
+
this.projectName = "default";
|
|
234
330
|
}
|
|
235
331
|
this.state = {
|
|
236
332
|
locks: {},
|
|
@@ -246,10 +342,27 @@ var NerveCenter = class {
|
|
|
246
342
|
}
|
|
247
343
|
async init() {
|
|
248
344
|
await this.loadState();
|
|
249
|
-
|
|
345
|
+
if (this.projectName === "default" && (this.useSupabase || !this.contextManager.apiUrl)) {
|
|
346
|
+
await this.detectProjectName();
|
|
347
|
+
}
|
|
250
348
|
if (this.useSupabase) {
|
|
251
349
|
await this.ensureProjectId();
|
|
252
350
|
}
|
|
351
|
+
if (this.contextManager.apiUrl) {
|
|
352
|
+
try {
|
|
353
|
+
const { liveNotepad, projectId } = await this.callCoordination(`sessions/sync?projectName=${this.projectName}`);
|
|
354
|
+
if (projectId) {
|
|
355
|
+
this._projectId = projectId;
|
|
356
|
+
logger.info(`NerveCenter: Resolved projectId from cloud: ${this._projectId}`);
|
|
357
|
+
}
|
|
358
|
+
if (liveNotepad && (!this.state.liveNotepad || this.state.liveNotepad.startsWith("Session Start:"))) {
|
|
359
|
+
this.state.liveNotepad = liveNotepad;
|
|
360
|
+
logger.info(`NerveCenter: Recovered live notepad from cloud for project: ${this.projectName}`);
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
logger.warn("Failed to sync project/notepad with Remote API. Using local/fallback.", e);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
253
366
|
}
|
|
254
367
|
async detectProjectName() {
|
|
255
368
|
try {
|
|
@@ -259,12 +372,16 @@ var NerveCenter = class {
|
|
|
259
372
|
if (config.project) {
|
|
260
373
|
this.projectName = config.project;
|
|
261
374
|
logger.info(`Detected project name from .axis/axis.json: ${this.projectName}`);
|
|
375
|
+
console.error(`[NerveCenter] Loaded project name '${this.projectName}' from ${axisConfigPath}`);
|
|
376
|
+
} else {
|
|
377
|
+
console.error(`[NerveCenter] .axis/axis.json found but no 'project' field.`);
|
|
262
378
|
}
|
|
263
379
|
} catch (e) {
|
|
380
|
+
console.error(`[NerveCenter] Could not load .axis/axis.json at ${path2.join(process.cwd(), ".axis", "axis.json")}: ${e}`);
|
|
264
381
|
}
|
|
265
382
|
}
|
|
266
383
|
async ensureProjectId() {
|
|
267
|
-
if (!this.supabase) return;
|
|
384
|
+
if (!this.supabase || this._projectId) return;
|
|
268
385
|
const { data: project, error } = await this.supabase.from("projects").select("id").eq("name", this.projectName).maybeSingle();
|
|
269
386
|
if (error) {
|
|
270
387
|
logger.error("Failed to load project", error);
|
|
@@ -281,6 +398,96 @@ var NerveCenter = class {
|
|
|
281
398
|
}
|
|
282
399
|
this._projectId = created.id;
|
|
283
400
|
}
|
|
401
|
+
async callCoordination(endpoint, method = "GET", body) {
|
|
402
|
+
logger.info(`[callCoordination] Starting - endpoint: ${endpoint}, method: ${method}`);
|
|
403
|
+
logger.info(`[callCoordination] apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET (" + this.contextManager.apiSecret.substring(0, 10) + "...)" : "NOT SET"}`);
|
|
404
|
+
if (!this.contextManager.apiUrl) {
|
|
405
|
+
logger.error("[callCoordination] Remote API not configured - apiUrl is:", this.contextManager.apiUrl);
|
|
406
|
+
throw new Error("Remote API not configured");
|
|
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
|
+
}
|
|
415
|
+
const url = this.contextManager.apiUrl.endsWith("/v1") ? `${this.contextManager.apiUrl}/${endpoint}` : `${this.contextManager.apiUrl}/v1/${endpoint}`;
|
|
416
|
+
logger.info(`[callCoordination] Full URL: ${method} ${url}`);
|
|
417
|
+
logger.info(`[callCoordination] Request body: ${body ? JSON.stringify({ ...body, projectName: this.projectName }) : "none"}`);
|
|
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.`);
|
|
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;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
throw new Error("callCoordination: unexpected end of retry loop");
|
|
490
|
+
}
|
|
284
491
|
jobFromRecord(record) {
|
|
285
492
|
return {
|
|
286
493
|
id: record.id,
|
|
@@ -290,69 +497,123 @@ var NerveCenter = class {
|
|
|
290
497
|
status: record.status,
|
|
291
498
|
assignedTo: record.assigned_to || void 0,
|
|
292
499
|
dependencies: record.dependencies || void 0,
|
|
500
|
+
completionKey: record.completion_key || void 0,
|
|
293
501
|
createdAt: Date.parse(record.created_at),
|
|
294
502
|
updatedAt: Date.parse(record.updated_at)
|
|
295
503
|
};
|
|
296
504
|
}
|
|
297
505
|
// --- Data Access Layers (Hybrid: Supabase > Local) ---
|
|
298
506
|
async listJobs() {
|
|
299
|
-
if (
|
|
300
|
-
|
|
507
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
508
|
+
const { data, error } = await this.supabase.from("jobs").select("id,title,description,priority,status,assigned_to,dependencies,created_at,updated_at").eq("project_id", this._projectId);
|
|
509
|
+
if (error || !data) {
|
|
510
|
+
logger.error("Failed to load jobs from Supabase", error);
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
return data.map((record) => this.jobFromRecord(record));
|
|
301
514
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
515
|
+
if (this.contextManager.apiUrl) {
|
|
516
|
+
try {
|
|
517
|
+
const url = `jobs?projectName=${this.projectName}`;
|
|
518
|
+
const res = await this.callCoordination(url);
|
|
519
|
+
return (res.jobs || []).map((record) => this.jobFromRecord(record));
|
|
520
|
+
} catch (e) {
|
|
521
|
+
logger.error("Failed to load jobs from API", e);
|
|
522
|
+
return Object.values(this.state.jobs);
|
|
523
|
+
}
|
|
306
524
|
}
|
|
307
|
-
return
|
|
525
|
+
return Object.values(this.state.jobs);
|
|
308
526
|
}
|
|
309
527
|
async getLocks() {
|
|
310
|
-
|
|
311
|
-
|
|
528
|
+
logger.info(`[getLocks] Starting - projectName: ${this.projectName}`);
|
|
529
|
+
logger.info(`[getLocks] Config - apiUrl: ${this.contextManager.apiUrl}, useSupabase: ${this.useSupabase}, hasSupabase: ${!!this.supabase}`);
|
|
530
|
+
if (this.contextManager.apiUrl) {
|
|
531
|
+
if (!this.useSupabase || !this.supabase) {
|
|
532
|
+
try {
|
|
533
|
+
logger.info(`[getLocks] Fetching locks from API for project: ${this.projectName}`);
|
|
534
|
+
const res = await this.callCoordination(`locks?projectName=${this.projectName}`);
|
|
535
|
+
logger.info(`[getLocks] API returned ${res.locks?.length || 0} locks`);
|
|
536
|
+
return (res.locks || []).map((row) => ({
|
|
537
|
+
agentId: row.agent_id,
|
|
538
|
+
filePath: row.file_path,
|
|
539
|
+
intent: row.intent,
|
|
540
|
+
userPrompt: row.user_prompt,
|
|
541
|
+
timestamp: Date.parse(row.updated_at || row.timestamp)
|
|
542
|
+
}));
|
|
543
|
+
} catch (e) {
|
|
544
|
+
logger.error(`[getLocks] Failed to fetch locks from API: ${e.message}`, e);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
312
547
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
548
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
549
|
+
try {
|
|
550
|
+
await this.supabase.rpc("clean_stale_locks", {
|
|
551
|
+
p_project_id: this._projectId,
|
|
552
|
+
p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
|
|
553
|
+
});
|
|
554
|
+
const { data, error } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId);
|
|
555
|
+
if (error) throw error;
|
|
556
|
+
return (data || []).map((row) => ({
|
|
557
|
+
agentId: row.agent_id,
|
|
558
|
+
filePath: row.file_path,
|
|
559
|
+
intent: row.intent,
|
|
560
|
+
userPrompt: row.user_prompt,
|
|
561
|
+
timestamp: Date.parse(row.updated_at)
|
|
562
|
+
}));
|
|
563
|
+
} catch (e) {
|
|
564
|
+
logger.warn("Failed to fetch locks from DB", e);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (this.contextManager.apiUrl) {
|
|
568
|
+
try {
|
|
569
|
+
const res = await this.callCoordination(`locks?projectName=${this.projectName}`);
|
|
570
|
+
return (res.locks || []).map((row) => ({
|
|
571
|
+
agentId: row.agent_id,
|
|
572
|
+
filePath: row.file_path,
|
|
573
|
+
intent: row.intent,
|
|
574
|
+
userPrompt: row.user_prompt,
|
|
575
|
+
timestamp: Date.parse(row.updated_at || row.timestamp)
|
|
576
|
+
}));
|
|
577
|
+
} catch (e) {
|
|
578
|
+
logger.error("Failed to fetch locks from API in fallback", e);
|
|
579
|
+
}
|
|
330
580
|
}
|
|
581
|
+
return Object.values(this.state.locks);
|
|
331
582
|
}
|
|
332
583
|
async getNotepad() {
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const { data, error } = await this.supabase.from("projects").select("live_notepad").eq("id", this._projectId).single();
|
|
337
|
-
if (error || !data) {
|
|
338
|
-
logger.error("Failed to fetch notepad", error);
|
|
339
|
-
return this.state.liveNotepad;
|
|
584
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
585
|
+
const { data, error } = await this.supabase.from("projects").select("live_notepad").eq("id", this._projectId).single();
|
|
586
|
+
if (!error && data) return data.live_notepad || "";
|
|
340
587
|
}
|
|
341
|
-
return
|
|
588
|
+
return this.state.liveNotepad;
|
|
342
589
|
}
|
|
343
590
|
async appendToNotepad(text) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
591
|
+
this.state.liveNotepad += text;
|
|
592
|
+
await this.saveState();
|
|
593
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
594
|
+
try {
|
|
595
|
+
await this.supabase.rpc("append_to_project_notepad", {
|
|
596
|
+
p_project_id: this._projectId,
|
|
597
|
+
p_text: text
|
|
598
|
+
});
|
|
599
|
+
} catch (e) {
|
|
600
|
+
logger.warn("Notepad RPC append failed", e);
|
|
601
|
+
}
|
|
348
602
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
603
|
+
if (this.contextManager.apiUrl) {
|
|
604
|
+
try {
|
|
605
|
+
const res = await this.callCoordination("sessions/sync", "POST", {
|
|
606
|
+
title: `Current Session: ${this.projectName}`,
|
|
607
|
+
context: this.state.liveNotepad,
|
|
608
|
+
metadata: { source: "mcp-server-live" }
|
|
609
|
+
});
|
|
610
|
+
if (res.projectId && !this._projectId) {
|
|
611
|
+
this._projectId = res.projectId;
|
|
612
|
+
logger.info(`NerveCenter: Captured projectId from sync API: ${this._projectId}`);
|
|
613
|
+
}
|
|
614
|
+
} catch (e) {
|
|
615
|
+
logger.warn("Failed to sync notepad to remote API", e);
|
|
616
|
+
}
|
|
356
617
|
}
|
|
357
618
|
}
|
|
358
619
|
async saveState() {
|
|
@@ -375,25 +636,35 @@ var NerveCenter = class {
|
|
|
375
636
|
async postJob(title, description, priority = "medium", dependencies = []) {
|
|
376
637
|
return await this.mutex.runExclusive(async () => {
|
|
377
638
|
let id = `job-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
|
|
639
|
+
const completionKey = Math.random().toString(36).substring(2, 10).toUpperCase();
|
|
378
640
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
379
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
380
641
|
const { data, error } = await this.supabase.from("jobs").insert({
|
|
381
642
|
project_id: this._projectId,
|
|
382
643
|
title,
|
|
383
644
|
description,
|
|
384
645
|
priority,
|
|
385
646
|
status: "todo",
|
|
386
|
-
assigned_to: null,
|
|
387
647
|
dependencies,
|
|
388
|
-
|
|
389
|
-
updated_at: now
|
|
648
|
+
completion_key: completionKey
|
|
390
649
|
}).select("id").single();
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
650
|
+
if (data?.id) id = data.id;
|
|
651
|
+
if (error) logger.error("Failed to post job to Supabase", error);
|
|
652
|
+
} else if (this.contextManager.apiUrl) {
|
|
653
|
+
try {
|
|
654
|
+
const data = await this.callCoordination("jobs", "POST", {
|
|
655
|
+
action: "post",
|
|
656
|
+
title,
|
|
657
|
+
description,
|
|
658
|
+
priority,
|
|
659
|
+
dependencies,
|
|
660
|
+
completion_key: completionKey
|
|
661
|
+
});
|
|
662
|
+
if (data?.id) id = data.id;
|
|
663
|
+
} catch (e) {
|
|
664
|
+
logger.error("Failed to post job to API", e);
|
|
395
665
|
}
|
|
396
|
-
}
|
|
666
|
+
}
|
|
667
|
+
if (!this.useSupabase && !this.contextManager.apiUrl) {
|
|
397
668
|
this.state.jobs[id] = {
|
|
398
669
|
id,
|
|
399
670
|
title,
|
|
@@ -402,21 +673,54 @@ var NerveCenter = class {
|
|
|
402
673
|
dependencies,
|
|
403
674
|
status: "todo",
|
|
404
675
|
createdAt: Date.now(),
|
|
405
|
-
updatedAt: Date.now()
|
|
676
|
+
updatedAt: Date.now(),
|
|
677
|
+
completionKey
|
|
406
678
|
};
|
|
407
679
|
}
|
|
408
680
|
const depText = dependencies.length ? ` (Depends on: ${dependencies.join(", ")})` : "";
|
|
409
681
|
const logEntry = `
|
|
410
682
|
- [JOB POSTED] [${priority.toUpperCase()}] ${title} (ID: ${id})${depText}`;
|
|
411
683
|
await this.appendToNotepad(logEntry);
|
|
412
|
-
|
|
413
|
-
return { jobId: id, status: "POSTED" };
|
|
684
|
+
return { jobId: id, status: "POSTED", completionKey };
|
|
414
685
|
});
|
|
415
686
|
}
|
|
416
687
|
async claimNextJob(agentId) {
|
|
417
688
|
return await this.mutex.runExclusive(async () => {
|
|
689
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
690
|
+
const { data, error } = await this.supabase.rpc("claim_next_job", {
|
|
691
|
+
p_project_id: this._projectId,
|
|
692
|
+
p_agent_id: agentId
|
|
693
|
+
});
|
|
694
|
+
if (error) {
|
|
695
|
+
logger.error("Failed to claim job via RPC", error);
|
|
696
|
+
} else if (data && data.status === "CLAIMED") {
|
|
697
|
+
const job2 = this.jobFromRecord(data.job);
|
|
698
|
+
await this.appendToNotepad(`
|
|
699
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
|
|
700
|
+
return { status: "CLAIMED", job: job2 };
|
|
701
|
+
}
|
|
702
|
+
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
703
|
+
}
|
|
704
|
+
if (this.contextManager.apiUrl) {
|
|
705
|
+
try {
|
|
706
|
+
const data = await this.callCoordination("jobs", "POST", {
|
|
707
|
+
action: "claim",
|
|
708
|
+
agentId
|
|
709
|
+
});
|
|
710
|
+
if (data && data.status === "CLAIMED") {
|
|
711
|
+
const job2 = this.jobFromRecord(data.job);
|
|
712
|
+
await this.appendToNotepad(`
|
|
713
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
|
|
714
|
+
return { status: "CLAIMED", job: job2 };
|
|
715
|
+
}
|
|
716
|
+
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
717
|
+
} catch (e) {
|
|
718
|
+
logger.error("Failed to claim job via API", e);
|
|
719
|
+
return { status: "NO_JOBS_AVAILABLE", message: `Claim failed: ${e.message}` };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
418
722
|
const priorities = ["critical", "high", "medium", "low"];
|
|
419
|
-
const allJobs =
|
|
723
|
+
const allJobs = Object.values(this.state.jobs);
|
|
420
724
|
const jobsById = new Map(allJobs.map((job2) => [job2.id, job2]));
|
|
421
725
|
const availableJobs = allJobs.filter((job2) => job2.status === "todo").filter((job2) => {
|
|
422
726
|
if (!job2.dependencies || job2.dependencies.length === 0) return true;
|
|
@@ -430,118 +734,110 @@ var NerveCenter = class {
|
|
|
430
734
|
if (availableJobs.length === 0) {
|
|
431
735
|
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
432
736
|
}
|
|
433
|
-
if (this.useSupabase && this.supabase) {
|
|
434
|
-
for (const candidate of availableJobs) {
|
|
435
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
436
|
-
const { data, error } = await this.supabase.from("jobs").update({
|
|
437
|
-
status: "in_progress",
|
|
438
|
-
assigned_to: agentId,
|
|
439
|
-
updated_at: now
|
|
440
|
-
}).eq("id", candidate.id).eq("status", "todo").select("id,title,description,priority,status,assigned_to,dependencies,created_at,updated_at");
|
|
441
|
-
if (error) {
|
|
442
|
-
logger.error("Failed to claim job", error);
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (data && data.length > 0) {
|
|
446
|
-
const job2 = this.jobFromRecord(data[0]);
|
|
447
|
-
await this.appendToNotepad(`
|
|
448
|
-
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
|
|
449
|
-
logger.info(`Job claimed`, { jobId: job2.id, agentId });
|
|
450
|
-
return { status: "CLAIMED", job: job2 };
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return { status: "NO_JOBS_AVAILABLE", message: "All available jobs were just claimed." };
|
|
454
|
-
}
|
|
455
737
|
const job = availableJobs[0];
|
|
456
738
|
job.status = "in_progress";
|
|
457
739
|
job.assignedTo = agentId;
|
|
458
740
|
job.updatedAt = Date.now();
|
|
459
|
-
this.
|
|
460
|
-
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job.title}
|
|
461
|
-
logger.info(`Job claimed`, { jobId: job.id, agentId });
|
|
462
|
-
await this.saveState();
|
|
741
|
+
await this.appendToNotepad(`
|
|
742
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job.title}`);
|
|
463
743
|
return { status: "CLAIMED", job };
|
|
464
744
|
});
|
|
465
745
|
}
|
|
466
746
|
async cancelJob(jobId, reason) {
|
|
467
747
|
return await this.mutex.runExclusive(async () => {
|
|
468
|
-
if (this.useSupabase && this.supabase) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
748
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
749
|
+
await this.supabase.from("jobs").update({ status: "cancelled", cancel_reason: reason, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId);
|
|
750
|
+
} else if (this.contextManager.apiUrl) {
|
|
751
|
+
try {
|
|
752
|
+
await this.callCoordination("jobs", "POST", { action: "update", jobId, status: "cancelled", cancel_reason: reason });
|
|
753
|
+
} catch (e) {
|
|
754
|
+
logger.error("Failed to cancel job via API", e);
|
|
472
755
|
}
|
|
473
|
-
this.state.liveNotepad += `
|
|
474
|
-
- [JOB CANCELLED] ${data[0].title} (ID: ${jobId}). Reason: ${reason}`;
|
|
475
|
-
await this.saveState();
|
|
476
|
-
return { status: "CANCELLED" };
|
|
477
756
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
job.updatedAt = Date.now();
|
|
482
|
-
this.state.liveNotepad += `
|
|
483
|
-
- [JOB CANCELLED] ${job.title} (ID: ${jobId}). Reason: ${reason}`;
|
|
484
|
-
await this.saveState();
|
|
485
|
-
return { status: "CANCELLED" };
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
async forceUnlock(filePath, adminReason) {
|
|
489
|
-
return await this.mutex.runExclusive(async () => {
|
|
490
|
-
if (this.useSupabase && this.supabase && this._projectId) {
|
|
491
|
-
const { error } = await this.supabase.from("locks").delete().eq("project_id", this._projectId).eq("file_path", filePath);
|
|
492
|
-
if (error) return { error: "DB Error" };
|
|
493
|
-
this.state.liveNotepad += `
|
|
494
|
-
- [ADMIN] Force unlocked '${filePath}'. Reason: ${adminReason}`;
|
|
757
|
+
if (this.state.jobs[jobId]) {
|
|
758
|
+
this.state.jobs[jobId].status = "cancelled";
|
|
759
|
+
this.state.jobs[jobId].updatedAt = Date.now();
|
|
495
760
|
await this.saveState();
|
|
496
|
-
return { status: "UNLOCKED" };
|
|
497
761
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
this.state.liveNotepad += `
|
|
502
|
-
- [ADMIN] Force unlocked '${filePath}'. Reason: ${adminReason}`;
|
|
503
|
-
await this.saveState();
|
|
504
|
-
return { status: "UNLOCKED", previousOwner: lock.agentId };
|
|
762
|
+
await this.appendToNotepad(`
|
|
763
|
+
- [JOB CANCELLED] ID: ${jobId}. Reason: ${reason}`);
|
|
764
|
+
return "Job cancelled.";
|
|
505
765
|
});
|
|
506
766
|
}
|
|
507
|
-
async completeJob(agentId, jobId, outcome) {
|
|
767
|
+
async completeJob(agentId, jobId, outcome, completionKey) {
|
|
508
768
|
return await this.mutex.runExclusive(async () => {
|
|
509
769
|
if (this.useSupabase && this.supabase) {
|
|
510
|
-
const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to").eq("id", jobId).single();
|
|
770
|
+
const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to,completion_key").eq("id", jobId).single();
|
|
511
771
|
if (error || !data) return { error: "Job not found" };
|
|
512
|
-
|
|
513
|
-
const
|
|
772
|
+
const isOwner2 = data.assigned_to === agentId;
|
|
773
|
+
const isKeyValid2 = completionKey && data.completion_key === completionKey;
|
|
774
|
+
if (!isOwner2 && !isKeyValid2) {
|
|
775
|
+
return { error: "You don't own this job and provided no valid key." };
|
|
776
|
+
}
|
|
777
|
+
const { error: updateError } = await this.supabase.from("jobs").update({ status: "done", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId);
|
|
514
778
|
if (updateError) return { error: "Failed to complete job" };
|
|
515
779
|
await this.appendToNotepad(`
|
|
516
|
-
- [JOB DONE]
|
|
517
|
-
|
|
780
|
+
- [JOB DONE] Agent '${agentId}' finished: ${data.title}
|
|
781
|
+
Outcome: ${outcome}`);
|
|
518
782
|
return { status: "COMPLETED" };
|
|
783
|
+
} else if (this.contextManager.apiUrl) {
|
|
784
|
+
try {
|
|
785
|
+
await this.callCoordination("jobs", "POST", {
|
|
786
|
+
action: "update",
|
|
787
|
+
jobId,
|
|
788
|
+
status: "done",
|
|
789
|
+
assigned_to: agentId,
|
|
790
|
+
completion_key: completionKey
|
|
791
|
+
});
|
|
792
|
+
await this.appendToNotepad(`
|
|
793
|
+
- [JOB DONE] Agent '${agentId}' finished: ${jobId}
|
|
794
|
+
Outcome: ${outcome}`);
|
|
795
|
+
return { status: "COMPLETED" };
|
|
796
|
+
} catch (e) {
|
|
797
|
+
logger.error("Failed to complete job via API", e);
|
|
798
|
+
}
|
|
519
799
|
}
|
|
520
800
|
const job = this.state.jobs[jobId];
|
|
521
801
|
if (!job) return { error: "Job not found" };
|
|
522
|
-
|
|
802
|
+
const isOwner = job.assignedTo === agentId;
|
|
803
|
+
const isKeyValid = completionKey && job.completionKey === completionKey;
|
|
804
|
+
if (!isOwner && !isKeyValid) {
|
|
805
|
+
return { error: "You don't own this job and provided no valid key." };
|
|
806
|
+
}
|
|
523
807
|
job.status = "done";
|
|
524
808
|
job.updatedAt = Date.now();
|
|
525
|
-
this.
|
|
526
|
-
- [JOB DONE]
|
|
527
|
-
|
|
809
|
+
await this.appendToNotepad(`
|
|
810
|
+
- [JOB DONE] Agent '${agentId}' finished: ${job.title}
|
|
811
|
+
Outcome: ${outcome}`);
|
|
528
812
|
return { status: "COMPLETED" };
|
|
529
813
|
});
|
|
530
814
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
815
|
+
async forceUnlock(filePath, reason) {
|
|
816
|
+
return await this.mutex.runExclusive(async () => {
|
|
817
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
818
|
+
await this.supabase.from("locks").delete().eq("project_id", this._projectId).eq("file_path", filePath);
|
|
819
|
+
} else if (this.contextManager.apiUrl) {
|
|
820
|
+
try {
|
|
821
|
+
await this.callCoordination("locks", "POST", { action: "unlock", filePath, reason });
|
|
822
|
+
} catch (e) {
|
|
823
|
+
logger.error("Failed to force unlock via API", e);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (this.state.locks[filePath]) {
|
|
827
|
+
delete this.state.locks[filePath];
|
|
828
|
+
await this.saveState();
|
|
829
|
+
}
|
|
830
|
+
await this.appendToNotepad(`
|
|
831
|
+
- [FORCE UNLOCK] ${filePath} unlocked by admin. Reason: ${reason}`);
|
|
832
|
+
return `File ${filePath} has been forcibly unlocked.`;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
async getCoreContext() {
|
|
539
836
|
const jobs = await this.listJobs();
|
|
540
|
-
const
|
|
541
|
-
(j) => `- [${j.status.toUpperCase()}] ${j.title} ${j.assignedTo ? "(" + j.assignedTo + ")" : "(Open)"}
|
|
542
|
-
ID: ${j.id}`
|
|
543
|
-
).join("\n");
|
|
837
|
+
const locks = await this.getLocks();
|
|
544
838
|
const notepad = await this.getNotepad();
|
|
839
|
+
const jobSummary = jobs.filter((j) => j.status !== "done" && j.status !== "cancelled").map((j) => `- [${j.status.toUpperCase()}] ${j.title} (ID: ${j.id}, Priority: ${j.priority}${j.assignedTo ? `, Assigned: ${j.assignedTo}` : ""})`).join("\n");
|
|
840
|
+
const lockSummary = locks.map((l) => `- ${l.filePath} (Locked by: ${l.agentId}, Intent: ${l.intent})`).join("\n");
|
|
545
841
|
return `# Active Session Context
|
|
546
842
|
|
|
547
843
|
## Job Board (Active Orchestration)
|
|
@@ -556,40 +852,89 @@ ${notepad}`;
|
|
|
556
852
|
// --- Decision & Orchestration ---
|
|
557
853
|
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
558
854
|
return await this.mutex.runExclusive(async () => {
|
|
559
|
-
|
|
560
|
-
|
|
855
|
+
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
856
|
+
if (this.contextManager.apiUrl) {
|
|
857
|
+
try {
|
|
858
|
+
const result = await this.callCoordination("locks", "POST", {
|
|
859
|
+
action: "lock",
|
|
860
|
+
filePath,
|
|
861
|
+
agentId,
|
|
862
|
+
intent,
|
|
863
|
+
userPrompt
|
|
864
|
+
});
|
|
865
|
+
if (result.status === "DENIED") {
|
|
866
|
+
logger.info(`[proposeFileAccess] DENIED by server: ${result.message}`);
|
|
867
|
+
return {
|
|
868
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
869
|
+
message: result.message || `File '${filePath}' is locked by another agent`,
|
|
870
|
+
currentLock: result.current_lock
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
logger.info(`[proposeFileAccess] GRANTED by server`);
|
|
874
|
+
await this.appendToNotepad(`
|
|
875
|
+
- [LOCK] ${agentId} locked ${filePath}
|
|
876
|
+
Intent: ${intent}`);
|
|
877
|
+
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
878
|
+
} catch (e) {
|
|
879
|
+
if (e.message && e.message.includes("409")) {
|
|
880
|
+
logger.info(`[proposeFileAccess] Lock conflict (409)`);
|
|
881
|
+
return {
|
|
882
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
883
|
+
message: `File '${filePath}' is locked by another agent`
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
logger.error(`[proposeFileAccess] API lock failed: ${e.message}`, e);
|
|
887
|
+
return { error: `Failed to acquire lock via API: ${e.message}` };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
891
|
+
try {
|
|
892
|
+
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
893
|
+
p_project_id: this._projectId,
|
|
894
|
+
p_file_path: filePath,
|
|
895
|
+
p_agent_id: agentId,
|
|
896
|
+
p_intent: intent,
|
|
897
|
+
p_user_prompt: userPrompt,
|
|
898
|
+
p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
|
|
899
|
+
});
|
|
900
|
+
if (error) throw error;
|
|
901
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
902
|
+
if (row && row.status === "DENIED") {
|
|
903
|
+
return {
|
|
904
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
905
|
+
message: `Conflict: File '${filePath}' is locked by '${row.owner_id}'`,
|
|
906
|
+
currentLock: {
|
|
907
|
+
agentId: row.owner_id,
|
|
908
|
+
filePath,
|
|
909
|
+
intent: row.intent,
|
|
910
|
+
timestamp: row.updated_at ? Date.parse(row.updated_at) : Date.now()
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
await this.appendToNotepad(`
|
|
915
|
+
- [LOCK] ${agentId} locked ${filePath}
|
|
916
|
+
Intent: ${intent}`);
|
|
917
|
+
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
918
|
+
} catch (e) {
|
|
919
|
+
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const existing = Object.values(this.state.locks).find((l) => l.filePath === filePath);
|
|
561
923
|
if (existing) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
if (!isStale && existing.agent_id !== agentId) {
|
|
924
|
+
const isStale = Date.now() - existing.timestamp > this.lockTimeout;
|
|
925
|
+
if (!isStale && existing.agentId !== agentId) {
|
|
565
926
|
return {
|
|
566
927
|
status: "REQUIRES_ORCHESTRATION",
|
|
567
|
-
message: `Conflict: File '${filePath}' is currently locked by
|
|
568
|
-
currentLock:
|
|
569
|
-
agentId: existing.agent_id,
|
|
570
|
-
intent: existing.intent,
|
|
571
|
-
timestamp: updatedAt
|
|
572
|
-
}
|
|
928
|
+
message: `Conflict: File '${filePath}' is currently locked by '${existing.agentId}'`,
|
|
929
|
+
currentLock: existing
|
|
573
930
|
};
|
|
574
931
|
}
|
|
575
932
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
file_path: filePath,
|
|
579
|
-
agent_id: agentId,
|
|
580
|
-
intent,
|
|
581
|
-
user_prompt: userPrompt,
|
|
582
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
583
|
-
}, { onConflict: "project_id,file_path" });
|
|
584
|
-
if (error) {
|
|
585
|
-
logger.error("Lock upsert failed", error);
|
|
586
|
-
return { status: "ERROR", message: "Database lock failed." };
|
|
587
|
-
}
|
|
933
|
+
this.state.locks[filePath] = { agentId, filePath, intent, userPrompt, timestamp: Date.now() };
|
|
934
|
+
await this.saveState();
|
|
588
935
|
await this.appendToNotepad(`
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
**Intent:** ${intent}
|
|
592
|
-
**Prompt:** "${userPrompt}"`);
|
|
936
|
+
- [LOCK] ${agentId} locked ${filePath}
|
|
937
|
+
Intent: ${intent}`);
|
|
593
938
|
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
594
939
|
});
|
|
595
940
|
}
|
|
@@ -605,7 +950,12 @@ ${notepad}`;
|
|
|
605
950
|
const content = await this.getNotepad();
|
|
606
951
|
const filename = `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.md`;
|
|
607
952
|
const historyPath = path2.join(process.cwd(), "history", filename);
|
|
608
|
-
|
|
953
|
+
try {
|
|
954
|
+
await fs2.mkdir(path2.dirname(historyPath), { recursive: true });
|
|
955
|
+
await fs2.writeFile(historyPath, content);
|
|
956
|
+
} catch (e) {
|
|
957
|
+
logger.warn("Failed to write local session log", e);
|
|
958
|
+
}
|
|
609
959
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
610
960
|
await this.supabase.from("sessions").insert({
|
|
611
961
|
project_id: this._projectId,
|
|
@@ -616,13 +966,18 @@ ${notepad}`;
|
|
|
616
966
|
await this.supabase.from("projects").update({ live_notepad: "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n" }).eq("id", this._projectId);
|
|
617
967
|
await this.supabase.from("jobs").delete().eq("project_id", this._projectId).in("status", ["done", "cancelled"]);
|
|
618
968
|
await this.supabase.from("locks").delete().eq("project_id", this._projectId);
|
|
619
|
-
} else {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
969
|
+
} else if (this.contextManager.apiUrl) {
|
|
970
|
+
try {
|
|
971
|
+
await this.callCoordination("sessions/finalize", "POST", { content });
|
|
972
|
+
} catch (e) {
|
|
973
|
+
logger.error("Failed to finalize session via API", e);
|
|
974
|
+
}
|
|
625
975
|
}
|
|
976
|
+
this.state.liveNotepad = "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n";
|
|
977
|
+
this.state.locks = {};
|
|
978
|
+
this.state.jobs = Object.fromEntries(
|
|
979
|
+
Object.entries(this.state.jobs).filter(([_, j]) => j.status !== "done" && j.status !== "cancelled")
|
|
980
|
+
);
|
|
626
981
|
await this.saveState();
|
|
627
982
|
return {
|
|
628
983
|
status: "SESSION_FINALIZED",
|
|
@@ -648,32 +1003,55 @@ ${conventions}`;
|
|
|
648
1003
|
}
|
|
649
1004
|
// --- Billing & Usage ---
|
|
650
1005
|
async getSubscriptionStatus(email) {
|
|
651
|
-
|
|
652
|
-
|
|
1006
|
+
logger.info(`[getSubscriptionStatus] Starting - email: ${email}`);
|
|
1007
|
+
logger.info(`[getSubscriptionStatus] Config - apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET" : "NOT SET"}, useSupabase: ${this.useSupabase}`);
|
|
1008
|
+
if (this.contextManager.apiUrl) {
|
|
1009
|
+
try {
|
|
1010
|
+
logger.info(`[getSubscriptionStatus] Attempting API call to: usage?email=${encodeURIComponent(email)}`);
|
|
1011
|
+
const result = await this.callCoordination(`usage?email=${encodeURIComponent(email)}`);
|
|
1012
|
+
logger.info(`[getSubscriptionStatus] API call successful: ${JSON.stringify(result).substring(0, 200)}`);
|
|
1013
|
+
return result;
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
logger.error(`[getSubscriptionStatus] API call failed: ${e.message}`, e);
|
|
1016
|
+
return { error: `API call failed: ${e.message}` };
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
logger.warn("[getSubscriptionStatus] No API URL configured");
|
|
653
1020
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1021
|
+
if (this.useSupabase && this.supabase) {
|
|
1022
|
+
const { data: profile, error } = await this.supabase.from("profiles").select("subscription_status, stripe_customer_id, current_period_end").eq("email", email).single();
|
|
1023
|
+
if (error || !profile) {
|
|
1024
|
+
return { status: "unknown", message: "Profile not found." };
|
|
1025
|
+
}
|
|
1026
|
+
const isActive = profile.subscription_status === "pro" || profile.current_period_end && new Date(profile.current_period_end) > /* @__PURE__ */ new Date();
|
|
1027
|
+
return {
|
|
1028
|
+
email,
|
|
1029
|
+
plan: isActive ? "Pro" : "Free",
|
|
1030
|
+
status: profile.subscription_status || "free",
|
|
1031
|
+
validUntil: profile.current_period_end
|
|
1032
|
+
};
|
|
657
1033
|
}
|
|
658
|
-
|
|
659
|
-
return {
|
|
660
|
-
email,
|
|
661
|
-
plan: isActive ? "Pro" : "Free",
|
|
662
|
-
status: profile.subscription_status || "free",
|
|
663
|
-
validUntil: profile.current_period_end
|
|
664
|
-
};
|
|
1034
|
+
return { error: "Coordination not configured. API URL not set and Supabase not available." };
|
|
665
1035
|
}
|
|
666
1036
|
async getUsageStats(email) {
|
|
667
|
-
|
|
668
|
-
|
|
1037
|
+
logger.info(`[getUsageStats] Starting - email: ${email}`);
|
|
1038
|
+
logger.info(`[getUsageStats] Config - apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET" : "NOT SET"}, useSupabase: ${this.useSupabase}`);
|
|
1039
|
+
if (this.contextManager.apiUrl) {
|
|
1040
|
+
try {
|
|
1041
|
+
logger.info(`[getUsageStats] Attempting API call to: usage?email=${encodeURIComponent(email)}`);
|
|
1042
|
+
const result = await this.callCoordination(`usage?email=${encodeURIComponent(email)}`);
|
|
1043
|
+
logger.info(`[getUsageStats] API call successful: ${JSON.stringify(result).substring(0, 200)}`);
|
|
1044
|
+
return { email, usageCount: result.usageCount || 0 };
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
logger.error(`[getUsageStats] API call failed: ${e.message}`, e);
|
|
1047
|
+
return { error: `API call failed: ${e.message}` };
|
|
1048
|
+
}
|
|
669
1049
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
email,
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// Hardcoded placeholder limit
|
|
676
|
-
};
|
|
1050
|
+
if (this.useSupabase && this.supabase) {
|
|
1051
|
+
const { data: profile } = await this.supabase.from("profiles").select("usage_count").eq("email", email).single();
|
|
1052
|
+
return { email, usageCount: profile?.usage_count || 0 };
|
|
1053
|
+
}
|
|
1054
|
+
return { error: "Coordination not configured. API URL not set and Supabase not available." };
|
|
677
1055
|
}
|
|
678
1056
|
};
|
|
679
1057
|
|
|
@@ -696,7 +1074,7 @@ var RagEngine = class {
|
|
|
696
1074
|
}
|
|
697
1075
|
async indexContent(filePath, content) {
|
|
698
1076
|
if (!this.projectId) {
|
|
699
|
-
|
|
1077
|
+
logger.error("RAG: Project ID missing.");
|
|
700
1078
|
return false;
|
|
701
1079
|
}
|
|
702
1080
|
try {
|
|
@@ -714,13 +1092,13 @@ var RagEngine = class {
|
|
|
714
1092
|
metadata: { filePath }
|
|
715
1093
|
});
|
|
716
1094
|
if (error) {
|
|
717
|
-
|
|
1095
|
+
logger.error("RAG Insert Error:", error);
|
|
718
1096
|
return false;
|
|
719
1097
|
}
|
|
720
1098
|
logger.info(`Indexed ${filePath}`);
|
|
721
1099
|
return true;
|
|
722
1100
|
} catch (e) {
|
|
723
|
-
|
|
1101
|
+
logger.error("RAG Error:", e);
|
|
724
1102
|
return false;
|
|
725
1103
|
}
|
|
726
1104
|
}
|
|
@@ -734,61 +1112,118 @@ var RagEngine = class {
|
|
|
734
1112
|
const embedding = resp.data[0].embedding;
|
|
735
1113
|
const { data, error } = await this.supabase.rpc("match_embeddings", {
|
|
736
1114
|
query_embedding: embedding,
|
|
737
|
-
match_threshold: 0.
|
|
1115
|
+
match_threshold: 0.1,
|
|
738
1116
|
match_count: limit,
|
|
739
1117
|
p_project_id: this.projectId
|
|
740
1118
|
});
|
|
741
1119
|
if (error || !data) {
|
|
742
|
-
|
|
1120
|
+
logger.error("RAG Search DB Error:", error);
|
|
743
1121
|
return [];
|
|
744
1122
|
}
|
|
745
1123
|
return data.map((d) => d.content);
|
|
746
1124
|
} catch (e) {
|
|
747
|
-
|
|
1125
|
+
logger.error("RAG Search Fail:", e);
|
|
748
1126
|
return [];
|
|
749
1127
|
}
|
|
750
1128
|
}
|
|
751
1129
|
};
|
|
752
1130
|
|
|
753
1131
|
// ../../src/local/mcp-server.ts
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1132
|
+
import path3 from "path";
|
|
1133
|
+
import fs3 from "fs";
|
|
1134
|
+
if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
|
|
1135
|
+
logger.info("Using configuration from MCP client (mcp.json)");
|
|
1136
|
+
} else {
|
|
1137
|
+
const cwd = process.cwd();
|
|
1138
|
+
const possiblePaths = [
|
|
1139
|
+
path3.join(cwd, ".env.local"),
|
|
1140
|
+
path3.join(cwd, "..", ".env.local"),
|
|
1141
|
+
path3.join(cwd, "..", "..", ".env.local"),
|
|
1142
|
+
path3.join(cwd, "shared-context", ".env.local"),
|
|
1143
|
+
path3.join(cwd, "..", "shared-context", ".env.local")
|
|
1144
|
+
];
|
|
1145
|
+
let envLoaded = false;
|
|
1146
|
+
for (const envPath of possiblePaths) {
|
|
1147
|
+
try {
|
|
1148
|
+
if (fs3.existsSync(envPath)) {
|
|
1149
|
+
logger.info(`[Fallback] Loading .env.local from: ${envPath}`);
|
|
1150
|
+
dotenv2.config({ path: envPath });
|
|
1151
|
+
envLoaded = true;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (!envLoaded) {
|
|
1158
|
+
logger.warn("No configuration found from MCP client (mcp.json) or .env.local");
|
|
1159
|
+
logger.warn("MCP server will use default API URL: https://useaxis.dev/api/v1");
|
|
1160
|
+
}
|
|
757
1161
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1162
|
+
logger.info("=== Axis MCP Server Starting ===");
|
|
1163
|
+
logger.info("Environment check:", {
|
|
1164
|
+
hasSHARED_CONTEXT_API_URL: !!process.env.SHARED_CONTEXT_API_URL,
|
|
1165
|
+
hasAXIS_API_KEY: !!process.env.AXIS_API_KEY,
|
|
1166
|
+
hasSHARED_CONTEXT_API_SECRET: !!process.env.SHARED_CONTEXT_API_SECRET,
|
|
1167
|
+
hasNEXT_PUBLIC_SUPABASE_URL: !!process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
1168
|
+
hasSUPABASE_SERVICE_ROLE_KEY: !!process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
1169
|
+
PROJECT_NAME: process.env.PROJECT_NAME || "default"
|
|
1170
|
+
});
|
|
1171
|
+
var apiUrl = process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_URL || "https://useaxis.dev/api/v1";
|
|
1172
|
+
var apiSecret = process.env.AXIS_API_KEY || process.env.SHARED_CONTEXT_API_SECRET || process.env.AXIS_API_SECRET;
|
|
1173
|
+
var useRemoteApiOnly = !!process.env.SHARED_CONTEXT_API_URL || !!process.env.AXIS_API_KEY;
|
|
1174
|
+
if (useRemoteApiOnly) {
|
|
1175
|
+
logger.info("Running in REMOTE API mode - Supabase credentials not needed locally.");
|
|
1176
|
+
logger.info(`Remote API: ${apiUrl}`);
|
|
1177
|
+
logger.info(`API Key: ${apiSecret ? apiSecret.substring(0, 15) + "..." : "NOT SET"}`);
|
|
1178
|
+
} else if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
1179
|
+
logger.warn("No remote API configured and Supabase credentials missing. Running in local/ephemeral mode.");
|
|
1180
|
+
} else {
|
|
1181
|
+
logger.info("Running in DIRECT SUPABASE mode (development).");
|
|
1182
|
+
}
|
|
1183
|
+
logger.info("ContextManager config:", {
|
|
1184
|
+
apiUrl,
|
|
1185
|
+
hasApiSecret: !!apiSecret,
|
|
1186
|
+
source: useRemoteApiOnly ? "MCP config (mcp.json)" : "default/fallback"
|
|
1187
|
+
});
|
|
1188
|
+
var manager = new ContextManager(apiUrl, apiSecret);
|
|
1189
|
+
logger.info("NerveCenter config:", {
|
|
1190
|
+
useRemoteApiOnly,
|
|
1191
|
+
supabaseUrl: useRemoteApiOnly ? "DISABLED (using remote API)" : process.env.NEXT_PUBLIC_SUPABASE_URL ? "SET" : "NOT SET",
|
|
1192
|
+
supabaseKey: useRemoteApiOnly ? "DISABLED (using remote API)" : process.env.SUPABASE_SERVICE_ROLE_KEY ? "SET" : "NOT SET",
|
|
1193
|
+
projectName: process.env.PROJECT_NAME || "default"
|
|
1194
|
+
});
|
|
762
1195
|
var nerveCenter = new NerveCenter(manager, {
|
|
763
|
-
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
764
|
-
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
1196
|
+
supabaseUrl: useRemoteApiOnly ? null : process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
1197
|
+
supabaseServiceRoleKey: useRemoteApiOnly ? null : process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
765
1198
|
projectName: process.env.PROJECT_NAME || "default"
|
|
766
1199
|
});
|
|
1200
|
+
logger.info("=== Axis MCP Server Initialized ===");
|
|
767
1201
|
var ragEngine;
|
|
768
|
-
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
1202
|
+
if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
769
1203
|
ragEngine = new RagEngine(
|
|
770
1204
|
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
771
1205
|
process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
772
1206
|
process.env.OPENAI_API_KEY || ""
|
|
773
1207
|
);
|
|
1208
|
+
logger.info("Local RAG Engine initialized.");
|
|
774
1209
|
}
|
|
775
1210
|
async function ensureFileSystem() {
|
|
776
1211
|
try {
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
const
|
|
1212
|
+
const fs4 = await import("fs/promises");
|
|
1213
|
+
const path4 = await import("path");
|
|
1214
|
+
const fsSync2 = await import("fs");
|
|
780
1215
|
const cwd = process.cwd();
|
|
781
1216
|
logger.info(`Server CWD: ${cwd}`);
|
|
782
|
-
const historyDir =
|
|
783
|
-
await
|
|
1217
|
+
const historyDir = path4.join(cwd, "history");
|
|
1218
|
+
await fs4.mkdir(historyDir, { recursive: true }).catch(() => {
|
|
784
1219
|
});
|
|
785
|
-
const axisDir =
|
|
786
|
-
const axisInstructions =
|
|
787
|
-
const legacyInstructions =
|
|
788
|
-
if (
|
|
1220
|
+
const axisDir = path4.join(cwd, ".axis");
|
|
1221
|
+
const axisInstructions = path4.join(axisDir, "instructions");
|
|
1222
|
+
const legacyInstructions = path4.join(cwd, "agent-instructions");
|
|
1223
|
+
if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
|
|
789
1224
|
logger.info("Using legacy agent-instructions directory");
|
|
790
1225
|
} else {
|
|
791
|
-
await
|
|
1226
|
+
await fs4.mkdir(axisInstructions, { recursive: true }).catch(() => {
|
|
792
1227
|
});
|
|
793
1228
|
const defaults = [
|
|
794
1229
|
["context.md", "# Project Context\n\n"],
|
|
@@ -796,11 +1231,11 @@ async function ensureFileSystem() {
|
|
|
796
1231
|
["activity.md", "# Activity Log\n\n"]
|
|
797
1232
|
];
|
|
798
1233
|
for (const [file, content] of defaults) {
|
|
799
|
-
const p =
|
|
1234
|
+
const p = path4.join(axisInstructions, file);
|
|
800
1235
|
try {
|
|
801
|
-
await
|
|
1236
|
+
await fs4.access(p);
|
|
802
1237
|
} catch {
|
|
803
|
-
await
|
|
1238
|
+
await fs4.writeFile(p, content);
|
|
804
1239
|
logger.info(`Created default context file: ${file}`);
|
|
805
1240
|
}
|
|
806
1241
|
}
|
|
@@ -826,17 +1261,18 @@ var UPDATE_CONTEXT_TOOL = "update_context";
|
|
|
826
1261
|
var SEARCH_CONTEXT_TOOL = "search_codebase";
|
|
827
1262
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
828
1263
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
};
|
|
1264
|
+
const files = await manager.listFiles();
|
|
1265
|
+
const resources = [
|
|
1266
|
+
{
|
|
1267
|
+
uri: "mcp://context/current",
|
|
1268
|
+
name: "Live Session Context",
|
|
1269
|
+
mimeType: "text/markdown",
|
|
1270
|
+
description: "The realtime state of the Nerve Center (Notepad + Locks)"
|
|
1271
|
+
},
|
|
1272
|
+
...files
|
|
1273
|
+
];
|
|
1274
|
+
logger.info(`[ListResources] Returning ${resources.length} resources to MCP client`);
|
|
1275
|
+
return { resources };
|
|
840
1276
|
} catch (error) {
|
|
841
1277
|
logger.error("Error listing resources", error);
|
|
842
1278
|
return { resources: [] };
|
|
@@ -850,7 +1286,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
850
1286
|
contents: [{
|
|
851
1287
|
uri,
|
|
852
1288
|
mimeType: "text/markdown",
|
|
853
|
-
text: await nerveCenter.
|
|
1289
|
+
text: await nerveCenter.getCoreContext()
|
|
854
1290
|
}]
|
|
855
1291
|
};
|
|
856
1292
|
}
|
|
@@ -873,192 +1309,193 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
873
1309
|
}
|
|
874
1310
|
});
|
|
875
1311
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
},
|
|
1048
|
-
{
|
|
1049
|
-
name: "index_file",
|
|
1050
|
-
description: "Force re-index a file into the RAG vector database.",
|
|
1051
|
-
inputSchema: {
|
|
1052
|
-
type: "object",
|
|
1053
|
-
properties: {
|
|
1054
|
-
filePath: { type: "string" },
|
|
1055
|
-
content: { type: "string" }
|
|
1056
|
-
},
|
|
1057
|
-
required: ["filePath", "content"]
|
|
1058
|
-
}
|
|
1312
|
+
const tools = [
|
|
1313
|
+
{
|
|
1314
|
+
name: READ_CONTEXT_TOOL,
|
|
1315
|
+
description: "**READ THIS FIRST** to understand the project's architecture, coding conventions, and active state.\n- Returns the content of core context files like `context.md` (Project Goals), `conventions.md` (Style Guide), or `activity.md`.\n- Usage: Call with `filename='context.md'` effectively.\n- Note: If you need the *current* runtime state (active locks, jobs), use the distinct resource `mcp://context/current` instead.",
|
|
1316
|
+
inputSchema: {
|
|
1317
|
+
type: "object",
|
|
1318
|
+
properties: {
|
|
1319
|
+
filename: { type: "string", description: "The name of the file to read (e.g., 'context.md', 'conventions.md')" }
|
|
1320
|
+
},
|
|
1321
|
+
required: ["filename"]
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
name: UPDATE_CONTEXT_TOOL,
|
|
1326
|
+
description: "**APPEND OR OVERWRITE** shared context files.\n- Use this to update the project's long-term memory (e.g., adding a new convention, updating the architectural goal).\n- For short-term updates (like 'I just fixed bug X'), use `update_shared_context` (Notepad) instead.\n- Supports `append: true` (default: false) to add to the end of a file.",
|
|
1327
|
+
inputSchema: {
|
|
1328
|
+
type: "object",
|
|
1329
|
+
properties: {
|
|
1330
|
+
filename: { type: "string", description: "File to update (e.g. 'activity.md')" },
|
|
1331
|
+
content: { type: "string", description: "The new content to write or append." },
|
|
1332
|
+
append: { type: "boolean", description: "Whether to append to the end of the file (true) or overwrite it (false). Default: false." }
|
|
1333
|
+
},
|
|
1334
|
+
required: ["filename", "content"]
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
name: SEARCH_CONTEXT_TOOL,
|
|
1339
|
+
description: "**SEMANTIC SEARCH** for the codebase.\n- Uses vector similarity to find relevant code snippets or documentation.\n- Best for: 'Where is the auth logic?', 'How do I handle billing?', 'Find the class that manages locks'.\n- Note: This searches *indexed* content only. For exact string matches, use `grep` (if available) or `warpgrep`.",
|
|
1340
|
+
inputSchema: {
|
|
1341
|
+
type: "object",
|
|
1342
|
+
properties: {
|
|
1343
|
+
query: { type: "string", description: "Natural language search query." }
|
|
1344
|
+
},
|
|
1345
|
+
required: ["query"]
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
// --- Billing & Usage ---
|
|
1349
|
+
{
|
|
1350
|
+
name: "get_subscription_status",
|
|
1351
|
+
description: "**BILLING CHECK**: specific to the Axis business logic.\n- Returns the user's subscription tier (Pro vs Free), Stripe customer ID, and current period end.\n- Critical for gating features behind paywalls.\n- Returns 'Profile not found' if the user doesn't exist in the database.",
|
|
1352
|
+
inputSchema: {
|
|
1353
|
+
type: "object",
|
|
1354
|
+
properties: {
|
|
1355
|
+
email: { type: "string", description: "User email to check." }
|
|
1356
|
+
},
|
|
1357
|
+
required: ["email"]
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
name: "get_usage_stats",
|
|
1362
|
+
description: "**API USAGE**: Returns a user's token usage and request counts.\n- Useful for debugging rate limits or explaining quota usage to users.",
|
|
1363
|
+
inputSchema: {
|
|
1364
|
+
type: "object",
|
|
1365
|
+
properties: {
|
|
1366
|
+
email: { type: "string", description: "User email to check." }
|
|
1367
|
+
},
|
|
1368
|
+
required: ["email"]
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
name: "search_docs",
|
|
1373
|
+
description: "**DOCUMENTATION SEARCH**: Searches the official Axis documentation (if indexed).\n- Use this when you need info on *how* to use Axis features, not just codebase structure.\n- Falls back to local RAG search if the remote API is unavailable.",
|
|
1374
|
+
inputSchema: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: {
|
|
1377
|
+
query: { type: "string", description: "Natural language search query." }
|
|
1378
|
+
},
|
|
1379
|
+
required: ["query"]
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
// --- Decision & Orchestration ---
|
|
1383
|
+
{
|
|
1384
|
+
name: "propose_file_access",
|
|
1385
|
+
description: "**CRITICAL: REQUEST FILE LOCK**.\n- **MUST** be called *before* editing any file to prevent conflicts with other agents.\n- Checks if another agent currently holds a lock.\n- Returns `GRANTED` if safe to proceed, or `REQUIRES_ORCHESTRATION` if someone else is editing.\n- Usage: Provide your `agentId` (e.g., 'cursor-agent'), `filePath` (absolute), and `intent` (what you are doing).\n- Note: Locks expire after 30 minutes. Use `force_unlock` only if you are certain a lock is stale and blocking progress.",
|
|
1386
|
+
inputSchema: {
|
|
1387
|
+
type: "object",
|
|
1388
|
+
properties: {
|
|
1389
|
+
agentId: { type: "string" },
|
|
1390
|
+
filePath: { type: "string" },
|
|
1391
|
+
intent: { type: "string" },
|
|
1392
|
+
userPrompt: { type: "string", description: "The full prompt provided by the user that initiated this action." }
|
|
1393
|
+
},
|
|
1394
|
+
required: ["agentId", "filePath", "intent", "userPrompt"]
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
name: "update_shared_context",
|
|
1399
|
+
description: "**LIVE NOTEPAD**: The project's short-term working memory.\n- **ALWAYS** call this after completing a significant step (e.g., 'Fixed bug in auth.ts', 'Ran tests, all passed').\n- This content is visible to *all* other agents immediately.\n- Think of this as a team chat or 'standup' update.",
|
|
1400
|
+
inputSchema: {
|
|
1401
|
+
type: "object",
|
|
1402
|
+
properties: {
|
|
1403
|
+
agentId: { type: "string" },
|
|
1404
|
+
text: { type: "string" }
|
|
1405
|
+
},
|
|
1406
|
+
required: ["agentId", "text"]
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
// --- Permanent Memory ---
|
|
1410
|
+
{
|
|
1411
|
+
name: "finalize_session",
|
|
1412
|
+
description: "**END OF SESSION HOUSEKEEPING**.\n- Archives the current Live Notepad to a permanent session log.\n- Clears all active locks and completed jobs.\n- Resets the Live Notepad for the next session.\n- Call this when the user says 'we are done' or 'start fresh'.",
|
|
1413
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
name: "get_project_soul",
|
|
1417
|
+
description: "**HIGH-LEVEL INTENT**: Returns the 'Soul' of the project.\n- Combines `context.md`, `conventions.md`, and other core directives into a single prompt.\n- Use this at the *start* of a conversation to ground yourself in the project's reality.",
|
|
1418
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
1419
|
+
},
|
|
1420
|
+
// --- Job Board (Task Orchestration) ---
|
|
1421
|
+
{
|
|
1422
|
+
name: "post_job",
|
|
1423
|
+
description: "**CREATE TICKET**: Post a new task to the Job Board.\n- Use this when you identify work that needs to be done but *cannot* be done right now (e.g., refactoring, new feature).\n- Supports `dependencies` (list of other Job IDs that must be done first).\n- Priority: low, medium, high, critical.",
|
|
1424
|
+
inputSchema: {
|
|
1425
|
+
type: "object",
|
|
1426
|
+
properties: {
|
|
1427
|
+
title: { type: "string" },
|
|
1428
|
+
description: { type: "string" },
|
|
1429
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
1430
|
+
dependencies: { type: "array", items: { type: "string" }, description: "Array of Job IDs that must be completed before this job can be claimed." }
|
|
1431
|
+
},
|
|
1432
|
+
required: ["title", "description"]
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
name: "cancel_job",
|
|
1437
|
+
description: "**KILL TICKET**: Cancel a job that is no longer needed.\n- Requires `jobId` and a `reason`.",
|
|
1438
|
+
inputSchema: {
|
|
1439
|
+
type: "object",
|
|
1440
|
+
properties: {
|
|
1441
|
+
jobId: { type: "string" },
|
|
1442
|
+
reason: { type: "string" }
|
|
1443
|
+
},
|
|
1444
|
+
required: ["jobId", "reason"]
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
name: "force_unlock",
|
|
1449
|
+
description: "**ADMIN OVERRIDE**: Break a file lock.\n- **WARNING**: Only use this if a lock is clearly stale or the locking agent has crashed.\n- Will forcibly remove the lock from the database.",
|
|
1450
|
+
inputSchema: {
|
|
1451
|
+
type: "object",
|
|
1452
|
+
properties: {
|
|
1453
|
+
filePath: { type: "string" },
|
|
1454
|
+
reason: { type: "string" }
|
|
1455
|
+
},
|
|
1456
|
+
required: ["filePath", "reason"]
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
name: "claim_next_job",
|
|
1461
|
+
description: "**AUTO-ASSIGNMENT**: Ask the Job Board for the next most important task.\n- Respects priority (Critical > High > ...) and dependencies (won't assign a job if its deps aren't done).\n- Returns the Job object if successful, or 'NO_JOBS_AVAILABLE'.\n- Use this when you are idle and looking for work.",
|
|
1462
|
+
inputSchema: {
|
|
1463
|
+
type: "object",
|
|
1464
|
+
properties: {
|
|
1465
|
+
agentId: { type: "string" }
|
|
1466
|
+
},
|
|
1467
|
+
required: ["agentId"]
|
|
1468
|
+
}
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
name: "complete_job",
|
|
1472
|
+
description: "**CLOSE TICKET**: Mark a job as done.\n- Requires `outcome` (what was done).\n- If you are not the assigned agent, you must provide the `completionKey`.",
|
|
1473
|
+
inputSchema: {
|
|
1474
|
+
type: "object",
|
|
1475
|
+
properties: {
|
|
1476
|
+
agentId: { type: "string" },
|
|
1477
|
+
jobId: { type: "string" },
|
|
1478
|
+
outcome: { type: "string" },
|
|
1479
|
+
completionKey: { type: "string", description: "Optional key to authorize completion if not the assigned agent." }
|
|
1480
|
+
},
|
|
1481
|
+
required: ["agentId", "jobId", "outcome"]
|
|
1059
1482
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
name: "index_file",
|
|
1486
|
+
description: "**UPDATE SEARCH INDEX**: Add a file's content to the RAG vector database.\n- Call this *immediately* after creating a new file or significantly refactoring an existing one.\n- Ensures future `search_codebase` calls return up-to-date results.",
|
|
1487
|
+
inputSchema: {
|
|
1488
|
+
type: "object",
|
|
1489
|
+
properties: {
|
|
1490
|
+
filePath: { type: "string" },
|
|
1491
|
+
content: { type: "string" }
|
|
1492
|
+
},
|
|
1493
|
+
required: ["filePath", "content"]
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
];
|
|
1497
|
+
logger.info(`[ListTools] Returning ${tools.length} tools to MCP client`);
|
|
1498
|
+
return { tools };
|
|
1062
1499
|
});
|
|
1063
1500
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1064
1501
|
const { name, arguments: args } = request.params;
|
|
@@ -1122,20 +1559,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1122
1559
|
}
|
|
1123
1560
|
if (name === "get_subscription_status") {
|
|
1124
1561
|
const email = String(args?.email);
|
|
1125
|
-
|
|
1126
|
-
|
|
1562
|
+
logger.info(`[get_subscription_status] Called with email: ${email}`);
|
|
1563
|
+
try {
|
|
1564
|
+
const result = await nerveCenter.getSubscriptionStatus(email);
|
|
1565
|
+
logger.info(`[get_subscription_status] Result: ${JSON.stringify(result).substring(0, 200)}`);
|
|
1566
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1567
|
+
} catch (e) {
|
|
1568
|
+
logger.error(`[get_subscription_status] Exception: ${e.message}`, e);
|
|
1569
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
1570
|
+
}
|
|
1127
1571
|
}
|
|
1128
1572
|
if (name === "get_usage_stats") {
|
|
1129
1573
|
const email = String(args?.email);
|
|
1130
|
-
|
|
1131
|
-
|
|
1574
|
+
logger.info(`[get_usage_stats] Called with email: ${email}`);
|
|
1575
|
+
try {
|
|
1576
|
+
const result = await nerveCenter.getUsageStats(email);
|
|
1577
|
+
logger.info(`[get_usage_stats] Result: ${JSON.stringify(result).substring(0, 200)}`);
|
|
1578
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1579
|
+
} catch (e) {
|
|
1580
|
+
logger.error(`[get_usage_stats] Exception: ${e.message}`, e);
|
|
1581
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
1582
|
+
}
|
|
1132
1583
|
}
|
|
1133
1584
|
if (name === "search_docs") {
|
|
1134
1585
|
const query = String(args?.query);
|
|
1135
1586
|
try {
|
|
1136
|
-
const formatted = await manager.searchContext(query);
|
|
1587
|
+
const formatted = await manager.searchContext(query, nerveCenter.currentProjectName);
|
|
1137
1588
|
return { content: [{ type: "text", text: formatted }] };
|
|
1138
1589
|
} catch (err) {
|
|
1590
|
+
if (ragEngine) {
|
|
1591
|
+
const results = await ragEngine.search(query);
|
|
1592
|
+
return { content: [{ type: "text", text: results.join("\n---\n") }] };
|
|
1593
|
+
}
|
|
1139
1594
|
return {
|
|
1140
1595
|
content: [{ type: "text", text: `Search Error: ${err}` }],
|
|
1141
1596
|
isError: true
|
|
@@ -1181,8 +1636,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1181
1636
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1182
1637
|
}
|
|
1183
1638
|
if (name === "complete_job") {
|
|
1184
|
-
const { agentId, jobId, outcome } = args;
|
|
1185
|
-
const result = await nerveCenter.completeJob(agentId, jobId, outcome);
|
|
1639
|
+
const { agentId, jobId, outcome, completionKey } = args;
|
|
1640
|
+
const result = await nerveCenter.completeJob(agentId, jobId, outcome, completionKey);
|
|
1186
1641
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1187
1642
|
}
|
|
1188
1643
|
throw new Error(`Tool not found: ${name}`);
|
|
@@ -1194,9 +1649,11 @@ async function main() {
|
|
|
1194
1649
|
ragEngine.setProjectId(nerveCenter.projectId);
|
|
1195
1650
|
logger.info(`Local RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
|
|
1196
1651
|
}
|
|
1652
|
+
logger.info("MCP server ready - all tools and resources registered");
|
|
1197
1653
|
const transport = new StdioServerTransport();
|
|
1198
1654
|
await server.connect(transport);
|
|
1199
1655
|
logger.info("Shared Context MCP Server running on stdio");
|
|
1656
|
+
logger.info("Server is now accepting tool calls from MCP clients");
|
|
1200
1657
|
}
|
|
1201
1658
|
main().catch((error) => {
|
|
1202
1659
|
logger.error("Server error", error);
|