@virsanghavi/axis-server 1.0.7 → 1.0.9
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 +47 -32
- package/dist/cli.js +12 -1
- package/dist/mcp-server.mjs +780 -442
- 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,47 @@ 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
|
+
import * as fsSync from "fs";
|
|
23
17
|
function getEffectiveInstructionsDir() {
|
|
24
18
|
const cwd = process.cwd();
|
|
25
19
|
const axisDir = path.resolve(cwd, ".axis");
|
|
26
20
|
const instructionsDir = path.resolve(axisDir, "instructions");
|
|
27
21
|
const legacyDir = path.resolve(cwd, "agent-instructions");
|
|
22
|
+
const sharedContextDir = path.resolve(cwd, "shared-context", "agent-instructions");
|
|
28
23
|
try {
|
|
29
|
-
if (
|
|
24
|
+
if (fsSync.existsSync(instructionsDir)) {
|
|
25
|
+
console.error(`[ContextManager] Using instructions dir: ${instructionsDir}`);
|
|
26
|
+
return instructionsDir;
|
|
27
|
+
}
|
|
30
28
|
} catch {
|
|
31
29
|
}
|
|
30
|
+
try {
|
|
31
|
+
if (fsSync.existsSync(legacyDir)) {
|
|
32
|
+
console.error(`[ContextManager] Using legacy dir: ${legacyDir}`);
|
|
33
|
+
return legacyDir;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
if (fsSync.existsSync(sharedContextDir)) {
|
|
39
|
+
console.error(`[ContextManager] Using shared-context dir: ${sharedContextDir}`);
|
|
40
|
+
return sharedContextDir;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
console.error(`[ContextManager] Fallback to legacy dir: ${legacyDir}`);
|
|
32
45
|
return legacyDir;
|
|
33
46
|
}
|
|
34
47
|
var ContextManager = class {
|
|
35
48
|
mutex;
|
|
36
49
|
apiUrl;
|
|
50
|
+
// Made public so NerveCenter can access it
|
|
37
51
|
apiSecret;
|
|
38
|
-
|
|
52
|
+
// Made public so NerveCenter can access it
|
|
53
|
+
constructor(apiUrl2, apiSecret2) {
|
|
39
54
|
this.mutex = new Mutex();
|
|
40
|
-
this.apiUrl =
|
|
41
|
-
this.apiSecret =
|
|
55
|
+
this.apiUrl = apiUrl2;
|
|
56
|
+
this.apiSecret = apiSecret2;
|
|
42
57
|
}
|
|
43
58
|
resolveFilePath(filename) {
|
|
44
59
|
if (!filename || filename.includes("\0")) {
|
|
@@ -221,16 +236,27 @@ var NerveCenter = class {
|
|
|
221
236
|
this.contextManager = contextManager;
|
|
222
237
|
this.stateFilePath = options.stateFilePath || STATE_FILE;
|
|
223
238
|
this.lockTimeout = options.lockTimeout || LOCK_TIMEOUT_DEFAULT;
|
|
224
|
-
|
|
225
|
-
const supabaseUrl = options.supabaseUrl
|
|
226
|
-
const supabaseKey = options.supabaseServiceRoleKey
|
|
227
|
-
if (
|
|
228
|
-
|
|
239
|
+
const hasRemoteApi = !!this.contextManager.apiUrl;
|
|
240
|
+
const supabaseUrl = options.supabaseUrl !== void 0 ? options.supabaseUrl : hasRemoteApi ? null : process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
241
|
+
const supabaseKey = options.supabaseServiceRoleKey !== void 0 ? options.supabaseServiceRoleKey : hasRemoteApi ? null : process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
242
|
+
if (supabaseUrl && supabaseKey) {
|
|
243
|
+
this.supabase = createClient(supabaseUrl, supabaseKey);
|
|
244
|
+
this.useSupabase = true;
|
|
245
|
+
logger.info("NerveCenter: Using direct Supabase persistence.");
|
|
246
|
+
} else if (this.contextManager.apiUrl) {
|
|
229
247
|
this.supabase = void 0;
|
|
230
248
|
this.useSupabase = false;
|
|
249
|
+
logger.info(`NerveCenter: Using Remote API persistence (${this.contextManager.apiUrl})`);
|
|
231
250
|
} else {
|
|
232
|
-
this.supabase =
|
|
233
|
-
this.useSupabase =
|
|
251
|
+
this.supabase = void 0;
|
|
252
|
+
this.useSupabase = false;
|
|
253
|
+
logger.warn("NerveCenter: Running in local-only mode. Coordination restricted to this machine.");
|
|
254
|
+
}
|
|
255
|
+
const explicitProjectName = options.projectName || process.env.PROJECT_NAME;
|
|
256
|
+
if (explicitProjectName) {
|
|
257
|
+
this.projectName = explicitProjectName;
|
|
258
|
+
} else {
|
|
259
|
+
this.projectName = "default";
|
|
234
260
|
}
|
|
235
261
|
this.state = {
|
|
236
262
|
locks: {},
|
|
@@ -246,10 +272,27 @@ var NerveCenter = class {
|
|
|
246
272
|
}
|
|
247
273
|
async init() {
|
|
248
274
|
await this.loadState();
|
|
249
|
-
|
|
275
|
+
if (this.projectName === "default" && (this.useSupabase || !this.contextManager.apiUrl)) {
|
|
276
|
+
await this.detectProjectName();
|
|
277
|
+
}
|
|
250
278
|
if (this.useSupabase) {
|
|
251
279
|
await this.ensureProjectId();
|
|
252
280
|
}
|
|
281
|
+
if (this.contextManager.apiUrl) {
|
|
282
|
+
try {
|
|
283
|
+
const { liveNotepad, projectId } = await this.callCoordination(`sessions/sync?projectName=${this.projectName}`);
|
|
284
|
+
if (projectId) {
|
|
285
|
+
this._projectId = projectId;
|
|
286
|
+
logger.info(`NerveCenter: Resolved projectId from cloud: ${this._projectId}`);
|
|
287
|
+
}
|
|
288
|
+
if (liveNotepad && (!this.state.liveNotepad || this.state.liveNotepad.startsWith("Session Start:"))) {
|
|
289
|
+
this.state.liveNotepad = liveNotepad;
|
|
290
|
+
logger.info(`NerveCenter: Recovered live notepad from cloud for project: ${this.projectName}`);
|
|
291
|
+
}
|
|
292
|
+
} catch (e) {
|
|
293
|
+
logger.warn("Failed to sync project/notepad with Remote API. Using local/fallback.", e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
253
296
|
}
|
|
254
297
|
async detectProjectName() {
|
|
255
298
|
try {
|
|
@@ -259,12 +302,16 @@ var NerveCenter = class {
|
|
|
259
302
|
if (config.project) {
|
|
260
303
|
this.projectName = config.project;
|
|
261
304
|
logger.info(`Detected project name from .axis/axis.json: ${this.projectName}`);
|
|
305
|
+
console.error(`[NerveCenter] Loaded project name '${this.projectName}' from ${axisConfigPath}`);
|
|
306
|
+
} else {
|
|
307
|
+
console.error(`[NerveCenter] .axis/axis.json found but no 'project' field.`);
|
|
262
308
|
}
|
|
263
309
|
} catch (e) {
|
|
310
|
+
console.error(`[NerveCenter] Could not load .axis/axis.json at ${path2.join(process.cwd(), ".axis", "axis.json")}: ${e}`);
|
|
264
311
|
}
|
|
265
312
|
}
|
|
266
313
|
async ensureProjectId() {
|
|
267
|
-
if (!this.supabase) return;
|
|
314
|
+
if (!this.supabase || this._projectId) return;
|
|
268
315
|
const { data: project, error } = await this.supabase.from("projects").select("id").eq("name", this.projectName).maybeSingle();
|
|
269
316
|
if (error) {
|
|
270
317
|
logger.error("Failed to load project", error);
|
|
@@ -281,6 +328,48 @@ var NerveCenter = class {
|
|
|
281
328
|
}
|
|
282
329
|
this._projectId = created.id;
|
|
283
330
|
}
|
|
331
|
+
async callCoordination(endpoint, method = "GET", body) {
|
|
332
|
+
logger.info(`[callCoordination] Starting - endpoint: ${endpoint}, method: ${method}`);
|
|
333
|
+
logger.info(`[callCoordination] apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET (" + this.contextManager.apiSecret.substring(0, 10) + "...)" : "NOT SET"}`);
|
|
334
|
+
if (!this.contextManager.apiUrl) {
|
|
335
|
+
logger.error("[callCoordination] Remote API not configured - apiUrl is:", this.contextManager.apiUrl);
|
|
336
|
+
throw new Error("Remote API not configured");
|
|
337
|
+
}
|
|
338
|
+
const url = this.contextManager.apiUrl.endsWith("/v1") ? `${this.contextManager.apiUrl}/${endpoint}` : `${this.contextManager.apiUrl}/v1/${endpoint}`;
|
|
339
|
+
logger.info(`[callCoordination] Full URL: ${method} ${url}`);
|
|
340
|
+
logger.info(`[callCoordination] Request body: ${body ? JSON.stringify({ ...body, projectName: this.projectName }) : "none"}`);
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(url, {
|
|
343
|
+
method,
|
|
344
|
+
headers: {
|
|
345
|
+
"Content-Type": "application/json",
|
|
346
|
+
"Authorization": `Bearer ${this.contextManager.apiSecret || ""}`
|
|
347
|
+
},
|
|
348
|
+
body: body ? JSON.stringify({ ...body, projectName: this.projectName }) : void 0
|
|
349
|
+
});
|
|
350
|
+
logger.info(`[callCoordination] Response status: ${response.status} ${response.statusText}`);
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
const text = await response.text();
|
|
353
|
+
logger.error(`[callCoordination] API Error Response (${response.status}): ${text}`);
|
|
354
|
+
if (response.status === 401) {
|
|
355
|
+
throw new Error(`Authentication failed (401): ${text}. Check if API key is valid and exists in api_keys table.`);
|
|
356
|
+
} else if (response.status === 500) {
|
|
357
|
+
throw new Error(`Server error (500): ${text}. Check Vercel logs for details.`);
|
|
358
|
+
} else {
|
|
359
|
+
throw new Error(`API Error (${response.status}): ${text}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
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
|
+
}
|
|
372
|
+
}
|
|
284
373
|
jobFromRecord(record) {
|
|
285
374
|
return {
|
|
286
375
|
id: record.id,
|
|
@@ -290,69 +379,122 @@ var NerveCenter = class {
|
|
|
290
379
|
status: record.status,
|
|
291
380
|
assignedTo: record.assigned_to || void 0,
|
|
292
381
|
dependencies: record.dependencies || void 0,
|
|
382
|
+
completionKey: record.completion_key || void 0,
|
|
293
383
|
createdAt: Date.parse(record.created_at),
|
|
294
384
|
updatedAt: Date.parse(record.updated_at)
|
|
295
385
|
};
|
|
296
386
|
}
|
|
297
387
|
// --- Data Access Layers (Hybrid: Supabase > Local) ---
|
|
298
388
|
async listJobs() {
|
|
299
|
-
if (
|
|
300
|
-
|
|
389
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
390
|
+
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);
|
|
391
|
+
if (error || !data) {
|
|
392
|
+
logger.error("Failed to load jobs from Supabase", error);
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
return data.map((record) => this.jobFromRecord(record));
|
|
301
396
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
397
|
+
if (this.contextManager.apiUrl) {
|
|
398
|
+
try {
|
|
399
|
+
const url = `jobs?projectName=${this.projectName}`;
|
|
400
|
+
const res = await this.callCoordination(url);
|
|
401
|
+
return (res.jobs || []).map((record) => this.jobFromRecord(record));
|
|
402
|
+
} catch (e) {
|
|
403
|
+
logger.error("Failed to load jobs from API", e);
|
|
404
|
+
return Object.values(this.state.jobs);
|
|
405
|
+
}
|
|
306
406
|
}
|
|
307
|
-
return
|
|
407
|
+
return Object.values(this.state.jobs);
|
|
308
408
|
}
|
|
309
409
|
async getLocks() {
|
|
310
|
-
|
|
311
|
-
|
|
410
|
+
logger.info(`[getLocks] Starting - projectName: ${this.projectName}`);
|
|
411
|
+
logger.info(`[getLocks] Config - apiUrl: ${this.contextManager.apiUrl}, useSupabase: ${this.useSupabase}, hasSupabase: ${!!this.supabase}`);
|
|
412
|
+
if (this.contextManager.apiUrl) {
|
|
413
|
+
if (!this.useSupabase || !this.supabase) {
|
|
414
|
+
try {
|
|
415
|
+
logger.info(`[getLocks] Fetching locks from API for project: ${this.projectName}`);
|
|
416
|
+
const res = await this.callCoordination(`locks?projectName=${this.projectName}`);
|
|
417
|
+
logger.info(`[getLocks] API returned ${res.locks?.length || 0} locks`);
|
|
418
|
+
return (res.locks || []).map((row) => ({
|
|
419
|
+
agentId: row.agent_id,
|
|
420
|
+
filePath: row.file_path,
|
|
421
|
+
intent: row.intent,
|
|
422
|
+
userPrompt: row.user_prompt,
|
|
423
|
+
timestamp: Date.parse(row.updated_at || row.timestamp)
|
|
424
|
+
}));
|
|
425
|
+
} catch (e) {
|
|
426
|
+
logger.error(`[getLocks] Failed to fetch locks from API: ${e.message}`, e);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
312
429
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
430
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
431
|
+
try {
|
|
432
|
+
await this.supabase.rpc("clean_stale_locks", {
|
|
433
|
+
p_project_id: this._projectId,
|
|
434
|
+
p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
|
|
435
|
+
});
|
|
436
|
+
const { data, error } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId);
|
|
437
|
+
if (error) throw error;
|
|
438
|
+
return (data || []).map((row) => ({
|
|
439
|
+
agentId: row.agent_id,
|
|
440
|
+
filePath: row.file_path,
|
|
441
|
+
intent: row.intent,
|
|
442
|
+
userPrompt: row.user_prompt,
|
|
443
|
+
timestamp: Date.parse(row.updated_at)
|
|
444
|
+
}));
|
|
445
|
+
} catch (e) {
|
|
446
|
+
logger.warn("Failed to fetch locks from DB", e);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (this.contextManager.apiUrl) {
|
|
450
|
+
try {
|
|
451
|
+
const res = await this.callCoordination(`locks?projectName=${this.projectName}`);
|
|
452
|
+
return (res.locks || []).map((row) => ({
|
|
453
|
+
agentId: row.agent_id,
|
|
454
|
+
filePath: row.file_path,
|
|
455
|
+
intent: row.intent,
|
|
456
|
+
userPrompt: row.user_prompt,
|
|
457
|
+
timestamp: Date.parse(row.updated_at || row.timestamp)
|
|
458
|
+
}));
|
|
459
|
+
} catch (e) {
|
|
460
|
+
logger.error("Failed to fetch locks from API in fallback", e);
|
|
461
|
+
}
|
|
330
462
|
}
|
|
463
|
+
return Object.values(this.state.locks);
|
|
331
464
|
}
|
|
332
465
|
async getNotepad() {
|
|
333
|
-
if (
|
|
334
|
-
|
|
466
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
467
|
+
const { data, error } = await this.supabase.from("projects").select("live_notepad").eq("id", this._projectId).single();
|
|
468
|
+
if (!error && data) return data.live_notepad || "";
|
|
335
469
|
}
|
|
336
|
-
|
|
337
|
-
if (error || !data) {
|
|
338
|
-
logger.error("Failed to fetch notepad", error);
|
|
339
|
-
return this.state.liveNotepad;
|
|
340
|
-
}
|
|
341
|
-
return data.live_notepad || "";
|
|
470
|
+
return this.state.liveNotepad;
|
|
342
471
|
}
|
|
343
472
|
async appendToNotepad(text) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
473
|
+
this.state.liveNotepad += text;
|
|
474
|
+
await this.saveState();
|
|
475
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
476
|
+
try {
|
|
477
|
+
await this.supabase.rpc("append_to_project_notepad", {
|
|
478
|
+
p_project_id: this._projectId,
|
|
479
|
+
p_text: text
|
|
480
|
+
});
|
|
481
|
+
} catch (e) {
|
|
482
|
+
}
|
|
348
483
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
484
|
+
if (this.contextManager.apiUrl) {
|
|
485
|
+
try {
|
|
486
|
+
const res = await this.callCoordination("sessions/sync", "POST", {
|
|
487
|
+
title: `Current Session: ${this.projectName}`,
|
|
488
|
+
context: this.state.liveNotepad,
|
|
489
|
+
metadata: { source: "mcp-server-live" }
|
|
490
|
+
});
|
|
491
|
+
if (res.projectId && !this._projectId) {
|
|
492
|
+
this._projectId = res.projectId;
|
|
493
|
+
logger.info(`NerveCenter: Captured projectId from sync API: ${this._projectId}`);
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {
|
|
496
|
+
logger.warn("Failed to sync notepad to remote API", e);
|
|
497
|
+
}
|
|
356
498
|
}
|
|
357
499
|
}
|
|
358
500
|
async saveState() {
|
|
@@ -375,25 +517,35 @@ var NerveCenter = class {
|
|
|
375
517
|
async postJob(title, description, priority = "medium", dependencies = []) {
|
|
376
518
|
return await this.mutex.runExclusive(async () => {
|
|
377
519
|
let id = `job-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
|
|
520
|
+
const completionKey = Math.random().toString(36).substring(2, 10).toUpperCase();
|
|
378
521
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
379
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
380
522
|
const { data, error } = await this.supabase.from("jobs").insert({
|
|
381
523
|
project_id: this._projectId,
|
|
382
524
|
title,
|
|
383
525
|
description,
|
|
384
526
|
priority,
|
|
385
527
|
status: "todo",
|
|
386
|
-
assigned_to: null,
|
|
387
528
|
dependencies,
|
|
388
|
-
|
|
389
|
-
updated_at: now
|
|
529
|
+
completion_key: completionKey
|
|
390
530
|
}).select("id").single();
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
531
|
+
if (data?.id) id = data.id;
|
|
532
|
+
if (error) logger.error("Failed to post job to Supabase", error);
|
|
533
|
+
} else if (this.contextManager.apiUrl) {
|
|
534
|
+
try {
|
|
535
|
+
const data = await this.callCoordination("jobs", "POST", {
|
|
536
|
+
action: "post",
|
|
537
|
+
title,
|
|
538
|
+
description,
|
|
539
|
+
priority,
|
|
540
|
+
dependencies,
|
|
541
|
+
completion_key: completionKey
|
|
542
|
+
});
|
|
543
|
+
if (data?.id) id = data.id;
|
|
544
|
+
} catch (e) {
|
|
545
|
+
logger.error("Failed to post job to API", e);
|
|
395
546
|
}
|
|
396
|
-
}
|
|
547
|
+
}
|
|
548
|
+
if (!this.useSupabase && !this.contextManager.apiUrl) {
|
|
397
549
|
this.state.jobs[id] = {
|
|
398
550
|
id,
|
|
399
551
|
title,
|
|
@@ -402,21 +554,54 @@ var NerveCenter = class {
|
|
|
402
554
|
dependencies,
|
|
403
555
|
status: "todo",
|
|
404
556
|
createdAt: Date.now(),
|
|
405
|
-
updatedAt: Date.now()
|
|
557
|
+
updatedAt: Date.now(),
|
|
558
|
+
completionKey
|
|
406
559
|
};
|
|
407
560
|
}
|
|
408
561
|
const depText = dependencies.length ? ` (Depends on: ${dependencies.join(", ")})` : "";
|
|
409
562
|
const logEntry = `
|
|
410
563
|
- [JOB POSTED] [${priority.toUpperCase()}] ${title} (ID: ${id})${depText}`;
|
|
411
564
|
await this.appendToNotepad(logEntry);
|
|
412
|
-
|
|
413
|
-
return { jobId: id, status: "POSTED" };
|
|
565
|
+
return { jobId: id, status: "POSTED", completionKey };
|
|
414
566
|
});
|
|
415
567
|
}
|
|
416
568
|
async claimNextJob(agentId) {
|
|
417
569
|
return await this.mutex.runExclusive(async () => {
|
|
570
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
571
|
+
const { data, error } = await this.supabase.rpc("claim_next_job", {
|
|
572
|
+
p_project_id: this._projectId,
|
|
573
|
+
p_agent_id: agentId
|
|
574
|
+
});
|
|
575
|
+
if (error) {
|
|
576
|
+
logger.error("Failed to claim job via RPC", error);
|
|
577
|
+
} else if (data && data.status === "CLAIMED") {
|
|
578
|
+
const job2 = this.jobFromRecord(data.job);
|
|
579
|
+
await this.appendToNotepad(`
|
|
580
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
|
|
581
|
+
return { status: "CLAIMED", job: job2 };
|
|
582
|
+
}
|
|
583
|
+
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
584
|
+
}
|
|
585
|
+
if (this.contextManager.apiUrl) {
|
|
586
|
+
try {
|
|
587
|
+
const data = await this.callCoordination("jobs", "POST", {
|
|
588
|
+
action: "claim",
|
|
589
|
+
agentId
|
|
590
|
+
});
|
|
591
|
+
if (data && data.status === "CLAIMED") {
|
|
592
|
+
const job2 = this.jobFromRecord(data.job);
|
|
593
|
+
await this.appendToNotepad(`
|
|
594
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`);
|
|
595
|
+
return { status: "CLAIMED", job: job2 };
|
|
596
|
+
}
|
|
597
|
+
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
598
|
+
} catch (e) {
|
|
599
|
+
logger.error("Failed to claim job via API", e);
|
|
600
|
+
return { status: "NO_JOBS_AVAILABLE", message: `Claim failed: ${e.message}` };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
418
603
|
const priorities = ["critical", "high", "medium", "low"];
|
|
419
|
-
const allJobs =
|
|
604
|
+
const allJobs = Object.values(this.state.jobs);
|
|
420
605
|
const jobsById = new Map(allJobs.map((job2) => [job2.id, job2]));
|
|
421
606
|
const availableJobs = allJobs.filter((job2) => job2.status === "todo").filter((job2) => {
|
|
422
607
|
if (!job2.dependencies || job2.dependencies.length === 0) return true;
|
|
@@ -430,118 +615,110 @@ var NerveCenter = class {
|
|
|
430
615
|
if (availableJobs.length === 0) {
|
|
431
616
|
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
432
617
|
}
|
|
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
618
|
const job = availableJobs[0];
|
|
456
619
|
job.status = "in_progress";
|
|
457
620
|
job.assignedTo = agentId;
|
|
458
621
|
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();
|
|
622
|
+
await this.appendToNotepad(`
|
|
623
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job.title}`);
|
|
463
624
|
return { status: "CLAIMED", job };
|
|
464
625
|
});
|
|
465
626
|
}
|
|
466
627
|
async cancelJob(jobId, reason) {
|
|
467
628
|
return await this.mutex.runExclusive(async () => {
|
|
468
|
-
if (this.useSupabase && this.supabase) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
629
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
630
|
+
await this.supabase.from("jobs").update({ status: "cancelled", cancel_reason: reason, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId);
|
|
631
|
+
} else if (this.contextManager.apiUrl) {
|
|
632
|
+
try {
|
|
633
|
+
await this.callCoordination("jobs", "POST", { action: "update", jobId, status: "cancelled", cancel_reason: reason });
|
|
634
|
+
} catch (e) {
|
|
635
|
+
logger.error("Failed to cancel job via API", e);
|
|
472
636
|
}
|
|
473
|
-
this.state.liveNotepad += `
|
|
474
|
-
- [JOB CANCELLED] ${data[0].title} (ID: ${jobId}). Reason: ${reason}`;
|
|
475
|
-
await this.saveState();
|
|
476
|
-
return { status: "CANCELLED" };
|
|
477
637
|
}
|
|
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}`;
|
|
638
|
+
if (this.state.jobs[jobId]) {
|
|
639
|
+
this.state.jobs[jobId].status = "cancelled";
|
|
640
|
+
this.state.jobs[jobId].updatedAt = Date.now();
|
|
495
641
|
await this.saveState();
|
|
496
|
-
return { status: "UNLOCKED" };
|
|
497
642
|
}
|
|
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 };
|
|
643
|
+
await this.appendToNotepad(`
|
|
644
|
+
- [JOB CANCELLED] ID: ${jobId}. Reason: ${reason}`);
|
|
645
|
+
return "Job cancelled.";
|
|
505
646
|
});
|
|
506
647
|
}
|
|
507
|
-
async completeJob(agentId, jobId, outcome) {
|
|
648
|
+
async completeJob(agentId, jobId, outcome, completionKey) {
|
|
508
649
|
return await this.mutex.runExclusive(async () => {
|
|
509
650
|
if (this.useSupabase && this.supabase) {
|
|
510
|
-
const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to").eq("id", jobId).single();
|
|
651
|
+
const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to,completion_key").eq("id", jobId).single();
|
|
511
652
|
if (error || !data) return { error: "Job not found" };
|
|
512
|
-
|
|
513
|
-
const
|
|
653
|
+
const isOwner2 = data.assigned_to === agentId;
|
|
654
|
+
const isKeyValid2 = completionKey && data.completion_key === completionKey;
|
|
655
|
+
if (!isOwner2 && !isKeyValid2) {
|
|
656
|
+
return { error: "You don't own this job and provided no valid key." };
|
|
657
|
+
}
|
|
658
|
+
const { error: updateError } = await this.supabase.from("jobs").update({ status: "done", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId);
|
|
514
659
|
if (updateError) return { error: "Failed to complete job" };
|
|
515
660
|
await this.appendToNotepad(`
|
|
516
|
-
- [JOB DONE]
|
|
517
|
-
|
|
661
|
+
- [JOB DONE] Agent '${agentId}' finished: ${data.title}
|
|
662
|
+
Outcome: ${outcome}`);
|
|
518
663
|
return { status: "COMPLETED" };
|
|
664
|
+
} else if (this.contextManager.apiUrl) {
|
|
665
|
+
try {
|
|
666
|
+
await this.callCoordination("jobs", "POST", {
|
|
667
|
+
action: "update",
|
|
668
|
+
jobId,
|
|
669
|
+
status: "done",
|
|
670
|
+
assigned_to: agentId,
|
|
671
|
+
completion_key: completionKey
|
|
672
|
+
});
|
|
673
|
+
await this.appendToNotepad(`
|
|
674
|
+
- [JOB DONE] Agent '${agentId}' finished: ${jobId}
|
|
675
|
+
Outcome: ${outcome}`);
|
|
676
|
+
return { status: "COMPLETED" };
|
|
677
|
+
} catch (e) {
|
|
678
|
+
logger.error("Failed to complete job via API", e);
|
|
679
|
+
}
|
|
519
680
|
}
|
|
520
681
|
const job = this.state.jobs[jobId];
|
|
521
682
|
if (!job) return { error: "Job not found" };
|
|
522
|
-
|
|
683
|
+
const isOwner = job.assignedTo === agentId;
|
|
684
|
+
const isKeyValid = completionKey && job.completionKey === completionKey;
|
|
685
|
+
if (!isOwner && !isKeyValid) {
|
|
686
|
+
return { error: "You don't own this job and provided no valid key." };
|
|
687
|
+
}
|
|
523
688
|
job.status = "done";
|
|
524
689
|
job.updatedAt = Date.now();
|
|
525
|
-
this.
|
|
526
|
-
- [JOB DONE]
|
|
527
|
-
|
|
690
|
+
await this.appendToNotepad(`
|
|
691
|
+
- [JOB DONE] Agent '${agentId}' finished: ${job.title}
|
|
692
|
+
Outcome: ${outcome}`);
|
|
528
693
|
return { status: "COMPLETED" };
|
|
529
694
|
});
|
|
530
695
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
696
|
+
async forceUnlock(filePath, reason) {
|
|
697
|
+
return await this.mutex.runExclusive(async () => {
|
|
698
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
699
|
+
await this.supabase.from("locks").delete().eq("project_id", this._projectId).eq("file_path", filePath);
|
|
700
|
+
} else if (this.contextManager.apiUrl) {
|
|
701
|
+
try {
|
|
702
|
+
await this.callCoordination("locks", "POST", { action: "unlock", filePath, reason });
|
|
703
|
+
} catch (e) {
|
|
704
|
+
logger.error("Failed to force unlock via API", e);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (this.state.locks[filePath]) {
|
|
708
|
+
delete this.state.locks[filePath];
|
|
709
|
+
await this.saveState();
|
|
710
|
+
}
|
|
711
|
+
await this.appendToNotepad(`
|
|
712
|
+
- [FORCE UNLOCK] ${filePath} unlocked by admin. Reason: ${reason}`);
|
|
713
|
+
return `File ${filePath} has been forcibly unlocked.`;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
async getCoreContext() {
|
|
539
717
|
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");
|
|
718
|
+
const locks = await this.getLocks();
|
|
544
719
|
const notepad = await this.getNotepad();
|
|
720
|
+
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");
|
|
721
|
+
const lockSummary = locks.map((l) => `- ${l.filePath} (Locked by: ${l.agentId}, Intent: ${l.intent})`).join("\n");
|
|
545
722
|
return `# Active Session Context
|
|
546
723
|
|
|
547
724
|
## Job Board (Active Orchestration)
|
|
@@ -556,40 +733,89 @@ ${notepad}`;
|
|
|
556
733
|
// --- Decision & Orchestration ---
|
|
557
734
|
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
558
735
|
return await this.mutex.runExclusive(async () => {
|
|
559
|
-
|
|
560
|
-
|
|
736
|
+
logger.info(`[proposeFileAccess] Starting - agentId: ${agentId}, filePath: ${filePath}`);
|
|
737
|
+
if (this.contextManager.apiUrl) {
|
|
738
|
+
try {
|
|
739
|
+
const result = await this.callCoordination("locks", "POST", {
|
|
740
|
+
action: "lock",
|
|
741
|
+
filePath,
|
|
742
|
+
agentId,
|
|
743
|
+
intent,
|
|
744
|
+
userPrompt
|
|
745
|
+
});
|
|
746
|
+
if (result.status === "DENIED") {
|
|
747
|
+
logger.info(`[proposeFileAccess] DENIED by server: ${result.message}`);
|
|
748
|
+
return {
|
|
749
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
750
|
+
message: result.message || `File '${filePath}' is locked by another agent`,
|
|
751
|
+
currentLock: result.current_lock
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
logger.info(`[proposeFileAccess] GRANTED by server`);
|
|
755
|
+
await this.appendToNotepad(`
|
|
756
|
+
- [LOCK] ${agentId} locked ${filePath}
|
|
757
|
+
Intent: ${intent}`);
|
|
758
|
+
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
759
|
+
} catch (e) {
|
|
760
|
+
if (e.message && e.message.includes("409")) {
|
|
761
|
+
logger.info(`[proposeFileAccess] Lock conflict (409)`);
|
|
762
|
+
return {
|
|
763
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
764
|
+
message: `File '${filePath}' is locked by another agent`
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
logger.error(`[proposeFileAccess] API lock failed: ${e.message}`, e);
|
|
768
|
+
return { error: `Failed to acquire lock via API: ${e.message}` };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
772
|
+
try {
|
|
773
|
+
const { data, error } = await this.supabase.rpc("try_acquire_lock", {
|
|
774
|
+
p_project_id: this._projectId,
|
|
775
|
+
p_file_path: filePath,
|
|
776
|
+
p_agent_id: agentId,
|
|
777
|
+
p_intent: intent,
|
|
778
|
+
p_user_prompt: userPrompt,
|
|
779
|
+
p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
|
|
780
|
+
});
|
|
781
|
+
if (error) throw error;
|
|
782
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
783
|
+
if (row && row.status === "DENIED") {
|
|
784
|
+
return {
|
|
785
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
786
|
+
message: `Conflict: File '${filePath}' is locked by '${row.owner_id}'`,
|
|
787
|
+
currentLock: {
|
|
788
|
+
agentId: row.owner_id,
|
|
789
|
+
filePath,
|
|
790
|
+
intent: row.intent,
|
|
791
|
+
timestamp: row.updated_at ? Date.parse(row.updated_at) : Date.now()
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
await this.appendToNotepad(`
|
|
796
|
+
- [LOCK] ${agentId} locked ${filePath}
|
|
797
|
+
Intent: ${intent}`);
|
|
798
|
+
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
799
|
+
} catch (e) {
|
|
800
|
+
logger.warn("[NerveCenter] Lock RPC failed. Falling back to local.", e);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const existing = Object.values(this.state.locks).find((l) => l.filePath === filePath);
|
|
561
804
|
if (existing) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
if (!isStale && existing.agent_id !== agentId) {
|
|
805
|
+
const isStale = Date.now() - existing.timestamp > this.lockTimeout;
|
|
806
|
+
if (!isStale && existing.agentId !== agentId) {
|
|
565
807
|
return {
|
|
566
808
|
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
|
-
}
|
|
809
|
+
message: `Conflict: File '${filePath}' is currently locked by '${existing.agentId}'`,
|
|
810
|
+
currentLock: existing
|
|
573
811
|
};
|
|
574
812
|
}
|
|
575
813
|
}
|
|
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
|
-
}
|
|
814
|
+
this.state.locks[filePath] = { agentId, filePath, intent, userPrompt, timestamp: Date.now() };
|
|
815
|
+
await this.saveState();
|
|
588
816
|
await this.appendToNotepad(`
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
**Intent:** ${intent}
|
|
592
|
-
**Prompt:** "${userPrompt}"`);
|
|
817
|
+
- [LOCK] ${agentId} locked ${filePath}
|
|
818
|
+
Intent: ${intent}`);
|
|
593
819
|
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
594
820
|
});
|
|
595
821
|
}
|
|
@@ -605,7 +831,12 @@ ${notepad}`;
|
|
|
605
831
|
const content = await this.getNotepad();
|
|
606
832
|
const filename = `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.md`;
|
|
607
833
|
const historyPath = path2.join(process.cwd(), "history", filename);
|
|
608
|
-
|
|
834
|
+
try {
|
|
835
|
+
await fs2.mkdir(path2.dirname(historyPath), { recursive: true });
|
|
836
|
+
await fs2.writeFile(historyPath, content);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
logger.warn("Failed to write local session log", e);
|
|
839
|
+
}
|
|
609
840
|
if (this.useSupabase && this.supabase && this._projectId) {
|
|
610
841
|
await this.supabase.from("sessions").insert({
|
|
611
842
|
project_id: this._projectId,
|
|
@@ -616,13 +847,18 @@ ${notepad}`;
|
|
|
616
847
|
await this.supabase.from("projects").update({ live_notepad: "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n" }).eq("id", this._projectId);
|
|
617
848
|
await this.supabase.from("jobs").delete().eq("project_id", this._projectId).in("status", ["done", "cancelled"]);
|
|
618
849
|
await this.supabase.from("locks").delete().eq("project_id", this._projectId);
|
|
619
|
-
} else {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
850
|
+
} else if (this.contextManager.apiUrl) {
|
|
851
|
+
try {
|
|
852
|
+
await this.callCoordination("sessions/finalize", "POST", { content });
|
|
853
|
+
} catch (e) {
|
|
854
|
+
logger.error("Failed to finalize session via API", e);
|
|
855
|
+
}
|
|
625
856
|
}
|
|
857
|
+
this.state.liveNotepad = "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n";
|
|
858
|
+
this.state.locks = {};
|
|
859
|
+
this.state.jobs = Object.fromEntries(
|
|
860
|
+
Object.entries(this.state.jobs).filter(([_, j]) => j.status !== "done" && j.status !== "cancelled")
|
|
861
|
+
);
|
|
626
862
|
await this.saveState();
|
|
627
863
|
return {
|
|
628
864
|
status: "SESSION_FINALIZED",
|
|
@@ -648,32 +884,55 @@ ${conventions}`;
|
|
|
648
884
|
}
|
|
649
885
|
// --- Billing & Usage ---
|
|
650
886
|
async getSubscriptionStatus(email) {
|
|
651
|
-
|
|
652
|
-
|
|
887
|
+
logger.info(`[getSubscriptionStatus] Starting - email: ${email}`);
|
|
888
|
+
logger.info(`[getSubscriptionStatus] Config - apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET" : "NOT SET"}, useSupabase: ${this.useSupabase}`);
|
|
889
|
+
if (this.contextManager.apiUrl) {
|
|
890
|
+
try {
|
|
891
|
+
logger.info(`[getSubscriptionStatus] Attempting API call to: usage?email=${encodeURIComponent(email)}`);
|
|
892
|
+
const result = await this.callCoordination(`usage?email=${encodeURIComponent(email)}`);
|
|
893
|
+
logger.info(`[getSubscriptionStatus] API call successful: ${JSON.stringify(result).substring(0, 200)}`);
|
|
894
|
+
return result;
|
|
895
|
+
} catch (e) {
|
|
896
|
+
logger.error(`[getSubscriptionStatus] API call failed: ${e.message}`, e);
|
|
897
|
+
return { error: `API call failed: ${e.message}` };
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
logger.warn("[getSubscriptionStatus] No API URL configured");
|
|
653
901
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
902
|
+
if (this.useSupabase && this.supabase) {
|
|
903
|
+
const { data: profile, error } = await this.supabase.from("profiles").select("subscription_status, stripe_customer_id, current_period_end").eq("email", email).single();
|
|
904
|
+
if (error || !profile) {
|
|
905
|
+
return { status: "unknown", message: "Profile not found." };
|
|
906
|
+
}
|
|
907
|
+
const isActive = profile.subscription_status === "pro" || profile.current_period_end && new Date(profile.current_period_end) > /* @__PURE__ */ new Date();
|
|
908
|
+
return {
|
|
909
|
+
email,
|
|
910
|
+
plan: isActive ? "Pro" : "Free",
|
|
911
|
+
status: profile.subscription_status || "free",
|
|
912
|
+
validUntil: profile.current_period_end
|
|
913
|
+
};
|
|
657
914
|
}
|
|
658
|
-
|
|
659
|
-
return {
|
|
660
|
-
email,
|
|
661
|
-
plan: isActive ? "Pro" : "Free",
|
|
662
|
-
status: profile.subscription_status || "free",
|
|
663
|
-
validUntil: profile.current_period_end
|
|
664
|
-
};
|
|
915
|
+
return { error: "Coordination not configured. API URL not set and Supabase not available." };
|
|
665
916
|
}
|
|
666
917
|
async getUsageStats(email) {
|
|
667
|
-
|
|
668
|
-
|
|
918
|
+
logger.info(`[getUsageStats] Starting - email: ${email}`);
|
|
919
|
+
logger.info(`[getUsageStats] Config - apiUrl: ${this.contextManager.apiUrl}, apiSecret: ${this.contextManager.apiSecret ? "SET" : "NOT SET"}, useSupabase: ${this.useSupabase}`);
|
|
920
|
+
if (this.contextManager.apiUrl) {
|
|
921
|
+
try {
|
|
922
|
+
logger.info(`[getUsageStats] Attempting API call to: usage?email=${encodeURIComponent(email)}`);
|
|
923
|
+
const result = await this.callCoordination(`usage?email=${encodeURIComponent(email)}`);
|
|
924
|
+
logger.info(`[getUsageStats] API call successful: ${JSON.stringify(result).substring(0, 200)}`);
|
|
925
|
+
return { email, usageCount: result.usageCount || 0 };
|
|
926
|
+
} catch (e) {
|
|
927
|
+
logger.error(`[getUsageStats] API call failed: ${e.message}`, e);
|
|
928
|
+
return { error: `API call failed: ${e.message}` };
|
|
929
|
+
}
|
|
669
930
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
email,
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// Hardcoded placeholder limit
|
|
676
|
-
};
|
|
931
|
+
if (this.useSupabase && this.supabase) {
|
|
932
|
+
const { data: profile } = await this.supabase.from("profiles").select("usage_count").eq("email", email).single();
|
|
933
|
+
return { email, usageCount: profile?.usage_count || 0 };
|
|
934
|
+
}
|
|
935
|
+
return { error: "Coordination not configured. API URL not set and Supabase not available." };
|
|
677
936
|
}
|
|
678
937
|
};
|
|
679
938
|
|
|
@@ -734,7 +993,7 @@ var RagEngine = class {
|
|
|
734
993
|
const embedding = resp.data[0].embedding;
|
|
735
994
|
const { data, error } = await this.supabase.rpc("match_embeddings", {
|
|
736
995
|
query_embedding: embedding,
|
|
737
|
-
match_threshold: 0.
|
|
996
|
+
match_threshold: 0.1,
|
|
738
997
|
match_count: limit,
|
|
739
998
|
p_project_id: this.projectId
|
|
740
999
|
});
|
|
@@ -751,44 +1010,101 @@ var RagEngine = class {
|
|
|
751
1010
|
};
|
|
752
1011
|
|
|
753
1012
|
// ../../src/local/mcp-server.ts
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1013
|
+
import path3 from "path";
|
|
1014
|
+
import fs3 from "fs";
|
|
1015
|
+
if (process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_KEY) {
|
|
1016
|
+
logger.info("Using configuration from MCP client (mcp.json)");
|
|
1017
|
+
} else {
|
|
1018
|
+
const cwd = process.cwd();
|
|
1019
|
+
const possiblePaths = [
|
|
1020
|
+
path3.join(cwd, ".env.local"),
|
|
1021
|
+
path3.join(cwd, "..", ".env.local"),
|
|
1022
|
+
path3.join(cwd, "..", "..", ".env.local"),
|
|
1023
|
+
path3.join(cwd, "shared-context", ".env.local"),
|
|
1024
|
+
path3.join(cwd, "..", "shared-context", ".env.local")
|
|
1025
|
+
];
|
|
1026
|
+
let envLoaded = false;
|
|
1027
|
+
for (const envPath of possiblePaths) {
|
|
1028
|
+
try {
|
|
1029
|
+
if (fs3.existsSync(envPath)) {
|
|
1030
|
+
logger.info(`[Fallback] Loading .env.local from: ${envPath}`);
|
|
1031
|
+
dotenv2.config({ path: envPath });
|
|
1032
|
+
envLoaded = true;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (!envLoaded) {
|
|
1039
|
+
logger.warn("No configuration found from MCP client (mcp.json) or .env.local");
|
|
1040
|
+
logger.warn("MCP server will use default API URL: https://aicontext.vercel.app/api/v1");
|
|
1041
|
+
}
|
|
757
1042
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1043
|
+
logger.info("=== Axis MCP Server Starting ===");
|
|
1044
|
+
logger.info("Environment check:", {
|
|
1045
|
+
hasSHARED_CONTEXT_API_URL: !!process.env.SHARED_CONTEXT_API_URL,
|
|
1046
|
+
hasAXIS_API_KEY: !!process.env.AXIS_API_KEY,
|
|
1047
|
+
hasSHARED_CONTEXT_API_SECRET: !!process.env.SHARED_CONTEXT_API_SECRET,
|
|
1048
|
+
hasNEXT_PUBLIC_SUPABASE_URL: !!process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
1049
|
+
hasSUPABASE_SERVICE_ROLE_KEY: !!process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
1050
|
+
PROJECT_NAME: process.env.PROJECT_NAME || "default"
|
|
1051
|
+
});
|
|
1052
|
+
var apiUrl = process.env.SHARED_CONTEXT_API_URL || process.env.AXIS_API_URL || "https://aicontext.vercel.app/api/v1";
|
|
1053
|
+
var apiSecret = process.env.AXIS_API_KEY || process.env.SHARED_CONTEXT_API_SECRET || process.env.AXIS_API_SECRET;
|
|
1054
|
+
var useRemoteApiOnly = !!process.env.SHARED_CONTEXT_API_URL || !!process.env.AXIS_API_KEY;
|
|
1055
|
+
if (useRemoteApiOnly) {
|
|
1056
|
+
logger.info("Running in REMOTE API mode - Supabase credentials not needed locally.");
|
|
1057
|
+
logger.info(`Remote API: ${apiUrl}`);
|
|
1058
|
+
logger.info(`API Key: ${apiSecret ? apiSecret.substring(0, 15) + "..." : "NOT SET"}`);
|
|
1059
|
+
} else if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
1060
|
+
logger.warn("No remote API configured and Supabase credentials missing. Running in local/ephemeral mode.");
|
|
1061
|
+
} else {
|
|
1062
|
+
logger.info("Running in DIRECT SUPABASE mode (development).");
|
|
1063
|
+
}
|
|
1064
|
+
logger.info("ContextManager config:", {
|
|
1065
|
+
apiUrl,
|
|
1066
|
+
hasApiSecret: !!apiSecret,
|
|
1067
|
+
source: useRemoteApiOnly ? "MCP config (mcp.json)" : "default/fallback"
|
|
1068
|
+
});
|
|
1069
|
+
var manager = new ContextManager(apiUrl, apiSecret);
|
|
1070
|
+
logger.info("NerveCenter config:", {
|
|
1071
|
+
useRemoteApiOnly,
|
|
1072
|
+
supabaseUrl: useRemoteApiOnly ? "DISABLED (using remote API)" : process.env.NEXT_PUBLIC_SUPABASE_URL ? "SET" : "NOT SET",
|
|
1073
|
+
supabaseKey: useRemoteApiOnly ? "DISABLED (using remote API)" : process.env.SUPABASE_SERVICE_ROLE_KEY ? "SET" : "NOT SET",
|
|
1074
|
+
projectName: process.env.PROJECT_NAME || "default"
|
|
1075
|
+
});
|
|
762
1076
|
var nerveCenter = new NerveCenter(manager, {
|
|
763
|
-
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
764
|
-
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
1077
|
+
supabaseUrl: useRemoteApiOnly ? null : process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
1078
|
+
supabaseServiceRoleKey: useRemoteApiOnly ? null : process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
765
1079
|
projectName: process.env.PROJECT_NAME || "default"
|
|
766
1080
|
});
|
|
1081
|
+
logger.info("=== Axis MCP Server Initialized ===");
|
|
767
1082
|
var ragEngine;
|
|
768
|
-
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
1083
|
+
if (!useRemoteApiOnly && process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
769
1084
|
ragEngine = new RagEngine(
|
|
770
1085
|
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
771
1086
|
process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
772
1087
|
process.env.OPENAI_API_KEY || ""
|
|
773
1088
|
);
|
|
1089
|
+
logger.info("Local RAG Engine initialized.");
|
|
774
1090
|
}
|
|
775
1091
|
async function ensureFileSystem() {
|
|
776
1092
|
try {
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
const
|
|
1093
|
+
const fs4 = await import("fs/promises");
|
|
1094
|
+
const path4 = await import("path");
|
|
1095
|
+
const fsSync2 = await import("fs");
|
|
780
1096
|
const cwd = process.cwd();
|
|
781
1097
|
logger.info(`Server CWD: ${cwd}`);
|
|
782
|
-
const historyDir =
|
|
783
|
-
await
|
|
1098
|
+
const historyDir = path4.join(cwd, "history");
|
|
1099
|
+
await fs4.mkdir(historyDir, { recursive: true }).catch(() => {
|
|
784
1100
|
});
|
|
785
|
-
const axisDir =
|
|
786
|
-
const axisInstructions =
|
|
787
|
-
const legacyInstructions =
|
|
788
|
-
if (
|
|
1101
|
+
const axisDir = path4.join(cwd, ".axis");
|
|
1102
|
+
const axisInstructions = path4.join(axisDir, "instructions");
|
|
1103
|
+
const legacyInstructions = path4.join(cwd, "agent-instructions");
|
|
1104
|
+
if (fsSync2.existsSync(legacyInstructions) && !fsSync2.existsSync(axisDir)) {
|
|
789
1105
|
logger.info("Using legacy agent-instructions directory");
|
|
790
1106
|
} else {
|
|
791
|
-
await
|
|
1107
|
+
await fs4.mkdir(axisInstructions, { recursive: true }).catch(() => {
|
|
792
1108
|
});
|
|
793
1109
|
const defaults = [
|
|
794
1110
|
["context.md", "# Project Context\n\n"],
|
|
@@ -796,11 +1112,11 @@ async function ensureFileSystem() {
|
|
|
796
1112
|
["activity.md", "# Activity Log\n\n"]
|
|
797
1113
|
];
|
|
798
1114
|
for (const [file, content] of defaults) {
|
|
799
|
-
const p =
|
|
1115
|
+
const p = path4.join(axisInstructions, file);
|
|
800
1116
|
try {
|
|
801
|
-
await
|
|
1117
|
+
await fs4.access(p);
|
|
802
1118
|
} catch {
|
|
803
|
-
await
|
|
1119
|
+
await fs4.writeFile(p, content);
|
|
804
1120
|
logger.info(`Created default context file: ${file}`);
|
|
805
1121
|
}
|
|
806
1122
|
}
|
|
@@ -826,17 +1142,18 @@ var UPDATE_CONTEXT_TOOL = "update_context";
|
|
|
826
1142
|
var SEARCH_CONTEXT_TOOL = "search_codebase";
|
|
827
1143
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
828
1144
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
};
|
|
1145
|
+
const files = await manager.listFiles();
|
|
1146
|
+
const resources = [
|
|
1147
|
+
{
|
|
1148
|
+
uri: "mcp://context/current",
|
|
1149
|
+
name: "Live Session Context",
|
|
1150
|
+
mimeType: "text/markdown",
|
|
1151
|
+
description: "The realtime state of the Nerve Center (Notepad + Locks)"
|
|
1152
|
+
},
|
|
1153
|
+
...files
|
|
1154
|
+
];
|
|
1155
|
+
logger.info(`[ListResources] Returning ${resources.length} resources to MCP client`);
|
|
1156
|
+
return { resources };
|
|
840
1157
|
} catch (error) {
|
|
841
1158
|
logger.error("Error listing resources", error);
|
|
842
1159
|
return { resources: [] };
|
|
@@ -850,7 +1167,7 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
850
1167
|
contents: [{
|
|
851
1168
|
uri,
|
|
852
1169
|
mimeType: "text/markdown",
|
|
853
|
-
text: await nerveCenter.
|
|
1170
|
+
text: await nerveCenter.getCoreContext()
|
|
854
1171
|
}]
|
|
855
1172
|
};
|
|
856
1173
|
}
|
|
@@ -873,192 +1190,193 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
873
1190
|
}
|
|
874
1191
|
});
|
|
875
1192
|
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
|
-
required: ["email"]
|
|
923
|
-
}
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
name: "get_usage_stats",
|
|
927
|
-
description: "Get API usage statistics for a user.",
|
|
928
|
-
inputSchema: {
|
|
929
|
-
type: "object",
|
|
930
|
-
properties: {
|
|
931
|
-
email: { type: "string", description: "User email to check." }
|
|
932
|
-
},
|
|
933
|
-
required: ["email"]
|
|
934
|
-
}
|
|
935
|
-
},
|
|
936
|
-
{
|
|
937
|
-
name: "search_docs",
|
|
938
|
-
description: "Search the Axis documentation.",
|
|
939
|
-
inputSchema: {
|
|
940
|
-
type: "object",
|
|
941
|
-
properties: {
|
|
942
|
-
query: { type: "string", description: "Search query." }
|
|
943
|
-
},
|
|
944
|
-
required: ["query"]
|
|
945
|
-
}
|
|
946
|
-
},
|
|
947
|
-
// --- Decision & Orchestration ---
|
|
948
|
-
{
|
|
949
|
-
name: "propose_file_access",
|
|
950
|
-
description: "Request a lock on a file. Checks for conflicts with other agents.",
|
|
951
|
-
inputSchema: {
|
|
952
|
-
type: "object",
|
|
953
|
-
properties: {
|
|
954
|
-
agentId: { type: "string" },
|
|
955
|
-
filePath: { type: "string" },
|
|
956
|
-
intent: { type: "string" },
|
|
957
|
-
userPrompt: { type: "string", description: "The full prompt provided by the user that initiated this action." }
|
|
958
|
-
},
|
|
959
|
-
required: ["agentId", "filePath", "intent", "userPrompt"]
|
|
960
|
-
}
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
name: "update_shared_context",
|
|
964
|
-
description: "Write to the in-memory Live Notepad.",
|
|
965
|
-
inputSchema: {
|
|
966
|
-
type: "object",
|
|
967
|
-
properties: {
|
|
968
|
-
agentId: { type: "string" },
|
|
969
|
-
text: { type: "string" }
|
|
970
|
-
},
|
|
971
|
-
required: ["agentId", "text"]
|
|
972
|
-
}
|
|
973
|
-
},
|
|
974
|
-
// --- Permanent Memory ---
|
|
975
|
-
{
|
|
976
|
-
name: "finalize_session",
|
|
977
|
-
description: "End the session, archive the notepad, and clear locks.",
|
|
978
|
-
inputSchema: { type: "object", properties: {}, required: [] }
|
|
979
|
-
},
|
|
980
|
-
{
|
|
981
|
-
name: "get_project_soul",
|
|
982
|
-
description: "Get high-level project goals and context.",
|
|
983
|
-
inputSchema: { type: "object", properties: {}, required: [] }
|
|
984
|
-
},
|
|
985
|
-
// --- Job Board (Task Orchestration) ---
|
|
986
|
-
{
|
|
987
|
-
name: "post_job",
|
|
988
|
-
description: "Post a new job/ticket. Supports priority and dependencies.",
|
|
989
|
-
inputSchema: {
|
|
990
|
-
type: "object",
|
|
991
|
-
properties: {
|
|
992
|
-
title: { type: "string" },
|
|
993
|
-
description: { type: "string" },
|
|
994
|
-
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
995
|
-
dependencies: { type: "array", items: { type: "string" } }
|
|
996
|
-
},
|
|
997
|
-
required: ["title", "description"]
|
|
998
|
-
}
|
|
999
|
-
},
|
|
1000
|
-
{
|
|
1001
|
-
name: "cancel_job",
|
|
1002
|
-
description: "Cancel a job that is no longer needed.",
|
|
1003
|
-
inputSchema: {
|
|
1004
|
-
type: "object",
|
|
1005
|
-
properties: {
|
|
1006
|
-
jobId: { type: "string" },
|
|
1007
|
-
reason: { type: "string" }
|
|
1008
|
-
},
|
|
1009
|
-
required: ["jobId", "reason"]
|
|
1010
|
-
}
|
|
1011
|
-
},
|
|
1012
|
-
{
|
|
1013
|
-
name: "force_unlock",
|
|
1014
|
-
description: "Admin tool to forcibly remove a lock from a file.",
|
|
1015
|
-
inputSchema: {
|
|
1016
|
-
type: "object",
|
|
1017
|
-
properties: {
|
|
1018
|
-
filePath: { type: "string" },
|
|
1019
|
-
reason: { type: "string" }
|
|
1020
|
-
},
|
|
1021
|
-
required: ["filePath", "reason"]
|
|
1022
|
-
}
|
|
1023
|
-
},
|
|
1024
|
-
{
|
|
1025
|
-
name: "claim_next_job",
|
|
1026
|
-
description: "Auto-assign the next available 'todo' job to yourself.",
|
|
1027
|
-
inputSchema: {
|
|
1028
|
-
type: "object",
|
|
1029
|
-
properties: {
|
|
1030
|
-
agentId: { type: "string" }
|
|
1031
|
-
},
|
|
1032
|
-
required: ["agentId"]
|
|
1033
|
-
}
|
|
1034
|
-
},
|
|
1035
|
-
{
|
|
1036
|
-
name: "complete_job",
|
|
1037
|
-
description: "Mark your assigned job as done.",
|
|
1038
|
-
inputSchema: {
|
|
1039
|
-
type: "object",
|
|
1040
|
-
properties: {
|
|
1041
|
-
agentId: { type: "string" },
|
|
1042
|
-
jobId: { type: "string" },
|
|
1043
|
-
outcome: { type: "string" }
|
|
1044
|
-
},
|
|
1045
|
-
required: ["agentId", "jobId", "outcome"]
|
|
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
|
-
}
|
|
1193
|
+
const tools = [
|
|
1194
|
+
{
|
|
1195
|
+
name: READ_CONTEXT_TOOL,
|
|
1196
|
+
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.",
|
|
1197
|
+
inputSchema: {
|
|
1198
|
+
type: "object",
|
|
1199
|
+
properties: {
|
|
1200
|
+
filename: { type: "string", description: "The name of the file to read (e.g., 'context.md', 'conventions.md')" }
|
|
1201
|
+
},
|
|
1202
|
+
required: ["filename"]
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: UPDATE_CONTEXT_TOOL,
|
|
1207
|
+
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.",
|
|
1208
|
+
inputSchema: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
properties: {
|
|
1211
|
+
filename: { type: "string", description: "File to update (e.g. 'activity.md')" },
|
|
1212
|
+
content: { type: "string", description: "The new content to write or append." },
|
|
1213
|
+
append: { type: "boolean", description: "Whether to append to the end of the file (true) or overwrite it (false). Default: false." }
|
|
1214
|
+
},
|
|
1215
|
+
required: ["filename", "content"]
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
name: SEARCH_CONTEXT_TOOL,
|
|
1220
|
+
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`.",
|
|
1221
|
+
inputSchema: {
|
|
1222
|
+
type: "object",
|
|
1223
|
+
properties: {
|
|
1224
|
+
query: { type: "string", description: "Natural language search query." }
|
|
1225
|
+
},
|
|
1226
|
+
required: ["query"]
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
// --- Billing & Usage ---
|
|
1230
|
+
{
|
|
1231
|
+
name: "get_subscription_status",
|
|
1232
|
+
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.",
|
|
1233
|
+
inputSchema: {
|
|
1234
|
+
type: "object",
|
|
1235
|
+
properties: {
|
|
1236
|
+
email: { type: "string", description: "User email to check." }
|
|
1237
|
+
},
|
|
1238
|
+
required: ["email"]
|
|
1059
1239
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
name: "get_usage_stats",
|
|
1243
|
+
description: "**API USAGE**: Returns a user's token usage and request counts.\n- Useful for debugging rate limits or explaining quota usage to users.",
|
|
1244
|
+
inputSchema: {
|
|
1245
|
+
type: "object",
|
|
1246
|
+
properties: {
|
|
1247
|
+
email: { type: "string", description: "User email to check." }
|
|
1248
|
+
},
|
|
1249
|
+
required: ["email"]
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: "search_docs",
|
|
1254
|
+
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.",
|
|
1255
|
+
inputSchema: {
|
|
1256
|
+
type: "object",
|
|
1257
|
+
properties: {
|
|
1258
|
+
query: { type: "string", description: "Natural language search query." }
|
|
1259
|
+
},
|
|
1260
|
+
required: ["query"]
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
// --- Decision & Orchestration ---
|
|
1264
|
+
{
|
|
1265
|
+
name: "propose_file_access",
|
|
1266
|
+
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.",
|
|
1267
|
+
inputSchema: {
|
|
1268
|
+
type: "object",
|
|
1269
|
+
properties: {
|
|
1270
|
+
agentId: { type: "string" },
|
|
1271
|
+
filePath: { type: "string" },
|
|
1272
|
+
intent: { type: "string" },
|
|
1273
|
+
userPrompt: { type: "string", description: "The full prompt provided by the user that initiated this action." }
|
|
1274
|
+
},
|
|
1275
|
+
required: ["agentId", "filePath", "intent", "userPrompt"]
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
name: "update_shared_context",
|
|
1280
|
+
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.",
|
|
1281
|
+
inputSchema: {
|
|
1282
|
+
type: "object",
|
|
1283
|
+
properties: {
|
|
1284
|
+
agentId: { type: "string" },
|
|
1285
|
+
text: { type: "string" }
|
|
1286
|
+
},
|
|
1287
|
+
required: ["agentId", "text"]
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
// --- Permanent Memory ---
|
|
1291
|
+
{
|
|
1292
|
+
name: "finalize_session",
|
|
1293
|
+
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'.",
|
|
1294
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
name: "get_project_soul",
|
|
1298
|
+
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.",
|
|
1299
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
1300
|
+
},
|
|
1301
|
+
// --- Job Board (Task Orchestration) ---
|
|
1302
|
+
{
|
|
1303
|
+
name: "post_job",
|
|
1304
|
+
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.",
|
|
1305
|
+
inputSchema: {
|
|
1306
|
+
type: "object",
|
|
1307
|
+
properties: {
|
|
1308
|
+
title: { type: "string" },
|
|
1309
|
+
description: { type: "string" },
|
|
1310
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
1311
|
+
dependencies: { type: "array", items: { type: "string" }, description: "Array of Job IDs that must be completed before this job can be claimed." }
|
|
1312
|
+
},
|
|
1313
|
+
required: ["title", "description"]
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
name: "cancel_job",
|
|
1318
|
+
description: "**KILL TICKET**: Cancel a job that is no longer needed.\n- Requires `jobId` and a `reason`.",
|
|
1319
|
+
inputSchema: {
|
|
1320
|
+
type: "object",
|
|
1321
|
+
properties: {
|
|
1322
|
+
jobId: { type: "string" },
|
|
1323
|
+
reason: { type: "string" }
|
|
1324
|
+
},
|
|
1325
|
+
required: ["jobId", "reason"]
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
name: "force_unlock",
|
|
1330
|
+
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.",
|
|
1331
|
+
inputSchema: {
|
|
1332
|
+
type: "object",
|
|
1333
|
+
properties: {
|
|
1334
|
+
filePath: { type: "string" },
|
|
1335
|
+
reason: { type: "string" }
|
|
1336
|
+
},
|
|
1337
|
+
required: ["filePath", "reason"]
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
name: "claim_next_job",
|
|
1342
|
+
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.",
|
|
1343
|
+
inputSchema: {
|
|
1344
|
+
type: "object",
|
|
1345
|
+
properties: {
|
|
1346
|
+
agentId: { type: "string" }
|
|
1347
|
+
},
|
|
1348
|
+
required: ["agentId"]
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
name: "complete_job",
|
|
1353
|
+
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`.",
|
|
1354
|
+
inputSchema: {
|
|
1355
|
+
type: "object",
|
|
1356
|
+
properties: {
|
|
1357
|
+
agentId: { type: "string" },
|
|
1358
|
+
jobId: { type: "string" },
|
|
1359
|
+
outcome: { type: "string" },
|
|
1360
|
+
completionKey: { type: "string", description: "Optional key to authorize completion if not the assigned agent." }
|
|
1361
|
+
},
|
|
1362
|
+
required: ["agentId", "jobId", "outcome"]
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "index_file",
|
|
1367
|
+
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.",
|
|
1368
|
+
inputSchema: {
|
|
1369
|
+
type: "object",
|
|
1370
|
+
properties: {
|
|
1371
|
+
filePath: { type: "string" },
|
|
1372
|
+
content: { type: "string" }
|
|
1373
|
+
},
|
|
1374
|
+
required: ["filePath", "content"]
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
];
|
|
1378
|
+
logger.info(`[ListTools] Returning ${tools.length} tools to MCP client`);
|
|
1379
|
+
return { tools };
|
|
1062
1380
|
});
|
|
1063
1381
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1064
1382
|
const { name, arguments: args } = request.params;
|
|
@@ -1122,20 +1440,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1122
1440
|
}
|
|
1123
1441
|
if (name === "get_subscription_status") {
|
|
1124
1442
|
const email = String(args?.email);
|
|
1125
|
-
|
|
1126
|
-
|
|
1443
|
+
logger.info(`[get_subscription_status] Called with email: ${email}`);
|
|
1444
|
+
try {
|
|
1445
|
+
const result = await nerveCenter.getSubscriptionStatus(email);
|
|
1446
|
+
logger.info(`[get_subscription_status] Result: ${JSON.stringify(result).substring(0, 200)}`);
|
|
1447
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
logger.error(`[get_subscription_status] Exception: ${e.message}`, e);
|
|
1450
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
1451
|
+
}
|
|
1127
1452
|
}
|
|
1128
1453
|
if (name === "get_usage_stats") {
|
|
1129
1454
|
const email = String(args?.email);
|
|
1130
|
-
|
|
1131
|
-
|
|
1455
|
+
logger.info(`[get_usage_stats] Called with email: ${email}`);
|
|
1456
|
+
try {
|
|
1457
|
+
const result = await nerveCenter.getUsageStats(email);
|
|
1458
|
+
logger.info(`[get_usage_stats] Result: ${JSON.stringify(result).substring(0, 200)}`);
|
|
1459
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
logger.error(`[get_usage_stats] Exception: ${e.message}`, e);
|
|
1462
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: e.message }, null, 2) }], isError: true };
|
|
1463
|
+
}
|
|
1132
1464
|
}
|
|
1133
1465
|
if (name === "search_docs") {
|
|
1134
1466
|
const query = String(args?.query);
|
|
1135
1467
|
try {
|
|
1136
|
-
const formatted = await manager.searchContext(query);
|
|
1468
|
+
const formatted = await manager.searchContext(query, nerveCenter.currentProjectName);
|
|
1137
1469
|
return { content: [{ type: "text", text: formatted }] };
|
|
1138
1470
|
} catch (err) {
|
|
1471
|
+
if (ragEngine) {
|
|
1472
|
+
const results = await ragEngine.search(query);
|
|
1473
|
+
return { content: [{ type: "text", text: results.join("\n---\n") }] };
|
|
1474
|
+
}
|
|
1139
1475
|
return {
|
|
1140
1476
|
content: [{ type: "text", text: `Search Error: ${err}` }],
|
|
1141
1477
|
isError: true
|
|
@@ -1181,8 +1517,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1181
1517
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1182
1518
|
}
|
|
1183
1519
|
if (name === "complete_job") {
|
|
1184
|
-
const { agentId, jobId, outcome } = args;
|
|
1185
|
-
const result = await nerveCenter.completeJob(agentId, jobId, outcome);
|
|
1520
|
+
const { agentId, jobId, outcome, completionKey } = args;
|
|
1521
|
+
const result = await nerveCenter.completeJob(agentId, jobId, outcome, completionKey);
|
|
1186
1522
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1187
1523
|
}
|
|
1188
1524
|
throw new Error(`Tool not found: ${name}`);
|
|
@@ -1194,9 +1530,11 @@ async function main() {
|
|
|
1194
1530
|
ragEngine.setProjectId(nerveCenter.projectId);
|
|
1195
1531
|
logger.info(`Local RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
|
|
1196
1532
|
}
|
|
1533
|
+
logger.info("MCP server ready - all tools and resources registered");
|
|
1197
1534
|
const transport = new StdioServerTransport();
|
|
1198
1535
|
await server.connect(transport);
|
|
1199
1536
|
logger.info("Shared Context MCP Server running on stdio");
|
|
1537
|
+
logger.info("Server is now accepting tool calls from MCP clients");
|
|
1200
1538
|
}
|
|
1201
1539
|
main().catch((error) => {
|
|
1202
1540
|
logger.error("Server error", error);
|