nexus-prime 7.9.3 → 7.9.4

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.
@@ -81,6 +81,20 @@ function stringifyDashboardJson(data) {
81
81
  return value;
82
82
  }, DASHBOARD_PRETTY_JSON ? 2 : 0);
83
83
  }
84
+ function resolveDashboardAppFile(relativePath) {
85
+ const roots = [
86
+ path.join(__dirname, 'app'),
87
+ path.resolve(__dirname, '..', '..', 'src', 'dashboard', 'app'),
88
+ ];
89
+ for (const root of roots) {
90
+ const resolved = path.resolve(root, relativePath);
91
+ if (resolved !== root && !resolved.startsWith(root + path.sep))
92
+ continue;
93
+ if (fs.existsSync(resolved))
94
+ return resolved;
95
+ }
96
+ return null;
97
+ }
84
98
  export class DashboardServer {
85
99
  server;
86
100
  runtimeProvider;
@@ -485,7 +499,12 @@ export class DashboardServer {
485
499
  'X-Content-Type-Options': 'nosniff',
486
500
  'X-Frame-Options': 'DENY',
487
501
  };
488
- const htmlPath = path.join(__dirname, 'app', 'index.html');
502
+ const htmlPath = resolveDashboardAppFile('index.html');
503
+ if (!htmlPath) {
504
+ res.writeHead(500);
505
+ res.end('Error loading LATTICE dashboard HTML: missing app/index.html');
506
+ return;
507
+ }
489
508
  fs.readFile(htmlPath, 'utf8', (err, data) => {
490
509
  if (err) {
491
510
  res.writeHead(500);
@@ -514,7 +533,12 @@ export class DashboardServer {
514
533
  res.end('Bad request');
515
534
  return;
516
535
  }
517
- const filePath = path.join(__dirname, 'app', rel);
536
+ const filePath = resolveDashboardAppFile(rel);
537
+ if (!filePath) {
538
+ res.writeHead(404);
539
+ res.end('Not found');
540
+ return;
541
+ }
518
542
  const ext = path.extname(filePath).toLowerCase();
519
543
  const ct = EXT_TYPES[ext] ?? 'application/octet-stream';
520
544
  fs.readFile(filePath, (err, data) => {
@@ -4,21 +4,30 @@
4
4
  * Connects via Stdio transport to the configured cr-graph binary.
5
5
  * Binary discovery order:
6
6
  * 1. NEXUS_CRG_BIN env var
7
- * 2. ~/.nexus-prime/cr-graph.json { "bin": "/path/to/binary", "args": [...] }
7
+ * 2. <repo>/.mcp.json code-review-graph entry
8
+ * 3. ~/.nexus-prime/cr-graph.json { "bin": "/path/to/binary", "args": [...] }
8
9
  *
9
10
  * Failure mode: when the binary is not found or the process fails to start,
10
11
  * the client returns `available = false` and all methods return empty results.
11
12
  * The orchestrator falls back to the n-gram index. No regression.
12
13
  */
14
+ interface CrGraphConfig {
15
+ bin: string;
16
+ args?: string[];
17
+ cwd?: string;
18
+ }
19
+ export declare function discoverCrGraphConfig(repoRoot?: string): CrGraphConfig | null;
13
20
  export declare class CodeReviewGraphClient {
14
21
  private client;
15
22
  private transport;
16
23
  private _available;
17
24
  private _initialized;
25
+ private initPromise;
18
26
  private repoRoot;
19
27
  constructor(repoRoot: string);
20
28
  get available(): boolean;
21
29
  init(): Promise<void>;
30
+ private initialize;
22
31
  /**
23
32
  * Search for file nodes relevant to a task description.
24
33
  * Returns absolute file paths, clamped to `limit`.
@@ -44,3 +53,4 @@ export declare class CodeReviewGraphClient {
44
53
  * orchestrator falls back to the n-gram index if unavailable.
45
54
  */
46
55
  export declare function ensureCrGraphBuilt(repoRoot: string): Promise<void>;
56
+ export {};
@@ -4,7 +4,8 @@
4
4
  * Connects via Stdio transport to the configured cr-graph binary.
5
5
  * Binary discovery order:
6
6
  * 1. NEXUS_CRG_BIN env var
7
- * 2. ~/.nexus-prime/cr-graph.json { "bin": "/path/to/binary", "args": [...] }
7
+ * 2. <repo>/.mcp.json code-review-graph entry
8
+ * 3. ~/.nexus-prime/cr-graph.json { "bin": "/path/to/binary", "args": [...] }
8
9
  *
9
10
  * Failure mode: when the binary is not found or the process fails to start,
10
11
  * the client returns `available = false` and all methods return empty results.
@@ -18,13 +19,18 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
18
19
  import { nexusEventBus } from './event-bus.js';
19
20
  const MIN_CRG_CANDIDATES = 5;
20
21
  const REQUEST_TIMEOUT_MS = 4_000;
21
- function discoverCrGraphConfig() {
22
+ const INIT_TIMEOUT_MS = 4_000;
23
+ export function discoverCrGraphConfig(repoRoot) {
22
24
  // 1. Env var
23
25
  const envBin = process.env['NEXUS_CRG_BIN'];
24
26
  if (envBin) {
25
27
  return { bin: envBin, args: [] };
26
28
  }
27
- // 2. ~/.nexus-prime/cr-graph.json
29
+ // 2. Repo-local MCP config.
30
+ const repoMcpConfig = repoRoot ? readMcpCrGraphConfig(path.join(repoRoot, '.mcp.json'), repoRoot) : null;
31
+ if (repoMcpConfig)
32
+ return repoMcpConfig;
33
+ // 3. ~/.nexus-prime/cr-graph.json
28
34
  try {
29
35
  const configPath = path.join(os.homedir(), '.nexus-prime', 'cr-graph.json');
30
36
  const raw = fs.readFileSync(configPath, 'utf8');
@@ -42,11 +48,37 @@ function discoverCrGraphConfig() {
42
48
  }
43
49
  return null;
44
50
  }
51
+ function readMcpCrGraphConfig(configPath, repoRoot) {
52
+ try {
53
+ const raw = fs.readFileSync(configPath, 'utf8');
54
+ const parsed = JSON.parse(raw);
55
+ const servers = parsed['mcpServers'];
56
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers))
57
+ return null;
58
+ const server = servers['code-review-graph'];
59
+ if (!server || typeof server !== 'object' || Array.isArray(server))
60
+ return null;
61
+ const record = server;
62
+ if (typeof record['command'] !== 'string' || !record['command'])
63
+ return null;
64
+ return {
65
+ bin: record['command'],
66
+ args: Array.isArray(record['args'])
67
+ ? record['args'].filter((arg) => typeof arg === 'string')
68
+ : [],
69
+ cwd: repoRoot,
70
+ };
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
45
76
  export class CodeReviewGraphClient {
46
77
  client = null;
47
78
  transport = null;
48
79
  _available = false;
49
80
  _initialized = false;
81
+ initPromise = null;
50
82
  repoRoot;
51
83
  constructor(repoRoot) {
52
84
  this.repoRoot = repoRoot;
@@ -55,10 +87,18 @@ export class CodeReviewGraphClient {
55
87
  return this._available;
56
88
  }
57
89
  async init() {
90
+ if (this.initPromise)
91
+ return this.initPromise;
58
92
  if (this._initialized)
59
93
  return;
94
+ this.initPromise = this.initialize().finally(() => {
95
+ this.initPromise = null;
96
+ });
97
+ return this.initPromise;
98
+ }
99
+ async initialize() {
60
100
  this._initialized = true;
61
- const config = discoverCrGraphConfig();
101
+ const config = discoverCrGraphConfig(this.repoRoot);
62
102
  if (!config)
63
103
  return;
64
104
  try {
@@ -69,11 +109,18 @@ export class CodeReviewGraphClient {
69
109
  stderr: 'ignore',
70
110
  });
71
111
  this.client = new Client({ name: 'nexus-prime', version: '1.0.0' }, { capabilities: {} });
72
- await this.client.connect(this.transport);
112
+ await Promise.race([
113
+ this.client.connect(this.transport),
114
+ new Promise((_, reject) => setTimeout(() => reject(new Error('cr-graph init timeout')), INIT_TIMEOUT_MS)),
115
+ ]);
73
116
  this._available = true;
74
117
  }
75
118
  catch {
76
119
  // binary not found or failed to start — degrade gracefully
120
+ try {
121
+ await this.transport?.close();
122
+ }
123
+ catch { /* best-effort */ }
77
124
  this.client = null;
78
125
  this.transport = null;
79
126
  this._available = false;
@@ -85,6 +132,7 @@ export class CodeReviewGraphClient {
85
132
  * Returns empty array when unavailable or on error.
86
133
  */
87
134
  async semanticSearchNodes(query, limit = 20) {
135
+ await this.init();
88
136
  if (!this._available || !this.client)
89
137
  return [];
90
138
  try {
@@ -148,7 +196,7 @@ export async function ensureCrGraphBuilt(repoRoot) {
148
196
  }
149
197
  }
150
198
  catch { /* stamp missing or corrupt — proceed */ }
151
- const config = discoverCrGraphConfig();
199
+ const config = discoverCrGraphConfig(repoRoot);
152
200
  if (!config)
153
201
  return; // binary not configured — nothing to do
154
202
  const transport = new StdioClientTransport({
@@ -41,7 +41,7 @@ export class KnowledgeFabricEngine {
41
41
  reused: true,
42
42
  },
43
43
  };
44
- this.persistBundle(bundle);
44
+ await this.persistBundle(bundle);
45
45
  return bundle;
46
46
  }
47
47
  const bundle = await this.composeStage(input, stage, cacheKey);
@@ -53,7 +53,7 @@ export class KnowledgeFabricEngine {
53
53
  reused: false,
54
54
  },
55
55
  });
56
- this.persistBundle(bundle);
56
+ await this.persistBundle(bundle);
57
57
  return bundle;
58
58
  }
59
59
  async composeStage(input, stage, cacheKey) {
@@ -299,7 +299,7 @@ export class KnowledgeFabricEngine {
299
299
  trace: snapshot?.modelTierTrace ?? [],
300
300
  };
301
301
  }
302
- persistBundle(bundle) {
302
+ async persistBundle(bundle) {
303
303
  const snapshot = {
304
304
  runtimeId: bundle.runtimeId,
305
305
  sessionId: bundle.sessionId,
@@ -325,8 +325,12 @@ export class KnowledgeFabricEngine {
325
325
  };
326
326
  // Update in-memory cache immediately so getSessionSnapshot has zero disk latency.
327
327
  this.snapshotCache.set(bundle.runtimeId, snapshot);
328
- // Persist to disk asynchronously — callers never await this return value.
329
- void this.persistBundleAsync(bundle.runtimeId, bundle.sessionId, snapshot);
328
+ try {
329
+ await this.persistBundleAsync(bundle.runtimeId, bundle.sessionId, snapshot);
330
+ }
331
+ catch {
332
+ // Snapshot persistence is best effort; the in-memory cache remains authoritative in-process.
333
+ }
330
334
  }
331
335
  async persistBundleAsync(runtimeId, sessionId, snapshot) {
332
336
  const runtimeDir = path.join(this.stateDir, runtimeId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.3",
3
+ "version": "7.9.4",
4
4
  "description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",