@virsanghavi/axis-server 1.0.8 → 1.1.0

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