@virsanghavi/axis-server 1.0.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/README.md +26 -0
- package/bin/cli.ts +60 -0
- package/dist/cli.js +69 -0
- package/dist/mcp-server.mjs +1095 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @virsanghavi/axis-server
|
|
2
|
+
|
|
3
|
+
The official Axis Shared Context MCP Server. This server mirrors your project structure and metadata to AI agents via the Model Context Protocol (MCP).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @virsanghavi/axis-server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Start the server:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
axis-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
The server expects standard Axis environment variables:
|
|
22
|
+
- `NEXT_PUBLIC_SUPABASE_URL`
|
|
23
|
+
- `SUPABASE_SERVICE_ROLE_KEY`
|
|
24
|
+
- `OPENAI_API_KEY`
|
|
25
|
+
|
|
26
|
+
These can be provided via a `.env` file in the current directory or as environment variables.
|
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
|
|
10
|
+
// ESM dirname shim
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name("axis-server")
|
|
16
|
+
.description("Start the Axis Shared Context MCP Server")
|
|
17
|
+
.version("1.0.0");
|
|
18
|
+
|
|
19
|
+
program.action(() => {
|
|
20
|
+
console.log(chalk.bold.blue("Axis MCP Server Starting..."));
|
|
21
|
+
|
|
22
|
+
// Locate the bundled server script
|
|
23
|
+
const serverScript = path.resolve(__dirname, "../dist/mcp-server.mjs");
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(serverScript)) {
|
|
26
|
+
console.error(chalk.red("Error: Server script not found."));
|
|
27
|
+
console.error(chalk.yellow(`Expected at: ${serverScript}`));
|
|
28
|
+
console.error(chalk.gray("Did you run 'npm run build'?"));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.gray(`Launching server context...`));
|
|
33
|
+
|
|
34
|
+
// Pass through all arguments from the CLI to the underlying server
|
|
35
|
+
const args = [serverScript, ...process.argv.slice(2)];
|
|
36
|
+
|
|
37
|
+
// Spawn the node process with the bundled script
|
|
38
|
+
const proc = spawn("node", args, {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
env: { ...process.env, FORCE_COLOR: '1' }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
proc.on("close", (code) => {
|
|
44
|
+
if (code !== 0) {
|
|
45
|
+
console.log(chalk.red(`Server process exited with code ${code}`));
|
|
46
|
+
} else {
|
|
47
|
+
console.log(chalk.green("Server stopped gracefully."));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Handle signals to cleanup child
|
|
52
|
+
process.on('SIGINT', () => {
|
|
53
|
+
proc.kill('SIGINT');
|
|
54
|
+
});
|
|
55
|
+
process.on('SIGTERM', () => {
|
|
56
|
+
proc.kill('SIGTERM');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program.parse();
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
|
+
|
|
30
|
+
// bin/cli.ts
|
|
31
|
+
var import_commander = require("commander");
|
|
32
|
+
var import_chalk = __toESM(require("chalk"));
|
|
33
|
+
var import_child_process = require("child_process");
|
|
34
|
+
var import_path = __toESM(require("path"));
|
|
35
|
+
var import_url = require("url");
|
|
36
|
+
var import_fs = __toESM(require("fs"));
|
|
37
|
+
var __filename2 = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
38
|
+
var __dirname = import_path.default.dirname(__filename2);
|
|
39
|
+
import_commander.program.name("axis-server").description("Start the Axis Shared Context MCP Server").version("1.0.0");
|
|
40
|
+
import_commander.program.action(() => {
|
|
41
|
+
console.log(import_chalk.default.bold.blue("Axis MCP Server Starting..."));
|
|
42
|
+
const serverScript = import_path.default.resolve(__dirname, "../dist/mcp-server.mjs");
|
|
43
|
+
if (!import_fs.default.existsSync(serverScript)) {
|
|
44
|
+
console.error(import_chalk.default.red("Error: Server script not found."));
|
|
45
|
+
console.error(import_chalk.default.yellow(`Expected at: ${serverScript}`));
|
|
46
|
+
console.error(import_chalk.default.gray("Did you run 'npm run build'?"));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
console.log(import_chalk.default.gray(`Launching server context...`));
|
|
50
|
+
const args = [serverScript, ...process.argv.slice(2)];
|
|
51
|
+
const proc = (0, import_child_process.spawn)("node", args, {
|
|
52
|
+
stdio: "inherit",
|
|
53
|
+
env: { ...process.env, FORCE_COLOR: "1" }
|
|
54
|
+
});
|
|
55
|
+
proc.on("close", (code) => {
|
|
56
|
+
if (code !== 0) {
|
|
57
|
+
console.log(import_chalk.default.red(`Server process exited with code ${code}`));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(import_chalk.default.green("Server stopped gracefully."));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
process.on("SIGINT", () => {
|
|
63
|
+
proc.kill("SIGINT");
|
|
64
|
+
});
|
|
65
|
+
process.on("SIGTERM", () => {
|
|
66
|
+
proc.kill("SIGTERM");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
import_commander.program.parse();
|
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
// ../../src/local/mcp-server.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
ListResourcesRequestSchema,
|
|
8
|
+
ReadResourceRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import dotenv2 from "dotenv";
|
|
11
|
+
|
|
12
|
+
// ../../src/local/context-manager.ts
|
|
13
|
+
import fs from "fs/promises";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { Mutex } from "async-mutex";
|
|
16
|
+
var INSTRUCTIONS_DIR = path.resolve(process.cwd(), "agent-instructions");
|
|
17
|
+
var ContextManager = class {
|
|
18
|
+
mutex;
|
|
19
|
+
apiUrl;
|
|
20
|
+
apiSecret;
|
|
21
|
+
constructor(apiUrl, apiSecret) {
|
|
22
|
+
this.mutex = new Mutex();
|
|
23
|
+
this.apiUrl = apiUrl;
|
|
24
|
+
this.apiSecret = apiSecret;
|
|
25
|
+
}
|
|
26
|
+
resolveFilePath(filename) {
|
|
27
|
+
if (!filename || filename.includes("\0")) {
|
|
28
|
+
throw new Error("Invalid filename");
|
|
29
|
+
}
|
|
30
|
+
const resolved = path.resolve(INSTRUCTIONS_DIR, filename);
|
|
31
|
+
if (!resolved.startsWith(INSTRUCTIONS_DIR + path.sep)) {
|
|
32
|
+
throw new Error("Invalid file path");
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
async listFiles() {
|
|
37
|
+
try {
|
|
38
|
+
const files = await fs.readdir(INSTRUCTIONS_DIR);
|
|
39
|
+
const docFiles = await this.listDocs();
|
|
40
|
+
const instructionFiles = files.filter((f) => f.endsWith(".md")).map((f) => ({
|
|
41
|
+
uri: `context://local/${f}`,
|
|
42
|
+
name: f,
|
|
43
|
+
mimeType: "text/markdown",
|
|
44
|
+
description: `Shared context file: ${f}`
|
|
45
|
+
}));
|
|
46
|
+
return [...instructionFiles, ...docFiles];
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("Error listing resources:", error);
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async listDocs() {
|
|
53
|
+
const docsDir = path.resolve(process.cwd(), "docs");
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(docsDir);
|
|
56
|
+
const files = await fs.readdir(docsDir);
|
|
57
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => ({
|
|
58
|
+
uri: `context://docs/${f}`,
|
|
59
|
+
name: `Docs: ${f}`,
|
|
60
|
+
mimeType: "text/markdown",
|
|
61
|
+
description: `Documentation file: ${f}`
|
|
62
|
+
}));
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async readFile(filename) {
|
|
68
|
+
if (filename.startsWith("docs/")) {
|
|
69
|
+
const docName = filename.replace("docs/", "");
|
|
70
|
+
const docPath = path.resolve(process.cwd(), "docs", docName);
|
|
71
|
+
if (!docPath.startsWith(path.resolve(process.cwd(), "docs"))) {
|
|
72
|
+
throw new Error("Invalid doc path");
|
|
73
|
+
}
|
|
74
|
+
return await fs.readFile(docPath, "utf-8");
|
|
75
|
+
}
|
|
76
|
+
const filePath = this.resolveFilePath(filename);
|
|
77
|
+
return await fs.readFile(filePath, "utf-8");
|
|
78
|
+
}
|
|
79
|
+
async updateFile(filename, content, append = false) {
|
|
80
|
+
const filePath = this.resolveFilePath(filename);
|
|
81
|
+
return await this.mutex.runExclusive(async () => {
|
|
82
|
+
if (append) {
|
|
83
|
+
await fs.appendFile(filePath, "\n" + content);
|
|
84
|
+
} else {
|
|
85
|
+
await fs.writeFile(filePath, content);
|
|
86
|
+
}
|
|
87
|
+
return `Updated ${filename}`;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async searchContext(query) {
|
|
91
|
+
if (!this.apiUrl) {
|
|
92
|
+
throw new Error("SHARED_CONTEXT_API_URL not configured.");
|
|
93
|
+
}
|
|
94
|
+
const response = await fetch(`${this.apiUrl}/search`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"Authorization": `Bearer ${this.apiSecret || ""}`
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({ query })
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
throw new Error(`API Error ${response.status}: ${text}`);
|
|
105
|
+
}
|
|
106
|
+
const result = await response.json();
|
|
107
|
+
if (result.results && Array.isArray(result.results)) {
|
|
108
|
+
return result.results.map(
|
|
109
|
+
(r) => `[Similarity: ${(r.similarity * 100).toFixed(1)}%] ${r.content}`
|
|
110
|
+
).join("\n\n---\n\n") || "No results found.";
|
|
111
|
+
}
|
|
112
|
+
throw new Error("No results format recognized.");
|
|
113
|
+
}
|
|
114
|
+
async embedContent(items) {
|
|
115
|
+
if (!this.apiUrl) {
|
|
116
|
+
console.warn("Skipping RAG embedding: SHARED_CONTEXT_API_URL not configured.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const response = await fetch(`${this.apiUrl}/embed`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"Authorization": `Bearer ${this.apiSecret || ""}`
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({ items })
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const text = await response.text();
|
|
129
|
+
throw new Error(`API Error ${response.status}: ${text}`);
|
|
130
|
+
}
|
|
131
|
+
return await response.json();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ../../src/local/nerve-center.ts
|
|
136
|
+
import { Mutex as Mutex2 } from "async-mutex";
|
|
137
|
+
import { createClient } from "@supabase/supabase-js";
|
|
138
|
+
import fs2 from "fs/promises";
|
|
139
|
+
import path2 from "path";
|
|
140
|
+
|
|
141
|
+
// ../../src/utils/logger.ts
|
|
142
|
+
var Logger = class {
|
|
143
|
+
level = "info" /* INFO */;
|
|
144
|
+
setLevel(level) {
|
|
145
|
+
this.level = level;
|
|
146
|
+
}
|
|
147
|
+
log(level, message, meta) {
|
|
148
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
149
|
+
console.log(JSON.stringify({
|
|
150
|
+
timestamp,
|
|
151
|
+
level,
|
|
152
|
+
message,
|
|
153
|
+
...meta
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
debug(message, meta) {
|
|
157
|
+
if (this.level === "debug" /* DEBUG */) this.log("debug" /* DEBUG */, message, meta);
|
|
158
|
+
}
|
|
159
|
+
info(message, meta) {
|
|
160
|
+
this.log("info" /* INFO */, message, meta);
|
|
161
|
+
}
|
|
162
|
+
warn(message, meta) {
|
|
163
|
+
this.log("warn" /* WARN */, message, meta);
|
|
164
|
+
}
|
|
165
|
+
error(message, error, meta) {
|
|
166
|
+
this.log("error" /* ERROR */, message, {
|
|
167
|
+
...meta,
|
|
168
|
+
error: error instanceof Error ? error.message : String(error),
|
|
169
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var logger = new Logger();
|
|
174
|
+
|
|
175
|
+
// ../../src/local/nerve-center.ts
|
|
176
|
+
var STATE_FILE = process.env.NERVE_CENTER_STATE_FILE || path2.join(process.cwd(), "history", "nerve-center-state.json");
|
|
177
|
+
var LOCK_TIMEOUT_DEFAULT = 30 * 60 * 1e3;
|
|
178
|
+
var NerveCenter = class {
|
|
179
|
+
mutex;
|
|
180
|
+
state;
|
|
181
|
+
contextManager;
|
|
182
|
+
stateFilePath;
|
|
183
|
+
lockTimeout;
|
|
184
|
+
supabase;
|
|
185
|
+
_projectId;
|
|
186
|
+
// Renamed backing field
|
|
187
|
+
projectName;
|
|
188
|
+
useSupabase;
|
|
189
|
+
/**
|
|
190
|
+
* @param contextManager - Instance of ContextManager for legacy operations
|
|
191
|
+
* @param options - Configuration options for state persistence and timeouts
|
|
192
|
+
*/
|
|
193
|
+
constructor(contextManager, options = {}) {
|
|
194
|
+
this.mutex = new Mutex2();
|
|
195
|
+
this.contextManager = contextManager;
|
|
196
|
+
this.stateFilePath = options.stateFilePath || STATE_FILE;
|
|
197
|
+
this.lockTimeout = options.lockTimeout || LOCK_TIMEOUT_DEFAULT;
|
|
198
|
+
this.projectName = options.projectName || process.env.PROJECT_NAME || "default";
|
|
199
|
+
const supabaseUrl = options.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
200
|
+
const supabaseKey = options.supabaseServiceRoleKey || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
201
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
202
|
+
throw new Error("CRITICAL: Supabase URL and Service Role Key are REQUIRED for NerveCenter persistence.");
|
|
203
|
+
}
|
|
204
|
+
this.supabase = createClient(supabaseUrl, supabaseKey);
|
|
205
|
+
this.useSupabase = true;
|
|
206
|
+
this.state = {
|
|
207
|
+
locks: {},
|
|
208
|
+
jobs: {},
|
|
209
|
+
liveNotepad: "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n"
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
get projectId() {
|
|
213
|
+
return this._projectId;
|
|
214
|
+
}
|
|
215
|
+
async init() {
|
|
216
|
+
await this.loadState();
|
|
217
|
+
if (this.useSupabase) {
|
|
218
|
+
await this.ensureProjectId();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async ensureProjectId() {
|
|
222
|
+
if (!this.supabase) return;
|
|
223
|
+
const { data: project, error } = await this.supabase.from("projects").select("id").eq("name", this.projectName).maybeSingle();
|
|
224
|
+
if (error) {
|
|
225
|
+
logger.error("Failed to load project", error);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (project?.id) {
|
|
229
|
+
this._projectId = project.id;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const { data: created, error: createError } = await this.supabase.from("projects").insert({ name: this.projectName }).select("id").single();
|
|
233
|
+
if (createError) {
|
|
234
|
+
logger.error("Failed to create project", createError);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this._projectId = created.id;
|
|
238
|
+
}
|
|
239
|
+
jobFromRecord(record) {
|
|
240
|
+
return {
|
|
241
|
+
id: record.id,
|
|
242
|
+
title: record.title,
|
|
243
|
+
description: record.description,
|
|
244
|
+
priority: record.priority,
|
|
245
|
+
status: record.status,
|
|
246
|
+
assignedTo: record.assigned_to || void 0,
|
|
247
|
+
dependencies: record.dependencies || void 0,
|
|
248
|
+
createdAt: Date.parse(record.created_at),
|
|
249
|
+
updatedAt: Date.parse(record.updated_at)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// --- Data Access Layers (Hybrid: Supabase > Local) ---
|
|
253
|
+
async listJobs() {
|
|
254
|
+
if (!this.useSupabase || !this.supabase || !this._projectId) {
|
|
255
|
+
return Object.values(this.state.jobs);
|
|
256
|
+
}
|
|
257
|
+
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);
|
|
258
|
+
if (error || !data) {
|
|
259
|
+
logger.error("Failed to load jobs", error);
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
return data.map((record) => this.jobFromRecord(record));
|
|
263
|
+
}
|
|
264
|
+
async getLocks() {
|
|
265
|
+
if (!this.useSupabase || !this.supabase || !this._projectId) {
|
|
266
|
+
return Object.values(this.state.locks);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
await this.supabase.rpc("clean_stale_locks", {
|
|
270
|
+
p_project_id: this._projectId,
|
|
271
|
+
p_timeout_seconds: Math.floor(this.lockTimeout / 1e3)
|
|
272
|
+
});
|
|
273
|
+
const { data, error } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId);
|
|
274
|
+
if (error) throw error;
|
|
275
|
+
return (data || []).map((row) => ({
|
|
276
|
+
agentId: row.agent_id,
|
|
277
|
+
filePath: row.file_path,
|
|
278
|
+
intent: row.intent,
|
|
279
|
+
userPrompt: row.user_prompt,
|
|
280
|
+
timestamp: Date.parse(row.updated_at)
|
|
281
|
+
}));
|
|
282
|
+
} catch (e) {
|
|
283
|
+
logger.warn("Failed to fetch locks from DB, falling back to local memory", e);
|
|
284
|
+
return Object.values(this.state.locks);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async saveState() {
|
|
288
|
+
try {
|
|
289
|
+
await fs2.mkdir(path2.dirname(this.stateFilePath), { recursive: true });
|
|
290
|
+
await fs2.writeFile(this.stateFilePath, JSON.stringify(this.state, null, 2));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error("Failed to persist state", error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async loadState() {
|
|
296
|
+
try {
|
|
297
|
+
const data = await fs2.readFile(this.stateFilePath, "utf-8");
|
|
298
|
+
this.state = JSON.parse(data);
|
|
299
|
+
logger.info("State loaded from disk");
|
|
300
|
+
} catch (_error) {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// --- Job Board Protocol (Active Orchestration) ---
|
|
304
|
+
async postJob(title, description, priority = "medium", dependencies = []) {
|
|
305
|
+
return await this.mutex.runExclusive(async () => {
|
|
306
|
+
let id = `job-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
|
|
307
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
308
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
309
|
+
const { data, error } = await this.supabase.from("jobs").insert({
|
|
310
|
+
project_id: this._projectId,
|
|
311
|
+
title,
|
|
312
|
+
description,
|
|
313
|
+
priority,
|
|
314
|
+
status: "todo",
|
|
315
|
+
assigned_to: null,
|
|
316
|
+
dependencies,
|
|
317
|
+
created_at: now,
|
|
318
|
+
updated_at: now
|
|
319
|
+
}).select("id").single();
|
|
320
|
+
if (error) {
|
|
321
|
+
logger.error("Failed to post job", error);
|
|
322
|
+
} else if (data?.id) {
|
|
323
|
+
id = data.id;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
this.state.jobs[id] = {
|
|
327
|
+
id,
|
|
328
|
+
title,
|
|
329
|
+
description,
|
|
330
|
+
priority,
|
|
331
|
+
dependencies,
|
|
332
|
+
status: "todo",
|
|
333
|
+
createdAt: Date.now(),
|
|
334
|
+
updatedAt: Date.now()
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const depText = dependencies.length ? ` (Depends on: ${dependencies.join(", ")})` : "";
|
|
338
|
+
this.state.liveNotepad += `
|
|
339
|
+
- [JOB POSTED] [${priority.toUpperCase()}] ${title} (ID: ${id})${depText}`;
|
|
340
|
+
logger.info(`Job posted: ${title}`, { jobId: id, priority });
|
|
341
|
+
await this.saveState();
|
|
342
|
+
return { jobId: id, status: "POSTED" };
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async claimNextJob(agentId) {
|
|
346
|
+
return await this.mutex.runExclusive(async () => {
|
|
347
|
+
const priorities = ["critical", "high", "medium", "low"];
|
|
348
|
+
const allJobs = await this.listJobs();
|
|
349
|
+
const jobsById = new Map(allJobs.map((job2) => [job2.id, job2]));
|
|
350
|
+
const availableJobs = allJobs.filter((job2) => job2.status === "todo").filter((job2) => {
|
|
351
|
+
if (!job2.dependencies || job2.dependencies.length === 0) return true;
|
|
352
|
+
return job2.dependencies.every((depId) => jobsById.get(depId)?.status === "done");
|
|
353
|
+
}).sort((a, b) => {
|
|
354
|
+
const pA = priorities.indexOf(a.priority);
|
|
355
|
+
const pB = priorities.indexOf(b.priority);
|
|
356
|
+
if (pA !== pB) return pA - pB;
|
|
357
|
+
return a.createdAt - b.createdAt;
|
|
358
|
+
});
|
|
359
|
+
if (availableJobs.length === 0) {
|
|
360
|
+
return { status: "NO_JOBS_AVAILABLE", message: "Relax. No open tickets (or dependencies not met)." };
|
|
361
|
+
}
|
|
362
|
+
if (this.useSupabase && this.supabase) {
|
|
363
|
+
for (const candidate of availableJobs) {
|
|
364
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
365
|
+
const { data, error } = await this.supabase.from("jobs").update({
|
|
366
|
+
status: "in_progress",
|
|
367
|
+
assigned_to: agentId,
|
|
368
|
+
updated_at: now
|
|
369
|
+
}).eq("id", candidate.id).eq("status", "todo").select("id,title,description,priority,status,assigned_to,dependencies,created_at,updated_at");
|
|
370
|
+
if (error) {
|
|
371
|
+
logger.error("Failed to claim job", error);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (data && data.length > 0) {
|
|
375
|
+
const job2 = this.jobFromRecord(data[0]);
|
|
376
|
+
this.state.liveNotepad += `
|
|
377
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job2.title}`;
|
|
378
|
+
logger.info(`Job claimed`, { jobId: job2.id, agentId });
|
|
379
|
+
await this.saveState();
|
|
380
|
+
return { status: "CLAIMED", job: job2 };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { status: "NO_JOBS_AVAILABLE", message: "All available jobs were just claimed." };
|
|
384
|
+
}
|
|
385
|
+
const job = availableJobs[0];
|
|
386
|
+
job.status = "in_progress";
|
|
387
|
+
job.assignedTo = agentId;
|
|
388
|
+
job.updatedAt = Date.now();
|
|
389
|
+
this.state.liveNotepad += `
|
|
390
|
+
- [JOB CLAIMED] Agent '${agentId}' picked up: ${job.title}`;
|
|
391
|
+
logger.info(`Job claimed`, { jobId: job.id, agentId });
|
|
392
|
+
await this.saveState();
|
|
393
|
+
return { status: "CLAIMED", job };
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async cancelJob(jobId, reason) {
|
|
397
|
+
return await this.mutex.runExclusive(async () => {
|
|
398
|
+
if (this.useSupabase && this.supabase) {
|
|
399
|
+
const { data, error } = await this.supabase.from("jobs").update({ status: "cancelled", cancel_reason: reason, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId).select("id,title");
|
|
400
|
+
if (error || !data || data.length === 0) {
|
|
401
|
+
return { error: "Job not found" };
|
|
402
|
+
}
|
|
403
|
+
this.state.liveNotepad += `
|
|
404
|
+
- [JOB CANCELLED] ${data[0].title} (ID: ${jobId}). Reason: ${reason}`;
|
|
405
|
+
await this.saveState();
|
|
406
|
+
return { status: "CANCELLED" };
|
|
407
|
+
}
|
|
408
|
+
const job = this.state.jobs[jobId];
|
|
409
|
+
if (!job) return { error: "Job not found" };
|
|
410
|
+
job.status = "cancelled";
|
|
411
|
+
job.updatedAt = Date.now();
|
|
412
|
+
this.state.liveNotepad += `
|
|
413
|
+
- [JOB CANCELLED] ${job.title} (ID: ${jobId}). Reason: ${reason}`;
|
|
414
|
+
await this.saveState();
|
|
415
|
+
return { status: "CANCELLED" };
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async forceUnlock(filePath, adminReason) {
|
|
419
|
+
return await this.mutex.runExclusive(async () => {
|
|
420
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
421
|
+
const { error } = await this.supabase.from("locks").delete().eq("project_id", this._projectId).eq("file_path", filePath);
|
|
422
|
+
if (error) return { error: "DB Error" };
|
|
423
|
+
this.state.liveNotepad += `
|
|
424
|
+
- [ADMIN] Force unlocked '${filePath}'. Reason: ${adminReason}`;
|
|
425
|
+
await this.saveState();
|
|
426
|
+
return { status: "UNLOCKED" };
|
|
427
|
+
}
|
|
428
|
+
const lock = this.state.locks[filePath];
|
|
429
|
+
if (!lock) return { message: "File was not locked." };
|
|
430
|
+
delete this.state.locks[filePath];
|
|
431
|
+
this.state.liveNotepad += `
|
|
432
|
+
- [ADMIN] Force unlocked '${filePath}'. Reason: ${adminReason}`;
|
|
433
|
+
await this.saveState();
|
|
434
|
+
return { status: "UNLOCKED", previousOwner: lock.agentId };
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async completeJob(agentId, jobId, outcome) {
|
|
438
|
+
return await this.mutex.runExclusive(async () => {
|
|
439
|
+
if (this.useSupabase && this.supabase) {
|
|
440
|
+
const { data, error } = await this.supabase.from("jobs").select("id,title,assigned_to").eq("id", jobId).single();
|
|
441
|
+
if (error || !data) return { error: "Job not found" };
|
|
442
|
+
if (data.assigned_to !== agentId) return { error: "You don't own this job." };
|
|
443
|
+
const { error: updateError } = await this.supabase.from("jobs").update({ status: "done", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", jobId).eq("assigned_to", agentId);
|
|
444
|
+
if (updateError) return { error: "Failed to complete job" };
|
|
445
|
+
this.state.liveNotepad += `
|
|
446
|
+
- [JOB DONE] ${data.title} by ${agentId}. Outcome: ${outcome}`;
|
|
447
|
+
logger.info(`Job completed`, { jobId, agentId });
|
|
448
|
+
await this.saveState();
|
|
449
|
+
return { status: "COMPLETED" };
|
|
450
|
+
}
|
|
451
|
+
const job = this.state.jobs[jobId];
|
|
452
|
+
if (!job) return { error: "Job not found" };
|
|
453
|
+
if (job.assignedTo !== agentId) return { error: "You don't own this job." };
|
|
454
|
+
job.status = "done";
|
|
455
|
+
job.updatedAt = Date.now();
|
|
456
|
+
this.state.liveNotepad += `
|
|
457
|
+
- [JOB DONE] ${job.title} by ${agentId}. Outcome: ${outcome}`;
|
|
458
|
+
await this.saveState();
|
|
459
|
+
return { status: "COMPLETED" };
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// --- Core State Management ---
|
|
463
|
+
async getLiveContext() {
|
|
464
|
+
const locks = await this.getLocks();
|
|
465
|
+
const lockSummary = locks.map(
|
|
466
|
+
(l) => `- [LOCKED] ${l.filePath} by ${l.agentId}
|
|
467
|
+
Intent: ${l.intent}
|
|
468
|
+
Prompt: "${l.userPrompt?.substring(0, 100)}..."`
|
|
469
|
+
).join("\n");
|
|
470
|
+
const jobs = await this.listJobs();
|
|
471
|
+
const jobSummary = jobs.map(
|
|
472
|
+
(j) => `- [${j.status.toUpperCase()}] ${j.title} ${j.assignedTo ? "(" + j.assignedTo + ")" : "(Open)"}
|
|
473
|
+
ID: ${j.id}`
|
|
474
|
+
).join("\n");
|
|
475
|
+
return `# Active Session Context
|
|
476
|
+
|
|
477
|
+
## Job Board (Active Orchestration)
|
|
478
|
+
${jobSummary || "No active jobs."}
|
|
479
|
+
|
|
480
|
+
## Task Registry (Locks)
|
|
481
|
+
${lockSummary || "No active locks."}
|
|
482
|
+
|
|
483
|
+
## Live Notepad
|
|
484
|
+
${this.state.liveNotepad}`;
|
|
485
|
+
}
|
|
486
|
+
// --- Decision & Orchestration ---
|
|
487
|
+
async proposeFileAccess(agentId, filePath, intent, userPrompt) {
|
|
488
|
+
return await this.mutex.runExclusive(async () => {
|
|
489
|
+
if (!this.supabase || !this._projectId) throw new Error("Database not connected");
|
|
490
|
+
const { data: existing } = await this.supabase.from("locks").select("*").eq("project_id", this._projectId).eq("file_path", filePath).maybeSingle();
|
|
491
|
+
if (existing) {
|
|
492
|
+
const updatedAt = new Date(existing.updated_at).getTime();
|
|
493
|
+
const isStale = Date.now() - updatedAt > this.lockTimeout;
|
|
494
|
+
if (!isStale && existing.agent_id !== agentId) {
|
|
495
|
+
return {
|
|
496
|
+
status: "REQUIRES_ORCHESTRATION",
|
|
497
|
+
message: `Conflict: File '${filePath}' is currently locked by agent '${existing.agent_id}'`,
|
|
498
|
+
currentLock: {
|
|
499
|
+
agentId: existing.agent_id,
|
|
500
|
+
intent: existing.intent,
|
|
501
|
+
timestamp: updatedAt
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const { error } = await this.supabase.from("locks").upsert({
|
|
507
|
+
project_id: this._projectId,
|
|
508
|
+
file_path: filePath,
|
|
509
|
+
agent_id: agentId,
|
|
510
|
+
intent,
|
|
511
|
+
user_prompt: userPrompt,
|
|
512
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
513
|
+
}, { onConflict: "project_id,file_path" });
|
|
514
|
+
if (error) {
|
|
515
|
+
logger.error("Lock upsert failed", error);
|
|
516
|
+
return { status: "ERROR", message: "Database lock failed." };
|
|
517
|
+
}
|
|
518
|
+
this.state.liveNotepad += `
|
|
519
|
+
|
|
520
|
+
### [${agentId}] Locked '${filePath}'
|
|
521
|
+
**Intent:** ${intent}
|
|
522
|
+
**Prompt:** "${userPrompt}"`;
|
|
523
|
+
await this.saveState();
|
|
524
|
+
return { status: "GRANTED", message: `Access granted for ${filePath}` };
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async updateSharedContext(text, agentId) {
|
|
528
|
+
return await this.mutex.runExclusive(async () => {
|
|
529
|
+
this.state.liveNotepad += `
|
|
530
|
+
- [${agentId}] ${text}`;
|
|
531
|
+
await this.saveState();
|
|
532
|
+
return "Notepad updated.";
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async finalizeSession() {
|
|
536
|
+
return await this.mutex.runExclusive(async () => {
|
|
537
|
+
const content = this.state.liveNotepad;
|
|
538
|
+
const filename = `session-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.md`;
|
|
539
|
+
const historyPath = path2.join(process.cwd(), "history", filename);
|
|
540
|
+
await fs2.writeFile(historyPath, content);
|
|
541
|
+
this.state.liveNotepad = "Session Start: " + (/* @__PURE__ */ new Date()).toISOString() + "\n";
|
|
542
|
+
this.state.locks = {};
|
|
543
|
+
if (this.useSupabase && this.supabase && this._projectId) {
|
|
544
|
+
await this.supabase.from("jobs").delete().eq("project_id", this._projectId).in("status", ["done", "cancelled"]);
|
|
545
|
+
await this.supabase.from("locks").delete().eq("project_id", this._projectId);
|
|
546
|
+
} else {
|
|
547
|
+
this.state.jobs = Object.fromEntries(
|
|
548
|
+
Object.entries(this.state.jobs).filter(([_, j]) => j.status !== "done" && j.status !== "cancelled")
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
await this.saveState();
|
|
552
|
+
return {
|
|
553
|
+
status: "SESSION_FINALIZED",
|
|
554
|
+
archivePath: historyPath
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async getProjectSoul() {
|
|
559
|
+
let soul = "## Project Soul\n";
|
|
560
|
+
try {
|
|
561
|
+
const context = await this.contextManager.readFile("context.md");
|
|
562
|
+
soul += `
|
|
563
|
+
### Context
|
|
564
|
+
${context}`;
|
|
565
|
+
const conventions = await this.contextManager.readFile("conventions.md");
|
|
566
|
+
soul += `
|
|
567
|
+
### Conventions
|
|
568
|
+
${conventions}`;
|
|
569
|
+
} catch (_e) {
|
|
570
|
+
soul += "\n(Could not read local context files)";
|
|
571
|
+
}
|
|
572
|
+
return soul;
|
|
573
|
+
}
|
|
574
|
+
// --- Billing & Usage ---
|
|
575
|
+
async getSubscriptionStatus(email) {
|
|
576
|
+
if (!this.useSupabase || !this.supabase) {
|
|
577
|
+
return { error: "Supabase not configured." };
|
|
578
|
+
}
|
|
579
|
+
const { data: profile, error } = await this.supabase.from("profiles").select("subscription_status, stripe_customer_id, current_period_end").eq("email", email).single();
|
|
580
|
+
if (error || !profile) {
|
|
581
|
+
return { status: "unknown", message: "Profile not found." };
|
|
582
|
+
}
|
|
583
|
+
const isActive = profile.subscription_status === "pro" || profile.current_period_end && new Date(profile.current_period_end) > /* @__PURE__ */ new Date();
|
|
584
|
+
return {
|
|
585
|
+
email,
|
|
586
|
+
plan: isActive ? "Pro" : "Free",
|
|
587
|
+
status: profile.subscription_status || "free",
|
|
588
|
+
validUntil: profile.current_period_end
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async getUsageStats(email) {
|
|
592
|
+
if (!this.useSupabase || !this.supabase) {
|
|
593
|
+
return { error: "Supabase not configured." };
|
|
594
|
+
}
|
|
595
|
+
const { data: profile } = await this.supabase.from("profiles").select("usage_count").eq("email", email).single();
|
|
596
|
+
return {
|
|
597
|
+
email,
|
|
598
|
+
usageCount: profile?.usage_count || 0,
|
|
599
|
+
limit: 1e3
|
|
600
|
+
// Hardcoded placeholder limit
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// ../../src/local/rag-engine.ts
|
|
606
|
+
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
607
|
+
import OpenAI from "openai";
|
|
608
|
+
import dotenv from "dotenv";
|
|
609
|
+
dotenv.config({ path: ".env.local" });
|
|
610
|
+
var RagEngine = class {
|
|
611
|
+
supabase;
|
|
612
|
+
openai;
|
|
613
|
+
projectId;
|
|
614
|
+
constructor(supabaseUrl, supabaseKey, openaiKey, projectId) {
|
|
615
|
+
this.supabase = createClient2(supabaseUrl, supabaseKey);
|
|
616
|
+
this.openai = new OpenAI({ apiKey: openaiKey });
|
|
617
|
+
this.projectId = projectId;
|
|
618
|
+
}
|
|
619
|
+
setProjectId(id) {
|
|
620
|
+
this.projectId = id;
|
|
621
|
+
}
|
|
622
|
+
async indexContent(filePath, content) {
|
|
623
|
+
if (!this.projectId) {
|
|
624
|
+
console.error("RAG: Project ID missing.");
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const resp = await this.openai.embeddings.create({
|
|
629
|
+
model: "text-embedding-3-small",
|
|
630
|
+
input: content.substring(0, 8e3)
|
|
631
|
+
// simplistic chunking
|
|
632
|
+
});
|
|
633
|
+
const embedding = resp.data[0].embedding;
|
|
634
|
+
await this.supabase.from("embeddings").delete().eq("project_id", this.projectId).contains("metadata", { filePath });
|
|
635
|
+
const { error } = await this.supabase.from("embeddings").insert({
|
|
636
|
+
project_id: this.projectId,
|
|
637
|
+
content,
|
|
638
|
+
embedding,
|
|
639
|
+
metadata: { filePath }
|
|
640
|
+
});
|
|
641
|
+
if (error) {
|
|
642
|
+
console.error("RAG Insert Error:", error);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
logger.info(`Indexed ${filePath}`);
|
|
646
|
+
return true;
|
|
647
|
+
} catch (e) {
|
|
648
|
+
console.error("RAG Error:", e);
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async search(query, limit = 5) {
|
|
653
|
+
if (!this.projectId) return [];
|
|
654
|
+
try {
|
|
655
|
+
const resp = await this.openai.embeddings.create({
|
|
656
|
+
model: "text-embedding-3-small",
|
|
657
|
+
input: query
|
|
658
|
+
});
|
|
659
|
+
const embedding = resp.data[0].embedding;
|
|
660
|
+
const { data, error } = await this.supabase.rpc("match_embeddings", {
|
|
661
|
+
query_embedding: embedding,
|
|
662
|
+
match_threshold: 0.5,
|
|
663
|
+
match_count: limit,
|
|
664
|
+
p_project_id: this.projectId
|
|
665
|
+
});
|
|
666
|
+
if (error || !data) {
|
|
667
|
+
console.error("RAG Search DB Error:", error);
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
return data.map((d) => d.content);
|
|
671
|
+
} catch (e) {
|
|
672
|
+
console.error("RAG Search Fail:", e);
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// ../../src/local/mcp-server.ts
|
|
679
|
+
dotenv2.config({ path: ".env.local" });
|
|
680
|
+
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
681
|
+
logger.error("CRITICAL: Supabase credentials missing. RAG & Persistence disabled.");
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
var manager = new ContextManager(
|
|
685
|
+
process.env.SHARED_CONTEXT_API_URL,
|
|
686
|
+
process.env.SHARED_CONTEXT_API_SECRET
|
|
687
|
+
);
|
|
688
|
+
var nerveCenter = new NerveCenter(manager, {
|
|
689
|
+
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
690
|
+
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
691
|
+
projectName: process.env.PROJECT_NAME || "default"
|
|
692
|
+
});
|
|
693
|
+
var ragEngine = new RagEngine(
|
|
694
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
695
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY,
|
|
696
|
+
process.env.OPENAI_API_KEY || ""
|
|
697
|
+
// Project ID is loaded async by NerveCenter... tricky dependency.
|
|
698
|
+
// We'll let NerveCenter expose it or pass it later.
|
|
699
|
+
);
|
|
700
|
+
var REQUIRED_DIRS = ["agent-instructions", "history"];
|
|
701
|
+
async function ensureFileSystem() {
|
|
702
|
+
const fs3 = await import("fs/promises");
|
|
703
|
+
const path3 = await import("path");
|
|
704
|
+
for (const d of REQUIRED_DIRS) {
|
|
705
|
+
const dirPath = path3.join(process.cwd(), d);
|
|
706
|
+
try {
|
|
707
|
+
await fs3.access(dirPath);
|
|
708
|
+
} catch {
|
|
709
|
+
logger.info("Creating required directory", { dir: d });
|
|
710
|
+
await fs3.mkdir(dirPath, { recursive: true });
|
|
711
|
+
if (d === "agent-instructions") {
|
|
712
|
+
await fs3.writeFile(path3.join(dirPath, "context.md"), "# Project Context\n\n");
|
|
713
|
+
await fs3.writeFile(path3.join(dirPath, "conventions.md"), "# Coding Conventions\n\n");
|
|
714
|
+
await fs3.writeFile(path3.join(dirPath, "activity.md"), "# Activity Log\n\n");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
var server = new Server(
|
|
720
|
+
{
|
|
721
|
+
name: "shared-context-server",
|
|
722
|
+
version: "1.0.0"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
capabilities: {
|
|
726
|
+
resources: {},
|
|
727
|
+
tools: {}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
var READ_CONTEXT_TOOL = "read_context";
|
|
732
|
+
var UPDATE_CONTEXT_TOOL = "update_context";
|
|
733
|
+
var SEARCH_CONTEXT_TOOL = "search_codebase";
|
|
734
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
735
|
+
try {
|
|
736
|
+
return {
|
|
737
|
+
resources: [
|
|
738
|
+
{
|
|
739
|
+
uri: "mcp://context/current",
|
|
740
|
+
name: "Live Session Context",
|
|
741
|
+
mimeType: "text/markdown",
|
|
742
|
+
description: "The realtime state of the Nerve Center (Notepad + Locks)"
|
|
743
|
+
},
|
|
744
|
+
...await manager.listFiles()
|
|
745
|
+
]
|
|
746
|
+
};
|
|
747
|
+
} catch (error) {
|
|
748
|
+
logger.error("Error listing resources", error);
|
|
749
|
+
return { resources: [] };
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
753
|
+
const uri = request.params.uri;
|
|
754
|
+
try {
|
|
755
|
+
if (uri === "mcp://context/current") {
|
|
756
|
+
return {
|
|
757
|
+
contents: [{
|
|
758
|
+
uri,
|
|
759
|
+
mimeType: "text/markdown",
|
|
760
|
+
text: await nerveCenter.getLiveContext()
|
|
761
|
+
}]
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
let fileName = uri;
|
|
765
|
+
if (uri.startsWith("context://local/")) {
|
|
766
|
+
fileName = uri.replace("context://local/", "");
|
|
767
|
+
} else if (uri.startsWith("context://docs/")) {
|
|
768
|
+
fileName = uri.replace("context://", "");
|
|
769
|
+
}
|
|
770
|
+
const content = await manager.readFile(fileName);
|
|
771
|
+
return {
|
|
772
|
+
contents: [{
|
|
773
|
+
uri,
|
|
774
|
+
mimeType: "text/markdown",
|
|
775
|
+
text: content
|
|
776
|
+
}]
|
|
777
|
+
};
|
|
778
|
+
} catch (_error) {
|
|
779
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
783
|
+
return {
|
|
784
|
+
tools: [
|
|
785
|
+
{
|
|
786
|
+
name: READ_CONTEXT_TOOL,
|
|
787
|
+
description: "Read the shared context files (context.md, conventions.md, activity.md)",
|
|
788
|
+
inputSchema: {
|
|
789
|
+
type: "object",
|
|
790
|
+
properties: {
|
|
791
|
+
filename: { type: "string", description: "The name of the file to read (e.g., 'context.md')" }
|
|
792
|
+
},
|
|
793
|
+
required: ["filename"]
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: UPDATE_CONTEXT_TOOL,
|
|
798
|
+
description: "Update a shared context file",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: {
|
|
802
|
+
filename: { type: "string", description: "File to update" },
|
|
803
|
+
content: { type: "string", description: "New content" },
|
|
804
|
+
append: { type: "boolean", description: "Whether to append or overwrite (default: overwrite)" }
|
|
805
|
+
},
|
|
806
|
+
required: ["filename", "content"]
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: SEARCH_CONTEXT_TOOL,
|
|
811
|
+
description: "Search the codebase using vector similarity.",
|
|
812
|
+
inputSchema: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
query: { type: "string", description: "Search query" }
|
|
816
|
+
},
|
|
817
|
+
required: ["query"]
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
// --- Billing & Usage ---
|
|
821
|
+
{
|
|
822
|
+
name: "get_subscription_status",
|
|
823
|
+
description: "Check the subscription status of a user (Pro vs Free).",
|
|
824
|
+
inputSchema: {
|
|
825
|
+
type: "object",
|
|
826
|
+
properties: {
|
|
827
|
+
email: { type: "string", description: "User email to check." }
|
|
828
|
+
},
|
|
829
|
+
required: ["email"]
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "get_usage_stats",
|
|
834
|
+
description: "Get API usage statistics for a user.",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: "object",
|
|
837
|
+
properties: {
|
|
838
|
+
email: { type: "string", description: "User email to check." }
|
|
839
|
+
},
|
|
840
|
+
required: ["email"]
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "search_docs",
|
|
845
|
+
description: "Search the Axis documentation.",
|
|
846
|
+
inputSchema: {
|
|
847
|
+
type: "object",
|
|
848
|
+
properties: {
|
|
849
|
+
query: { type: "string", description: "Search query." }
|
|
850
|
+
},
|
|
851
|
+
required: ["query"]
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
// --- Decision & Orchestration ---
|
|
855
|
+
{
|
|
856
|
+
name: "propose_file_access",
|
|
857
|
+
description: "Request a lock on a file. Checks for conflicts with other agents.",
|
|
858
|
+
inputSchema: {
|
|
859
|
+
type: "object",
|
|
860
|
+
properties: {
|
|
861
|
+
agentId: { type: "string" },
|
|
862
|
+
filePath: { type: "string" },
|
|
863
|
+
intent: { type: "string" },
|
|
864
|
+
userPrompt: { type: "string", description: "The full prompt provided by the user that initiated this action." }
|
|
865
|
+
},
|
|
866
|
+
required: ["agentId", "filePath", "intent", "userPrompt"]
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: "update_shared_context",
|
|
871
|
+
description: "Write to the in-memory Live Notepad.",
|
|
872
|
+
inputSchema: {
|
|
873
|
+
type: "object",
|
|
874
|
+
properties: {
|
|
875
|
+
agentId: { type: "string" },
|
|
876
|
+
text: { type: "string" }
|
|
877
|
+
},
|
|
878
|
+
required: ["agentId", "text"]
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
// --- Permanent Memory ---
|
|
882
|
+
{
|
|
883
|
+
name: "finalize_session",
|
|
884
|
+
description: "End the session, archive the notepad, and clear locks.",
|
|
885
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: "get_project_soul",
|
|
889
|
+
description: "Get high-level project goals and context.",
|
|
890
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
891
|
+
},
|
|
892
|
+
// --- Job Board (Task Orchestration) ---
|
|
893
|
+
{
|
|
894
|
+
name: "post_job",
|
|
895
|
+
description: "Post a new job/ticket. Supports priority and dependencies.",
|
|
896
|
+
inputSchema: {
|
|
897
|
+
type: "object",
|
|
898
|
+
properties: {
|
|
899
|
+
title: { type: "string" },
|
|
900
|
+
description: { type: "string" },
|
|
901
|
+
priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
|
|
902
|
+
dependencies: { type: "array", items: { type: "string" } }
|
|
903
|
+
},
|
|
904
|
+
required: ["title", "description"]
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: "cancel_job",
|
|
909
|
+
description: "Cancel a job that is no longer needed.",
|
|
910
|
+
inputSchema: {
|
|
911
|
+
type: "object",
|
|
912
|
+
properties: {
|
|
913
|
+
jobId: { type: "string" },
|
|
914
|
+
reason: { type: "string" }
|
|
915
|
+
},
|
|
916
|
+
required: ["jobId", "reason"]
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "force_unlock",
|
|
921
|
+
description: "Admin tool to forcibly remove a lock from a file.",
|
|
922
|
+
inputSchema: {
|
|
923
|
+
type: "object",
|
|
924
|
+
properties: {
|
|
925
|
+
filePath: { type: "string" },
|
|
926
|
+
reason: { type: "string" }
|
|
927
|
+
},
|
|
928
|
+
required: ["filePath", "reason"]
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "claim_next_job",
|
|
933
|
+
description: "Auto-assign the next available 'todo' job to yourself.",
|
|
934
|
+
inputSchema: {
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: {
|
|
937
|
+
agentId: { type: "string" }
|
|
938
|
+
},
|
|
939
|
+
required: ["agentId"]
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
name: "complete_job",
|
|
944
|
+
description: "Mark your assigned job as done.",
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: "object",
|
|
947
|
+
properties: {
|
|
948
|
+
agentId: { type: "string" },
|
|
949
|
+
jobId: { type: "string" },
|
|
950
|
+
outcome: { type: "string" }
|
|
951
|
+
},
|
|
952
|
+
required: ["agentId", "jobId", "outcome"]
|
|
953
|
+
}
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
name: "index_file",
|
|
957
|
+
description: "Force re-index a file into the RAG vector database.",
|
|
958
|
+
inputSchema: {
|
|
959
|
+
type: "object",
|
|
960
|
+
properties: {
|
|
961
|
+
filePath: { type: "string" },
|
|
962
|
+
content: { type: "string" }
|
|
963
|
+
},
|
|
964
|
+
required: ["filePath", "content"]
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
]
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
971
|
+
const { name, arguments: args } = request.params;
|
|
972
|
+
logger.info("Tool call", { name });
|
|
973
|
+
if (name === READ_CONTEXT_TOOL) {
|
|
974
|
+
const filename = String(args?.filename);
|
|
975
|
+
try {
|
|
976
|
+
const data = await manager.readFile(filename);
|
|
977
|
+
return {
|
|
978
|
+
content: [{ type: "text", text: data }]
|
|
979
|
+
};
|
|
980
|
+
} catch (err) {
|
|
981
|
+
return {
|
|
982
|
+
content: [{ type: "text", text: `Error reading file: ${err}` }],
|
|
983
|
+
isError: true
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (name === UPDATE_CONTEXT_TOOL) {
|
|
988
|
+
const filename = String(args?.filename);
|
|
989
|
+
const content = String(args?.content);
|
|
990
|
+
const append = Boolean(args?.append);
|
|
991
|
+
try {
|
|
992
|
+
await manager.updateFile(filename, content, append);
|
|
993
|
+
return {
|
|
994
|
+
content: [{ type: "text", text: `Updated ${filename}` }]
|
|
995
|
+
};
|
|
996
|
+
} catch (err) {
|
|
997
|
+
return {
|
|
998
|
+
content: [{ type: "text", text: `Error updating file: ${err}` }],
|
|
999
|
+
isError: true
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (name === "index_file") {
|
|
1004
|
+
const filePath = String(args?.filePath);
|
|
1005
|
+
const content = String(args?.content);
|
|
1006
|
+
const success = await ragEngine.indexContent(filePath, content);
|
|
1007
|
+
return { content: [{ type: "text", text: success ? "Indexed." : "Failed." }] };
|
|
1008
|
+
}
|
|
1009
|
+
if (name === SEARCH_CONTEXT_TOOL) {
|
|
1010
|
+
const query = String(args?.query);
|
|
1011
|
+
const results = await ragEngine.search(query);
|
|
1012
|
+
return { content: [{ type: "text", text: results.join("\n---\n") }] };
|
|
1013
|
+
}
|
|
1014
|
+
if (name === "get_subscription_status") {
|
|
1015
|
+
const email = String(args?.email);
|
|
1016
|
+
const result = await nerveCenter.getSubscriptionStatus(email);
|
|
1017
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1018
|
+
}
|
|
1019
|
+
if (name === "get_usage_stats") {
|
|
1020
|
+
const email = String(args?.email);
|
|
1021
|
+
const result = await nerveCenter.getUsageStats(email);
|
|
1022
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1023
|
+
}
|
|
1024
|
+
if (name === "search_docs") {
|
|
1025
|
+
const query = String(args?.query);
|
|
1026
|
+
try {
|
|
1027
|
+
const formatted = await manager.searchContext(query);
|
|
1028
|
+
return { content: [{ type: "text", text: formatted }] };
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
return {
|
|
1031
|
+
content: [{ type: "text", text: `Search Error: ${err}` }],
|
|
1032
|
+
isError: true
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (name === "propose_file_access") {
|
|
1037
|
+
const { agentId, filePath, intent, userPrompt } = args;
|
|
1038
|
+
const result = await nerveCenter.proposeFileAccess(agentId, filePath, intent, userPrompt);
|
|
1039
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1040
|
+
}
|
|
1041
|
+
if (name === "update_shared_context") {
|
|
1042
|
+
const { agentId, text } = args;
|
|
1043
|
+
const result = await nerveCenter.updateSharedContext(text, agentId);
|
|
1044
|
+
return { content: [{ type: "text", text: result }] };
|
|
1045
|
+
}
|
|
1046
|
+
if (name === "finalize_session") {
|
|
1047
|
+
const result = await nerveCenter.finalizeSession();
|
|
1048
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1049
|
+
}
|
|
1050
|
+
if (name === "get_project_soul") {
|
|
1051
|
+
const result = await nerveCenter.getProjectSoul();
|
|
1052
|
+
return { content: [{ type: "text", text: result }] };
|
|
1053
|
+
}
|
|
1054
|
+
if (name === "post_job") {
|
|
1055
|
+
const { title, description, priority, dependencies } = args;
|
|
1056
|
+
const result = await nerveCenter.postJob(title, description, priority, dependencies);
|
|
1057
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1058
|
+
}
|
|
1059
|
+
if (name === "cancel_job") {
|
|
1060
|
+
const { jobId, reason } = args;
|
|
1061
|
+
const result = await nerveCenter.cancelJob(jobId, reason);
|
|
1062
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1063
|
+
}
|
|
1064
|
+
if (name === "force_unlock") {
|
|
1065
|
+
const { filePath, reason } = args;
|
|
1066
|
+
const result = await nerveCenter.forceUnlock(filePath, reason);
|
|
1067
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1068
|
+
}
|
|
1069
|
+
if (name === "claim_next_job") {
|
|
1070
|
+
const { agentId } = args;
|
|
1071
|
+
const result = await nerveCenter.claimNextJob(agentId);
|
|
1072
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1073
|
+
}
|
|
1074
|
+
if (name === "complete_job") {
|
|
1075
|
+
const { agentId, jobId, outcome } = args;
|
|
1076
|
+
const result = await nerveCenter.completeJob(agentId, jobId, outcome);
|
|
1077
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1078
|
+
}
|
|
1079
|
+
throw new Error(`Tool not found: ${name}`);
|
|
1080
|
+
});
|
|
1081
|
+
async function main() {
|
|
1082
|
+
await ensureFileSystem();
|
|
1083
|
+
await nerveCenter.init();
|
|
1084
|
+
if (nerveCenter.projectId) {
|
|
1085
|
+
ragEngine.setProjectId(nerveCenter.projectId);
|
|
1086
|
+
logger.info(`RAG Engine linked to Project ID: ${nerveCenter.projectId}`);
|
|
1087
|
+
}
|
|
1088
|
+
const transport = new StdioServerTransport();
|
|
1089
|
+
await server.connect(transport);
|
|
1090
|
+
logger.info("Shared Context MCP Server running on stdio");
|
|
1091
|
+
}
|
|
1092
|
+
main().catch((error) => {
|
|
1093
|
+
logger.error("Server error", error);
|
|
1094
|
+
process.exit(1);
|
|
1095
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@virsanghavi/axis-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Axis MCP Server CLI",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"axis-server": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node dist/cli.js",
|
|
11
|
+
"build": "tsup bin/cli.ts --format cjs --out-dir dist --clean --shims && tsup ../../src/local/mcp-server.ts --format esm --out-dir dist",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"commander": "^11.0.0",
|
|
16
|
+
"chalk": "^5.3.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
18
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
19
|
+
"async-mutex": "^0.5.0",
|
|
20
|
+
"dotenv": "^16.3.1",
|
|
21
|
+
"fs-extra": "^11.2.0",
|
|
22
|
+
"openai": "^4.24.0",
|
|
23
|
+
"zod": "^3.22.4",
|
|
24
|
+
"execa": "^8.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.0.0",
|
|
28
|
+
"@types/fs-extra": "^11.0.4",
|
|
29
|
+
"tsup": "^8.0.1",
|
|
30
|
+
"tsx": "^4.7.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|